mirror of
https://github.com/rqlite/rqlite.git
synced 2026-01-25 04:16:26 +00:00
Support EXPLAIN INSERT
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
## v9.3.11 (January 6th 2026)
|
||||
### Implementation changes and bug fixes
|
||||
- [PR #2438](https://github.com/rqlite/rqlite/pull/2438): Correctly handle `EXPLAIN QUERY PLAN` for mutations. Fixes issue [#2433](https://github.com/rqlite/rqlite/issues/2433).
|
||||
- [PR #2436](https://github.com/rqlite/rqlite/pull/2436): Add unit testing for EXPLAIN SELECT at DB level.
|
||||
|
||||
## v9.3.10 (January 5th 2026)
|
||||
### Implementation changes and bug fixes
|
||||
- [PR #2427](https://github.com/rqlite/rqlite/pull/2427): Remove any temporary WAL files if persisting a Snapshot fails or is not even invoked.
|
||||
|
||||
@@ -440,6 +440,7 @@ type Statement struct {
|
||||
Parameters []*Parameter `protobuf:"bytes,2,rep,name=parameters,proto3" json:"parameters,omitempty"`
|
||||
ForceQuery bool `protobuf:"varint,3,opt,name=forceQuery,proto3" json:"forceQuery,omitempty"`
|
||||
ForceStall bool `protobuf:"varint,4,opt,name=forceStall,proto3" json:"forceStall,omitempty"`
|
||||
SqlExplain bool `protobuf:"varint,5,opt,name=sql_explain,json=sqlExplain,proto3" json:"sql_explain,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -502,6 +503,13 @@ func (x *Statement) GetForceStall() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Statement) GetSqlExplain() bool {
|
||||
if x != nil {
|
||||
return x.SqlExplain
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Transaction bool `protobuf:"varint,1,opt,name=transaction,proto3" json:"transaction,omitempty"`
|
||||
@@ -2094,7 +2102,7 @@ const file_command_proto_rawDesc = "" +
|
||||
"\x01y\x18\x04 \x01(\fH\x00R\x01y\x12\x0e\n" +
|
||||
"\x01s\x18\x05 \x01(\tH\x00R\x01s\x12\x12\n" +
|
||||
"\x04name\x18\x06 \x01(\tR\x04nameB\a\n" +
|
||||
"\x05value\"\x91\x01\n" +
|
||||
"\x05value\"\xb2\x01\n" +
|
||||
"\tStatement\x12\x10\n" +
|
||||
"\x03sql\x18\x01 \x01(\tR\x03sql\x122\n" +
|
||||
"\n" +
|
||||
@@ -2105,7 +2113,9 @@ const file_command_proto_rawDesc = "" +
|
||||
"forceQuery\x12\x1e\n" +
|
||||
"\n" +
|
||||
"forceStall\x18\x04 \x01(\bR\n" +
|
||||
"forceStall\"\xa7\x01\n" +
|
||||
"forceStall\x12\x1f\n" +
|
||||
"\vsql_explain\x18\x05 \x01(\bR\n" +
|
||||
"sqlExplain\"\xa7\x01\n" +
|
||||
"\aRequest\x12 \n" +
|
||||
"\vtransaction\x18\x01 \x01(\bR\vtransaction\x122\n" +
|
||||
"\n" +
|
||||
|
||||
@@ -20,6 +20,7 @@ message Statement {
|
||||
repeated Parameter parameters = 2;
|
||||
bool forceQuery = 3;
|
||||
bool forceStall = 4;
|
||||
bool sql_explain = 5;
|
||||
}
|
||||
|
||||
message Request {
|
||||
|
||||
@@ -51,13 +51,15 @@ func Process(stmts []*proto.Statement, rwrand, rwtime bool) (retErr error) {
|
||||
lowered := strings.ToLower(stmts[i].Sql)
|
||||
if (!rwtime || !ContainsTime(lowered)) &&
|
||||
(!rwrand || !ContainsRandom(lowered)) &&
|
||||
!ContainsReturning(lowered) {
|
||||
!ContainsReturning(lowered) &&
|
||||
!ContainsExplain(lowered) {
|
||||
continue
|
||||
}
|
||||
parsed, err := rsql.NewParser(strings.NewReader(stmts[i].Sql)).ParseStatement()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, stmts[i].SqlExplain = parsed.(*sql.ExplainStatement)
|
||||
rewriter := NewRewriter()
|
||||
rewriter.RewriteRand = rwrand
|
||||
rewriter.RewriteTime = rwtime
|
||||
@@ -110,6 +112,13 @@ func ContainsReturning(stmt string) bool {
|
||||
return strings.Contains(stmt, "returning ")
|
||||
}
|
||||
|
||||
// ContainsExplain returns true if the statement contains an EXPLAIN clause.
|
||||
// The function performs a lower-case comparison so it is up to the caller to
|
||||
// ensure the statement is lower-cased.
|
||||
func ContainsExplain(stmt string) bool {
|
||||
return strings.Contains(stmt, "explain ")
|
||||
}
|
||||
|
||||
// Rewriter rewrites SQL statements.
|
||||
type Rewriter struct {
|
||||
RewriteRand bool
|
||||
|
||||
2
db/db.go
2
db/db.go
@@ -1168,7 +1168,7 @@ func (db *DB) queryWithConn(ctx context.Context, req *command.Request, xTime boo
|
||||
allRows = append(allRows, rows)
|
||||
continue
|
||||
}
|
||||
if !readOnly {
|
||||
if !readOnly && !stmt.SqlExplain {
|
||||
stats.Add(numQueryErrors, 1)
|
||||
rows = &command.QueryRows{
|
||||
Error: "attempt to change database via query operation",
|
||||
|
||||
@@ -1443,16 +1443,13 @@ func (s *Service) handleQuery(w http.ResponseWriter, r *http.Request, qp QueryPa
|
||||
}
|
||||
stats.Add(numQueryStmtsRx, int64(len(queries)))
|
||||
|
||||
// No point rewriting queries if they don't go through the Raft log, since they
|
||||
// will never be replayed from the log anyway.
|
||||
if qp.Level() == proto.ConsistencyLevel_STRONG {
|
||||
if !qp.NoParse() {
|
||||
fmt.Println("Processing queries:", queries)
|
||||
if err := sql.Process(queries, qp.NoRewriteRandom(), !qp.NoRewriteTime()); err != nil {
|
||||
http.Error(w, fmt.Sprintf("SQL rewrite: %s", err.Error()), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp := NewResponse()
|
||||
resp.Results.AssociativeJSON = qp.Associative()
|
||||
|
||||
@@ -2153,13 +2153,17 @@ func (s *Store) Noop(id string) (raft.ApplyFuture, error) {
|
||||
}
|
||||
|
||||
// RORWCount returns the number of read-only and read-write statements in the
|
||||
// given ExecuteQueryRequest.
|
||||
// given ExecuteQueryRequest. EXPLAIN statements are always considered read-only.
|
||||
func (s *Store) RORWCount(eqr *proto.ExecuteQueryRequest) (nRW, nRO int) {
|
||||
for _, stmt := range eqr.Request.Statements {
|
||||
sql := stmt.Sql
|
||||
if sql == "" {
|
||||
continue
|
||||
}
|
||||
if stmt.SqlExplain {
|
||||
nRO++
|
||||
continue
|
||||
}
|
||||
ro, err := s.db.StmtReadOnly(sql)
|
||||
if err == nil && ro {
|
||||
nRO++
|
||||
|
||||
@@ -759,6 +759,78 @@ func Test_SingleNodeExecuteQuery_Linearizable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SingleNodeExecuteQuery_EXPLAIN(t *testing.T) {
|
||||
s, ln := mustNewStore(t)
|
||||
defer ln.Close()
|
||||
|
||||
if err := s.Open(); err != nil {
|
||||
t.Fatalf("failed to open single-node store: %s", err.Error())
|
||||
}
|
||||
if err := s.Bootstrap(NewServer(s.ID(), s.Addr(), true)); err != nil {
|
||||
t.Fatalf("failed to bootstrap single-node store: %s", err.Error())
|
||||
}
|
||||
defer s.Close(true)
|
||||
_, err := s.WaitForLeader(10 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Error waiting for leader: %s", err)
|
||||
}
|
||||
|
||||
er := executeRequestFromString(`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
|
||||
false, false)
|
||||
_, _, err = s.Execute(er)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to execute on single node: %s", err.Error())
|
||||
}
|
||||
|
||||
// Simple read-only statement.
|
||||
eqr := executeQueryRequestFromString("EXPLAIN QUERY PLAN SELECT * FROM foo",
|
||||
proto.ConsistencyLevel_WEAK, false, false)
|
||||
eqr.Request.Statements[0].SqlExplain = true
|
||||
resp, _, _, err := s.Request(eqr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to perform EXPLAIN SELECT on single node: %s", err.Error())
|
||||
}
|
||||
if !strings.Contains(asJSON(resp), "SCAN foo") { // Simple check that it looks right
|
||||
t.Fatalf("unexpected results for EXPLAIN QUERY PLAN\ngot: %s", asJSON(resp))
|
||||
}
|
||||
|
||||
// SQLite C code considers this a read-write statement so check that
|
||||
// the Store handles this by converting to query.
|
||||
eqr = executeQueryRequestFromString(`EXPLAIN QUERY PLAN INSERT INTO foo(name) VALUES("fiona")`,
|
||||
proto.ConsistencyLevel_WEAK, false, false)
|
||||
eqr.Request.Statements[0].SqlExplain = true
|
||||
resp, _, _, err = s.Request(eqr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to perform EXPLAIN INSERT on single node: %s", err.Error())
|
||||
}
|
||||
if !strings.Contains(asJSON(resp), "columns") { // Simple check that it looks right
|
||||
t.Fatalf("unexpected results for EXPLAIN QUERY PLAN\ngot: %s", asJSON(resp))
|
||||
}
|
||||
|
||||
// Check that EXPLAIN sent directory to query endpoint also works OK.
|
||||
qr := queryRequestFromString("EXPLAIN QUERY PLAN SELECT * FROM foo", false, false)
|
||||
qr.Level = proto.ConsistencyLevel_WEAK
|
||||
qr.Request.Statements[0].SqlExplain = true
|
||||
rows, _, _, err := s.Query(qr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to perform EXPLAIN SELECT on single node: %s", err.Error())
|
||||
}
|
||||
if !strings.Contains(asJSON(rows), "SCAN foo") { // Simple check that it looks right
|
||||
t.Fatalf("unexpected results for EXPLAIN QUERY PLAN\ngot: %s", asJSON(rows))
|
||||
}
|
||||
|
||||
qr = queryRequestFromString(`EXPLAIN QUERY PLAN INSERT INTO foo(name) VALUES("fiona")`, false, false)
|
||||
qr.Level = proto.ConsistencyLevel_WEAK
|
||||
qr.Request.Statements[0].SqlExplain = true
|
||||
rows, _, _, err = s.Query(qr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to perform EXPLAIN SELECT on single node: %s", err.Error())
|
||||
}
|
||||
if !strings.Contains(asJSON(rows), "columns") { // Simple check that it looks right
|
||||
t.Fatalf("unexpected results for EXPLAIN QUERY PLAN\ngot: %s", asJSON(rows))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SingleNodeExecuteQuery_RETURNING(t *testing.T) {
|
||||
s, ln := mustNewStore(t)
|
||||
defer ln.Close()
|
||||
|
||||
@@ -942,6 +942,32 @@ func Test_SingleNode_RETURNING_KeywordAsIdent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SingleNode_EXPLAIN(t *testing.T) {
|
||||
node := mustNewLeaderNode("leader1")
|
||||
defer node.Deprovision()
|
||||
|
||||
_, err := node.Execute(`CREATE TABLE foo (id integer not null primary key, name text)`)
|
||||
if err != nil {
|
||||
t.Fatalf(`CREATE TABLE failed: %s`, err.Error())
|
||||
}
|
||||
|
||||
res, err := node.Query(`EXPLAIN QUERY PLAN INSERT INTO foo(name) VALUES("declan")`)
|
||||
if err != nil {
|
||||
t.Fatalf(`EXPLAIN failed: %s`, err.Error())
|
||||
}
|
||||
if !strings.Contains(res, "notused") {
|
||||
t.Fatalf("EXPLAIN result does not appear valid: %s", res)
|
||||
}
|
||||
|
||||
res, err = node.Query(`EXPLAIN QUERY PLAN SELECT * FROM foo`)
|
||||
if err != nil {
|
||||
t.Fatalf(`EXPLAIN failed: %s`, err.Error())
|
||||
}
|
||||
if !strings.Contains(res, "notused") {
|
||||
t.Fatalf("EXPLAIN result does not appear valid: %s", res)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SingleNodeQueued(t *testing.T) {
|
||||
node := mustNewLeaderNode("leader1")
|
||||
defer node.Deprovision()
|
||||
|
||||
Reference in New Issue
Block a user