Quellcode durchsuchen

Merge pull request #7 from database-pizza/feat/build-identity

feat(pgserver): expose build identity separately from server_version
Danilo Fragoso vor 1 Tag
Ursprung
Commit
1110d20174

+ 16 - 0
pkg/httpserver/handler.go

@@ -395,6 +395,22 @@ func (s *Server) handleSchemaTable(w http.ResponseWriter, r *http.Request) {
 	writeJSON(w, http.StatusOK, resp, pretty)
 }
 
+// handleVersion reports the build identity and the protocol compatibility
+// floor separately, so clients can pin behavior to a specific engine build.
+func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
+	resp := map[string]interface{}{
+		"name":            "pizzasql",
+		"version":         version.String(),
+		"commit":          version.Commit,
+		"date":            version.Date,
+		"dirty":           version.Dirty != "",
+		"postgres_compat": version.PostgreSQLCompatVersion,
+		"sqlite_compat":   executor.SQLiteCompatVersion,
+	}
+	pretty := r.URL.Query().Get("pretty") == "true"
+	writeJSON(w, http.StatusOK, resp, pretty)
+}
+
 // sqlStateHTTPError maps engine errors onto the same SQLSTATE codes the
 // PostgreSQL endpoint reports. The executor returns plain errors on released
 // builds, so the unique-violation and unsupported-feature markers are matched

+ 1 - 0
pkg/httpserver/server.go

@@ -161,6 +161,7 @@ func (s *Server) init() *Server {
 	mux.HandleFunc("/schema/tables", s.handleSchemaTables)
 	mux.HandleFunc("/schema/tables/", s.handleSchemaTable)
 	mux.HandleFunc("/health", s.handleHealth)
+	mux.HandleFunc("/version", s.handleVersion)
 	mux.HandleFunc("/stats", s.handleStats)
 	mux.HandleFunc("/metrics", s.handleMetrics)
 	mux.HandleFunc("/transaction/begin", s.handleTransactionBegin)

+ 39 - 0
pkg/httpserver/version_test.go

@@ -0,0 +1,39 @@
+package httpserver
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/danfragoso/pizzasql-next/pkg/executor"
+	"github.com/danfragoso/pizzasql-next/pkg/version"
+)
+
+func TestVersionEndpointReportsBuildAndCompatibility(t *testing.T) {
+	server := setupKVTestServer(t)
+
+	r := httptest.NewRequest(http.MethodGet, "/version", nil)
+	w := httptest.NewRecorder()
+	server.server.Handler.ServeHTTP(w, r)
+
+	if w.Code != http.StatusOK {
+		t.Fatalf("status = %d body %s", w.Code, w.Body.String())
+	}
+	var resp map[string]interface{}
+	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
+		t.Fatalf("decode: %v", err)
+	}
+	if got := resp["version"]; got != version.String() {
+		t.Fatalf("version = %v, want %q", got, version.String())
+	}
+	if got := resp["postgres_compat"]; got != version.PostgreSQLCompatVersion {
+		t.Fatalf("postgres_compat = %v, want %q", got, version.PostgreSQLCompatVersion)
+	}
+	if got := resp["sqlite_compat"]; got != executor.SQLiteCompatVersion {
+		t.Fatalf("sqlite_compat = %v, want %q", got, executor.SQLiteCompatVersion)
+	}
+	if _, ok := resp["commit"]; !ok {
+		t.Fatal("response is missing commit")
+	}
+}

+ 12 - 3
pkg/pgserver/connection.go

@@ -20,6 +20,7 @@ import (
 	"github.com/danfragoso/pizzasql-next/pkg/parser"
 	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
 	"github.com/danfragoso/pizzasql-next/pkg/storage"
+	"github.com/danfragoso/pizzasql-next/pkg/version"
 )
 
 // Connection represents a client connection
@@ -142,7 +143,15 @@ func (c *Connection) Handle() error {
 	}
 
 	// Send parameter status messages
