xiangfu0 opened a new pull request, #19664:
URL: https://github.com/apache/pinot/pull/19664

   Labels: `feature`, `backward-incompat`, `release-notes`, `extension-point`
   
   ## Summary
   
   `CalciteSqlParser.extractSqlNodeAndOptions` only classified 
`SqlInsertFromFile` as DML, so a Calcite `SqlDelete`
   (`DELETE FROM t WHERE ...`, which the grammar already parses) fell through 
to the DQL branch. It was then compiled as
   a query and rejected with a confusing error (`ClassCastException` to 
`SqlSelect` in the single-stage compiler,
   "Unsupported SQL query" in the multi-stage planner, the same cast failure on 
the controller `/sql` DQL path).
   
   This change makes Pinot parse `DELETE` as DML and lets a deployment plug in 
how rows are deleted. Pinot itself does
   not delete rows: the default executor answers that `DELETE` is not supported.
   
   - `CalciteSqlParser` classifies `SqlDelete` as `PinotSqlType.DML`, so the 
broker (`/query/sql`, `/query`) and the
     controller (`/sql`) hand it to their `SqlQueryExecutor`, like `INSERT INTO 
... FROM FILE`.
   - New `DeleteStatement` (`org.apache.pinot.sql.parsers.dml`), returned by 
`DataManipulationStatementParser` for
     `DELETE`:
     - `getTableName()`: `table` or `database.table` as written.
     - `getPredicate()`: the WHERE clause serialized back into Pinot SQL. It 
must parse
       (`CalciteSqlParser.compileToExpression`) into the same expression as the 
statement, or the statement is rejected.
       Literal quotes are escaped. Identifiers are quoted where they were 
quoted, and where needed to keep their name:
       Calcite unparses identifiers named like SQL functions without arguments 
(`user`, `pi`, `current_date`, ...) as
       upper-cased keywords, so they are quoted. The predicate is parsed but 
not validated as a filter of the table
       (columns, aggregations): the executor validates it.
     - `getDatabase()`: from the `database` option, e.g. `SET database = 
'...'`. Executors combine it with the
       `database` header the way queries do.
     - `getOptions()`: the statement's own options, from `SET` statements and 
the request `queryOptions` (`SET` takes
       precedence), i.e. without the database and the query and request options.
   
     `DELETE` without a WHERE clause, with a table alias, or with a WHERE 
clause Pinot cannot parse as an expression
     (e.g. a subquery) is rejected. So is another spelling of the `database` 
