Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rqlite for openSUSE:Factory checked in at 2026-01-08 15:27:08 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rqlite (Old) and /work/SRC/openSUSE:Factory/.rqlite.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rqlite" Thu Jan 8 15:27:08 2026 rev:39 rq:1325841 version:9.3.11 Changes: -------- --- /work/SRC/openSUSE:Factory/rqlite/rqlite.changes 2026-01-06 17:46:59.095551933 +0100 +++ /work/SRC/openSUSE:Factory/.rqlite.new.1928/rqlite.changes 2026-01-08 15:28:28.791685656 +0100 @@ -1,0 +2,6 @@ +Wed Jan 07 19:06:58 UTC 2026 - Andreas Stieger <[email protected]> + +- Update to version 9.3.11: + * Correctly handle EXPLAIN QUERY PLAN for mutations + +------------------------------------------------------------------- Old: ---- rqlite-9.3.10.tar.xz New: ---- rqlite-9.3.11.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rqlite.spec ++++++ --- /var/tmp/diff_new_pack.ly14fK/_old 2026-01-08 15:28:29.351708902 +0100 +++ /var/tmp/diff_new_pack.ly14fK/_new 2026-01-08 15:28:29.351708902 +0100 @@ -17,7 +17,7 @@ Name: rqlite -Version: 9.3.10 +Version: 9.3.11 Release: 0 Summary: Distributed relational database built on SQLite License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.ly14fK/_old 2026-01-08 15:28:29.387710397 +0100 +++ /var/tmp/diff_new_pack.ly14fK/_new 2026-01-08 15:28:29.391710563 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/rqlite/rqlite.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v9.3.10</param> + <param name="revision">v9.3.11</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.ly14fK/_old 2026-01-08 15:28:29.427712057 +0100 +++ /var/tmp/diff_new_pack.ly14fK/_new 2026-01-08 15:28:29.431712223 +0100 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rqlite/rqlite.git</param> - <param name="changesrevision">fdd7ac15bf191ab7c850e60e5cd928484553c57a</param> + <param name="changesrevision">9589c3caa5da939fb3e2bbb143e0b06b301ac1dd</param> </service> </servicedata> (No newline at EOF) ++++++ rqlite-9.3.10.tar.xz -> rqlite-9.3.11.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/CHANGELOG.md new/rqlite-9.3.11/CHANGELOG.md --- old/rqlite-9.3.10/CHANGELOG.md 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/CHANGELOG.md 2026-01-07 05:16:04.000000000 +0100 @@ -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. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/command/proto/command.pb.go new/rqlite-9.3.11/command/proto/command.pb.go --- old/rqlite-9.3.10/command/proto/command.pb.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/command/proto/command.pb.go 2026-01-07 05:16:04.000000000 +0100 @@ -440,6 +440,7 @@ 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 @@ 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 @@ "\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 @@ "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" + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/command/proto/command.proto new/rqlite-9.3.11/command/proto/command.proto --- old/rqlite-9.3.10/command/proto/command.proto 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/command/proto/command.proto 2026-01-07 05:16:04.000000000 +0100 @@ -20,6 +20,7 @@ repeated Parameter parameters = 2; bool forceQuery = 3; bool forceStall = 4; + bool sql_explain = 5; } message Request { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/command/sql/processor.go new/rqlite-9.3.11/command/sql/processor.go --- old/rqlite-9.3.10/command/sql/processor.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/command/sql/processor.go 2026-01-07 05:16:04.000000000 +0100 @@ -51,13 +51,15 @@ 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 @@ 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/db/db.go new/rqlite-9.3.11/db/db.go --- old/rqlite-9.3.10/db/db.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/db/db.go 2026-01-07 05:16:04.000000000 +0100 @@ -1168,7 +1168,7 @@ 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", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/db/db_common_test.go new/rqlite-9.3.11/db/db_common_test.go --- old/rqlite-9.3.10/db/db_common_test.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/db/db_common_test.go 2026-01-07 05:16:04.000000000 +0100 @@ -106,6 +106,64 @@ } } +func Test_DB_ExplainSelect(t *testing.T) { + db, path := mustCreateOnDiskDatabaseWAL() + defer os.Remove(path) + defer db.Close() + + // Create a table + r, err := db.ExecuteStringStmt("CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)") + if err != nil { + t.Fatalf("failed to create table: %s", err.Error()) + } + if exp, got := `[{}]`, asJSON(r); exp != got { + t.Fatalf("unexpected results for create table, expected %s, got %s", exp, got) + } + + // Insert a record + r, err = db.ExecuteStringStmt(`INSERT INTO foo(id, name) VALUES(1, "test")`) + if err != nil { + t.Fatalf("failed to insert record: %s", err.Error()) + } + if exp, got := `[{"last_insert_id":1,"rows_affected":1}]`, asJSON(r); exp != got { + t.Fatalf("unexpected results for insert, expected %s, got %s", exp, got) + } + + // Execute EXPLAIN SELECT + q, err := db.QueryStringStmt("EXPLAIN SELECT * FROM foo") + if err != nil { + t.Fatalf("failed to execute EXPLAIN SELECT: %s", err.Error()) + } + + // Check that we got results with expected columns + if len(q) == 0 { + t.Fatalf("EXPLAIN SELECT returned no results") + } + if len(q[0].Columns) == 0 { + t.Fatalf("EXPLAIN SELECT returned no columns") + } + + // Verify that columns include typical EXPLAIN columns (like "addr", "opcode", etc.) + columns := q[0].Columns + hasAddr := false + hasOpcode := false + hasComment := false + for _, col := range columns { + if col == "addr" { + hasAddr = true + } + if col == "opcode" { + hasOpcode = true + } + if col == "comment" { + hasComment = true + } + } + if !hasAddr || !hasOpcode || !hasComment { + t.Fatalf("EXPLAIN SELECT did not return expected columns (addr, opcode), got: %v", columns) + } +} + func Test_DB_TableCreationFTS(t *testing.T) { db, path := mustCreateOnDiskDatabaseWAL() defer os.Remove(path) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/http/service.go new/rqlite-9.3.11/http/service.go --- old/rqlite-9.3.10/http/service.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/http/service.go 2026-01-07 05:16:04.000000000 +0100 @@ -1443,14 +1443,11 @@ } 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() { - if err := sql.Process(queries, qp.NoRewriteRandom(), !qp.NoRewriteTime()); err != nil { - http.Error(w, fmt.Sprintf("SQL rewrite: %s", err.Error()), http.StatusInternalServerError) - return - } + 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 } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/store/store.go new/rqlite-9.3.11/store/store.go --- old/rqlite-9.3.10/store/store.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/store/store.go 2026-01-07 05:16:04.000000000 +0100 @@ -2153,13 +2153,17 @@ } // 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++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/store/store_test.go new/rqlite-9.3.11/store/store_test.go --- old/rqlite-9.3.10/store/store_test.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/store/store_test.go 2026-01-07 05:16:04.000000000 +0100 @@ -759,6 +759,78 @@ } } +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() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.3.10/system_test/single_node_test.go new/rqlite-9.3.11/system_test/single_node_test.go --- old/rqlite-9.3.10/system_test/single_node_test.go 2026-01-05 14:44:28.000000000 +0100 +++ new/rqlite-9.3.11/system_test/single_node_test.go 2026-01-07 05:16:04.000000000 +0100 @@ -942,6 +942,32 @@ } } +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() ++++++ vendor.tar.xz ++++++
