registry/internal/database/postgres.go
nate nowack 72f9f80d68
perf(db): use pk id for lookups and ordering (#390)
summary
- use the indexed primary key (`id`) for `getbyid`, `updateserver`, and
list ordering/cursor, instead of extracting `_meta...id` from jsonb
- in `updateserver`, error if the body tries to change the id; set
`meta.official.id` when missing

changes
- database: `getbyid` → `where id = $1`
- database: `updateserver` → `where id = $2`; fail on id mismatch; set
id if missing
- database: `list` orders by `id` and paginates via `id > $cursor`
- no schema or api changes

<details>
<summary>performance</summary>

- env: repo docker compose postgres, 5k–10k rows, warm cache
- getbyid: `id` ≈ 0.02 ms vs jsonb path ≈ 0.80 ms
- update: `id` ≈ 0.15 ms vs jsonb path ≈ 0.78 ms
- list (limit 100): order by `id` ≈ 0.13 ms vs jsonb path id ≈ 2.24 ms
(~17x slower)

</details>

<details>
<summary>explain analyze snippets</summary>

getbyid by `id`
```
Index Scan using idx_benchmark_servers_id on benchmark_servers (actual time≈0.01–0.02 ms)
Buffers: shared hit≈3
```

getbyid by jsonb path
```
Seq Scan on benchmark_servers (actual time≈0.43–0.83 ms)
Filter: ((value->'_meta'->'io.modelcontextprotocol.registry/official'->>'id') = $1)
Rows Removed by Filter: ≈4999
Buffers: shared hit≈455
```

update by `id`
```
Update on benchmark_servers …
-> Index Scan using idx_benchmark_servers_id … (actual time≈0.007 ms)
Execution Time: ≈0.15 ms
```

update by jsonb path
```
Update on benchmark_servers …
-> Seq Scan on benchmark_servers … Rows Removed by Filter: ≈4999
Execution Time: ≈0.78 ms
```
</details>

<details>
<summary>reproduce quickly</summary>

- start postgres: `docker compose up -d postgres`
- create a simple table (id varchar pk, value jsonb) with several
thousand rows
- compare plans: `explain (analyze, buffers) select value from … where
id = $1` vs the jsonb path predicate
</details>

---------

Co-authored-by: Adam Jones <adamj@anthropic.com>
2025-09-09 08:10:55 -07:00

266 lines
7 KiB
Go

package database
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
)
// PostgreSQL is an implementation of the Database interface using PostgreSQL
type PostgreSQL struct {
conn *pgx.Conn
}
// NewPostgreSQL creates a new instance of the PostgreSQL database
func NewPostgreSQL(ctx context.Context, connectionURI string) (*PostgreSQL, error) {
conn, err := pgx.Connect(ctx, connectionURI)
if err != nil {
return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err)
}
// Test the connection
if err = conn.Ping(ctx); err != nil {
return nil, fmt.Errorf("failed to ping PostgreSQL: %w", err)
}
// Run migrations
migrator := NewMigrator(conn)
if err := migrator.Migrate(ctx); err != nil {
return nil, fmt.Errorf("failed to run database migrations: %w", err)
}
return &PostgreSQL{
conn: conn,
}, nil
}
//nolint:cyclop // Database filtering logic is inherently complex but clear
func (db *PostgreSQL) List(
ctx context.Context,
filter *ServerFilter,
cursor string,
limit int,
) ([]*apiv0.ServerJSON, string, error) {
if limit <= 0 {
limit = 10
}
if ctx.Err() != nil {
return nil, "", ctx.Err()
}
// Build WHERE clause for filtering
var whereConditions []string
args := []any{}
argIndex := 1
// Add filters using JSON operators
if filter != nil {
if filter.Name != nil {
whereConditions = append(whereConditions, fmt.Sprintf("value->>'name' = $%d", argIndex))
args = append(args, *filter.Name)
argIndex++
}
if filter.RemoteURL != nil {
whereConditions = append(whereConditions, fmt.Sprintf("EXISTS (SELECT 1 FROM jsonb_array_elements(value->'remotes') AS remote WHERE remote->>'url' = $%d)", argIndex))
args = append(args, *filter.RemoteURL)
argIndex++
}
if filter.UpdatedSince != nil {
whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry/official'->>'updated_at')::timestamp > $%d", argIndex))
args = append(args, *filter.UpdatedSince)
argIndex++
}
if filter.SubstringName != nil {
whereConditions = append(whereConditions, fmt.Sprintf("value->>'name' ILIKE $%d", argIndex))
args = append(args, "%"+*filter.SubstringName+"%")
argIndex++
}
if filter.Version != nil {
whereConditions = append(whereConditions, fmt.Sprintf("(value->'version_detail'->>'version') = $%d", argIndex))
args = append(args, *filter.Version)
argIndex++
}
if filter.IsLatest != nil {
whereConditions = append(whereConditions, fmt.Sprintf("(value->'_meta'->'io.modelcontextprotocol.registry/official'->>'is_latest')::boolean = $%d", argIndex))
args = append(args, *filter.IsLatest)
argIndex++
}
}
// Add cursor pagination using primary key ID
if cursor != "" {
if _, err := uuid.Parse(cursor); err != nil {
return nil, "", fmt.Errorf("invalid cursor format: %w", err)
}
whereConditions = append(whereConditions, fmt.Sprintf("id > $%d", argIndex))
args = append(args, cursor)
argIndex++
}
// Build the WHERE clause
whereClause := ""
if len(whereConditions) > 0 {
whereClause = "WHERE " + strings.Join(whereConditions, " AND ")
}
// Simple query on servers table
query := fmt.Sprintf(`
SELECT value
FROM servers
%s
ORDER BY id
LIMIT $%d
`, whereClause, argIndex)
args = append(args, limit)
rows, err := db.conn.Query(ctx, query, args...)
if err != nil {
return nil, "", fmt.Errorf("failed to query servers: %w", err)
}
defer rows.Close()
var results []*apiv0.ServerJSON
for rows.Next() {
var valueJSON []byte
err := rows.Scan(&valueJSON)
if err != nil {
return nil, "", fmt.Errorf("failed to scan server row: %w", err)
}
// Parse the complete ServerJSON from JSONB
var serverJSON apiv0.ServerJSON
if err := json.Unmarshal(valueJSON, &serverJSON); err != nil {
return nil, "", fmt.Errorf("failed to unmarshal server JSON: %w", err)
}
results = append(results, &serverJSON)
}
if err := rows.Err(); err != nil {
return nil, "", fmt.Errorf("error iterating rows: %w", err)
}
// Determine next cursor using registry metadata ID
nextCursor := ""
if len(results) > 0 && len(results) >= limit {
lastResult := results[len(results)-1]
if lastResult.Meta != nil && lastResult.Meta.Official != nil {
nextCursor = lastResult.Meta.Official.ID
}
}
return results, nextCursor, nil
}
func (db *PostgreSQL) GetByID(ctx context.Context, id string) (*apiv0.ServerJSON, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
query := `
SELECT value
FROM servers
WHERE id = $1
`
var valueJSON []byte
err := db.conn.QueryRow(ctx, query, id).Scan(&valueJSON)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to get server by ID: %w", err)
}
// Parse the complete ServerJSON from JSONB
var serverJSON apiv0.ServerJSON
if err := json.Unmarshal(valueJSON, &serverJSON); err != nil {
return nil, fmt.Errorf("failed to unmarshal server JSON: %w", err)
}
return &serverJSON, nil
}
// CreateServer adds a new server to the database
func (db *PostgreSQL) CreateServer(ctx context.Context, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
// Get the ID from the registry metadata
if server.Meta == nil || server.Meta.Official == nil {
return nil, fmt.Errorf("server must have registry metadata with ID")
}
id := server.Meta.Official.ID
// Marshal the complete server to JSONB
valueJSON, err := json.Marshal(server)
if err != nil {
return nil, fmt.Errorf("failed to marshal server JSON: %w", err)
}
// Insert into simple servers table
query := `
INSERT INTO servers (id, value)
VALUES ($1, $2)
`
_, err = db.conn.Exec(ctx, query, id, valueJSON)
if err != nil {
return nil, fmt.Errorf("failed to insert server: %w", err)
}
return server, nil
}
// UpdateServer updates an existing server record with new server details
func (db *PostgreSQL) UpdateServer(ctx context.Context, id string, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
// Validate that meta structure exists and ID matches path
if server.Meta == nil || server.Meta.Official == nil || server.Meta.Official.ID != id {
return nil, fmt.Errorf("%w: io.modelcontextprotocol.registry/official.id must match path id (%s)", ErrInvalidInput, id)
}
// Marshal updated server
valueJSON, err := json.Marshal(server)
if err != nil {
return nil, fmt.Errorf("failed to marshal updated server: %w", err)
}
// Update the complete server record in simple table
query := `
UPDATE servers
SET value = $1
WHERE id = $2
`
result, err := db.conn.Exec(ctx, query, valueJSON, id)
if err != nil {
return nil, fmt.Errorf("failed to update server: %w", err)
}
if result.RowsAffected() == 0 {
return nil, ErrNotFound
}
return server, nil
}
// Close closes the database connection
func (db *PostgreSQL) Close() error {
return db.conn.Close(context.Background())
}