diff --git a/CHANGELOG.md b/CHANGELOG.md index fe7b149c..9000ebe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **`component_mcp` bounded row consumer** (RFC 0001 Phase 5, [#256](https://github.com/askdba/myvector/issues/256)). `run_query` sessions use the component's own row and value consumer services, which stop retaining rows past 1001 rows or 4 MiB of values while a statement runs, so a large explicit `LIMIT` or wide rows no longer buffer the whole result in `mysqld`. The budget counts values plus the per-row and per-cell overhead of the default consumer, NULL cells included. `scripts/test-mcp-endpoint.sh` now has 41 checks, including peak RSS for `LIMIT 1000000` and for 1 MB rows, 8000 NULL columns, DOUBLE widths, and errors raised after some rows. - **`component_mcp` (RFC 0001 Phase 4 spike, MySQL 26.10).** An MCP Streamable HTTP endpoint inside `mysqld` on MySQL Router's HTTP server (the libraries `component_mysql_rest_service` links), built in the server tree with `MYVECTOR_BUILD_MCP_ENDPOINT=ON ./scripts/build-component-26.10-docker.sh`. It is not for production. - **Endpoint:** loopback, plain HTTP, one bearer token (`mcp_http.token_sha256`) mapped to one account (`mcp_http.account`). - **Tools:** `list_databases` (tool pack) and `run_query` (one statement). diff --git a/docs/MCP_COMPONENTS.md b/docs/MCP_COMPONENTS.md index 4b112f96..bc76dc6a 100644 --- a/docs/MCP_COMPONENTS.md +++ b/docs/MCP_COMPONENTS.md @@ -191,7 +191,7 @@ An MCP **Streamable HTTP** endpoint (JSON-RPC 2.0 over `POST /mcp`) inside `mysq - **Network and auth:** loopback only (`127.0.0.1`) and plain HTTP, with no TLS yet. One bearer token, mapped to one account. - **Tools:** `list_databases` (through the `mysql_mcp` tool pack) and `run_query` (one statement). - **Results:** `run_query` returns at most 1000 rows and 4 MB of serialized rows (escapes counted), as `{"rows": [{"col": value, ...}], "truncated": true|false}`. A value is a JSON string, `null`, or `{"base64": "..."}` when its bytes are not valid UTF-8 (binary data). The session's `sql_select_limit` stops the server at 1001 rows for statements without their own `LIMIT`. - - **Known spike limit:** the command service's default consumer collects every row while a statement runs, so a statement with a large explicit `LIMIT` is buffered in full in `mysqld` before the reply is cut. Bounding that needs a custom row consumer (`mysql_command_consumer` services), which is Phase 5 work. + - **Bounded consumer:** for statements with their own `LIMIT`, the server's default command-service consumer would collect every row while the statement runs. `component_mcp` sessions select its own row and value consumer services (`mysql_text_consumer_*_v1.mcp`), which pass rows on to the default consumer until the statement has produced 1001 rows or 4 MiB of values. Later rows are dropped as they arrive, and a row that crosses the byte budget is never half stored. The statement still runs to its end; `mcp.max_execution_time_ms`, when set, bounds its time. The budget also counts what the default consumer keeps per row and per cell (a pointer each, NULL cells included), so wide results of NULLs are bounded too. `SELECT ... LIMIT 1000000` and 100 rows of 1 MB each leave `mysqld`'s peak RSS unchanged (without it: +144 MiB and +61 MiB). - **Missing:** the `account_locked` check, a per-account session cap and column masking. All are in Phase 5. ### Build and install @@ -253,10 +253,11 @@ Facts recorded for Phase 5: ### Tests -`scripts/test-mcp-endpoint.sh ` (30 checks) installs the guard, the tool pack and `component_mcp` on the official image, then checks: +`scripts/test-mcp-endpoint.sh ` installs the guard, the tool pack and `component_mcp` on the official image, then checks: - bearer auth, `initialize`, `notifications/initialized`, `tools/list` and the JSON-RPC errors; - both tools, and that `run_query` runs as the mapped account; +- the result bounds: replies cut at 1000 rows or 4 MB, and the bounded consumer (peak RSS for `LIMIT 1000000` and for 1 MB rows, 8000 NULL columns, NULL-only rows, errors raised after some rows or past the bound); - that through the endpoint an `MCP_USER` account's write and a `mysql` read are refused by the guard, and a multi-statement is a parse error; - that nothing is written and no session leaks; - that remapped to an account without `MCP_USER`, the same write is not restricted. diff --git a/scripts/test-mcp-endpoint.sh b/scripts/test-mcp-endpoint.sh index db9b1049..5763d8a3 100755 --- a/scripts/test-mcp-endpoint.sh +++ b/scripts/test-mcp-endpoint.sh @@ -38,6 +38,7 @@ sql "INSTALL COMPONENT 'file://component_mcp_guard'; GRANT MCP_ADMIN ON *.* TO r CREATE DATABASE d1; CREATE TABLE d1.t (i INT); INSERT INTO d1.t VALUES (1); CREATE TABLE d1.big (n INT); INSERT INTO d1.big WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM s WHERE n < 999) SELECT n FROM s; INSERT INTO d1.big SELECT n + 1000 FROM d1.big WHERE n <= 500; +CREATE TABLE d1.dbl (d DOUBLE(255,0)); INSERT INTO d1.dbl VALUES (1e250); CREATE USER mcp@'%' IDENTIFIED BY 'p'; GRANT SELECT, INSERT ON d1.* TO mcp@'%'; GRANT SELECT ON mysql.* TO mcp@'%'; GRANT EXECUTE ON mysql_mcp.* TO mcp@'%'; GRANT MCP_USER ON *.* TO mcp@'%'; CREATE USER app@'%' IDENTIFIED BY 'p'; GRANT SELECT, INSERT ON d1.* TO app@'%'; GRANT EXECUTE ON mysql_mcp.* TO app@'%';" @@ -58,6 +59,38 @@ expect "escaped values count against the bound (no HTTP 500)" "$(call run_query expect "bytes that are not UTF-8 are returned as base64" "$(call run_query '{"sql":"SELECT X'"'"'FF'"'"' AS b"}')" 'b\\":\{\\"base64\\":\\"/w==\\"\}' expect "valid UTF-8 stays a string" "$(call run_query '{"sql":"SELECT CONVERT(X'"'"'C3A9'"'"' USING utf8mb4) AS s"}')" 's\\":\\"é\\"' expect "a value over the byte bound is not sent" "$(call run_query '{"sql":"SELECT REPEAT(\"x\", 5000000) AS v"}' | head -c 300)" 'rows\\":\[\],\\"truncated\\":true' +# The bounded consumer: the server keeps at most 1001 rows and 4 MiB of values while a +# statement runs, whatever its LIMIT. mysqld is PID 1; its peak RSS (VmHWM) must grow by under +# 32 MiB, not by the size of the full result (about 100 MB for each; without the bounded +# consumer it grew by 61 and 144 MiB). The wide case runs first, as the peak only rises. +hwm_kb() { docker exec "$C" awk '/^VmHWM/ {print $2}' /proc/1/status; } +hwm0=$(hwm_kb) +wide_reply=$(call run_query '{"sql":"SELECT REPEAT(\"x\", 1000000) AS v FROM d1.big LIMIT 100"}') +expect "wide rows: the reply is bounded" "$(printf '%s' "$wide_reply" | grep -o '\\"v\\":' | wc -l) $(printf '%s' "$wide_reply" | grep -o 'truncated\\":true' | head -1)" "^[1-4] truncated" +expect "wide rows: bytes past the bound are not retained" "$(( ($(hwm_kb) - hwm0) / 1024 )) MiB" "^([0-9]|[12][0-9]|3[01]) MiB$" +hwm0=$(hwm_kb) +long_reply=$(call run_query '{"sql":"SELECT REPEAT(\"x\", 100) AS v FROM d1.big a, d1.big b LIMIT 1000000"}') +expect "LIMIT 1000000: the reply is bounded" "$(printf '%s' "$long_reply" | grep -o '\\"v\\":' | wc -l) $(printf '%s' "$long_reply" | grep -o 'truncated\\":true' | head -1)" "^1000 truncated" +expect "LIMIT 1000000: rows past the bound are not retained" "$(( ($(hwm_kb) - hwm0) / 1024 )) MiB" "^([0-9]|[12][0-9]|3[01]) MiB$" +expect "a NULL-only result is not cut" "$(call run_query '{"sql":"SELECT NULL AS z FROM d1.big LIMIT 3"}')" 'z\\":null\},\{\\"z\\":null\},\{\\"z\\":null\}\],\\"truncated\\":false' +# NULL cells count against the byte budget too (the DOM consumer keeps a pointer per cell): 1001 +# rows of 8000 NULLs would hold 64 MB. The reply's own 4 MB cut comes first either way, so the +# check is on mysqld's peak RSS. Building the reply for 8000 columns takes memory of its own, so +# the same query with LIMIT 60 (under the budget, so retained the same way either way) runs first: +# the large LIMIT may then raise the peak only by what the consumer retains past it: under 16 MiB +# (measured +3 MiB; without the NULL accounting, +37 MiB). +nulls=$(seq -s, -f 'NULL AS n%g' 1 8000) +call run_query "{\"sql\":\"SELECT $nulls FROM d1.big LIMIT 60\"}" >/dev/null +hwm0=$(hwm_kb) +null_reply=$(call run_query "{\"sql\":\"SELECT $nulls FROM d1.big a, d1.big b LIMIT 5000\"}") +expect "8000 NULL columns: the reply is bounded and truncated" "$(printf '%s' "$null_reply" | grep -o 'n8000\\":null' | head -1) $(printf '%s' "$null_reply" | grep -o 'truncated\\":true' | head -1)" 'n8000\\":null truncated' +expect "8000 NULL columns: NULL cells past the bound are not retained" "$(( ($(hwm_kb) - hwm0) / 1024 )) MiB" "^([0-9]|1[0-5]) MiB$" +# DOUBLE values are charged their formatted length: 500 rows of 100 short DOUBLEs fit. +dbls=$(seq -s, -f 'n * 1.5e0 AS d%g' 1 100) +expect "500 rows of 100 DOUBLE columns are not cut" "$(call run_query "{\"sql\":\"SELECT $dbls FROM d1.big LIMIT 500\"}" | grep -o 'd100\\":' | wc -l | tr -d ' ')" "^500$" +expect "a fixed-decimal DOUBLE (251 digits) is returned in full" "$(call run_query '{"sql":"SELECT d AS big FROM d1.dbl"}' | grep -o 'big\\":\\"1[0-9]*' | wc -c | tr -d ' ')" "^260$" +expect "an error after some rows is reported, not a partial result" "$(call run_query '{"sql":"SELECT n, IF(n = 500, (SELECT 1 UNION SELECT 2), n) AS x FROM d1.big"}')" 'Subquery returns more than 1 row.*"isError":true' +expect "an error past the row bound is reported" "$(call run_query '{"sql":"SELECT n, IF(n = 1500, (SELECT 1 UNION SELECT 2), n) AS x FROM d1.big a LIMIT 1000000"}')" 'Subquery returns more than 1 row.*"isError":true' expect "run_query read as the mapped account" "$(call run_query '{"sql":"SELECT i, CURRENT_USER() AS who FROM d1.t"}')" 'i\\":\\"1\\",\\"who\\":\\"mcp@%' # Phase 4 exit criterion: the guard's policy reaches run_query in impersonated sessions. expect "guard: an MCP account's write is refused" "$(call run_query '{"sql":"INSERT INTO d1.t VALUES (2)"}')" 'Error 1290: [^"]*mcp.read_only[^"]*"}],"isError":true' diff --git a/src/mcp/endpoint/component_mcp.cc b/src/mcp/endpoint/component_mcp.cc index 1854d2da..f1006aa4 100644 --- a/src/mcp/endpoint/component_mcp.cc +++ b/src/mcp/endpoint/component_mcp.cc @@ -37,9 +37,11 @@ #include #include #include +#include #include #include #include +#include "mysql/strings/dtoa.h" #include #include @@ -78,6 +80,15 @@ REQUIRES_SERVICE_PLACEHOLDER(mysql_audit_api_connection); // component_mcp_guard's query consumer: requiring it makes the server refuse to install the // endpoint without the guard, and to uninstall the guard while the endpoint is installed. REQUIRES_SERVICE_PLACEHOLDER_AS(event_tracking_query, mcp_guard_query); +// The server's default (DOM) text consumer: the bounded consumer below passes rows on to it. +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_row_factory_v1, dom_row_factory); +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_get_null_v1, dom_get_null); +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_get_integer_v1, dom_get_integer); +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_get_longlong_v1, dom_get_longlong); +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_get_decimal_v1, dom_get_decimal); +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_get_double_v1, dom_get_double); +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_get_date_time_v1, dom_get_date_time); +REQUIRES_SERVICE_PLACEHOLDER_AS(mysql_text_consumer_get_string_v1, dom_get_string); // Defined by Router's harness (its component log handler); set at init. extern SERVICE_TYPE(log_builtins) *log_bi; @@ -180,14 +191,169 @@ bool token_ok(const std::string *authorization) { // Result bounds: the reply keeps at most kMaxRows rows and kMaxBytes of serialized rows (the // tool result then escapes them once more inside its text, so the HTTP reply stays well under -// its 16 MiB limit). The session's -// sql_select_limit stops the server past kMaxRows + 1 rows for statements without their own -// LIMIT. The command service's default consumer collects every row while the statement runs -// (store_result only hands that buffer over), so a statement with a large explicit LIMIT is -// still held in full by the server: a bounded custom consumer is Phase 5 work. +// its 16 MiB limit). The session's sql_select_limit stops the server past kMaxRows + 1 rows +// for statements without their own LIMIT. For the others, the bounded consumer below keeps +// the server from retaining more than kMaxRows + 1 rows and kMaxBytes of values while the +// statement runs (the default consumer collects every row; store_result only hands them over). constexpr unsigned long long kMaxRows = 1000; constexpr size_t kMaxBytes = 4u << 20; +// ----- Bounded text consumer ---------------------------------------------------------------- +// +// A run_as() session selects these row and value services instead of the default ones. Each +// passes rows on to the default (DOM) consumer, so store_result() and fetch_row() work as +// before, until the statement has produced kMaxRows + 1 rows or kMaxBytes (values, plus the +// DOM consumer's per-cell and per-row overhead); +// later rows are dropped as they arrive. A row that crosses the byte budget is aborted in the +// DOM consumer, so it is never half stored. The statement runs on the calling thread, which +// holds its budget in tl_budget; without one, the services pass everything on. + +struct RowBudget { + unsigned long long max_rows = kMaxRows + 1; + size_t max_bytes = kMaxBytes; + unsigned long long kept = 0; + size_t bytes = 0; + bool in_row = false; // the current row was passed on to the DOM consumer + bool row_over = false; // the current row crossed the byte budget + bool full = false; // later rows are dropped + bool dropped = false; // a row was dropped: the result is truncated +}; + +thread_local RowBudget *tl_budget = nullptr; + +// Budget for one value of a passed-on row; false drops it (and the rest of the row). +bool take(size_t n) { + RowBudget *b = tl_budget; + if (b->row_over) return false; + if (n > b->max_bytes - b->bytes) { + b->row_over = b->full = b->dropped = true; + return false; + } + b->bytes += n; + return true; +} + +// Upper bounds for the text form of non-string values (DOM ctx stages them as text). DOUBLE +// values are charged their exact length instead (double_text_bytes). +constexpr size_t kNumberBytes = 24, kDecimalBytes = 88, kTimeBytes = 32; + +// The length of a DOUBLE as the DOM consumer formats it: in full with a fixed number of +// decimals (my_fcvt, up to FLOATING_POINT_BUFFER bytes), otherwise in %g style (my_gcvt). +size_t double_text_bytes(double v, unsigned int decimals) { + char buf[FLOATING_POINT_BUFFER + 1]; + return decimals < DECIMAL_NOT_SPECIFIED + ? my_fcvt(v, static_cast(decimals), buf, nullptr) + : my_gcvt(v, MY_GCVT_ARG_FLOAT, FLOATING_POINT_BUFFER, buf, nullptr); +} +// What the DOM consumer keeps besides the values: a pointer per cell (NULL cells included, +// so a wide result of NULLs is bounded too), and per row its list node and end pointer. +constexpr size_t kCellBytes = sizeof(char *), kRowBytes = 4 * sizeof(char *); + +// True: the value belongs to a row the DOM consumer should not see. bytes() gives the value's +// size; it runs only for a value of a passed-on row that is still within the budget. +template +bool skip_value_sized(Bytes bytes) { + RowBudget *b = tl_budget; + return b != nullptr && (!b->in_row || b->row_over || !take(kCellBytes + bytes())); +} + +bool skip_value(size_t n) { + return skip_value_sized([n] { return n; }); +} + +DEFINE_BOOL_METHOD(bounded_start_row, (SRV_CTX_H ctx)) { + RowBudget *b = tl_budget; + if (b == nullptr) return dom_row_factory->start_row(ctx); + b->row_over = false; + if (b->full) { + b->in_row = false; + b->dropped = true; + return false; + } + b->in_row = true; + take(kRowBytes); // over budget, the row is marked and end_row() aborts it + return dom_row_factory->start_row(ctx); +} + +DEFINE_BOOL_METHOD(bounded_abort_row, (SRV_CTX_H ctx)) { + RowBudget *b = tl_budget; + if (b == nullptr) return dom_row_factory->abort_row(ctx); + if (!b->in_row) return false; + b->in_row = false; + return dom_row_factory->abort_row(ctx); +} + +DEFINE_BOOL_METHOD(bounded_end_row, (SRV_CTX_H ctx)) { + RowBudget *b = tl_budget; + if (b == nullptr) return dom_row_factory->end_row(ctx); + if (!b->in_row) return false; + b->in_row = false; + if (b->row_over) return dom_row_factory->abort_row(ctx); + if (++b->kept >= b->max_rows) b->full = true; + return dom_row_factory->end_row(ctx); +} + +DEFINE_BOOL_METHOD(bounded_get_null, (SRV_CTX_H ctx)) { + return skip_value(0) ? false : dom_get_null->get(ctx); +} + +DEFINE_BOOL_METHOD(bounded_get_integer, (SRV_CTX_H ctx, long long v)) { + return skip_value(kNumberBytes) ? false : dom_get_integer->get(ctx, v); +} + +DEFINE_BOOL_METHOD(bounded_get_longlong, (SRV_CTX_H ctx, long long v, unsigned int is_unsigned)) { + return skip_value(kNumberBytes) ? false : dom_get_longlong->get(ctx, v, is_unsigned); +} + +DEFINE_BOOL_METHOD(bounded_get_decimal, (SRV_CTX_H ctx, const DECIMAL_T_H v)) { + return skip_value(kDecimalBytes) ? false : dom_get_decimal->get(ctx, v); +} + +DEFINE_BOOL_METHOD(bounded_get_double, (SRV_CTX_H ctx, double v, unsigned int decimals)) { + // Formatting costs time: dropped rows skip it. + return skip_value_sized([&] { return double_text_bytes(v, decimals); }) + ? false + : dom_get_double->get(ctx, v, decimals); +} + +DEFINE_BOOL_METHOD(bounded_get_date, (SRV_CTX_H ctx, const MYSQL_TIME_H v)) { + return skip_value(kTimeBytes) ? false : dom_get_date_time->get_date(ctx, v); +} + +DEFINE_BOOL_METHOD(bounded_get_time, (SRV_CTX_H ctx, const MYSQL_TIME_H v, unsigned int p)) { + return skip_value(kTimeBytes) ? false : dom_get_date_time->get_time(ctx, v, p); +} + +DEFINE_BOOL_METHOD(bounded_get_datetime, (SRV_CTX_H ctx, const MYSQL_TIME_H v, unsigned int p)) { + return skip_value(kTimeBytes) ? false : dom_get_date_time->get_datatime(ctx, v, p); +} + +DEFINE_BOOL_METHOD(bounded_get_string, + (SRV_CTX_H ctx, const char *const v, size_t n, const char *const coll)) { + return skip_value(n) ? false : dom_get_string->get_string(ctx, v, n, coll); +} + +// The option and service name pairs a run_as() session sets. +constexpr std::pair kBoundedConsumer[] = { + {MYSQL_TEXT_CONSUMER_ROW_FACTORY, "mysql_text_consumer_row_factory_v1.mcp"}, + {MYSQL_TEXT_CONSUMER_GET_NULL, "mysql_text_consumer_get_null_v1.mcp"}, + {MYSQL_TEXT_CONSUMER_GET_INTEGER, "mysql_text_consumer_get_integer_v1.mcp"}, + {MYSQL_TEXT_CONSUMER_GET_LONGLONG, "mysql_text_consumer_get_longlong_v1.mcp"}, + {MYSQL_TEXT_CONSUMER_GET_DECIMAL, "mysql_text_consumer_get_decimal_v1.mcp"}, + {MYSQL_TEXT_CONSUMER_GET_DOUBLE, "mysql_text_consumer_get_double_v1.mcp"}, + {MYSQL_TEXT_CONSUMER_GET_DATE_TIME, "mysql_text_consumer_get_date_time_v1.mcp"}, + {MYSQL_TEXT_CONSUMER_GET_STRING, "mysql_text_consumer_get_string_v1.mcp"}, +}; + +// Sets tl_budget for the calling thread while a statement runs. +class BudgetScope { + public: + explicit BudgetScope(RowBudget *b) { tl_budget = b; } + ~BudgetScope() { tl_budget = nullptr; } + BudgetScope(const BudgetScope &) = delete; + BudgetScope &operator=(const BudgetScope &) = delete; +}; + struct SqlResult { bool ok = false; unsigned int sql_errno = 0; @@ -266,6 +432,21 @@ void read_error(MYSQL_H h, SqlResult *r) { r->error = msg != nullptr ? msg : ""; } +bool set_bounded_consumer(MYSQL_H h) { + for (const auto &[option, service] : kBoundedConsumer) + if (mysql_service_mysql_command_options->set(h, option, service)) return false; + return true; +} + +// Runs sql under a fresh row budget; *truncated reports whether rows were dropped. +bool bounded_query(MYSQL_H h, const std::string &sql, bool *truncated) { + RowBudget budget; + BudgetScope scope(&budget); + const bool failed = mysql_service_mysql_command_query->query(h, sql.data(), sql.size()); + *truncated = budget.dropped; + return failed; +} + // Runs one statement as account (user@host) and returns its first result set. SqlResult run_as(const std::string &account, const std::string &sql) { SqlResult r; @@ -288,7 +469,7 @@ SqlResult run_as(const std::string &account, const std::string &sql) { mysql_service_mysql_command_options->set(h, MYSQL_COMMAND_CLIENT_FLAGS, &client_flags) || mysql_service_mysql_command_options->set(h, MYSQL_COMMAND_USER_NAME, user.c_str()) || mysql_service_mysql_command_options->set(h, MYSQL_COMMAND_HOST_NAME, host.c_str()) || - mysql_service_mysql_command_factory->connect(h)) { + !set_bounded_consumer(h) || mysql_service_mysql_command_factory->connect(h)) { read_error(h, &r); if (r.error.empty()) r.error = "cannot open a session for " + account; } else { @@ -301,7 +482,7 @@ SqlResult run_as(const std::string &account, const std::string &sql) { r.error = "session refused"; } else if (mysql_service_mysql_command_query->query(h, kLimit.data(), kLimit.size())) { read_error(h, &r); - } else if (mysql_service_mysql_command_query->query(h, sql.data(), sql.size())) { + } else if (bounded_query(h, sql, &r.truncated)) { read_error(h, &r); } else { r.ok = true; @@ -592,8 +773,32 @@ mysql_service_status_t mcp_deinit() { } // namespace +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_row_factory_v1) +bounded_start_row, bounded_abort_row, bounded_end_row END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_get_null_v1) +bounded_get_null END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_get_integer_v1) +bounded_get_integer END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_get_longlong_v1) +bounded_get_longlong END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_get_decimal_v1) +bounded_get_decimal END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_get_double_v1) +bounded_get_double END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_get_date_time_v1) +bounded_get_date, bounded_get_time, bounded_get_datetime END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mcp, mysql_text_consumer_get_string_v1) +bounded_get_string END_SERVICE_IMPLEMENTATION(); + BEGIN_COMPONENT_PROVIDES(mcp) -END_COMPONENT_PROVIDES(); +PROVIDES_SERVICE(mcp, mysql_text_consumer_row_factory_v1), + PROVIDES_SERVICE(mcp, mysql_text_consumer_get_null_v1), + PROVIDES_SERVICE(mcp, mysql_text_consumer_get_integer_v1), + PROVIDES_SERVICE(mcp, mysql_text_consumer_get_longlong_v1), + PROVIDES_SERVICE(mcp, mysql_text_consumer_get_decimal_v1), + PROVIDES_SERVICE(mcp, mysql_text_consumer_get_double_v1), + PROVIDES_SERVICE(mcp, mysql_text_consumer_get_date_time_v1), + PROVIDES_SERVICE(mcp, mysql_text_consumer_get_string_v1), END_COMPONENT_PROVIDES(); BEGIN_COMPONENT_REQUIRES(mcp) REQUIRES_SERVICE(log_builtins), REQUIRES_SERVICE(log_builtins_string), @@ -604,6 +809,22 @@ REQUIRES_SERVICE(log_builtins), REQUIRES_SERVICE(log_builtins_string), REQUIRES_SERVICE(mysql_command_field_metadata), REQUIRES_SERVICE(mysql_command_error_info), REQUIRES_SERVICE(mysql_command_thread), REQUIRES_SERVICE(mysql_audit_api_connection), REQUIRES_SERVICE_IMPLEMENTATION_AS(event_tracking_query, mcp_guard, mcp_guard_query), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_row_factory_v1, mysql_server, + dom_row_factory), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_get_null_v1, mysql_server, + dom_get_null), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_get_integer_v1, mysql_server, + dom_get_integer), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_get_longlong_v1, mysql_server, + dom_get_longlong), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_get_decimal_v1, mysql_server, + dom_get_decimal), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_get_double_v1, mysql_server, + dom_get_double), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_get_date_time_v1, mysql_server, + dom_get_date_time), + REQUIRES_SERVICE_IMPLEMENTATION_AS(mysql_text_consumer_get_string_v1, mysql_server, + dom_get_string), END_COMPONENT_REQUIRES(); BEGIN_COMPONENT_METADATA(mcp)