Pre-flight Checklist
Primary Contact Name
Kaiwang Chen
Primary Contact Email
kaiwang.chen@gmail.com
Company / Organization
a public cloud provider
Role
Software Engineer
Additional Authors / Contributors
No response
Component
Server
Target Release (Optional)
9.x
Roadmap Section
Extensibility/Ecosystem
Related Issues / Pull Requests / References (Optional)
- Related feature request: Bug#120978 (Allow components to register custom SQL table functions)
- Interface model: WL#8020 (UDF registration component service)
Executive Summary
MySQL supports exactly one table function today: JSON_TABLE. A table
function appears in the FROM clause and produces a relation (rows with a
fixed set of typed columns). There is currently no public extension point
that lets a plugin or component contribute a new table function, so users
who need set-returning functions must work around it by materializing
temp tables by hand, abusing a scalar UDF that returns a serialized blob,
or exposing data through a PERFORMANCE_SCHEMA plugin table (visible only
under performance_schema and unable to take call-time arguments).
This proposal adds a component service, table_function_registration, that
lets a loadable component register a named table function. Once
registered, the function is usable anywhere a table reference is allowed,
exactly like JSON_TABLE:
SELECT * FROM generate_series(1, 5) AS t; -- 1,2,3,4,5
The design deliberately mirrors the udf_registration service (WL#8020) so
that component authors familiar with UDF registration can register table
functions with no new concepts, and it is expressed as a second concrete
implementation of the server's existing Table_function abstraction so
that the resolver, optimizer, iterator, EXPLAIN and view machinery are
reused unchanged.
User / Developer Stories
- As an extension author, I want to register a set-returning function
from my component, so that my data source can appear in a FROM clause
and participate in joins, WHERE, GROUP BY, views and CTEs.
- As a SQL user, I want a generate_series(start, stop [, step]) generator
like PostgreSQL's, so that I can produce numeric ranges without a base
table.
- As a DBA, I want argument-driven diagnostic views (e.g.
buffer_pool_pages(instance_id)), which PERFORMANCE_SCHEMA plugin tables
cannot offer.
- As an integrator, I want to bridge an external system into SQL as a
table function so it composes with the rest of a query.
Proposed Scope
- A component service to register/unregister a table function by name.
- A row-writer service for components to emit typed rows.
- An argument-reader service so functions can read call-time arguments
(constants, expressions and ? placeholders), re-evaluated per execution.
- One new grammar alternative in the existing table_function: rule; a
server-side registry and a Table_function adapter subclass.
- Two demo components (all-types demo + generate_series) and an MTR suite.
Out of Scope / Future Work
- No SQL DDL (no CREATE TABLE FUNCTION); functions live only for the
lifetime of the registering component, like component-registered UDFs.
- No schema (database) qualification, overloading, or data-dictionary
persistence. The name lives in a single process-global namespace keyed
by name only, matching the UDF / JSON_TABLE model rather than
PostgreSQL's schema-scoped pg_proc functions. Schema-scoped table
functions are a possible follow-up.
- No per-function GRANT/REVOKE in the first iteration (see Security).
- No table functions that mutate data.
References
- WL#8020: UDF registration component service (interface model).
- Existing Table_function abstraction (sql/table_function.{h,cc}) and
MaterializedTableFunctionIterator, reused unchanged.
- PostgreSQL set-returning functions (prior art for generate_series).
Functional Requirements
- The server MUST let a component register a table function by name; after
INSTALL COMPONENT the name MUST be usable in any session's FROM clause
as name(args) AS alias (alias mandatory, like JSON_TABLE).
- The name MUST be reachable only from a table-reference position; it MUST
NOT resolve in the scalar-function namespace (SELECT name() MUST still
fail with ER_SP_DOES_NOT_EXIST), and a bare "FROM name" without
parentheses MUST NOT resolve to the table function.
- The result MUST be a read-only relation (SELECT_ACL only), participating
fully in projection, filtering, JOIN, ORDER BY/LIMIT, aggregation,
subqueries, UNION, derived tables, CTEs, views and PREPARE/EXECUTE.
- EXPLAIN MUST classify the source as a materialized table function.
- A table function MAY accept call-time arguments and read their values;
arguments MAY be expressions or ? placeholders and MUST be re-evaluated
on every execution.
- UNINSTALL COMPONENT MUST NOT unload a component while one of its table
functions is executing in another session; in that case it MUST fail
with ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE, and MUST succeed once no
execution is in flight.
Non-functional Requirements
- Compatibility: no new reserved keyword; existing statements and
JSON_TABLE behaviour unchanged.
- Security: registration is a privileged operation (INSTALL COMPONENT);
argument expressions remain subject to column-level privilege checks.
- Concurrency: registry access is rwlock-protected; the unload handshake
uses a usage_count with the same semantics as udf_func::usage_count.
- Portability: the service ABI is pure C so any language with a C FFI can
register table functions.
- Maintainability: implemented as a Table_function subclass so downstream
code is reused unchanged.
Impact Areas
Summary of the Approach
Add three component services and a thin server-side adapter. A process-
global registry maps a name to a descriptor of component callbacks. The
grammar gains one alternative in the existing table_function: rule that
resolves the name against the registry and constructs a Table_function
subclass (Table_function_dynamic), which drives the component's
describe/fill/cleanup callbacks. Everything below the Table_function
abstraction (resolver, optimizer, materialization iterator, EXPLAIN) is
unchanged.
User Interface
INSTALL COMPONENT "file://component_generate_series";
SELECT * FROM generate_series(1, 5) AS t; -- 1..5
SELECT * FROM generate_series(1, 10, 2) AS t; -- 1,3,5,7,9
UNINSTALL COMPONENT "file://component_generate_series";
Configuration / Knobs
No new system variables or CLI options. New extension points only:
- table_function_registration : register/unregister a table function
- table_function_row_writer : set_null/set_longlong/set_double/
set_string/emit_row
- table_function_args : arg_count/get_longlong/get_double/
get_string
Observability
Registered table functions appear in EXPLAIN as a materialized table
function access path. No new instrumentation is added in the first
iteration; Performance Schema exposure of registered functions is a
possible follow-up.
User Procedure
An extension author writes describe/fill/cleanup callbacks, calls
register_table_function() from the component's init and
unregister_table_function() from deinit. A DBA installs the component;
any authenticated session may then call the function in a FROM clause.
Security Considerations
Registration requires INSTALL COMPONENT. The result relation carries
SELECT_ACL like a derived table, and argument expressions that reference
real columns still go through normal column-level privilege checks. This
matches the current model of JSON_TABLE and component-registered UDFs:
once installed, any session may call the function. A future extension may
add an EXECUTE_TABLE_FUNCTION dynamic privilege or a per-function
privilege declared at registration time.
Compatibility and Behavior Changes
No new reserved keyword (dispatch is on a non-reserved identifier followed
by '(' inside the existing grammar rule). No on-disk format, data-
dictionary or replication change. JSON_TABLE behaviour is unchanged; a
table function is an execution-time construct only.
Block Diagram
flowchart TD
A["Component (init)"] -->|register_table_function| R["Tf_registry (global, rwlock)"]
Q["SQL: FROM name(args) AS t"] --> P["Parser: table_function rule\nPT_table_factor_dynamic_function"]
P -->|find(name)| R
P --> ADA["Table_function_dynamic (adapter)"]
ADA -->|describe / fill / cleanup| A
ADA --> IT["MaterializedTableFunctionIterator (unchanged)"]
ADA -->|row_writer + args services| A
U["UNINSTALL COMPONENT"] -->|unregister -> remove()| R
R -.->|usage_count CAS 1->0 fails while in use| U
Interface Specification
BEGIN_SERVICE_DEFINITION(table_function_registration)
register_table_function(name, describe_cb, fill_cb, cleanup_cb)
unregister_table_function(name, was_present)
END_SERVICE_DEFINITION
BEGIN_SERVICE_DEFINITION(table_function_row_writer)
set_null / set_longlong / set_double / set_string / emit_row
END_SERVICE_DEFINITION
BEGIN_SERVICE_DEFINITION(table_function_args)
arg_count / get_longlong / get_double / get_string
END_SERVICE_DEFINITION
struct Tf_column_def { name; type; length; decimals; not_null; is_unsigned; };
describe_cb(thd, args, out_columns, out_n_columns, out_state)
fill_cb(state, args, row_writer)
cleanup_cb(state)
Registry: name -> descriptor (std::unique_ptr, stable address); each
descriptor has a usage_count (== udf_func::usage_count semantics): register
reserves 1, find() increments under read lock, release() decrements,
remove() erases only by CAS'ing 1 -> 0 (fails while borrowed -> UNINSTALL
fails with ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE).
Proposed Implementation Plan
- Registry + Table_function_dynamic adapter (sql/table_function_dynamic).
- Service headers + implementations; register with mysql_server.
- One grammar alternative + PT_table_factor_dynamic_function node.
- Argument-reader service and adapter arg accessors.
- Demo components (all-types + generate_series) and MTR suite.
- Documentation / worklog.
QA Notes
- Edge cases: missing alias; unknown/unregistered name; scalar-style
invocation; bare table-style reference; duplicate INSTALL; double
UNINSTALL; empty and single-element ranges; zero step; wrong argument
count; large ranges; NULL-complement in LEFT JOIN.
- Regression risks: grammar ambiguity between table name and table
function (mitigated by using IDENT_sys and dispatching on the trailing
'('); result-table reuse across re-execution (result table is reset per
fill).
- Suggested test coverage: basic/errors/advanced/unload-in-use plus a
generate_series matrix (constants, expressions, ? placeholders,
PREPARE/EXECUTE re-execution, custom/negative step); error paths assert
ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT and ER_WRONG_ARGUMENTS.
- Automation opportunities: the concurrent unload-in-use case is driven by
a DEBUG_SYNC point, making the lifecycle handshake deterministically
testable.
Pre-flight Checklist
Primary Contact Name
Kaiwang Chen
Primary Contact Email
kaiwang.chen@gmail.com
Company / Organization
a public cloud provider
Role
Software Engineer
Additional Authors / Contributors
No response
Component
Server
Target Release (Optional)
9.x
Roadmap Section
Extensibility/Ecosystem
Related Issues / Pull Requests / References (Optional)
Executive Summary
MySQL supports exactly one table function today: JSON_TABLE. A table
function appears in the FROM clause and produces a relation (rows with a
fixed set of typed columns). There is currently no public extension point
that lets a plugin or component contribute a new table function, so users
who need set-returning functions must work around it by materializing
temp tables by hand, abusing a scalar UDF that returns a serialized blob,
or exposing data through a PERFORMANCE_SCHEMA plugin table (visible only
under performance_schema and unable to take call-time arguments).
This proposal adds a component service, table_function_registration, that
lets a loadable component register a named table function. Once
registered, the function is usable anywhere a table reference is allowed,
exactly like JSON_TABLE:
The design deliberately mirrors the udf_registration service (WL#8020) so
that component authors familiar with UDF registration can register table
functions with no new concepts, and it is expressed as a second concrete
implementation of the server's existing Table_function abstraction so
that the resolver, optimizer, iterator, EXPLAIN and view machinery are
reused unchanged.
User / Developer Stories
from my component, so that my data source can appear in a FROM clause
and participate in joins, WHERE, GROUP BY, views and CTEs.
like PostgreSQL's, so that I can produce numeric ranges without a base
table.
buffer_pool_pages(instance_id)), which PERFORMANCE_SCHEMA plugin tables
cannot offer.
table function so it composes with the rest of a query.
Proposed Scope
(constants, expressions and ? placeholders), re-evaluated per execution.
server-side registry and a Table_function adapter subclass.
Out of Scope / Future Work
lifetime of the registering component, like component-registered UDFs.
persistence. The name lives in a single process-global namespace keyed
by name only, matching the UDF / JSON_TABLE model rather than
PostgreSQL's schema-scoped pg_proc functions. Schema-scoped table
functions are a possible follow-up.
References
MaterializedTableFunctionIterator, reused unchanged.
Functional Requirements
INSTALL COMPONENT the name MUST be usable in any session's FROM clause
as name(args) AS alias (alias mandatory, like JSON_TABLE).
NOT resolve in the scalar-function namespace (SELECT name() MUST still
fail with ER_SP_DOES_NOT_EXIST), and a bare "FROM name" without
parentheses MUST NOT resolve to the table function.
fully in projection, filtering, JOIN, ORDER BY/LIMIT, aggregation,
subqueries, UNION, derived tables, CTEs, views and PREPARE/EXECUTE.
arguments MAY be expressions or ? placeholders and MUST be re-evaluated
on every execution.
functions is executing in another session; in that case it MUST fail
with ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE, and MUST succeed once no
execution is in flight.
Non-functional Requirements
JSON_TABLE behaviour unchanged.
argument expressions remain subject to column-level privilege checks.
uses a usage_count with the same semantics as udf_func::usage_count.
register table functions.
code is reused unchanged.
Impact Areas
Summary of the Approach
Add three component services and a thin server-side adapter. A process-
global registry maps a name to a descriptor of component callbacks. The
grammar gains one alternative in the existing table_function: rule that
resolves the name against the registry and constructs a Table_function
subclass (Table_function_dynamic), which drives the component's
describe/fill/cleanup callbacks. Everything below the Table_function
abstraction (resolver, optimizer, materialization iterator, EXPLAIN) is
unchanged.
User Interface
Configuration / Knobs
No new system variables or CLI options. New extension points only:
set_string/emit_row
get_string
Observability
Registered table functions appear in EXPLAIN as a materialized table
function access path. No new instrumentation is added in the first
iteration; Performance Schema exposure of registered functions is a
possible follow-up.
User Procedure
An extension author writes describe/fill/cleanup callbacks, calls
register_table_function() from the component's init and
unregister_table_function() from deinit. A DBA installs the component;
any authenticated session may then call the function in a FROM clause.
Security Considerations
Registration requires INSTALL COMPONENT. The result relation carries
SELECT_ACL like a derived table, and argument expressions that reference
real columns still go through normal column-level privilege checks. This
matches the current model of JSON_TABLE and component-registered UDFs:
once installed, any session may call the function. A future extension may
add an EXECUTE_TABLE_FUNCTION dynamic privilege or a per-function
privilege declared at registration time.
Compatibility and Behavior Changes
No new reserved keyword (dispatch is on a non-reserved identifier followed
by '(' inside the existing grammar rule). No on-disk format, data-
dictionary or replication change. JSON_TABLE behaviour is unchanged; a
table function is an execution-time construct only.
Block Diagram
flowchart TD
A["Component (init)"] -->|register_table_function| R["Tf_registry (global, rwlock)"]
Q["SQL: FROM name(args) AS t"] --> P["Parser: table_function rule\nPT_table_factor_dynamic_function"]
P -->|find(name)| R
P --> ADA["Table_function_dynamic (adapter)"]
ADA -->|describe / fill / cleanup| A
ADA --> IT["MaterializedTableFunctionIterator (unchanged)"]
ADA -->|row_writer + args services| A
U["UNINSTALL COMPONENT"] -->|unregister -> remove()| R
R -.->|usage_count CAS 1->0 fails while in use| U
Interface Specification
BEGIN_SERVICE_DEFINITION(table_function_registration)
register_table_function(name, describe_cb, fill_cb, cleanup_cb)
unregister_table_function(name, was_present)
END_SERVICE_DEFINITION
BEGIN_SERVICE_DEFINITION(table_function_row_writer)
set_null / set_longlong / set_double / set_string / emit_row
END_SERVICE_DEFINITION
BEGIN_SERVICE_DEFINITION(table_function_args)
arg_count / get_longlong / get_double / get_string
END_SERVICE_DEFINITION
struct Tf_column_def { name; type; length; decimals; not_null; is_unsigned; };
describe_cb(thd, args, out_columns, out_n_columns, out_state)
fill_cb(state, args, row_writer)
cleanup_cb(state)
Registry: name -> descriptor (std::unique_ptr, stable address); each
descriptor has a usage_count (== udf_func::usage_count semantics): register
reserves 1, find() increments under read lock, release() decrements,
remove() erases only by CAS'ing 1 -> 0 (fails while borrowed -> UNINSTALL
fails with ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE).
Proposed Implementation Plan
QA Notes
invocation; bare table-style reference; duplicate INSTALL; double
UNINSTALL; empty and single-element ranges; zero step; wrong argument
count; large ranges; NULL-complement in LEFT JOIN.
function (mitigated by using IDENT_sys and dispatching on the trailing
'('); result-table reuse across re-execution (result table is reset per
fill).
generate_series matrix (constants, expressions, ? placeholders,
PREPARE/EXECUTE re-execution, custom/negative step); error paths assert
ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT and ER_WRONG_ARGUMENTS.
a DEBUG_SYNC point, making the lifecycle handshake deterministically
testable.