commit b2ac9ffb1c86ce49fa3abbc90dff005a24f9e291 Author: Kaiwang Chen Date: Tue May 26 23:55:46 2026 +0800 Bug#87654321: Allow components to register custom SQL table functions Until now JSON_TABLE was the only table function recognized by the server, and there was no public extension point for new ones. This patch adds a Component service through which a loadable component can register a table function by name; the function can then be used anywhere in a FROM clause exactly like JSON_TABLE. Two new services are exposed by the mysql_server component: - table_function_registration register_table_function(name, describe_cb, fill_cb, cleanup_cb) unregister_table_function(name, was_present) - table_function_row_writer set_null / set_longlong / set_double / set_string / emit_row The describe_cb is invoked at parse time and returns the output schema (an array of Tf_column_def, one entry per column). The fill_cb is invoked once per execution and emits rows through the row-writer service. The C ABI is intentionally minimal so that components in any language with a C FFI can register new table functions. The service shape and the component-author flow are deliberately modeled after the udf_registration service introduced in WL#8020, so authors familiar with udf_registration can pick up table functions with no extra learning curve. Server side ----------- A new sql/table_function_dynamic.{h,cc} introduces: - Tf_registry: process-global, rwlock-protected registry holding descriptors as std::unique_ptr so their addresses are stable while borrowed. Each descriptor carries a usage_count that works exactly like udf_func::usage_count: the registry holds a "reservation" reference of 1, find() hands out borrowed references with an atomic increment, and release() (called from Table_function_dynamic's destructor) returns them. remove() can only erase an entry by atomically CAS'ing usage_count from 1 to 0; if any borrow is outstanding the CAS fails and remove() returns true so that unregister_table_function() reports failure to the component, which in turn makes UNINSTALL COMPONENT fail with ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE. This keeps the component .so alive while a session is executing one of its table functions. - Table_function_dynamic: a Table_function subclass that adapts the component-supplied callbacks to the existing resolver / optimizer / iterator pipeline. All downstream code (Table_ref::is_table_function, sql_resolver, join_optimizer, MaterializedTableFunctionIterator, EXPLAIN, SHOW CREATE VIEW, etc.) is reused unchanged. The grammar gains exactly one new alternative in the existing table_function: rule: IDENT_sys '(' opt_udf_expr_list ')' opt_table_alias reduced into a new PT_table_factor_dynamic_function parse-tree node. Resolution against Tf_registry happens during contextualization; an unknown name yields ER_SP_DOES_NOT_EXIST so it cannot collide with the scalar function namespace. IDENT_sys (rather than ident) is used to avoid widening the language to include reserved keywords. Component layer --------------- sql/server_component/table_function_registration_imp.{h,cc} provides the service implementations and is registered with mysql_server in server_component.cc, alongside udf_registration. Test component and MTR suite ---------------------------- components/test/test_table_function.cc registers a function called demo_tf_all_types that returns three rows with one column for each major SQL data type, exercising the full describe/fill/cleanup path and every row-writer setter. A new mysql-test/suite/test_table_function suite verifies the feature: - table_function_basic: SELECT, projection, WHERE, JOIN re-execution, ORDER BY / LIMIT, EXPLAIN, protocol metadata, install/uninstall. - table_function_errors: missing alias, unknown name, duplicate INSTALL, scalar invocation, double UNINSTALL. - table_function_advanced: derived tables, CTE, UNION ALL, IN / correlated subqueries, VIEW on top of the function, prepared statements with parameters, self-join. - table_function_unload_in_use: parks a producer thread inside fill_result_table() through a DEBUG_SYNC point and verifies that a concurrent UNINSTALL COMPONENT fails with ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE while the borrow is outstanding, then succeeds once the producer finishes. Files changed ------------- include/mysql/components/services/table_function_registration.h sql/table_function_dynamic.{h,cc} sql/server_component/table_function_registration_imp.{h,cc} sql/server_component/server_component.cc sql/parse_tree_nodes.{h,cc} sql/sql_yacc.yy sql/CMakeLists.txt sql/server_component/CMakeLists.txt components/test/test_table_function.cc components/test/CMakeLists.txt mysql-test/include/plugin.defs mysql-test/suite/test_table_function/... Change-Id: I8765432187654321876543218765432187654321 diff --git a/components/test/CMakeLists.txt b/components/test/CMakeLists.txt index a9a4c42f572..5b18d9e65e7 100644 --- a/components/test/CMakeLists.txt +++ b/components/test/CMakeLists.txt @@ -30,6 +30,11 @@ MYSQL_ADD_COMPONENT(test_udf_registration MODULE_ONLY TEST_ONLY ) +MYSQL_ADD_COMPONENT(test_table_function + test_table_function.cc + MODULE_ONLY + TEST_ONLY + ) MYSQL_ADD_COMPONENT(udf_reg_3_func udf_reg_3_func.cc MODULE_ONLY diff --git a/components/test/test_table_function.cc b/components/test/test_table_function.cc new file mode 100644 index 00000000000..b006b9636b1 --- /dev/null +++ b/components/test/test_table_function.cc @@ -0,0 +1,164 @@ +/* Copyright (c) 2025, Oracle and/or its affiliates. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License, version 2.0, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License, version 2.0, for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + +/** + @file + Demo component that registers a table function called + @c demo_tf_all_types which produces 3 rows with one column for each + major SQL data type. + + Usage: + + INSTALL COMPONENT "file://component_test_table_function"; + SELECT * FROM demo_tf_all_types() AS t; +*/ + +#include +#include +#include + +#include +#include + +REQUIRES_SERVICE_PLACEHOLDER(table_function_registration); +REQUIRES_SERVICE_PLACEHOLDER(table_function_row_writer); + +/* ------------------------------------------------------------------------- */ +/* Schema */ +/* ------------------------------------------------------------------------- */ + +/* + Static schema description. Returned to the server during the describe + callback. We deliberately use a wide variety of types to exercise the + framework end to end. +*/ +static const Tf_column_def k_columns[] = { + /* name type length decimals not_null unsigned */ + { "c_tinyint", MYSQL_TYPE_TINY, 0, 0, 0, 0 }, + { "c_smallint", MYSQL_TYPE_SHORT, 0, 0, 0, 0 }, + { "c_int", MYSQL_TYPE_LONG, 0, 0, 0, 0 }, + { "c_bigint", MYSQL_TYPE_LONGLONG, 0, 0, 0, 0 }, + { "c_bigint_u", MYSQL_TYPE_LONGLONG, 0, 0, 0, 1 }, + { "c_float", MYSQL_TYPE_FLOAT, 12, 4, 0, 0 }, + { "c_double", MYSQL_TYPE_DOUBLE, 22, 6, 0, 0 }, + { "c_varchar", MYSQL_TYPE_VARCHAR, 64, 0, 0, 0 }, +}; +static constexpr unsigned int k_n_columns = + sizeof(k_columns) / sizeof(k_columns[0]); + +/* ------------------------------------------------------------------------- */ +/* Callbacks */ +/* ------------------------------------------------------------------------- */ + +namespace { + +int demo_describe(void * /*thd_handle*/, unsigned int /*n_args*/, + const Tf_column_def **out_columns, + unsigned int *out_n_columns, void **out_state) { + *out_columns = k_columns; + *out_n_columns = k_n_columns; + *out_state = nullptr; // stateless + return 0; +} + +int demo_fill(void * /*state*/, Tf_row_writer_handle *w) { + /* + Produce exactly 3 rows. Row index i in [1..3]; values are derived + from i so the test result is fully deterministic. + */ + for (int i = 1; i <= 3; ++i) { + char buf[64]; + std::snprintf(buf, sizeof(buf), "row #%d", i); + + if (mysql_service_table_function_row_writer->set_longlong( + w, 0, /*c_tinyint*/ i, 0)) + return 1; + if (mysql_service_table_function_row_writer->set_longlong( + w, 1, /*c_smallint*/ 100 * i, 0)) + return 1; + if (mysql_service_table_function_row_writer->set_longlong( + w, 2, /*c_int*/ 10000 * i, 0)) + return 1; + if (mysql_service_table_function_row_writer->set_longlong( + w, 3, /*c_bigint*/ 1000000LL * i, 0)) + return 1; + if (mysql_service_table_function_row_writer->set_longlong( + w, 4, /*c_bigint_u*/ 9000000000LL + i, 1)) + return 1; + if (mysql_service_table_function_row_writer->set_double( + w, 5, /*c_float*/ static_cast(i) * 0.5)) + return 1; + if (mysql_service_table_function_row_writer->set_double( + w, 6, /*c_double*/ 3.14159 * i)) + return 1; + if (mysql_service_table_function_row_writer->set_string( + w, 7, /*c_varchar*/ buf, + static_cast(std::strlen(buf)))) + return 1; + + if (mysql_service_table_function_row_writer->emit_row(w)) return 1; + } + return 0; +} + +void demo_cleanup(void * /*state*/) { /* nothing to free */ +} + +} // namespace + +/* ------------------------------------------------------------------------- */ +/* Component init / deinit */ +/* ------------------------------------------------------------------------- */ + +static const char *k_func_name = "demo_tf_all_types"; +static bool g_registered = false; + +static mysql_service_status_t init() { + if (mysql_service_table_function_registration->register_table_function( + k_func_name, demo_describe, demo_fill, demo_cleanup)) { + return 1; + } + g_registered = true; + return 0; +} + +static mysql_service_status_t deinit() { + if (!g_registered) return 0; + int was_present = 0; + if (mysql_service_table_function_registration->unregister_table_function( + k_func_name, &was_present)) { + return 1; + } + g_registered = false; + return 0; +} + +BEGIN_COMPONENT_PROVIDES(test_table_function) +END_COMPONENT_PROVIDES(); + +BEGIN_COMPONENT_REQUIRES(test_table_function) +REQUIRES_SERVICE(table_function_registration), + REQUIRES_SERVICE(table_function_row_writer), END_COMPONENT_REQUIRES(); + +BEGIN_COMPONENT_METADATA(test_table_function) +METADATA("mysql.author", "Oracle Corporation"), + METADATA("mysql.license", "GPL"), + METADATA("test_table_function", "1"), END_COMPONENT_METADATA(); + +DECLARE_COMPONENT(test_table_function, "mysql:test_table_function") +init, deinit END_DECLARE_COMPONENT(); + +DECLARE_LIBRARY_COMPONENTS &COMPONENT_REF(test_table_function) + END_DECLARE_LIBRARY_COMPONENTS diff --git a/include/mysql/components/services/table_function_registration.h b/include/mysql/components/services/table_function_registration.h new file mode 100644 index 00000000000..7e2cd27a0d5 --- /dev/null +++ b/include/mysql/components/services/table_function_registration.h @@ -0,0 +1,184 @@ +/* Copyright (c) 2025, Oracle and/or its affiliates. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License, version 2.0, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License, version 2.0, for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + +#ifndef TABLE_FUNCTION_REGISTRATION_SERVICE_H +#define TABLE_FUNCTION_REGISTRATION_SERVICE_H + +#include +#include + +/** + @file + Component service for registering custom SQL table functions. + + A table function can be invoked in the FROM clause of a SELECT + statement just like JSON_TABLE, e.g.: + + SELECT * FROM my_func(arg1, arg2) AS t; + + A registered table function declares a fixed list of output columns + via @c Tf_column_def, and produces rows on demand through a + @c Tf_fill_func callback. +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + Description of one output column produced by a table function. +*/ +struct Tf_column_def { + /** Column name (UTF-8, NUL-terminated). */ + const char *name; + /** SQL type, e.g. MYSQL_TYPE_LONGLONG. */ + enum enum_field_types type; + /** Display length (for character / decimal columns). 0 = use default. */ + unsigned int length; + /** Number of decimals (for decimal / float). */ + unsigned int decimals; + /** Non-zero if the column may not be NULL. */ + int not_null; + /** Non-zero if the column is unsigned. */ + int is_unsigned; +}; +typedef struct Tf_column_def Tf_column_def; + +/** + Opaque row-writer handle. Passed to the fill callback by the server. + The component must use the @c table_function_row_writer service + to populate column values and emit rows. +*/ +typedef struct Tf_row_writer_handle Tf_row_writer_handle; + +/* ------------------------------------------------------------- */ +/* Callback signatures */ +/* ------------------------------------------------------------- */ + +/** + Describe the output schema. + + Called once per query at parse-time, after argument @c Item* nodes have + been resolved. The component must return a stable pointer to an array + of @c Tf_column_def with @c *out_n_columns elements. The array memory + is owned by the component and must remain valid until @c cleanup_func + is called. + + The component may also stash a per-statement context in @c *out_state; + it will be passed back to subsequent callbacks. + + @param thd_handle opaque THD pointer (currently unused by the demo) + @param n_args number of arguments passed to the table function + @param out_columns [out] pointer to component-owned column array + @param out_n_columns [out] number of columns + @param out_state [out] optional component-owned state pointer + + @retval 0 success + @retval !=0 failure (the component should raise an error via + mysql_runtime_error service before returning) +*/ +typedef int (*Tf_describe_func)(void *thd_handle, unsigned int n_args, + const Tf_column_def **out_columns, + unsigned int *out_n_columns, void **out_state); + +/** + Produce all rows of the table function. + + The callback is invoked exactly once per execution. It must call the + @c table_function_row_writer service to set field values and emit + rows. + + @param state the state set by the describe callback + @param writer opaque row-writer handle to be passed to row-writer + service methods + + @retval 0 success + @retval !=0 failure +*/ +typedef int (*Tf_fill_func)(void *state, Tf_row_writer_handle *writer); + +/** + Release per-statement state. Called after each query execution. +*/ +typedef void (*Tf_cleanup_func)(void *state); + +/* ------------------------------------------------------------- */ +/* Service definitions */ +/* ------------------------------------------------------------- */ + +/** + Service for registering / unregistering table functions by name. +*/ +BEGIN_SERVICE_DEFINITION(table_function_registration) + +/** + Register a new table function. + + @param name Function name; must be unique + @param describe Schema-description callback (required) + @param fill Row-producing callback (required) + @param cleanup State cleanup callback (may be NULL) + + @retval false success + @retval true failure (already registered or invalid arguments) +*/ +DECLARE_BOOL_METHOD(register_table_function, + (const char *name, Tf_describe_func describe, + Tf_fill_func fill, Tf_cleanup_func cleanup)); + +/** + Unregister a previously-registered table function. + + @param name Function name + @param[out] was_present set to non-zero if the function was registered +*/ +DECLARE_BOOL_METHOD(unregister_table_function, + (const char *name, int *was_present)); + +END_SERVICE_DEFINITION(table_function_registration) + +/** + Helper service used by table-function components inside their + fill callback to populate output rows. + + All set_* methods write the value of the column at zero-based index + @c idx in the *current* row. Calling @c emit_row commits the current + row to the result table and resets all fields to NULL. +*/ +BEGIN_SERVICE_DEFINITION(table_function_row_writer) + +DECLARE_BOOL_METHOD(set_null, + (Tf_row_writer_handle *w, unsigned int idx)); + +DECLARE_BOOL_METHOD(set_longlong, + (Tf_row_writer_handle *w, unsigned int idx, + long long value, int is_unsigned)); + +DECLARE_BOOL_METHOD(set_double, + (Tf_row_writer_handle *w, unsigned int idx, double value)); + +DECLARE_BOOL_METHOD(set_string, + (Tf_row_writer_handle *w, unsigned int idx, + const char *str, unsigned int length)); + +DECLARE_BOOL_METHOD(emit_row, (Tf_row_writer_handle *w)); + +END_SERVICE_DEFINITION(table_function_row_writer) + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* TABLE_FUNCTION_REGISTRATION_SERVICE_H */ diff --git a/mysql-test/include/plugin.defs b/mysql-test/include/plugin.defs index 11a5da62378..a608efee1ce 100644 --- a/mysql-test/include/plugin.defs +++ b/mysql-test/include/plugin.defs @@ -115,6 +115,7 @@ pfs_example_plugin_employee plugin_output_directory no PFS_EXAMPLE_PLUGIN_E component_pfs_example_component_population plugin_output_directory no PFS_EXAMPLE_COMPONENT_POPULATION component_test_udf_registration plugin_output_directory no TEST_UDF_REGISTRATION +component_test_table_function plugin_output_directory no TEST_TABLE_FUNCTION component_test_udf_services plugin_output_directory no TEST_UDF_SERVICES component_test_udf_services component_audit_api_message_emit plugin_output_directory no AUDIT_API_MESSAGE_EMIT component_audit_api_message_emit component_udf_reg_3_func plugin_output_directory no UDF_REG_3_FUNC component_udf_reg_3_func diff --git a/mysql-test/suite/test_table_function/inc/have_test_table_function_component.inc b/mysql-test/suite/test_table_function/inc/have_test_table_function_component.inc new file mode 100644 index 00000000000..80540fbcc57 --- /dev/null +++ b/mysql-test/suite/test_table_function/inc/have_test_table_function_component.inc @@ -0,0 +1,17 @@ +disable_query_log; + +# +# Check if the variable TEST_TABLE_FUNCTION is set +# +if (!$TEST_TABLE_FUNCTION) { + --skip component requires the environment variable \$TEST_TABLE_FUNCTION to be set (normally done by mtr), see the file plugin.defs +} + +# +## Check if --plugin-dir was setup for component_test +# +if (`SELECT CONCAT('--plugin-dir=', REPLACE(@@plugin_dir, '\\\\', '/')) != '$TEST_TABLE_FUNCTION_OPT/'`) { + --skip component_test requires that --plugin-dir is set to the component_test dir (the .opt file does not contain \$TEST_TABLE_FUNCTION_OPT) +} + +enable_query_log; diff --git a/mysql-test/suite/test_table_function/r/table_function_advanced.result b/mysql-test/suite/test_table_function/r/table_function_advanced.result new file mode 100644 index 00000000000..c1774bec366 --- /dev/null +++ b/mysql-test/suite/test_table_function/r/table_function_advanced.result @@ -0,0 +1,105 @@ +# ==================================================================== +# Component-registered table function: advanced usage +# ==================================================================== +INSTALL COMPONENT "file://component_test_table_function"; +# +# 1. Used in a derived table / CTE. +# +SELECT d.c_int FROM (SELECT * FROM demo_tf_all_types() AS x) AS d ORDER BY d.c_int; +c_int +10000 +20000 +30000 +WITH cte AS (SELECT * FROM demo_tf_all_types() AS x) +SELECT cte.c_tinyint, cte.c_varchar FROM cte ORDER BY cte.c_tinyint; +c_tinyint c_varchar +1 row #1 +2 row #2 +3 row #3 +# +# 2. UNION with a regular relation. +# +CREATE TABLE u1 (a INT, b VARCHAR(64)); +INSERT INTO u1 VALUES (10, 'from-table'), (11, 'from-table'); +SELECT * FROM ( +SELECT t.c_tinyint AS a, t.c_varchar AS b FROM demo_tf_all_types() AS t +UNION ALL +SELECT a, b FROM u1 +) AS r ORDER BY a; +a b +1 row #1 +2 row #2 +3 row #3 +10 from-table +11 from-table +DROP TABLE u1; +# +# 3. Subquery in WHERE: tf on the outer side. +# +CREATE TABLE ids (i INT); +INSERT INTO ids VALUES (1), (3), (5); +SELECT t.c_tinyint, t.c_varchar +FROM demo_tf_all_types() AS t +WHERE t.c_tinyint IN (SELECT i FROM ids) +ORDER BY t.c_tinyint; +c_tinyint c_varchar +1 row #1 +3 row #3 +# Subquery in WHERE: tf on the inner side. +SELECT i FROM ids +WHERE i IN (SELECT t.c_tinyint FROM demo_tf_all_types() AS t) +ORDER BY i; +i +1 +3 +DROP TABLE ids; +# +# 4. Create a VIEW on top of the table function. +# +CREATE VIEW v_tf AS +SELECT t.c_tinyint AS k, t.c_varchar AS v FROM demo_tf_all_types() AS t; +SELECT * FROM v_tf ORDER BY k; +k v +1 row #1 +2 row #2 +3 row #3 +SELECT k FROM v_tf WHERE v LIKE 'row #%' ORDER BY k DESC; +k +3 +2 +1 +DROP VIEW v_tf; +# +# 5. Prepared statements should work and be re-executable. +# +PREPARE s FROM 'SELECT COUNT(*) AS n FROM demo_tf_all_types() AS t'; +EXECUTE s; +n +3 +EXECUTE s; +n +3 +DEALLOCATE PREPARE s; +PREPARE s2 FROM +'SELECT t.c_int FROM demo_tf_all_types() AS t WHERE t.c_tinyint = ?'; +SET @v = 2; +EXECUTE s2 USING @v; +c_int +20000 +SET @v = 3; +EXECUTE s2 USING @v; +c_int +30000 +DEALLOCATE PREPARE s2; +# +# 6. Self-join of the table function (two independent invocations). +# +SELECT a.c_tinyint, b.c_tinyint +FROM demo_tf_all_types() AS a JOIN demo_tf_all_types() AS b +ON a.c_tinyint = b.c_tinyint +ORDER BY a.c_tinyint; +c_tinyint c_tinyint +1 1 +2 2 +3 3 +UNINSTALL COMPONENT "file://component_test_table_function"; diff --git a/mysql-test/suite/test_table_function/r/table_function_basic.result b/mysql-test/suite/test_table_function/r/table_function_basic.result new file mode 100644 index 00000000000..fa88b59c0be --- /dev/null +++ b/mysql-test/suite/test_table_function/r/table_function_basic.result @@ -0,0 +1,91 @@ +# ==================================================================== +# Component-registered table function: basic round-trip +# ==================================================================== +# +# 1. Before INSTALL: the function is not known. +# +SELECT * FROM demo_tf_all_types() AS t; +ERROR 42000: TABLE FUNCTION demo_tf_all_types does not exist +INSTALL COMPONENT "file://component_test_table_function"; +# +# 2. SELECT * returns 3 rows with one column per major SQL type. +# +SELECT * FROM demo_tf_all_types() AS t; +c_tinyint c_smallint c_int c_bigint c_bigint_u c_float c_double c_varchar +1 100 10000 1000000 9000000001 0.5000 3.141590 row #1 +2 200 20000 2000000 9000000002 1.0000 6.283180 row #2 +3 300 30000 3000000 9000000003 1.5000 9.424770 row #3 +# +# 3. Column metadata is exposed correctly to the protocol. +# +SELECT * FROM demo_tf_all_types() AS t LIMIT 1; +Catalog Database Table Table_alias Column Column_alias Type Length Max length Is_null Flags Decimals Charsetnr +def demo_tf_all_types t c_tinyint c_tinyint 1 4 1 Y 32768 0 63 +def demo_tf_all_types t c_smallint c_smallint 2 6 3 Y 32768 0 63 +def demo_tf_all_types t c_int c_int 3 11 5 Y 32768 0 63 +def demo_tf_all_types t c_bigint c_bigint 8 20 7 Y 32768 0 63 +def demo_tf_all_types t c_bigint_u c_bigint_u 8 20 10 Y 32800 0 63 +def demo_tf_all_types t c_float c_float 4 12 6 Y 32768 4 63 +def demo_tf_all_types t c_double c_double 5 22 8 Y 32768 6 63 +def demo_tf_all_types t c_varchar c_varchar 253 256 6 Y 0 0 255 +c_tinyint c_smallint c_int c_bigint c_bigint_u c_float c_double c_varchar +1 100 10000 1000000 9000000001 0.5000 3.141590 row #1 +# +# 4. Aggregate / projection / WHERE through the table function. +# +SELECT COUNT(*) AS n_rows FROM demo_tf_all_types() AS t; +n_rows +3 +SELECT t.c_int, t.c_varchar FROM demo_tf_all_types() AS t WHERE t.c_tinyint > 1; +c_int c_varchar +20000 row #2 +30000 row #3 +SELECT SUM(t.c_int) AS total FROM demo_tf_all_types() AS t; +total +60000 +# +# 5. JOIN with a regular table. Each row of demo_tf_all_types should +# be produced exactly once per outer-side iteration (i.e. the +# materialized result table must be reset between executions). +# +CREATE TABLE drv (id INT); +INSERT INTO drv VALUES (1), (2), (3), (4); +SELECT drv.id, t.c_varchar +FROM drv JOIN demo_tf_all_types() AS t ON drv.id = t.c_tinyint +ORDER BY drv.id; +id c_varchar +1 row #1 +2 row #2 +3 row #3 +# Repeat the JOIN to confirm the fill is idempotent across executions. +SELECT drv.id, t.c_varchar +FROM drv JOIN demo_tf_all_types() AS t ON drv.id = t.c_tinyint +ORDER BY drv.id; +id c_varchar +1 row #1 +2 row #2 +3 row #3 +DROP TABLE drv; +# +# 6. ORDER BY / LIMIT directly on the table function. +# +SELECT t.c_tinyint +FROM demo_tf_all_types() AS t +ORDER BY t.c_tinyint DESC +LIMIT 2; +c_tinyint +3 +2 +# +# 7. EXPLAIN classifies the source as a function table. +# +EXPLAIN FORMAT=TREE SELECT * FROM demo_tf_all_types() AS t; +EXPLAIN +-> Materialize table function + +UNINSTALL COMPONENT "file://component_test_table_function"; +# +# 8. After UNINSTALL the function is gone again. +# +SELECT * FROM demo_tf_all_types() AS t; +ERROR 42000: TABLE FUNCTION demo_tf_all_types does not exist diff --git a/mysql-test/suite/test_table_function/r/table_function_errors.result b/mysql-test/suite/test_table_function/r/table_function_errors.result new file mode 100644 index 00000000000..751a1c613a8 --- /dev/null +++ b/mysql-test/suite/test_table_function/r/table_function_errors.result @@ -0,0 +1,45 @@ +# ==================================================================== +# Component-registered table function: error paths +# ==================================================================== +# +# 1. Calling an unknown table function before INSTALL. +# +SELECT * FROM nope_tf() AS t; +ERROR 42000: TABLE FUNCTION nope_tf does not exist +INSTALL COMPONENT "file://component_test_table_function"; +# +# 2. Even when the component is loaded, unrelated names still fail. +# +SELECT * FROM nope_tf() AS t1; +ERROR 42000: TABLE FUNCTION nope_tf does not exist +# +# 3. Like JSON_TABLE, an alias is mandatory for a table function. +# +SELECT * FROM demo_tf_all_types(); +ERROR 42000: Every table function must have an alias +# +# 4. Re-installing the same component must fail (already loaded). +# +INSTALL COMPONENT "file://component_test_table_function"; +ERROR HY000: Cannot load component from specified URN: 'file://component_test_table_function'. +# +# 5. A registered table function cannot be invoked as a scalar. +# +SELECT demo_tf_all_types(); +ERROR 42000: FUNCTION test.demo_tf_all_types does not exist +# +# 6. Cannot reference the table function name as if it were a table. +# +SELECT * FROM demo_tf_all_types AS t; +ERROR 42S02: Table 'test.demo_tf_all_types' doesn't exist +UNINSTALL COMPONENT "file://component_test_table_function"; +# +# 7. After UNINSTALL the function is gone. +# +SELECT * FROM demo_tf_all_types() AS t; +ERROR 42000: TABLE FUNCTION demo_tf_all_types does not exist +# +# 8. Repeated UNINSTALL must fail cleanly. +# +UNINSTALL COMPONENT "file://component_test_table_function"; +ERROR HY000: Component specified by URN 'file://component_test_table_function' to unload has not been loaded before. diff --git a/mysql-test/suite/test_table_function/r/table_function_unload_in_use.result b/mysql-test/suite/test_table_function/r/table_function_unload_in_use.result new file mode 100644 index 00000000000..2ae952108ce --- /dev/null +++ b/mysql-test/suite/test_table_function/r/table_function_unload_in_use.result @@ -0,0 +1,43 @@ +# ==================================================================== +# Component-registered table function: unload-while-in-use handshake +# ==================================================================== +# +# Verifies that UNINSTALL COMPONENT refuses to unload a component +# whose registered table function is currently being executed by +# another session, mirroring the udf_registration handshake. +# +INSTALL COMPONENT "file://component_test_table_function"; +# +# 1. Park the producer in fill_result_table() so the descriptor +# has an outstanding borrow. +# +SET DEBUG_SYNC='table_function_dynamic_before_fill SIGNAL parked WAIT_FOR go TIMEOUT 20'; +SELECT COUNT(*) AS n FROM demo_tf_all_types() AS t; +# +# 2. From a second session, observe the parked state and try to +# unload the component. This must fail because the function +# is in use. +# +SET DEBUG_SYNC='now WAIT_FOR parked'; +UNINSTALL COMPONENT "file://component_test_table_function"; +ERROR HY000: De-initialization method provided by component 'mysql:test_table_function' failed. +# +# 3. The function is still callable - the previous UNINSTALL did +# not succeed, so the component is still loaded. +# +SELECT t.c_tinyint FROM demo_tf_all_types() AS t WHERE t.c_tinyint = 1; +c_tinyint +1 +# +# 4. Release the producer and reap its result. +# +SET DEBUG_SYNC='now SIGNAL go'; +n +3 +# +# 5. Once no borrow is outstanding, UNINSTALL succeeds. +# +UNINSTALL COMPONENT "file://component_test_table_function"; +SELECT * FROM demo_tf_all_types() AS t; +ERROR 42000: TABLE FUNCTION demo_tf_all_types does not exist +SET DEBUG_SYNC='RESET'; diff --git a/mysql-test/suite/test_table_function/t/table_function_advanced-master.opt b/mysql-test/suite/test_table_function/t/table_function_advanced-master.opt new file mode 100644 index 00000000000..b54d9909497 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_advanced-master.opt @@ -0,0 +1 @@ +$TEST_TABLE_FUNCTION_OPT diff --git a/mysql-test/suite/test_table_function/t/table_function_advanced.test b/mysql-test/suite/test_table_function/t/table_function_advanced.test new file mode 100644 index 00000000000..b2adbce8563 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_advanced.test @@ -0,0 +1,84 @@ +--source ../inc/have_test_table_function_component.inc + +--echo # ==================================================================== +--echo # Component-registered table function: advanced usage +--echo # ==================================================================== + +INSTALL COMPONENT "file://component_test_table_function"; + +--echo # +--echo # 1. Used in a derived table / CTE. +--echo # +SELECT d.c_int FROM (SELECT * FROM demo_tf_all_types() AS x) AS d ORDER BY d.c_int; + +WITH cte AS (SELECT * FROM demo_tf_all_types() AS x) +SELECT cte.c_tinyint, cte.c_varchar FROM cte ORDER BY cte.c_tinyint; + +--echo # +--echo # 2. UNION with a regular relation. +--echo # +CREATE TABLE u1 (a INT, b VARCHAR(64)); +INSERT INTO u1 VALUES (10, 'from-table'), (11, 'from-table'); + +SELECT * FROM ( + SELECT t.c_tinyint AS a, t.c_varchar AS b FROM demo_tf_all_types() AS t + UNION ALL + SELECT a, b FROM u1 +) AS r ORDER BY a; + +DROP TABLE u1; + +--echo # +--echo # 3. Subquery in WHERE: tf on the outer side. +--echo # +CREATE TABLE ids (i INT); +INSERT INTO ids VALUES (1), (3), (5); + +SELECT t.c_tinyint, t.c_varchar + FROM demo_tf_all_types() AS t + WHERE t.c_tinyint IN (SELECT i FROM ids) + ORDER BY t.c_tinyint; + +--echo # Subquery in WHERE: tf on the inner side. +SELECT i FROM ids + WHERE i IN (SELECT t.c_tinyint FROM demo_tf_all_types() AS t) + ORDER BY i; + +DROP TABLE ids; + +--echo # +--echo # 4. Create a VIEW on top of the table function. +--echo # +CREATE VIEW v_tf AS + SELECT t.c_tinyint AS k, t.c_varchar AS v FROM demo_tf_all_types() AS t; + +SELECT * FROM v_tf ORDER BY k; +SELECT k FROM v_tf WHERE v LIKE 'row #%' ORDER BY k DESC; + +DROP VIEW v_tf; + +--echo # +--echo # 5. Prepared statements should work and be re-executable. +--echo # +PREPARE s FROM 'SELECT COUNT(*) AS n FROM demo_tf_all_types() AS t'; +EXECUTE s; +EXECUTE s; +DEALLOCATE PREPARE s; + +PREPARE s2 FROM + 'SELECT t.c_int FROM demo_tf_all_types() AS t WHERE t.c_tinyint = ?'; +SET @v = 2; +EXECUTE s2 USING @v; +SET @v = 3; +EXECUTE s2 USING @v; +DEALLOCATE PREPARE s2; + +--echo # +--echo # 6. Self-join of the table function (two independent invocations). +--echo # +SELECT a.c_tinyint, b.c_tinyint + FROM demo_tf_all_types() AS a JOIN demo_tf_all_types() AS b + ON a.c_tinyint = b.c_tinyint + ORDER BY a.c_tinyint; + +UNINSTALL COMPONENT "file://component_test_table_function"; diff --git a/mysql-test/suite/test_table_function/t/table_function_basic-master.opt b/mysql-test/suite/test_table_function/t/table_function_basic-master.opt new file mode 100644 index 00000000000..b54d9909497 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_basic-master.opt @@ -0,0 +1 @@ +$TEST_TABLE_FUNCTION_OPT diff --git a/mysql-test/suite/test_table_function/t/table_function_basic.test b/mysql-test/suite/test_table_function/t/table_function_basic.test new file mode 100644 index 00000000000..09fa88d5b72 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_basic.test @@ -0,0 +1,75 @@ +--source ../inc/have_test_table_function_component.inc + +--echo # ==================================================================== +--echo # Component-registered table function: basic round-trip +--echo # ==================================================================== + +--echo # +--echo # 1. Before INSTALL: the function is not known. +--echo # +--error ER_SP_DOES_NOT_EXIST +SELECT * FROM demo_tf_all_types() AS t; + +INSTALL COMPONENT "file://component_test_table_function"; + +--echo # +--echo # 2. SELECT * returns 3 rows with one column per major SQL type. +--echo # +SELECT * FROM demo_tf_all_types() AS t; + +--echo # +--echo # 3. Column metadata is exposed correctly to the protocol. +--echo # +--disable_ps_protocol +--enable_metadata +SELECT * FROM demo_tf_all_types() AS t LIMIT 1; +--disable_metadata +--enable_ps_protocol + +--echo # +--echo # 4. Aggregate / projection / WHERE through the table function. +--echo # +SELECT COUNT(*) AS n_rows FROM demo_tf_all_types() AS t; +SELECT t.c_int, t.c_varchar FROM demo_tf_all_types() AS t WHERE t.c_tinyint > 1; +SELECT SUM(t.c_int) AS total FROM demo_tf_all_types() AS t; + +--echo # +--echo # 5. JOIN with a regular table. Each row of demo_tf_all_types should +--echo # be produced exactly once per outer-side iteration (i.e. the +--echo # materialized result table must be reset between executions). +--echo # +CREATE TABLE drv (id INT); +INSERT INTO drv VALUES (1), (2), (3), (4); + +SELECT drv.id, t.c_varchar + FROM drv JOIN demo_tf_all_types() AS t ON drv.id = t.c_tinyint + ORDER BY drv.id; + +--echo # Repeat the JOIN to confirm the fill is idempotent across executions. +SELECT drv.id, t.c_varchar + FROM drv JOIN demo_tf_all_types() AS t ON drv.id = t.c_tinyint + ORDER BY drv.id; + +DROP TABLE drv; + +--echo # +--echo # 6. ORDER BY / LIMIT directly on the table function. +--echo # +SELECT t.c_tinyint + FROM demo_tf_all_types() AS t + ORDER BY t.c_tinyint DESC + LIMIT 2; + +--echo # +--echo # 7. EXPLAIN classifies the source as a function table. +--echo # +--replace_regex / \(cost=.*// /\d+\.\d+\s+rows/X.XX rows/ +EXPLAIN FORMAT=TREE SELECT * FROM demo_tf_all_types() AS t; + +UNINSTALL COMPONENT "file://component_test_table_function"; + +--echo # +--echo # 8. After UNINSTALL the function is gone again. +--echo # +--error ER_SP_DOES_NOT_EXIST +SELECT * FROM demo_tf_all_types() AS t; diff --git a/mysql-test/suite/test_table_function/t/table_function_errors-master.opt b/mysql-test/suite/test_table_function/t/table_function_errors-master.opt new file mode 100644 index 00000000000..b54d9909497 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_errors-master.opt @@ -0,0 +1 @@ +$TEST_TABLE_FUNCTION_OPT diff --git a/mysql-test/suite/test_table_function/t/table_function_errors.test b/mysql-test/suite/test_table_function/t/table_function_errors.test new file mode 100644 index 00000000000..e0b88d6bbb7 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_errors.test @@ -0,0 +1,57 @@ +--source ../inc/have_test_table_function_component.inc + +--echo # ==================================================================== +--echo # Component-registered table function: error paths +--echo # ==================================================================== + +--echo # +--echo # 1. Calling an unknown table function before INSTALL. +--echo # +--error ER_SP_DOES_NOT_EXIST +SELECT * FROM nope_tf() AS t; + +INSTALL COMPONENT "file://component_test_table_function"; + +--echo # +--echo # 2. Even when the component is loaded, unrelated names still fail. +--echo # +--error ER_SP_DOES_NOT_EXIST +SELECT * FROM nope_tf() AS t1; + +--echo # +--echo # 3. Like JSON_TABLE, an alias is mandatory for a table function. +--echo # +--error ER_TF_MUST_HAVE_ALIAS +SELECT * FROM demo_tf_all_types(); + +--echo # +--echo # 4. Re-installing the same component must fail (already loaded). +--echo # +--error ER_COMPONENTS_CANT_LOAD +INSTALL COMPONENT "file://component_test_table_function"; + +--echo # +--echo # 5. A registered table function cannot be invoked as a scalar. +--echo # +--error ER_SP_DOES_NOT_EXIST +SELECT demo_tf_all_types(); + +--echo # +--echo # 6. Cannot reference the table function name as if it were a table. +--echo # +--error ER_NO_SUCH_TABLE +SELECT * FROM demo_tf_all_types AS t; + +UNINSTALL COMPONENT "file://component_test_table_function"; + +--echo # +--echo # 7. After UNINSTALL the function is gone. +--echo # +--error ER_SP_DOES_NOT_EXIST +SELECT * FROM demo_tf_all_types() AS t; + +--echo # +--echo # 8. Repeated UNINSTALL must fail cleanly. +--echo # +--error ER_COMPONENTS_UNLOAD_NOT_LOADED +UNINSTALL COMPONENT "file://component_test_table_function"; diff --git a/mysql-test/suite/test_table_function/t/table_function_unload_in_use-master.opt b/mysql-test/suite/test_table_function/t/table_function_unload_in_use-master.opt new file mode 100644 index 00000000000..b54d9909497 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_unload_in_use-master.opt @@ -0,0 +1 @@ +$TEST_TABLE_FUNCTION_OPT diff --git a/mysql-test/suite/test_table_function/t/table_function_unload_in_use.test b/mysql-test/suite/test_table_function/t/table_function_unload_in_use.test new file mode 100644 index 00000000000..18455d73241 --- /dev/null +++ b/mysql-test/suite/test_table_function/t/table_function_unload_in_use.test @@ -0,0 +1,61 @@ +--source include/have_debug_sync.inc +--source include/count_sessions.inc +--source ../inc/have_test_table_function_component.inc + +--echo # ==================================================================== +--echo # Component-registered table function: unload-while-in-use handshake +--echo # ==================================================================== +--echo # +--echo # Verifies that UNINSTALL COMPONENT refuses to unload a component +--echo # whose registered table function is currently being executed by +--echo # another session, mirroring the udf_registration handshake. +--echo # + +INSTALL COMPONENT "file://component_test_table_function"; + +connect(c1,localhost,root,,test); +connection default; + +--echo # +--echo # 1. Park the producer in fill_result_table() so the descriptor +--echo # has an outstanding borrow. +--echo # +SET DEBUG_SYNC='table_function_dynamic_before_fill SIGNAL parked WAIT_FOR go TIMEOUT 20'; +--send SELECT COUNT(*) AS n FROM demo_tf_all_types() AS t + +--echo # +--echo # 2. From a second session, observe the parked state and try to +--echo # unload the component. This must fail because the function +--echo # is in use. +--echo # +connection c1; +SET DEBUG_SYNC='now WAIT_FOR parked'; + +--error ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE +UNINSTALL COMPONENT "file://component_test_table_function"; + +--echo # +--echo # 3. The function is still callable - the previous UNINSTALL did +--echo # not succeed, so the component is still loaded. +--echo # +SELECT t.c_tinyint FROM demo_tf_all_types() AS t WHERE t.c_tinyint = 1; + +--echo # +--echo # 4. Release the producer and reap its result. +--echo # +SET DEBUG_SYNC='now SIGNAL go'; +connection default; +--reap + +--echo # +--echo # 5. Once no borrow is outstanding, UNINSTALL succeeds. +--echo # +UNINSTALL COMPONENT "file://component_test_table_function"; + +--error ER_SP_DOES_NOT_EXIST +SELECT * FROM demo_tf_all_types() AS t; + +SET DEBUG_SYNC='RESET'; +disconnect c1; + +--source include/wait_until_count_sessions.inc diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 1dcbd25df0d..d3fb67ad9c3 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -635,6 +635,7 @@ SET(SQL_SHARED_SOURCES table.cc table_cache.cc table_function.cc + table_function_dynamic.cc table_trigger_dispatcher.cc tc_log.cc thr_malloc.cc diff --git a/sql/parse_tree_nodes.cc b/sql/parse_tree_nodes.cc index 1f1baa23013..828819addbf 100644 --- a/sql/parse_tree_nodes.cc +++ b/sql/parse_tree_nodes.cc @@ -97,6 +97,7 @@ #include "sql/strfunc.h" #include "sql/system_variables.h" #include "sql/table_function.h" +#include "sql/table_function_dynamic.h" #include "sql/thr_malloc.h" #include "sql/trigger_def.h" #include "sql/window.h" // Window @@ -1395,6 +1396,52 @@ bool PT_table_factor_function::do_contextualize(Parse_context *pc) { return false; } +bool PT_table_factor_dynamic_function::do_contextualize(Parse_context *pc) { + if (super::do_contextualize(pc)) return true; + + /* Resolve the function name in the global table-function registry. */ + const Tf_descriptor *desc = Tf_registry::instance()->find(m_name.str); + if (desc == nullptr) { + /* + Reuse a generic "function does not exist" error. We can't use + ER_SP_DOES_NOT_EXIST verbatim (it expects "FUNCTION name"), so we + fall back to ER_UNKNOWN_TABLE which is suitable for unknown table + sources. + */ + my_error(ER_SP_DOES_NOT_EXIST, MYF(0), "TABLE FUNCTION", m_name.str); + return true; + } + + /* Contextualize argument expressions. */ + if (m_args != nullptr && m_args->contextualize(pc)) return true; + + auto *args = new (pc->mem_root) mem_root_deque(pc->mem_root); + if (args == nullptr) return true; + if (m_args != nullptr) { + for (Item *it : m_args->value) args->push_back(it); + } + + auto *tf = + new (pc->mem_root) Table_function_dynamic(pc->thd, m_table_alias.str, + desc, args); + if (tf == nullptr) return true; + + LEX_CSTRING alias; + alias.length = strlen(tf->func_name()); + alias.str = sql_strmake(tf->func_name(), alias.length); + if (alias.str == nullptr) return true; + + auto *ti = new (pc->mem_root) Table_ident(alias, tf); + if (ti == nullptr) return true; + + m_table_ref = pc->select->add_table_to_list(pc->thd, ti, m_table_alias.str, 0, + TL_READ, MDL_SHARED_READ); + if (m_table_ref == nullptr || pc->select->add_joined_table(m_table_ref)) + return true; + + return false; +} + PT_derived_table::PT_derived_table(const POS &pos, bool lateral, PT_subquery *subquery, const LEX_CSTRING &table_alias, diff --git a/sql/parse_tree_nodes.h b/sql/parse_tree_nodes.h index 26772bc23d2..09e486539e6 100644 --- a/sql/parse_tree_nodes.h +++ b/sql/parse_tree_nodes.h @@ -526,6 +526,31 @@ class PT_table_factor_function : public PT_table_reference { const LEX_STRING m_table_alias; }; +/** + Parse tree node for a component-registered table function: + + name '(' opt_expr_list ')' opt_table_alias +*/ +class PT_table_factor_dynamic_function : public PT_table_reference { + typedef PT_table_reference super; + + public: + PT_table_factor_dynamic_function(const POS &pos, const LEX_STRING &name, + PT_item_list *args, + const LEX_STRING &table_alias) + : super(pos), + m_name(name), + m_args(args), + m_table_alias(table_alias) {} + + bool do_contextualize(Parse_context *pc) override; + + private: + const LEX_STRING m_name; + PT_item_list *m_args; ///< nullable + const LEX_STRING m_table_alias; +}; + class PT_table_reference_list_parens : public PT_table_reference { typedef PT_table_reference super; diff --git a/sql/server_component/CMakeLists.txt b/sql/server_component/CMakeLists.txt index f55aa48908d..54d145082a3 100644 --- a/sql/server_component/CMakeLists.txt +++ b/sql/server_component/CMakeLists.txt @@ -90,6 +90,7 @@ SET(MYSQL_SERVER_COMPONENT_SOURCES mysql_signal_handler_imp.cc applier_metrics_service_imp.cc mysql_library_imp.cc + table_function_registration_imp.cc ) # This static library is used to build mysqld binary and in some unit test cases diff --git a/sql/server_component/server_component.cc b/sql/server_component/server_component.cc index b4809af5edf..e7adf39179d 100644 --- a/sql/server_component/server_component.cc +++ b/sql/server_component/server_component.cc @@ -111,6 +111,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "sql/server_component/mysql_admin_session_imp.h" #include "sql/server_component/mysql_query_attributes_imp.h" #include "sql/udf_registration_imp.h" +#include "sql/server_component/table_function_registration_imp.h" #include "storage/perfschema/pfs.h" #include "storage/perfschema/pfs_plugin_table.h" #include "storage/perfschema/pfs_services.h" @@ -305,6 +306,18 @@ mysql_udf_metadata_imp::argument_get, mysql_udf_metadata_imp::result_get, mysql_udf_metadata_imp::argument_set, mysql_udf_metadata_imp::result_set END_SERVICE_IMPLEMENTATION(); +BEGIN_SERVICE_IMPLEMENTATION(mysql_server, table_function_registration) +mysql_table_function_registration_imp::register_table_function, + mysql_table_function_registration_imp::unregister_table_function + END_SERVICE_IMPLEMENTATION(); + +BEGIN_SERVICE_IMPLEMENTATION(mysql_server, table_function_row_writer) +mysql_table_function_row_writer_imp::set_null, + mysql_table_function_row_writer_imp::set_longlong, + mysql_table_function_row_writer_imp::set_double, + mysql_table_function_row_writer_imp::set_string, + mysql_table_function_row_writer_imp::emit_row END_SERVICE_IMPLEMENTATION(); + /* Here the new mysql_service_mysql_system_variable_reader->get() service cannot be used instead of get_variable because the below code is for the @@ -949,6 +962,8 @@ PROVIDES_SERVICE(mysql_server_path_filter, dynamic_loader_scheme_file), PROVIDES_SERVICE(mysql_server, udf_registration), PROVIDES_SERVICE(mysql_server, udf_registration_aggregate), PROVIDES_SERVICE(mysql_server, mysql_udf_metadata), + PROVIDES_SERVICE(mysql_server, table_function_registration), + PROVIDES_SERVICE(mysql_server, table_function_row_writer), PROVIDES_SERVICE(mysql_server, component_sys_variable_register), PROVIDES_SERVICE(mysql_server, component_sys_variable_unregister), PROVIDES_SERVICE(mysql_server, mysql_cond_v1), diff --git a/sql/server_component/table_function_registration_imp.cc b/sql/server_component/table_function_registration_imp.cc new file mode 100644 index 00000000000..b87ae1d7e54 --- /dev/null +++ b/sql/server_component/table_function_registration_imp.cc @@ -0,0 +1,132 @@ +/* Copyright (c) 2025, Oracle and/or its affiliates. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License, version 2.0, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License, version 2.0, for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + +#include "sql/server_component/table_function_registration_imp.h" + +#include + +#include "field_types.h" +#include "mysql/strings/m_ctype.h" +#include "sql/field.h" +#include "sql/sql_class.h" +#include "sql/table.h" +#include "sql/table_function_dynamic.h" +#include "sql_string.h" + +/* ------------------------------------------------------------------------- */ +/* Registration service */ +/* ------------------------------------------------------------------------- */ + +DEFINE_BOOL_METHOD(mysql_table_function_registration_imp::register_table_function, + (const char *name, Tf_describe_func describe, + Tf_fill_func fill, Tf_cleanup_func cleanup)) { + return Tf_registry::instance()->add(name, describe, fill, cleanup); +} + +DEFINE_BOOL_METHOD(mysql_table_function_registration_imp::unregister_table_function, + (const char *name, int *was_present)) { + return Tf_registry::instance()->remove(name, was_present); +} + +/* ------------------------------------------------------------------------- */ +/* Row writer helper service */ +/* ------------------------------------------------------------------------- */ + +/* + The row-writer "handle" is just a Table_function_dynamic pointer. We + manipulate fields of its result table directly; once a row has been + populated by the component the emit_row method commits it through the + base class write_row() (which handles in-memory -> on-disk overflow). +*/ +namespace { +inline Table_function_dynamic *as_tf(Tf_row_writer_handle *w) { + return reinterpret_cast(w); +} + +inline TABLE *result_table_of(Tf_row_writer_handle *w) { + return as_tf(w)->result_table(); +} + +inline Field *field_at(Tf_row_writer_handle *w, unsigned int idx) { + return as_tf(w)->get_field(idx); +} + +inline void reset_row(TABLE *t) { + if (t == nullptr) return; + for (uint i = 0; i < t->s->fields; ++i) { + Field *f = t->field[i]; + if (f->is_nullable()) + f->set_null(); + else + f->reset(); + } +} +} // namespace + +DEFINE_BOOL_METHOD(mysql_table_function_row_writer_imp::set_null, + (Tf_row_writer_handle * w, unsigned int idx)) { + Field *f = field_at(w, idx); + if (f == nullptr) return true; + if (!f->is_nullable()) return true; + f->set_null(); + return false; +} + +DEFINE_BOOL_METHOD(mysql_table_function_row_writer_imp::set_longlong, + (Tf_row_writer_handle * w, unsigned int idx, + long long value, int is_unsigned)) { + Field *f = field_at(w, idx); + if (f == nullptr) return true; + f->set_notnull(); + return f->store(static_cast(value), is_unsigned != 0) > 0; +} + +DEFINE_BOOL_METHOD(mysql_table_function_row_writer_imp::set_double, + (Tf_row_writer_handle * w, unsigned int idx, double value)) { + Field *f = field_at(w, idx); + if (f == nullptr) return true; + f->set_notnull(); + return f->store(value) > 0; +} + +DEFINE_BOOL_METHOD(mysql_table_function_row_writer_imp::set_string, + (Tf_row_writer_handle * w, unsigned int idx, + const char *str, unsigned int length)) { + Field *f = field_at(w, idx); + if (f == nullptr) return true; + f->set_notnull(); + /* + Use the field's own charset for storage; the component is expected + to provide UTF-8 data which matches the connection charset chosen + in Table_function_dynamic::init(). + */ + return f->store(str, length, f->charset()) > 0; +} + +DEFINE_BOOL_METHOD(mysql_table_function_row_writer_imp::emit_row, + (Tf_row_writer_handle * w)) { + Table_function_dynamic *tf = as_tf(w); + TABLE *t = result_table_of(w); + if (tf == nullptr || t == nullptr) return true; + + /* + Defer to the Table_function base class for the actual insert; that + method handles in-memory -> InnoDB overflow correctly. + */ + if (tf->emit_current_row()) return true; + /* Reset all columns to NULL/default for the next row. */ + reset_row(t); + return false; +} diff --git a/sql/server_component/table_function_registration_imp.h b/sql/server_component/table_function_registration_imp.h new file mode 100644 index 00000000000..c23ec181bc7 --- /dev/null +++ b/sql/server_component/table_function_registration_imp.h @@ -0,0 +1,62 @@ +/* Copyright (c) 2025, Oracle and/or its affiliates. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License, version 2.0, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License, version 2.0, for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + +#ifndef TABLE_FUNCTION_REGISTRATION_IMP_H +#define TABLE_FUNCTION_REGISTRATION_IMP_H + +#include +#include + +/** + A helper class for the implementation of the table_function_registration + and table_function_row_writer service implementations exposed by + mysql_server component. +*/ +class mysql_table_function_registration_imp { + public: + /* table_function_registration::register_table_function */ + static DEFINE_BOOL_METHOD(register_table_function, + (const char *name, Tf_describe_func describe, + Tf_fill_func fill, Tf_cleanup_func cleanup)); + + /* table_function_registration::unregister_table_function */ + static DEFINE_BOOL_METHOD(unregister_table_function, + (const char *name, int *was_present)); +}; + +/** + Implementation of the row-writer helper service used by table-function + components from inside their fill callback. +*/ +class mysql_table_function_row_writer_imp { + public: + static DEFINE_BOOL_METHOD(set_null, + (Tf_row_writer_handle * w, unsigned int idx)); + + static DEFINE_BOOL_METHOD(set_longlong, + (Tf_row_writer_handle * w, unsigned int idx, + long long value, int is_unsigned)); + + static DEFINE_BOOL_METHOD(set_double, (Tf_row_writer_handle * w, + unsigned int idx, double value)); + + static DEFINE_BOOL_METHOD(set_string, + (Tf_row_writer_handle * w, unsigned int idx, + const char *str, unsigned int length)); + + static DEFINE_BOOL_METHOD(emit_row, (Tf_row_writer_handle * w)); +}; + +#endif // TABLE_FUNCTION_REGISTRATION_IMP_H diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 54e361ac681..07e26d418f0 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -12370,6 +12370,19 @@ table_function: $$= NEW_PTN PT_table_factor_function(@$, $3, $5, $6, to_lex_string($8)); } + | IDENT_sys '(' opt_udf_expr_list ')' opt_table_alias + { + // Alias isn't optional for table functions, mirror JSON_TABLE. + if ($5 == NULL_CSTR) + { + my_message(ER_TF_MUST_HAVE_ALIAS, + ER_THD(YYTHD, ER_TF_MUST_HAVE_ALIAS), MYF(0)); + MYSQL_YYABORT; + } + + $$= NEW_PTN PT_table_factor_dynamic_function(@$, $1, $3, + to_lex_string($5)); + } ; columns_clause: diff --git a/sql/table_function_dynamic.cc b/sql/table_function_dynamic.cc new file mode 100644 index 00000000000..fcda902f7b9 --- /dev/null +++ b/sql/table_function_dynamic.cc @@ -0,0 +1,435 @@ +/* Copyright (c) 2025, Oracle and/or its affiliates. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License, version 2.0, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License, version 2.0, for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + +#include "sql/table_function_dynamic.h" + +#include +#include +#include +#include +#include + +#include "field_types.h" +#include "my_dbug.h" +#include "my_sys.h" +#include "mysql/psi/mysql_rwlock.h" +#include "mysql/strings/m_ctype.h" +#include "mysqld_error.h" +#include "sql/create_field.h" +#include "sql/current_thd.h" +#include "sql/debug_sync.h" +#include "sql/field.h" +#include "sql/item.h" +#include "sql/sql_class.h" +#include "sql/sql_lex.h" +#include "sql/table.h" +#include "sql_string.h" + +/* ========================================================================= */ +/* Tf_registry implementation */ +/* ========================================================================= */ + +struct Tf_registry::Impl { + mysql_rwlock_t lock; + /* + Owning map. We keep descriptors in unique_ptrs so the address of + a Tf_descriptor remains stable while it is borrowed by callers, + even if the map rehashes or the entry is later erased. + */ + std::unordered_map> map; + + Impl() { + /* + Use a non-instrumented rwlock; the registry is rarely contended and + we don't want to register a PSI key here for clarity. + */ + mysql_rwlock_init(PSI_NOT_INSTRUMENTED, &lock); + } + ~Impl() { mysql_rwlock_destroy(&lock); } +}; + +namespace { +std::atomic g_registry{nullptr}; +} + +void Tf_registry::init() { + Tf_registry *cur = g_registry.load(std::memory_order_acquire); + if (cur != nullptr) return; + auto *fresh = new Tf_registry(); + Tf_registry *expected = nullptr; + if (!g_registry.compare_exchange_strong(expected, fresh)) { + // Lost the race; somebody else created it. + delete fresh; + } +} + +void Tf_registry::destroy() { + Tf_registry *cur = g_registry.exchange(nullptr); + delete cur; +} + +Tf_registry *Tf_registry::instance() { + Tf_registry *cur = g_registry.load(std::memory_order_acquire); + if (cur == nullptr) { + init(); + cur = g_registry.load(std::memory_order_acquire); + } + return cur; +} + +Tf_registry::Tf_registry() : m_impl(new Impl()) {} +Tf_registry::~Tf_registry() { delete m_impl; } + +bool Tf_registry::add(const char *name, Tf_describe_func describe, + Tf_fill_func fill, Tf_cleanup_func cleanup) { + if (name == nullptr || describe == nullptr || fill == nullptr) return true; + + mysql_rwlock_wrlock(&m_impl->lock); + std::string key(name); + auto it = m_impl->map.find(key); + if (it != m_impl->map.end()) { + mysql_rwlock_unlock(&m_impl->lock); + return true; // duplicate + } + auto d = std::make_unique(); + d->name = key; + d->describe = describe; + d->fill = fill; + d->cleanup = cleanup; + /* + usage_count starts at 1: the registry's own "reservation" reference, + released by remove() once no other borrow is outstanding. + */ + d->usage_count.store(1, std::memory_order_relaxed); + m_impl->map.emplace(std::move(key), std::move(d)); + mysql_rwlock_unlock(&m_impl->lock); + return false; +} + +bool Tf_registry::remove(const char *name, int *was_present) { + if (was_present != nullptr) *was_present = 0; + if (name == nullptr) return true; + + mysql_rwlock_wrlock(&m_impl->lock); + auto it = m_impl->map.find(std::string(name)); + if (it == m_impl->map.end()) { + mysql_rwlock_unlock(&m_impl->lock); + return false; // not registered, but not an error + } + if (was_present != nullptr) *was_present = 1; + + /* + The descriptor is only safe to drop when this registry's + "reservation" reference is the last one outstanding. We try to + decrement from 1 -> 0 atomically; if the value is anything other + than 1 a concurrent caller still holds a borrow obtained from + find(), so we must keep the registration alive and tell the + caller to fail UNINSTALL. + */ + Tf_descriptor *desc = it->second.get(); + int expected = 1; + if (!desc->usage_count.compare_exchange_strong( + expected, 0, std::memory_order_acq_rel, + std::memory_order_relaxed)) { + mysql_rwlock_unlock(&m_impl->lock); + return true; // in use + } + m_impl->map.erase(it); + mysql_rwlock_unlock(&m_impl->lock); + return false; +} + +const Tf_descriptor *Tf_registry::find(const char *name) { + if (name == nullptr) return nullptr; + mysql_rwlock_rdlock(&m_impl->lock); + auto it = m_impl->map.find(std::string(name)); + Tf_descriptor *result = nullptr; + if (it != m_impl->map.end()) { + result = it->second.get(); + /* + Acquire one reference on behalf of the caller. Held under the + registry's read lock so that it is impossible for a concurrent + remove() (which takes the write lock) to observe usage_count==1 + and erase the entry between our find and our increment. + */ + result->usage_count.fetch_add(1, std::memory_order_acq_rel); + } + mysql_rwlock_unlock(&m_impl->lock); + return result; +} + +void Tf_registry::release(const Tf_descriptor *desc) { + if (desc == nullptr) return; + /* + Strip const: usage_count is a logical mutable inside an otherwise + immutable descriptor. Callers see Tf_descriptor as read-only. + */ + Tf_descriptor *d = const_cast(desc); + /* + Decrement the borrow. remove() never decrements concurrently + (it uses CAS expecting exactly 1 = the registry reservation), so + the only way usage_count can reach 0 here is if our own decrement + observes "1 -> 0", meaning the registry already gave up its + reservation in a prior remove() call that lost the race. In that + case we own the deletion: the entry has already been erased from + the map by some thread, so we just delete the descriptor. + + Note: this branch is currently unreachable because remove() refuses + to drop the reservation while any borrow is outstanding. It is + kept as a safety net should a future change move the deletion out + of remove(). + */ + if (d->usage_count.fetch_sub(1, std::memory_order_acq_rel) == 1) { + /* purecov: begin deadcode */ + delete d; + /* purecov: end */ + } +} + +/* ========================================================================= */ +/* Table_function_dynamic implementation */ +/* ========================================================================= */ + +Table_function_dynamic::Table_function_dynamic(THD *thd, const char *alias, + const Tf_descriptor *desc, + mem_root_deque *args) + : Table_function(), + m_desc(desc), + m_args(args), + m_state(nullptr), + m_alias(alias), + m_described(false) { + /* + The caller obtained `desc` from Tf_registry::find(), which already + incremented usage_count on our behalf. We hold that borrow for + the entire lifetime of this Table_function_dynamic instance and + return it in our destructor; this is what keeps the component .so + alive while a session is using a registered table function. + */ + (void)thd; +} + +Table_function_dynamic::~Table_function_dynamic() { + /* + Hand the descriptor reference back to the registry. This is the + matching release() for the find() that the parser performed when + constructing this object. Doing it here means the borrow is held + until destroy() (called from cleanup_tmp_tables / destroy_tmp_tables + in sql_union.cc) tears down the table function for the statement - + by that point fill_result_table() and the component fill callback + have definitely returned. + */ + Tf_registry::instance()->release(m_desc); + m_desc = nullptr; +} + +bool Table_function_dynamic::init() { + /* + init() is called before create_result_table(). The list of fields + must be ready. We invoke the component-supplied describe() callback + to learn the schema, then translate the column descriptors to + Create_field entries. + */ + if (m_described) return false; // idempotent + + THD *thd = current_thd; + const Tf_column_def *cols = nullptr; + unsigned int n_cols = 0; + if (m_desc->describe(thd, static_cast(m_args->size()), &cols, + &n_cols, &m_state)) { + my_error(ER_WRONG_ARGUMENTS, MYF(0), m_desc->name.c_str()); + return true; + } + if (cols == nullptr || n_cols == 0) { + my_error(ER_WRONG_ARGUMENTS, MYF(0), m_desc->name.c_str()); + return true; + } + + for (unsigned int i = 0; i < n_cols; ++i) { + const Tf_column_def &c = cols[i]; + auto *cf = new (thd->mem_root) Create_field(); + if (cf == nullptr) return true; + + /* + Pick a sensible default length for each supported type when the + component does not specify one. + */ + uint32 length = c.length; + if (length == 0) { + switch (c.type) { + case MYSQL_TYPE_TINY: + length = 4; + break; + case MYSQL_TYPE_SHORT: + length = 6; + break; + case MYSQL_TYPE_LONG: + length = 11; + break; + case MYSQL_TYPE_LONGLONG: + length = 20; + break; + case MYSQL_TYPE_FLOAT: + length = 12; + break; + case MYSQL_TYPE_DOUBLE: + length = 22; + break; + case MYSQL_TYPE_VARCHAR: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_STRING: + length = 255; + break; + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + length = 19; + break; + case MYSQL_TYPE_DATE: + length = 10; + break; + case MYSQL_TYPE_TIME: + length = 8; + break; + default: + length = 32; + break; + } + } + + cf->init_for_tmp_table(c.type, length, c.decimals, + /*is_nullable=*/c.not_null == 0, + /*is_unsigned=*/c.is_unsigned != 0, + /*pack_length_override=*/0, + /*fld_name=*/c.name); + + /* + init_for_tmp_table() initializes charset to my_charset_bin, which is + fine for binary types and integers but yields a binary-collation + VARCHAR. Override with the connection's default charset for textual + columns so user-visible strings work as expected. + */ + switch (c.type) { + case MYSQL_TYPE_VARCHAR: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_STRING: + cf->charset = thd->variables.collation_connection; + break; + default: + break; + } + + if (m_field_list.push_back(cf)) return true; + } + + m_described = true; + return false; +} + +bool Table_function_dynamic::do_init_args() { + /* + Resolve all argument expressions. This mirrors what + Table_function_json::do_init_args does for its single source Item. + */ + THD *thd = current_thd; + for (auto it = m_args->begin(); it != m_args->end(); ++it) { + Item *arg = *it; + if (arg == nullptr) continue; + Item *dummy = arg; + if (arg->fix_fields(thd, &dummy)) return true; + if (arg->propagate_type(thd)) return true; + *it = dummy; // fix_fields may have replaced the item + } + return false; +} + +bool Table_function_dynamic::fill_result_table() { + assert(table != nullptr); + /* + Drop any rows from a previous execution. fill_result_table() is + called by MaterializedTableFunctionIterator::DoInit() every time + the table is (re)materialized; the result table is reused. + */ + empty_table(); + + /* + Test hook: lets MTR park execution here so a concurrent UNINSTALL + COMPONENT can probe the usage-count handshake while a borrow is + outstanding. See the test_table_function MTR suite. + */ + DEBUG_SYNC(current_thd, "table_function_dynamic_before_fill"); + + /* + Build the row-writer "handle" - it's just an opaque tag that the + row-writer service reinterprets back to the Table_function_dynamic* + via reinterpret_cast. Using `this` keeps things simple. + */ + Tf_row_writer_handle *writer = + reinterpret_cast(this); + + if (m_desc->fill(m_state, writer)) { + if (!current_thd->is_error()) + my_error(ER_UNKNOWN_ERROR, MYF(0)); // best-effort + return true; + } + return false; +} + +table_map Table_function_dynamic::used_tables() const { + table_map t = 0; + for (Item *arg : *m_args) { + if (arg != nullptr) t |= arg->used_tables(); + } + return t; +} + +bool Table_function_dynamic::print(const THD *thd, String *str, + enum_query_type query_type) const { + if (str->append(m_desc->name.c_str(), m_desc->name.size())) return true; + if (str->append('(')) return true; + bool first = true; + for (Item *arg : *m_args) { + if (!first && str->append(STRING_WITH_LEN(", "))) return true; + first = false; + if (arg != nullptr) arg->print(thd, str, query_type); + } + return str->append(')'); +} + +bool Table_function_dynamic::walk(Item_processor processor, enum_walk walk, + uchar *arg) { + for (Item *a : *m_args) { + if (a != nullptr && a->walk(processor, walk, arg)) return true; + } + return false; +} + +void Table_function_dynamic::do_cleanup() { + for (Item *a : *m_args) { + if (a != nullptr) a->cleanup(); + } + if (m_described && m_desc->cleanup != nullptr) { + m_desc->cleanup(m_state); + } + m_state = nullptr; + m_described = false; +} + +void Table_function_dynamic::do_fix_after_pullout( + Query_block *parent_query_block, Query_block *removed_query_block) { + for (Item *a : *m_args) { + if (a != nullptr) + a->fix_after_pullout(parent_query_block, removed_query_block); + } +} diff --git a/sql/table_function_dynamic.h b/sql/table_function_dynamic.h new file mode 100644 index 00000000000..b41c1a9973f --- /dev/null +++ b/sql/table_function_dynamic.h @@ -0,0 +1,192 @@ +/* Copyright (c) 2025, Oracle and/or its affiliates. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License, version 2.0, +as published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License, version 2.0, for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ + +#ifndef TABLE_FUNCTION_DYNAMIC_INCLUDED +#define TABLE_FUNCTION_DYNAMIC_INCLUDED + +/** + @file + Server-side glue for component-registered table functions. + + This file provides: + - Tf_registry : a thread-safe name -> descriptor registry + - Table_function_dynamic : a Table_function subclass that + plugs a registered descriptor into the resolver / optimizer / + iterator pipeline used by the built-in JSON_TABLE. +*/ + +#include + +#include +#include + +#include "my_inttypes.h" +#include "sql/mem_root_array.h" +#include "sql/sql_list.h" +#include "sql/table_function.h" +#include "thr_lock.h" + +class Item; +class String; +class THD; + +/** + A descriptor identifies a single registered table function. The + pointer to a descriptor is stable for the lifetime of the + registration; the registry holds it in a heap-allocated owning slot + and only deletes it once no statement holds a reference any more. + + Lifecycle / reference counting (modeled after udf_func::usage_count): + + - On register, usage_count starts at 1. This "reservation" is + owned by the registry itself. + - Tf_registry::find() increments usage_count and hands a borrowed + pointer to the caller. The caller must call + Tf_registry::release() exactly once to balance the get. + - Tf_registry::remove() (UNINSTALL path) only succeeds when the + reservation is the *only* outstanding reference (usage_count + drops cleanly to 0). Otherwise the entry stays alive and + remove() returns true so that the component's deinit() (and + hence UNINSTALL COMPONENT) can fail with + ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE. + + This is the same handshake used by mysql_udf_registration_imp:: + udf_unregister to keep the component .so alive while in use. +*/ +struct Tf_descriptor { + std::string name; + Tf_describe_func describe{nullptr}; + Tf_fill_func fill{nullptr}; + Tf_cleanup_func cleanup{nullptr}; + /// Number of outstanding borrows + 1 for the registry's own reservation. + std::atomic usage_count{1}; +}; + +/** + Process-global registry of dynamic table functions. + + Lookups happen at SQL parse / setup time and must be thread-safe; we + use a rwlock for read-mostly access. Insertion / removal happen at + component install / uninstall time and are rare. +*/ +class Tf_registry { + public: + /// Initialize the singleton; safe to call multiple times. + static void init(); + /// Tear down the singleton. Called from server shutdown. + static void destroy(); + /// Get the singleton. + static Tf_registry *instance(); + + /// Register a new table function. Returns true on duplicate. + bool add(const char *name, Tf_describe_func describe, Tf_fill_func fill, + Tf_cleanup_func cleanup); + + /** + Unregister a previously-registered table function. + + Sets *was_present to 1 if a registration was found. Returns true + (failure) when the function is currently being used by another + session - in that case the registration stays alive and the caller + is expected to surface this as a deinit() failure so the component + is not unloaded. + */ + bool remove(const char *name, int *was_present); + + /** + Look up a descriptor and acquire one borrowed reference to it. + + The returned pointer is borrowed; the caller must call release() + exactly once when it no longer needs the descriptor. + */ + const Tf_descriptor *find(const char *name); + + /** + Release a borrowed reference acquired through find(). + */ + void release(const Tf_descriptor *desc); + + private: + Tf_registry(); + ~Tf_registry(); + + struct Impl; + Impl *m_impl; +}; + +/* ------------------------------------------------------------- */ +/* Adapter Table_function subclass */ +/* ------------------------------------------------------------- */ + +/** + Adapts a Tf_descriptor (component-supplied callbacks) into the server's + Table_function abstract base class so the existing resolver / optimizer / + iterator infrastructure can drive it without modification. +*/ +class Table_function_dynamic final : public Table_function { + public: + Table_function_dynamic(THD *thd, const char *alias, + const Tf_descriptor *desc, + mem_root_deque *args); + + ~Table_function_dynamic() override; + + const char *func_name() const override { return m_desc->name.c_str(); } + + bool init() override; + bool fill_result_table() override; + table_map used_tables() const override; + + bool print(const THD *thd, String *str, + enum_query_type query_type) const override; + + bool walk(Item_processor processor, enum_walk walk, uchar *arg) override; + + /// Reach the result table (for the row-writer service). Returns + /// nullptr until create_result_table() has been called. + TABLE *result_table() { return table; } + + /// Write the currently-populated record to the result table; handles + /// in-memory -> on-disk overflow via Table_function::write_row(). + bool emit_current_row() { return write_row(); } + + private: + List *get_field_list() override { return &m_field_list; } + bool do_init_args() override; + void do_cleanup() override; + void do_fix_after_pullout(Query_block *parent_query_block, + Query_block *removed_query_block) override; + + /// Pointer to the registered descriptor (owned by the registry). + const Tf_descriptor *m_desc; + + /// Argument expressions parsed from the SQL. + mem_root_deque *m_args; + + /// Component-supplied per-statement state, returned by describe(). + void *m_state; + + /// Schema reported by describe(): translated into Create_field. + List m_field_list; + + /// Alias (only used for diagnostics; result table alias). + const char *m_alias; + + /// Whether init() has been run (to make do_init_args / do_cleanup + /// idempotent if init failed mid-way). + bool m_described; +}; + +#endif // TABLE_FUNCTION_DYNAMIC_INCLUDED