From 6d958871f08c1c9d018ef380e1d892cec6fe3319 Mon Sep 17 00:00:00 2001
From: Ajeet D'Souza <98ajeet@gmail.com>
Date: Thu, 27 Mar 2025 01:42:28 +0530
Subject: [PATCH] Add rowid column to FTS5 tables

---
 .../virtual_table/sqlite/go/models.go         |  8 ++--
 .../virtual_table/sqlite/go/query.sql.go      | 39 +++++++++++++++----
 .../testdata/virtual_table/sqlite/query.sql   |  4 ++
 internal/engine/sqlite/convert.go             |  7 ++++
 4 files changed, 47 insertions(+), 11 deletions(-)

diff --git a/internal/endtoend/testdata/virtual_table/sqlite/go/models.go b/internal/endtoend/testdata/virtual_table/sqlite/go/models.go
index 12823070a0..f85063a5e7 100644
--- a/internal/endtoend/testdata/virtual_table/sqlite/go/models.go
+++ b/internal/endtoend/testdata/virtual_table/sqlite/go/models.go
@@ -9,7 +9,8 @@ import (
 )
 
 type Ft struct {
-	B string
+	Rowid int64
+	B     string
 }
 
 type Tbl struct {
@@ -21,6 +22,7 @@ type Tbl struct {
 }
 
 type TblFt struct {
-	B string
-	C string
+	Rowid int64
+	B     string
+	C     string
 }
diff --git a/internal/endtoend/testdata/virtual_table/sqlite/go/query.sql.go b/internal/endtoend/testdata/virtual_table/sqlite/go/query.sql.go
index 903d99641f..2a927f90ab 100644
--- a/internal/endtoend/testdata/virtual_table/sqlite/go/query.sql.go
+++ b/internal/endtoend/testdata/virtual_table/sqlite/go/query.sql.go
@@ -65,15 +65,20 @@ SELECT b, c FROM tbl_ft
 WHERE b MATCH ?
 `
 
-func (q *Queries) SelectAllColsTblFt(ctx context.Context, b string) ([]TblFt, error) {
+type SelectAllColsTblFtRow struct {
+	B string
+	C string
+}
+
+func (q *Queries) SelectAllColsTblFt(ctx context.Context, b string) ([]SelectAllColsTblFtRow, error) {
 	rows, err := q.db.QueryContext(ctx, selectAllColsTblFt, b)
 	if err != nil {
 		return nil, err
 	}
 	defer rows.Close()
-	var items []TblFt
+	var items []SelectAllColsTblFtRow
 	for rows.Next() {
-		var i TblFt
+		var i SelectAllColsTblFtRow
 		if err := rows.Scan(&i.B, &i.C); err != nil {
 			return nil, err
 		}
@@ -89,14 +94,15 @@ func (q *Queries) SelectAllColsTblFt(ctx context.Context, b string) ([]TblFt, er
 }
 
 const selectBm25Func = `-- name: SelectBm25Func :many
-SELECT b, c, bm25(tbl_ft, 2.0) FROM tbl_ft
+SELECT rowid, b, c, bm25(tbl_ft, 2.0) FROM tbl_ft
 WHERE b MATCH ? ORDER BY bm25(tbl_ft)
 `
 
 type SelectBm25FuncRow struct {
-	B    string
-	C    string
-	Bm25 float64
+	Rowid int64
+	B     string
+	C     string
+	Bm25  float64
 }
 
 func (q *Queries) SelectBm25Func(ctx context.Context, b string) ([]SelectBm25FuncRow, error) {
@@ -108,7 +114,12 @@ func (q *Queries) SelectBm25Func(ctx context.Context, b string) ([]SelectBm25Fun
 	var items []SelectBm25FuncRow
 	for rows.Next() {
 		var i SelectBm25FuncRow
-		if err := rows.Scan(&i.B, &i.C, &i.Bm25); err != nil {
+		if err := rows.Scan(
+			&i.Rowid,
+			&i.B,
+			&i.C,
+			&i.Bm25,
+		); err != nil {
 			return nil, err
 		}
 		items = append(items, i)
@@ -206,6 +217,18 @@ func (q *Queries) SelectOneColTblFt(ctx context.Context, b string) ([]string, er
 	return items, nil
 }
 
+const selectRowID = `-- name: SelectRowID :one
+SELECT rowid FROM ft
+LIMIT 1
+`
+
+func (q *Queries) SelectRowID(ctx context.Context) (int64, error) {
+	row := q.db.QueryRowContext(ctx, selectRowID)
+	var rowid int64
+	err := row.Scan(&rowid)
+	return rowid, err
+}
+
 const selectSnippetFunc = `-- name: SelectSnippetFunc :many
 SELECT snippet(tbl_ft, 0, '<b>', '</b>', 'aa', ?) FROM tbl_ft
 `
diff --git a/internal/endtoend/testdata/virtual_table/sqlite/query.sql b/internal/endtoend/testdata/virtual_table/sqlite/query.sql
index ad8eeeae40..7fea42ae0a 100644
--- a/internal/endtoend/testdata/virtual_table/sqlite/query.sql
+++ b/internal/endtoend/testdata/virtual_table/sqlite/query.sql
@@ -1,3 +1,7 @@
+-- name: SelectRowID :one
+SELECT rowid FROM ft
+LIMIT 1;
+
 -- name: SelectAllColsFt :many
 SELECT b FROM ft
 WHERE b MATCH ?;
diff --git a/internal/engine/sqlite/convert.go b/internal/engine/sqlite/convert.go
index 29c06fb285..64d0ea4c32 100644
--- a/internal/engine/sqlite/convert.go
+++ b/internal/engine/sqlite/convert.go
@@ -146,6 +146,13 @@ func (c *cc) convertCreate_virtual_table_fts5(n *parser.Create_virtual_table_stm
 		IfNotExists: n.EXISTS_() != nil,
 	}
 
+	// All FTS5 virtual tables implicitly contain a 'rowid' column.
+	stmt.Cols = append(stmt.Cols, &ast.ColumnDef{
+		Colname:   "rowid",
+		IsNotNull: true,
+		TypeName:  &ast.TypeName{Name: "integer"},
+	})
+
 	for _, arg := range n.AllModule_argument() {
 		var columnName string