Repository navigation
MDEV-41962 Fallback SQL parser for vendor-dialect statements offloaded to an external analytical engine - #6141
Open
mariadb-ReneRamirez wants to merge 6 commits into
Open
mariadb-ReneRamirez wants to merge 6 commits into
mariadb-ReneRamirez wants to merge 6 commits into
Conversation
This fixes a bug that predates MDEV-41962, in the SQL service for plugins (MDEV-19275). When a query run through mysql_real_connect_local() fails after it has sent its metadata or some of its rows, for example a SELECT whose subquery returns more than one row on a later row, or a KILL QUERY between rows, loc_read_rows() reports the error with embedded_get_error(), which frees the dataset but not the fields and rows in its memory root. They stay allocated until the server shuts down. loc_read_rows() now frees the memory root first. It comes first in the MDEV-41962 series because the tests of the SQL extension framework run into it (SQL of a handler killed between rows), and it does not depend on the rest of the series, so it can also go to the lowest maintained branch on its own. plugins.test_sql_service runs such a query on the client's connection and on a connection of the plugin's own; without the fix, a debug build reports the lost bytes at shutdown.
A new plugin type, MariaDB_SQL_EXTENSION_PLUGIN, for plugins that handle SQL statements the server's parser rejects. This commit adds only the type and its descriptor (include/mysql/plugin_sql_extension.h): a list of keyword prefixes or a recognize() callback (one or the other), execute(), and a priority among recognizers. The registry that consults such plugins after a parse failure follows in a later commit; until then a plugin of this type can be declared and installed but is never called. sql_plugin.cc: the type's name and interface versions, and a new plugin_lock_by_name_ready() next to the existing plugin_lock_ready(): both return NULL unless the plugin is READY, so a plugin that UNINSTALL has marked deleted while still in use is not handed out again. Part of REST-5 (SQL extension registry for server plugins).
sql/sql_extension_matcher.{h,cc}: a trie whose edges are uppercased
keywords rather than bytes, used by the SQL extension registry to match
the prefixes plugins declare ("CREATE REST", "CREATE OR REPLACE REST").
The word reader skips whitespace and the three comment forms with the
connection charset and consumes multi-byte characters whole, never
splitting one. Matching is one sweep over the first words of a
statement, each word a binary search among the keywords that may follow
the previous one, so the cost grows with the length of the matched
prefix and only logarithmically with the number of registered prefixes.
The trie holds claims (a prefix owned by one plugin, with overlap
detection between owners) and removes one owner's entries without
rebuilding the others'.
sql_ext_statement_length() finds where the statement at the start of a
text ends: its first ';' outside quoted strings, quoted identifiers and
comments, read as the server lexer reads them (doubled quotes, backslash
escapes unless NO_BACKSLASH_ESCAPES, "..." an identifier under
ANSI_QUOTES, multi-byte characters whole; executable comments count as
comments, as for the prefix walk). The registry ends a prefix plugin's
statement there, so no plugin needs a scanner of its own.
Keywords, prefixes and node arrays are allocated with my_malloc() and
Dynamic_array under a PSI memory key the registry provides, so that
memory is instrumented; the nodes come from new (std::nothrow).
Allocation failures are reported, not thrown.
Unit tests in unittest/sql/sql_extension_matcher-t.cc.
Part of REST-5 (SQL extension registry for server plugins).
The framework that lets a MariaDB_SQL_EXTENSION_PLUGIN handle statements
the server's parser rejects.
- sql/sql_extension.{h,cc}: a registry of the installed extension
plugins (a keyword trie of their prefixes, and their recognizers by
descending priority then name) behind an rwlock, with an atomic plugin
count so that the failed-parse path costs one load while no plugin is
installed. INSTALL refuses a descriptor that is invalid
(ER_SQL_EXTENSION_BAD_DESCRIPTOR), whose prefix overlaps another
plugin's (ER_SQL_EXTENSION_PREFIX_OVERLAP), or that cannot be
registered for lack of memory; a plugin's prefixes are added all or
none. Start-up aborts if the registry itself cannot be allocated.
The registry finds a plugin's entry by its st_plugin_int, so the
plugin's st_plugin_int::data is left to the plugin's init().
- sql_extension_dispatch(): called from exactly three places after
parse_sql() failed, mysql_parse(), Prepared_statement::prepare() and
execute_server_code(). Only on ER_PARSE_ERROR, the code of every
syntax error of the parser; never for replication appliers
(asynchronous or Galera), stored functions, triggers, stored program
bodies or bootstrap. On a claim the parse error is cleared, the LEX
restarted with SQLCOM_SQL_EXTENSION and Sql_cmd_sql_extension, the
statement never cached (query cache included: its lookup ran before
the parse). A prefix plugin's statement ends at its first ';' outside
quotes and comments (sql_ext_statement_length(), with the session's
sql_mode), a recognizer's where it says; the rest of a multi-statement
packet goes to the normal loop. A statement that is not claimed keeps
its parse error byte for byte, unless the error says why it was not:
a syntax error near trailing text that cannot follow the claim,
ER_SQL_EXTENSION_INVALID_RESULT for a recognizer that claims past the
end, ER_OUT_OF_RESOURCES for a plugin that cannot be pinned, and
ER_SQL_EXTENSION_NESTED for a claim while another extension statement
runs on the thread. EXECUTE of a prepared extension statement is
refused then too: EXECUTE does not parse, so Sql_cmd_sql_extension
checks again.
- Sql_cmd_sql_extension: locks the plugin by name for each execution,
so a prepared statement survives UNINSTALL and INSTALL (EXECUTE in
between fails with ER_PLUGIN_IS_NOT_LOADED); sends OK when the plugin
sent nothing, ER_SQL_EXTENSION_FAILED when it failed without an error.
- Com_sql_extension and the statement/sql/sql_extension PSI event; the
new statement class raises performance_schema_max_statement_classes by
one (perfschema results and mysqld--help re-recorded; the new
LOCK_sql_extension rwlock also shifts the rows that
dml_setup_instruments lists). The registry memory,
memory/sql/sql_extension, is global (PSI_FLAG_GLOBAL), so
perfschema.ortho_iter lists it with the other global instruments.
Test plugin plugin/test_sql_extension (TEST SQLEXT OK|ERROR|FAIL|ECHO,
the prefixes WITH SQLEXT and CREATE PROCEDURE SQLEXT, which start valid
SQL, a recognizer, a plugin whose prefix overlaps another's, which
counts its init() and deinit() calls, and plugins with an invalid
descriptor) and the
mysql-test suite sql_extension: install (a refused plugin's deinit()
runs once), load at startup, dispatch (including statements the parser
rejects with an error other than a syntax error, a stored program body
that fails to parse, and a recognizer that claims past the end of the
statement), prepared statements, multi-statements, stored programs,
UNINSTALL during recognize(), the statement in the performance_schema
statement history, a replication applier that must not hand a statement
to a plugin (rpl_applier), and SQL a plugin runs through the plain SQL
service (plain_service, with test_sql_service). The unit test of the
trie covers running out of memory in a debug build.
Part of REST-5 (SQL extension registry for server plugins).
include/mysql/service_sql_extension.h, service sql_extension_service 0x0100, included from plugin_sql_extension.h (not from services.h, so only extension plugins see it): what a plugin's execute() uses to answer a statement. - sql_extension_send_result_set(): a materialized result set of STRING, BINARY, INTEGER, UNSIGNED, DOUBLE, DECIMAL, DATE, TIME, DATETIME or JSON columns, every cell given as text and checked before anything is sent (ER_SQL_EXTENSION_INVALID_RESULT names the cell or the column). The metadata follows the cells: string widths in characters, DECIMAL precision and scale, fractional seconds, nullability; JSON carries format=json. Rows go through the typed Protocol::store_*() functions, so the binary protocol gets native values. Each row counts as a row sent (Rows_sent of the slow log, ROWS_SENT of the performance schema). - sql_extension_send_ok(): an OK with affected rows and a message. - sql_extension_check_access(): the client's privileges on a database or a table, with bits of its own that map exactly to DB_ACLS (a static_assert keeps them so); the database-level check computes the effective privileges by name, as fill_effective_table_privileges() does, since check_access() would read the name as a GRANT pattern. - sql_extension_client_flags(): whether the client accepts several results, and whether its packet may hold several statements. - Several results per statement, as CALL sends them: the final packet of the latest result is held back and sent with SERVER_MORE_RESULTS_EXISTS when the next one starts; one result is sent as before. An error after results follows them; a client that does not accept several results gets the error in place of the end of the result it was sent, as when a SELECT fails after some rows. - Notes and warnings are added with my_printf_error() and ME_NOTE or ME_WARNING; the service has no function for them. - THD::send_kill_message() returns whether it raised an error (LIMIT ROWS EXAMINED stops a statement without one), so that a service function called after a kill fails with the kill error; its message comes from the new private THD::killed_message(). Test plugin: ROWS, EMPTY, NULLS, TYPES, BAD <type> <text>, BAD2, BADSET, BADTHD, INFO, WARN, ACCESS, TWICE, MULTI, FLAGS, a second recognizer with priority 10, a prefix plugin with a priority, which INSTALL refuses, and a plugin whose init() fails. Tests: install (both refused, a failed init() not followed by deinit()), install_startup (all the test plugins at startup), plain_service (results through the plain SQL service), service (results, metadata, also with no rows or only NULLs, prepared statements, every column type at its limits, a DECIMAL column too wide only across rows, malformed result sets, calls on the wrong THD, access and name checks, every invalid argument), service_lctn (access checks with lower_case_table_names=1), results (several results), results_debug (clients without multi-results, a result the plugin set outside the service), priority (recognizers by priority, not by installation order; a claimed prefix before any recognizer), perfschema (rows of result sets counted as rows sent). Part of REST-5 (SQL extension registry for server plugins).
Most extension plugins implement their statements by running ordinary SQL. The framework makes that correct for every plugin through a new mode of the plugin SQL service's local connection, rather than leaving privileges, logging and KILL to each plugin. - Protocol_local gets the serial of the extension statement it serves (as_client() when non-zero; sql_prepare.cc, entry point mysql_real_connect_local_as_client()): the SQL runs on the client's own THD with the client's security context, so its grants apply; with the client's sql_log_bin, so changes replicate as plain SQL and a replica needs no plugin; in the client's transaction; under LOCK TABLES with the client's locked tables, like SQL in a stored procedure; a pending KILL QUERY fails the SQL instead of being reset; an error the handler raised before running the SQL is not cleared; and each statement counts its own LIMIT ROWS EXAMINED. Outside an extension statement the entry point refuses, rather than fall back to the plain service. The existing behaviour of the service is untouched for its current users. - sql_extension_sql_as_client(thd): true while the THD executes SQL on behalf of an extension statement, derived from the state the nesting guard keeps (no new THD field). Such SQL leaves the statement timer to the outer statement, as a stored procedure's statements leave it, so max_statement_time covers the whole extension statement. It leaves the kill state set too, which a stored procedure's statements do not do: the extension statement, and any SQL its handler runs after the kill, fail with the kill error, even when the handler ignores the error its SQL got; THD::killed_error() gives that error for the handle. This holds for the as-client connection, for forwarded SQL and for the plain SQL service when a handler uses it on the client's THD; the private THD that service creates under LOCK TABLES is not the client's and is not affected. - Likewise, SQL whose error rolls back the client's transaction (a deadlock, a lock wait timeout with innodb_rollback_on_timeout) fails the extension statement with that error, even when the handler drops it, so the client knows its transaction is gone: trans_rollback_implicit() notes the error through sql_extension_note_implicit_rollback() when an engine asked for the rollback (not for CHECK TABLE of a corrupt table, which succeeds). A handler's own error is kept, as for a kill, and a stored program whose handler takes the error and does not let it out succeeds, as when the client calls it (sql_extension_note_error_handled(), sql_extension_note_sql_done()). - sql_extension_connect_as_client(): that connection for a plugin. A connection kept past its statement is refused, by a serial unique to each extension statement of the server, and so is one used from another thread. - sql_extension_forward_sql(): runs one statement as the client on the client's protocol (execute_sql_as_client()), so the client receives the server's own result sets, metadata and OK, including every result of a CALL; its warnings and error become the extension statement's. select_send::send_eof() leaves the final packet to the extension statement. - Two guards of the earlier commits are now tested with SQL a handler runs. An extension statement claimed by that SQL is refused with ER_SQL_EXTENSION_NESTED, also through EXECUTE of a prepared one and for the plain SQL service's private THD under LOCK TABLES: the guard is a thread-local on purpose. And nothing a handler runs is stored by the query cache, which matters now that forwarded SELECTs reach the client: a carrier whose text starts like a SELECT was looked up before the parse, and the first forwarded SELECT would otherwise be stored under the carrier's text and served to the next client sending it without the plugin. Test plugin: SQL, SQL2, LOCAL SQL, LOCAL SQL2, KEEP, REUSE, THREAD, [LOCAL] SWALLOW [OK|ROWS] (also of a script), [LOCAL] RAISE, NOTHD, BADCALL, FORWARD, SCRIPT; WITH SQLEXT also serves as a carrier the query cache looks up. Tests: - service: handler SQL with the client's grants, current database, transaction and LOCK TABLES; SQL that changes the session or the transaction (COMMIT, USE, LOCK TABLES...); EXECUTE of an extension statement refused; KILL QUERY, KILL CONNECTION and max_statement_time, also for a handler that ignores the kill error or goes on after it; a deadlock that rolls back the client's transaction, dropped by the handler (in a transaction and in autocommit, after an OK or rows, through the plain service), replaced by the handler's own error, or handled by a stored procedure (also by an outer procedure's handler, through the forwarding service too, and not undone by a later unrelated error), but not when the procedure's handler re-raises it or another error, nor by a later error of the same number; a lock wait timeout without innodb_rollback_on_timeout, which leaves the transaction; CHECK TABLE of a corrupt table; an error raised before the handler's SQL; LIMIT ROWS EXAMINED; a connection kept past its statement or used from another thread; invalid calls, also from a thread with no THD. - service_threadpool: a kept connection under the thread pool. - service_debug: SQL a handler runs after the timeout does not run. - service_kill: KILL QUERY through the plain SQL service too. - results: forwarded results, also through the binary protocol, their insert id, results of extension statements inside a CALL, scripts, KILL and max_statement_time of forwarded SQL. - results_debug: a forwarded CALL or a script for a client without multi-results, KILL between two statements of a script. - service_binlog, rpl: handler SQL is logged as plain SQL and applied by a replica without the plugin, also when the handler fails after changing data. - galera.galera_sql_extension: handler SQL replicates as plain SQL, also when the handler fails after changing data; INSTALL PLUGIN replicates as DDL, so every node has the plugin. - rollback_on_timeout: a lock wait timeout with innodb_rollback_on_timeout fails the statement, even when the handler drops it, in a transaction and in autocommit, unless a stored procedure handles it. - galera.galera_sql_extension_bf_abort: a transaction aborted by another node's while handler SQL runs gets ER_LOCK_DEADLOCK, as the same SQL sent directly, also when the handler drops the error. - qcache; nesting in dispatch; UNINSTALL during a statement in lifecycle. Part of REST-5 (SQL extension registry for server plugins).
mariadb-ReneRamirez
force-pushed
the
12.3-MDEV-41962
branch
from
November 13, 2026 18:16
c96a93d to
38a857b
Compare
vuvova
self-requested a review
November 13, 2026 18:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
It adds an SQL extension framework to the server. A plugin can claim SQL statements that the server's parser rejects and handle them itself, for example
CREATE REST SERVICE .... This is the basis for a MariaDB REST Service plugin.The commits, in order
A fix to the plugin SQL service (on its own). A query that failed partway through its rows (
loc_read_rows) leaked memory until shutdown. This is an old bug, not part of the new feature, so it can also go to older branches separately.A new plugin type,
MariaDB_SQL_EXTENSION_PLUGIN. A plugin either lists keyword prefixes or provides arecognize()callback. It also has anexecute()function and a priority. This commit only adds the type; nothing calls it yet.A keyword prefix matcher (
sql/sql_extension_matcher.*). It matches prefixes word by word, ignoring case, whitespace and comments, and handles multi-byte characters. It refuses a prefix that overlaps another plugin's. It also finds where a statement ends: the first;that is not inside quotes or comments, read the way the server's lexer reads it. Unit tests are included.The registry and dispatch (
sql/sql_extension.*).ER_PARSE_ERROR) leads to the plugins being asked. This happens from three places:mysql_parse,PREPARE, and the code that runs SQL internally.SQLCOM_SQL_EXTENSION. It works with prepared statements and multi-statement packets.UNINSTALL/INSTALLof its plugin.A service for answering the client (
service_sql_extension.h).Running the handler's SQL as the client. Most handlers will work by running ordinary SQL. A new mode of the local SQL service runs that SQL on the client's own session, with:
LOCK TABLESandsql_log_bin, so binary logs and replicas see plain SQL and a replica needs no plugin;KILL,max_statement_timeand implicit rollbacks (deadlock, lock wait timeout) reported to the client, even if the handler ignores the error;sql_extension_forward_sql(), which sends the server's own results for one statement straight to the client;Tests
plugin/test_sql_extension, with many commands that exercise each path.sql_extension, covering install, dispatch, prepared statements, results, the service, KILL and timeouts, the binary log, replication, the thread pool and the query cache.