option (e.g. `SET DATABASE = ...`):
     queries only read `database`, so a `DELETE` must not read another one.
   - `SqlQueryExecutor.executeDMLStatement` hands a `DeleteStatement` to a new
     `protected BrokerResponse executeDelete(DeleteStatement statement, 
@Nullable Map<String, String> headers)`. The
     default answers with a `QUERY_VALIDATION` error ("DELETE is not supported 
by this Pinot cluster"). Like other DML,
     the statement reaches the executor without table-level authorization or 
query logging, so implementations
     authorize the caller with the request headers, as documented on the method.
   - `BaseBrokerStarter` and `BaseControllerStarter` create their executor with 
a new
     `protected SqlQueryExecutor createSqlQueryExecutor()`. Deployments that 
can delete rows, e.g. by purging the matching
     rows from the segments with a minion task, override it to return a 
`SqlQueryExecutor` subclass implementing
     `executeDelete`.
   - `SqlQueryExecutor.executeDMLStatement` answers a DML statement it cannot 
parse with a `SQL_PARSING` error response
     instead of throwing, which surfaced as an HTTP 500 on the broker.
   - New `QueryOptionsUtils.isQueryOptionKey(String)`: whether a key, ignoring 
case, is a query or request option rather
     than an option of a statement: a `QueryOptionKey`, `trace`, `database`, 
the legacy `groupByMode` and
     `responseFormat` that the Java client still sends with every request, or a 
key registered with
     `registerSqlQueryOptionKey`. `DeleteStatement` uses it to tell its options 
from query options. Registered keys are
     per process, so brokers and controllers should register the same ones.
   - The options of a DML statement configure it (e.g. a dry run, or the 
`taskName` of `INSERT INTO ... FROM FILE`), so
     dropping them would silently change what it does. A DML statement with SQL 
options now fails with
     `QUERY_VALIDATION` when the request sets `sqlOptionsMode=IGNORE`, and a 
DML statement using the legacy
     `OPTION(...)` syntax fails to parse on a broker configured to ignore that 
syntax
     (`pinot.broker.query.option.legacySyntaxMode=IGNORE`). Queries are 
unaffected. The docs of both settings say so.
   - The controller `GET /sql` rejects DML with a `QUERY_VALIDATION` error, as 
the broker `GET /query/sql` already rejects
     it (`onlyDql`): a GET must not modify data, e.g. when a browser holding 
credentials follows a crafted link.
     `POST /sql` still executes DML. This also applies to `INSERT INTO ... FROM 
FILE`, which a GET could previously run.
   
   `EXPLAIN PLAN FOR DELETE ...` is unchanged (still an explain statement).
   
   **Backward incompatible:**
   - Clients that submit `INSERT INTO ... FROM FILE` with the controller `GET 
/sql` must switch to `POST /sql`.
   - Clients that send DML statements with `sqlOptionsMode=IGNORE`, or with 
`OPTION(...)` on a broker that ignores that
     syntax, must drop that mode or move the options to the request.
   
   ## Testing
   
   - `DeleteStatementTest`:
     - parsing of the table, with database and quoted names;
     - the options split: query options, request options such as `trace` and 
`groupByMode`, registered keys and the
       database are excluded; request `queryOptions` are included;
     - rejections: no WHERE, alias, `a.b.c`, subquery, `SET DATABASE`;
     - dispatch through `DataManipulationStatementParser`;
     - 20 WHERE clause round trips that compile into the same filter expression 
as the original: quotes, unicode,
       reserved words, `IN`, `BETWEEN`, `LIKE`, `IS NULL`, functions, `CAST`, 
`JSON_MATCH`, `TEXT_MATCH`, timestamps,
       `CASE`, and columns named `user`, `pi`, `current_date`, 
`current_timestamp` in any case;
     - the exact serialization of such columns.
   - `SqlQueryExecutorTest`:
     - `DELETE` on the default executor returns a `QUERY_VALIDATION` "not 
supported" error without contacting the
       controller;
     - an invalid `DELETE` returns `SQL_PARSING`;
     - an executor overriding `executeDelete` receives the parsed statement and 
the request headers.
   - `QueryOptionsUtilsTest#testIsQueryOptionKey`.
   - `SqlOptionsModeTest`:
     - DML with SQL options fails under `sqlOptionsMode=IGNORE`, for `SET` and 
`OPTION(...)`, `DELETE` and
       `INSERT INTO ... FROM FILE`;
     - DML without SQL options keeps the request options;
     - DML with `OPTION(...)` fails to parse when the legacy syntax is ignored, 
while `SET` keeps working.
   - `CalciteSqlCompilerTest#testDeleteIsClassifiedAsDml`: classification, SET 
options, target table and condition, and
     a DELETE cannot be combined with another executable statement.
   - `PinotQueryResourceTest#testDmlOnGetQueryEndpointReturnsValidationError` 
and
     `#testDmlOnPostQueryEndpointIsExecuted`: GET /sql rejects DELETE and 
INSERT INTO FILE; POST /sql dispatches DELETE
     to the DML executor.
   - These pass, along with `RequestUtilsTest`, `InsertIntoFileTest`, 
`PinotDdlParserTest`,
     `BaseSingleStageBrokerRequestHandlerTest` and `PinotClientRequestTest`. 
Checkstyle, spotless and license checks
     pass.
   - A downstream executor that overrides `executeDelete` and 
`createSqlQueryExecutor` runs end to end against this
     change: `DELETE` through the broker and the controller, dry run, repeated 
deletes.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   
   https://claude.ai/code/session_019Gm6pH91CjfJQ4kWdQBLac
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to