-	if err := c.sendParameterStatus("server_version", "14.0 (PizzaSQL)"); err != nil {
+	if err := c.sendParameterStatus("server_version", version.PostgreSQLCompatVersion+" (PizzaSQL)"); err != nil {
+		return err
+	}
+	// Build identity is a separate, custom ParameterStatus so the PostgreSQL
+	// compatibility string above stays stable for version-sniffing drivers.
+	if err := c.sendParameterStatus("pizzasql.version", version.String()); err != nil {
+		return err
+	}
+	if err := c.sendParameterStatus("pizzasql.commit", version.Commit); err != nil {
 		return err
 	}
 	if err := c.sendParameterStatus("server_encoding", "UTF8"); err != nil {
@@ -1236,7 +1245,7 @@ func (c *Connection) handleVersionQuery() error {
 		return err
 	}
 
-	row := []interface{}{"PostgreSQL 14.0 (PizzaSQL)"}
+	row := []interface{}{"PostgreSQL " + version.PostgreSQLCompatVersion + " (PizzaSQL)"}
 	if err := c.sendDataRow(row, columns); err != nil {
 		return err
 	}
@@ -1281,7 +1290,7 @@ func (c *Connection) handleShowCommand(sqlUpper string) error {
 
 	if strings.Contains(sqlUpper, "SERVER_VERSION") {
 		name = "server_version"
-		value = "14.0"
+		value = version.PostgreSQLCompatVersion
 	} else if strings.Contains(sqlUpper, "SERVER_ENCODING") {
 		name = "server_encoding"
 		value = "UTF8"

+ 107 - 0
pkg/pgserver/identity_test.go

@@ -0,0 +1,107 @@
+package pgserver
+
+import (
+	"bufio"
+	"encoding/binary"
+	"io"
+	"net"
+	"testing"
+
+	"github.com/danfragoso/pizzasql-next/pkg/storage"
+	"github.com/danfragoso/pizzasql-next/pkg/testkv"
+	"github.com/danfragoso/pizzasql-next/pkg/version"
+)
+
+func startupMessage(params map[string]string) []byte {
+	size := 4 + 4
+	for k, v := range params {
+		size += len(k) + 1 + len(v) + 1
+	}
+	size++ // terminator
+	buf := make([]byte, 0, size)
+	buf = binary.BigEndian.AppendUint32(buf, uint32(size))
+	buf = binary.BigEndian.AppendUint32(buf, 196608) // protocol 3.0
+	for k, v := range params {
+		buf = append(buf, k...)
+		buf = append(buf, 0)
+		buf = append(buf, v...)
+		buf = append(buf, 0)
+	}
+	return append(buf, 0)
+}
+
+func TestStartupReportsBuildIdentitySeparately(t *testing.T) {
+	kv := testkv.New(t)
+	pool := kv.Pool(2)
+	t.Cleanup(func() { pool.Close() })
+	dbManager := storage.NewDatabaseManager(pool, nil)
+
+	server, client := net.Pipe()
+	t.Cleanup(func() {
+		client.Close()
+		server.Close()
+	})
+
+	conn := NewConnection(server, dbManager, true)
+	done := make(chan error, 1)
+	go func() { done <- conn.Handle() }()
+
+	go client.Write(startupMessage(map[string]string{"user": "tester", "database": "identity"}))
+
+	reader := bufio.NewReader(client)
+	statuses := map[string]string{}
+	for {
+		msg, err := ReadMessage(reader)
+		if err != nil {
+			t.Fatalf("read startup response: %v", err)
+		}
+		if msg.Type == MsgParameterStatus {
+			data := msg.Data
+			key, pos, err := readCString(data, 0)
+			if err != nil {
+				t.Fatalf("ParameterStatus key: %v", err)
+			}
+			value, _, err := readCString(data, pos)
+			if err != nil {
+				t.Fatalf("ParameterStatus value: %v", err)
+			}
+			statuses[key] = value
+		}
+		if msg.Type == MsgReadyForQuery {
+			break
+		}
+	}
+
+	if got := statuses["server_version"]; got != version.PostgreSQLCompatVersion+" (PizzaSQL)" {
+		t.Fatalf("server_version = %q, want PostgreSQL compatibility string", got)
+	}
+	if got := statuses["pizzasql.version"]; got != version.String() {
+		t.Fatalf("pizzasql.version = %q, want %q", got, version.String())
+	}
+	if got := statuses["pizzasql.commit"]; got != version.Commit {
+		t.Fatalf("pizzasql.commit = %q, want %q", got, version.Commit)
+	}
+
+	client.Write([]byte{'X', 0, 0, 0, 4}) // Terminate
+	if err := <-done; err != nil && err != io.EOF {
+		t.Fatalf("Handle: %v", err)
+	}
+}
+
+func TestVersionFunctionKeepsPostgresCompatibilityString(t *testing.T) {
+	c, client := newDataConnection(t)
+	msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("SELECT version()"), 0)})
+	values := dataRowValues(t, msgs[1])
+	if len(values) != 1 || values[0] != "PostgreSQL "+version.PostgreSQLCompatVersion+" (PizzaSQL)" {
+		t.Fatalf("version() = %v", values)
+	}
+}
+
+func TestPizzasqlVersionFunctionReturnsBuild(t *testing.T) {
+	c, client := newDataConnection(t)
+	msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("SELECT pizzasql_version()"), 0)})
+	values := dataRowValues(t, msgs[1])
+	if len(values) != 1 || values[0] != version.String() {
+		t.Fatalf("pizzasql_version() = %v, want %q", values, version.String())
+	}
+}

+ 5 - 0
pkg/version/version.go

@@ -6,6 +6,11 @@ import (
 	"strings"
 )
 
+// PostgreSQLCompatVersion is the PostgreSQL server version reported to clients
+// for driver compatibility. It is intentionally independent of the PizzaSQL
+// build version returned by String() and pizzasql_version().
+const PostgreSQLCompatVersion = "14.0"
+
 var (
 	Version = "dev"
 	Commit  = "unknown"