feat(resolvemcp): replace per-model tools with fixed meta tools, guarded filter writes and a function registry

Tools: list_tables, describe_table, select_table, insert_into_table, update_table,
delete_from_table, list_functions, call_function. Visibility follows the model rules.
Filter-based update/delete require filters (never dropped silently), cap the matched rows
(MaxWriteRows), support dry_run, and need a single-use confirm token bound to caller, table,
filters, data and the matched rows. RegisterFunction adds Go-callback and SQL-procedure
functions run in a transaction with BeforeCall/AfterCall hooks. Per-model tools and
resources are removed.

fix(pgsql): UPDATE with SET and a multi-placeholder WHERE renumbered the WHERE parameters
wrongly ($1, $2 became $3, $2); shift them in one pass.
This commit is contained in:
Hein
2026-10-01 13:40:00 +02:00
parent 276c3814d8
commit e49c3a916e
11 changed files with 1562 additions and 370 deletions
+13 -14
View File
@@ -5,7 +5,9 @@ import (
"database/sql"
"fmt"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -767,6 +769,9 @@ func (p *PgSQLInsertQuery) Scan(ctx context.Context, dest interface{}) (err erro
return nil
}
// placeholderRe matches a numbered SQL parameter such as $12.
var placeholderRe = regexp.MustCompile(`\$\d+`)
// PgSQLUpdateQuery implements UpdateQuery for PostgreSQL
type PgSQLUpdateQuery struct {
db *sql.DB
@@ -897,23 +902,17 @@ func (p *PgSQLUpdateQuery) Exec(ctx context.Context) (res common.Result, err err
p.tableName,
strings.Join(setClauses, ", "))
// Update WHERE clause parameter numbers to continue after SET parameters
// WHERE placeholders were numbered from $1 as the clauses were added; shift every one past
// the SET parameters in a single pass (replacing one number at a time would rewrite a
// number it had just produced, e.g. "$1, $2" -> "$3, $2").
if len(p.whereClauses) > 0 {
shift := len(setArgs)
updatedWhereClauses := make([]string, 0, len(p.whereClauses))
for _, whereClause := range p.whereClauses {
// Find and replace parameter placeholders
updatedClause := whereClause
paramNum := i
// Count how many parameters are in this WHERE clause
placeholderCount := strings.Count(whereClause, "$")
for j := 0; j < placeholderCount; j++ {
oldParam := fmt.Sprintf("$%d", j+1)
newParam := fmt.Sprintf("$%d", paramNum)
updatedClause = strings.Replace(updatedClause, oldParam, newParam, 1)
paramNum++
}
updatedWhereClauses = append(updatedWhereClauses, updatedClause)
i = paramNum
updatedWhereClauses = append(updatedWhereClauses, placeholderRe.ReplaceAllStringFunc(whereClause, func(m string) string {
n, _ := strconv.Atoi(m[1:])
return fmt.Sprintf("$%d", n+shift)
}))
}
p.whereClauses = updatedWhereClauses
}
@@ -627,3 +627,23 @@ func TestRawSQL(t *testing.T) {
assert.NoError(t, mock.ExpectationsWereMet())
}
// WHERE placeholders must be shifted past the SET parameters without rewriting numbers the
// shift itself produced: "a = ? AND b = ?" must stay in order.
func TestPgSQLUpdateQuery_WherePlaceholdersAfterSet(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
mock.ExpectExec(`UPDATE users SET name = \$1 WHERE a = \$2 AND b = \$3 AND "id" IN \(\$4, \$5\)`).
WithArgs("n", 10, 20, 7, 8).
WillReturnResult(sqlmock.NewResult(0, 2))
adapter := NewPgSQLAdapter(db)
_, err = adapter.NewUpdate().Table("users").SetMap(map[string]interface{}{"name": "n"}).
Where("a = ? AND b = ?", 10, 20).
Where(`"id" IN (?, ?)`, 7, 8).
Exec(context.Background())
require.NoError(t, err)
require.NoError(t, mock.ExpectationsWereMet())
}