You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							893 lines
						
					
					
						
							26 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							893 lines
						
					
					
						
							26 KiB
						
					
					
				| package postgres | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"encoding/binary" | |
| 	"fmt" | |
| 	"io" | |
| 	"strconv" | |
| 	"strings" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/query/engine" | |
| 	"github.com/seaweedfs/seaweedfs/weed/query/sqltypes" | |
| 	"github.com/seaweedfs/seaweedfs/weed/util/sqlutil" | |
| 	"github.com/seaweedfs/seaweedfs/weed/util/version" | |
| ) | |
| 
 | |
| // mapErrorToPostgreSQLCode maps SeaweedFS SQL engine errors to appropriate PostgreSQL error codes | |
| func mapErrorToPostgreSQLCode(err error) string { | |
| 	if err == nil { | |
| 		return "00000" // Success | |
| 	} | |
| 
 | |
| 	// Use typed errors for robust error mapping | |
| 	switch err.(type) { | |
| 	case engine.ParseError: | |
| 		return "42601" // Syntax error | |
|  | |
| 	case engine.TableNotFoundError: | |
| 		return "42P01" // Undefined table | |
|  | |
| 	case engine.ColumnNotFoundError: | |
| 		return "42703" // Undefined column | |
|  | |
| 	case engine.UnsupportedFeatureError: | |
| 		return "0A000" // Feature not supported | |
|  | |
| 	case engine.AggregationError: | |
| 		// Aggregation errors are usually function-related issues | |
| 		return "42883" // Undefined function (aggregation function issues) | |
|  | |
| 	case engine.DataSourceError: | |
| 		// Data source errors are usually access or connection issues | |
| 		return "08000" // Connection exception | |
|  | |
| 	case engine.OptimizationError: | |
| 		// Optimization failures are usually feature limitations | |
| 		return "0A000" // Feature not supported | |
|  | |
| 	case engine.NoSchemaError: | |
| 		// Topic exists but no schema available | |
| 		return "42P01" // Undefined table (treat as table not found) | |
| 	} | |
| 
 | |
| 	// Fallback: analyze error message for backward compatibility with non-typed errors | |
| 	errLower := strings.ToLower(err.Error()) | |
| 
 | |
| 	// Parsing and syntax errors | |
| 	if strings.Contains(errLower, "parse error") || strings.Contains(errLower, "syntax") { | |
| 		return "42601" // Syntax error | |
| 	} | |
| 
 | |
| 	// Unsupported features | |
| 	if strings.Contains(errLower, "unsupported") || strings.Contains(errLower, "not supported") { | |
| 		return "0A000" // Feature not supported | |
| 	} | |
| 
 | |
| 	// Table/topic not found | |
| 	if strings.Contains(errLower, "not found") || | |
| 		(strings.Contains(errLower, "topic") && strings.Contains(errLower, "available")) { | |
| 		return "42P01" // Undefined table | |
| 	} | |
| 
 | |
| 	// Column-related errors | |
| 	if strings.Contains(errLower, "column") || strings.Contains(errLower, "field") { | |
| 		return "42703" // Undefined column | |
| 	} | |
| 
 | |
| 	// Multi-table or complex query limitations | |
| 	if strings.Contains(errLower, "single table") || strings.Contains(errLower, "join") { | |
| 		return "0A000" // Feature not supported | |
| 	} | |
| 
 | |
| 	// Default to generic syntax/access error | |
| 	return "42000" // Syntax error or access rule violation | |
| } | |
| 
 | |
| // handleMessage processes a single PostgreSQL protocol message | |
| func (s *PostgreSQLServer) handleMessage(session *PostgreSQLSession) error { | |
| 	// Read message type | |
| 	msgType := make([]byte, 1) | |
| 	_, err := io.ReadFull(session.reader, msgType) | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	// Read message length | |
| 	length := make([]byte, 4) | |
| 	_, err = io.ReadFull(session.reader, length) | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	msgLength := binary.BigEndian.Uint32(length) - 4 | |
| 	msgBody := make([]byte, msgLength) | |
| 	if msgLength > 0 { | |
| 		_, err = io.ReadFull(session.reader, msgBody) | |
| 		if err != nil { | |
| 			return err | |
| 		} | |
| 	} | |
| 
 | |
| 	// Process message based on type | |
| 	switch msgType[0] { | |
| 	case PG_MSG_QUERY: | |
| 		return s.handleSimpleQuery(session, string(msgBody[:len(msgBody)-1])) // Remove null terminator | |
| 	case PG_MSG_PARSE: | |
| 		return s.handleParse(session, msgBody) | |
| 	case PG_MSG_BIND: | |
| 		return s.handleBind(session, msgBody) | |
| 	case PG_MSG_EXECUTE: | |
| 		return s.handleExecute(session, msgBody) | |
| 	case PG_MSG_DESCRIBE: | |
| 		return s.handleDescribe(session, msgBody) | |
| 	case PG_MSG_CLOSE: | |
| 		return s.handleClose(session, msgBody) | |
| 	case PG_MSG_FLUSH: | |
| 		return s.handleFlush(session) | |
| 	case PG_MSG_SYNC: | |
| 		return s.handleSync(session) | |
| 	case PG_MSG_TERMINATE: | |
| 		return io.EOF // Signal connection termination | |
| 	default: | |
| 		return s.sendError(session, "08P01", fmt.Sprintf("unknown message type: %c", msgType[0])) | |
| 	} | |
| } | |
| 
 | |
| // handleSimpleQuery processes a simple query message | |
| func (s *PostgreSQLServer) handleSimpleQuery(session *PostgreSQLSession, query string) error { | |
| 	glog.V(2).Infof("PostgreSQL Query (ID: %d): %s", session.processID, query) | |
| 
 | |
| 	// Add comprehensive error recovery to prevent crashes | |
| 	defer func() { | |
| 		if r := recover(); r != nil { | |
| 			glog.Errorf("Panic in handleSimpleQuery (ID: %d): %v", session.processID, r) | |
| 			// Try to send error message | |
| 			s.sendError(session, "XX000", fmt.Sprintf("Internal error: %v", r)) | |
| 			// Try to send ReadyForQuery to keep connection alive | |
| 			s.sendReadyForQuery(session) | |
| 		} | |
| 	}() | |
| 
 | |
| 	// Handle USE database commands for session context | |
| 	parts := strings.Fields(strings.TrimSpace(query)) | |
| 	if len(parts) >= 2 && strings.ToUpper(parts[0]) == "USE" { | |
| 		// Re-join the parts after "USE" to handle names with spaces, then trim. | |
| 		dbName := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(query), parts[0])) | |
| 
 | |
| 		// Unquote if necessary (handle quoted identifiers like "my-database") | |
| 		if len(dbName) > 1 && dbName[0] == '"' && dbName[len(dbName)-1] == '"' { | |
| 			dbName = dbName[1 : len(dbName)-1] | |
| 		} else if len(dbName) > 1 && dbName[0] == '`' && dbName[len(dbName)-1] == '`' { | |
| 			// Also handle backtick quotes for MySQL/other client compatibility | |
| 			dbName = dbName[1 : len(dbName)-1] | |
| 		} | |
| 
 | |
| 		session.database = dbName | |
| 		s.sqlEngine.GetCatalog().SetCurrentDatabase(dbName) | |
| 
 | |
| 		// Send command complete for USE | |
| 		err := s.sendCommandComplete(session, "USE") | |
| 		if err != nil { | |
| 			return err | |
| 		} | |
| 		// Send ReadyForQuery and exit (don't continue processing) | |
| 		return s.sendReadyForQuery(session) | |
| 	} | |
| 
 | |
| 	// Set database context in SQL engine if session database is different from current | |
| 	if session.database != "" && session.database != s.sqlEngine.GetCatalog().GetCurrentDatabase() { | |
| 		s.sqlEngine.GetCatalog().SetCurrentDatabase(session.database) | |
| 	} | |
| 
 | |
| 	// Split query string into individual statements to handle multi-statement queries | |
| 	queries := sqlutil.SplitStatements(query) | |
| 
 | |
| 	// Execute each statement sequentially | |
| 	for _, singleQuery := range queries { | |
| 		cleanQuery := strings.TrimSpace(singleQuery) | |
| 		if cleanQuery == "" { | |
| 			continue // Skip empty statements | |
| 		} | |
| 
 | |
| 		// Handle PostgreSQL-specific system queries directly | |
| 		if systemResult := s.handleSystemQuery(session, cleanQuery); systemResult != nil { | |
| 			err := s.sendSystemQueryResult(session, systemResult, cleanQuery) | |
| 			if err != nil { | |
| 				return err | |
| 			} | |
| 			continue // Continue with next statement | |
| 		} | |
| 
 | |
| 		// Execute using PostgreSQL-compatible SQL engine for proper dialect support | |
| 		ctx := context.Background() | |
| 		var result *engine.QueryResult | |
| 		var err error | |
| 
 | |
| 		// Execute SQL query with panic recovery to prevent crashes | |
| 		func() { | |
| 			defer func() { | |
| 				if r := recover(); r != nil { | |
| 					glog.Errorf("Panic in SQL execution (ID: %d, Query: %s): %v", session.processID, cleanQuery, r) | |
| 					err = fmt.Errorf("internal error during SQL execution: %v", r) | |
| 				} | |
| 			}() | |
| 
 | |
| 			// Use the main sqlEngine (now uses CockroachDB parser for PostgreSQL compatibility) | |
| 			result, err = s.sqlEngine.ExecuteSQL(ctx, cleanQuery) | |
| 		}() | |
| 
 | |
| 		if err != nil { | |
| 			// Send error message but keep connection alive | |
| 			errorCode := mapErrorToPostgreSQLCode(err) | |
| 			sendErr := s.sendError(session, errorCode, err.Error()) | |
| 			if sendErr != nil { | |
| 				return sendErr | |
| 			} | |
| 			// Send ReadyForQuery to keep connection alive | |
| 			return s.sendReadyForQuery(session) | |
| 		} | |
| 
 | |
| 		if result.Error != nil { | |
| 			// Send error message but keep connection alive | |
| 			errorCode := mapErrorToPostgreSQLCode(result.Error) | |
| 			sendErr := s.sendError(session, errorCode, result.Error.Error()) | |
| 			if sendErr != nil { | |
| 				return sendErr | |
| 			} | |
| 			// Send ReadyForQuery to keep connection alive | |
| 			return s.sendReadyForQuery(session) | |
| 		} | |
| 
 | |
| 		// Send results for this statement | |
| 		if len(result.Columns) > 0 { | |
| 			// Send row description | |
| 			err = s.sendRowDescription(session, result) | |
| 			if err != nil { | |
| 				return err | |
| 			} | |
| 
 | |
| 			// Send data rows | |
| 			for _, row := range result.Rows { | |
| 				err = s.sendDataRow(session, row) | |
| 				if err != nil { | |
| 					return err | |
| 				} | |
| 			} | |
| 		} | |
| 
 | |
| 		// Send command complete for this statement | |
| 		tag := s.getCommandTag(cleanQuery, len(result.Rows)) | |
| 		err = s.sendCommandComplete(session, tag) | |
| 		if err != nil { | |
| 			return err | |
| 		} | |
| 	} | |
| 
 | |
| 	// Send ready for query after all statements are processed | |
| 	return s.sendReadyForQuery(session) | |
| } | |
| 
 | |
| // SystemQueryResult represents the result of a system query | |
| type SystemQueryResult struct { | |
| 	Columns []string | |
| 	Rows    [][]string | |
| } | |
| 
 | |
| // handleSystemQuery handles PostgreSQL system queries directly | |
| func (s *PostgreSQLServer) handleSystemQuery(session *PostgreSQLSession, query string) *SystemQueryResult { | |
| 	// Trim and normalize query | |
| 	query = strings.TrimSpace(query) | |
| 	query = strings.TrimSuffix(query, ";") | |
| 	queryLower := strings.ToLower(query) | |
| 
 | |
| 	// Handle essential PostgreSQL system queries | |
| 	switch queryLower { | |
| 	case "select version()": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"version"}, | |
| 			Rows:    [][]string{{fmt.Sprintf("SeaweedFS %s (PostgreSQL 14.0 compatible)", version.VERSION_NUMBER)}}, | |
| 		} | |
| 	case "select current_database()": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"current_database"}, | |
| 			Rows:    [][]string{{s.config.Database}}, | |
| 		} | |
| 	case "select current_user": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"current_user"}, | |
| 			Rows:    [][]string{{"seaweedfs"}}, | |
| 		} | |
| 	case "select current_setting('server_version')": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"server_version"}, | |
| 			Rows:    [][]string{{fmt.Sprintf("%s (SeaweedFS)", version.VERSION_NUMBER)}}, | |
| 		} | |
| 	case "select current_setting('server_encoding')": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"server_encoding"}, | |
| 			Rows:    [][]string{{"UTF8"}}, | |
| 		} | |
| 	case "select current_setting('client_encoding')": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"client_encoding"}, | |
| 			Rows:    [][]string{{"UTF8"}}, | |
| 		} | |
| 	} | |
| 
 | |
| 	// Handle transaction commands (no-op for read-only) | |
| 	switch queryLower { | |
| 	case "begin", "start transaction": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"status"}, | |
| 			Rows:    [][]string{{"BEGIN"}}, | |
| 		} | |
| 	case "commit": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"status"}, | |
| 			Rows:    [][]string{{"COMMIT"}}, | |
| 		} | |
| 	case "rollback": | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"status"}, | |
| 			Rows:    [][]string{{"ROLLBACK"}}, | |
| 		} | |
| 	} | |
| 
 | |
| 	// If starts with SET, return a no-op | |
| 	if strings.HasPrefix(queryLower, "set ") { | |
| 		return &SystemQueryResult{ | |
| 			Columns: []string{"status"}, | |
| 			Rows:    [][]string{{"SET"}}, | |
| 		} | |
| 	} | |
| 
 | |
| 	// Return nil to use SQL engine | |
| 	return nil | |
| } | |
| 
 | |
| // sendSystemQueryResult sends the result of a system query | |
| func (s *PostgreSQLServer) sendSystemQueryResult(session *PostgreSQLSession, result *SystemQueryResult, query string) error { | |
| 	// Add panic recovery to prevent crashes in system query results | |
| 	defer func() { | |
| 		if r := recover(); r != nil { | |
| 			glog.Errorf("Panic in sendSystemQueryResult (ID: %d, Query: %s): %v", session.processID, query, r) | |
| 			// Try to send error and continue | |
| 			s.sendError(session, "XX000", fmt.Sprintf("Internal error in system query: %v", r)) | |
| 		} | |
| 	}() | |
| 
 | |
| 	// Create column descriptions for system query results | |
| 	columns := make([]string, len(result.Columns)) | |
| 	for i, col := range result.Columns { | |
| 		columns[i] = col | |
| 	} | |
| 
 | |
| 	// Convert to sqltypes.Value format | |
| 	var sqlRows [][]sqltypes.Value | |
| 	for _, row := range result.Rows { | |
| 		sqlRow := make([]sqltypes.Value, len(row)) | |
| 		for i, cell := range row { | |
| 			sqlRow[i] = sqltypes.NewVarChar(cell) | |
| 		} | |
| 		sqlRows = append(sqlRows, sqlRow) | |
| 	} | |
| 
 | |
| 	// Send row description (create a temporary QueryResult for consistency) | |
| 	tempResult := &engine.QueryResult{ | |
| 		Columns: columns, | |
| 		Rows:    sqlRows, | |
| 	} | |
| 	err := s.sendRowDescription(session, tempResult) | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	// Send data rows | |
| 	for _, row := range sqlRows { | |
| 		err = s.sendDataRow(session, row) | |
| 		if err != nil { | |
| 			return err | |
| 		} | |
| 	} | |
| 
 | |
| 	// Send command complete | |
| 	tag := s.getCommandTag(query, len(result.Rows)) | |
| 	err = s.sendCommandComplete(session, tag) | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	// Send ready for query | |
| 	return s.sendReadyForQuery(session) | |
| } | |
| 
 | |
| // handleParse processes a Parse message (prepared statement) | |
| func (s *PostgreSQLServer) handleParse(session *PostgreSQLSession, msgBody []byte) error { | |
| 	// Parse message format: statement_name\0query\0param_count(int16)[param_type(int32)...] | |
| 	parts := strings.Split(string(msgBody), "\x00") | |
| 	if len(parts) < 2 { | |
| 		return s.sendError(session, "08P01", "invalid Parse message format") | |
| 	} | |
| 
 | |
| 	stmtName := parts[0] | |
| 	query := parts[1] | |
| 
 | |
| 	// Create prepared statement | |
| 	stmt := &PreparedStatement{ | |
| 		Name:       stmtName, | |
| 		Query:      query, | |
| 		ParamTypes: []uint32{}, | |
| 		Fields:     []FieldDescription{}, | |
| 	} | |
| 
 | |
| 	session.preparedStmts[stmtName] = stmt | |
| 
 | |
| 	// Send parse complete | |
| 	return s.sendParseComplete(session) | |
| } | |
| 
 | |
| // handleBind processes a Bind message | |
| func (s *PostgreSQLServer) handleBind(session *PostgreSQLSession, msgBody []byte) error { | |
| 	// For now, simple implementation | |
| 	// In full implementation, would parse parameters and create portal | |
|  | |
| 	// Send bind complete | |
| 	return s.sendBindComplete(session) | |
| } | |
| 
 | |
| // handleExecute processes an Execute message | |
| func (s *PostgreSQLServer) handleExecute(session *PostgreSQLSession, msgBody []byte) error { | |
| 	// Parse portal name | |
| 	parts := strings.Split(string(msgBody), "\x00") | |
| 	if len(parts) == 0 { | |
| 		return s.sendError(session, "08P01", "invalid Execute message format") | |
| 	} | |
| 
 | |
| 	portalName := parts[0] | |
| 
 | |
| 	// For now, execute as simple query | |
| 	// In full implementation, would use portal with parameters | |
| 	glog.V(2).Infof("PostgreSQL Execute portal (ID: %d): %s", session.processID, portalName) | |
| 
 | |
| 	// Send command complete | |
| 	err := s.sendCommandComplete(session, "SELECT 0") | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	return nil | |
| } | |
| 
 | |
| // handleDescribe processes a Describe message | |
| func (s *PostgreSQLServer) handleDescribe(session *PostgreSQLSession, msgBody []byte) error { | |
| 	if len(msgBody) < 2 { | |
| 		return s.sendError(session, "08P01", "invalid Describe message format") | |
| 	} | |
| 
 | |
| 	objectType := msgBody[0] // 'S' for statement, 'P' for portal | |
| 	objectName := string(msgBody[1:]) | |
| 
 | |
| 	glog.V(2).Infof("PostgreSQL Describe %c (ID: %d): %s", objectType, session.processID, objectName) | |
| 
 | |
| 	// For now, send empty row description | |
| 	tempResult := &engine.QueryResult{ | |
| 		Columns: []string{}, | |
| 		Rows:    [][]sqltypes.Value{}, | |
| 	} | |
| 	return s.sendRowDescription(session, tempResult) | |
| } | |
| 
 | |
| // handleClose processes a Close message | |
| func (s *PostgreSQLServer) handleClose(session *PostgreSQLSession, msgBody []byte) error { | |
| 	if len(msgBody) < 2 { | |
| 		return s.sendError(session, "08P01", "invalid Close message format") | |
| 	} | |
| 
 | |
| 	objectType := msgBody[0] // 'S' for statement, 'P' for portal | |
| 	objectName := string(msgBody[1:]) | |
| 
 | |
| 	switch objectType { | |
| 	case 'S': | |
| 		delete(session.preparedStmts, objectName) | |
| 	case 'P': | |
| 		delete(session.portals, objectName) | |
| 	} | |
| 
 | |
| 	// Send close complete | |
| 	return s.sendCloseComplete(session) | |
| } | |
| 
 | |
| // handleFlush processes a Flush message | |
| func (s *PostgreSQLServer) handleFlush(session *PostgreSQLSession) error { | |
| 	return session.writer.Flush() | |
| } | |
| 
 | |
| // handleSync processes a Sync message | |
| func (s *PostgreSQLServer) handleSync(session *PostgreSQLSession) error { | |
| 	// Reset transaction state if needed | |
| 	session.transactionState = PG_TRANS_IDLE | |
| 
 | |
| 	// Send ready for query | |
| 	return s.sendReadyForQuery(session) | |
| } | |
| 
 | |
| // sendParameterStatus sends a parameter status message | |
| func (s *PostgreSQLServer) sendParameterStatus(session *PostgreSQLSession, name, value string) error { | |
| 	msg := make([]byte, 0) | |
| 	msg = append(msg, PG_RESP_PARAMETER) | |
| 
 | |
| 	// Calculate length | |
| 	length := 4 + len(name) + 1 + len(value) + 1 | |
| 	lengthBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(lengthBytes, uint32(length)) | |
| 	msg = append(msg, lengthBytes...) | |
| 
 | |
| 	// Add name and value | |
| 	msg = append(msg, []byte(name)...) | |
| 	msg = append(msg, 0) // null terminator | |
| 	msg = append(msg, []byte(value)...) | |
| 	msg = append(msg, 0) // null terminator | |
|  | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendBackendKeyData sends backend key data | |
| func (s *PostgreSQLServer) sendBackendKeyData(session *PostgreSQLSession) error { | |
| 	msg := make([]byte, 13) | |
| 	msg[0] = PG_RESP_BACKEND_KEY | |
| 	binary.BigEndian.PutUint32(msg[1:5], 12) | |
| 	binary.BigEndian.PutUint32(msg[5:9], session.processID) | |
| 	binary.BigEndian.PutUint32(msg[9:13], session.secretKey) | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendReadyForQuery sends ready for query message | |
| func (s *PostgreSQLServer) sendReadyForQuery(session *PostgreSQLSession) error { | |
| 	msg := make([]byte, 6) | |
| 	msg[0] = PG_RESP_READY | |
| 	binary.BigEndian.PutUint32(msg[1:5], 5) | |
| 	msg[5] = session.transactionState | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendRowDescription sends row description message | |
| func (s *PostgreSQLServer) sendRowDescription(session *PostgreSQLSession, result *engine.QueryResult) error { | |
| 	msg := make([]byte, 0) | |
| 	msg = append(msg, PG_RESP_ROW_DESC) | |
| 
 | |
| 	// Calculate message length | |
| 	length := 4 + 2 // length + field count | |
| 	for _, col := range result.Columns { | |
| 		length += len(col) + 1 + 4 + 2 + 4 + 2 + 4 + 2 // name + null + tableOID + attrNum + typeOID + typeSize + typeMod + format | |
| 	} | |
| 
 | |
| 	lengthBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(lengthBytes, uint32(length)) | |
| 	msg = append(msg, lengthBytes...) | |
| 
 | |
| 	// Field count | |
| 	fieldCountBytes := make([]byte, 2) | |
| 	binary.BigEndian.PutUint16(fieldCountBytes, uint16(len(result.Columns))) | |
| 	msg = append(msg, fieldCountBytes...) | |
| 
 | |
| 	// Field descriptions | |
| 	for i, col := range result.Columns { | |
| 		// Field name | |
| 		msg = append(msg, []byte(col)...) | |
| 		msg = append(msg, 0) // null terminator | |
|  | |
| 		// Table OID (0 for no table) | |
| 		tableOID := make([]byte, 4) | |
| 		binary.BigEndian.PutUint32(tableOID, 0) | |
| 		msg = append(msg, tableOID...) | |
| 
 | |
| 		// Attribute number | |
| 		attrNum := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(attrNum, uint16(i+1)) | |
| 		msg = append(msg, attrNum...) | |
| 
 | |
| 		// Type OID (determine from schema if available, fallback to data inference) | |
| 		typeOID := s.getPostgreSQLTypeFromSchema(result, col, i) | |
| 		typeOIDBytes := make([]byte, 4) | |
| 		binary.BigEndian.PutUint32(typeOIDBytes, typeOID) | |
| 		msg = append(msg, typeOIDBytes...) | |
| 
 | |
| 		// Type size (-1 for variable length) | |
| 		typeSize := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(typeSize, 0xFFFF) // -1 as uint16 | |
| 		msg = append(msg, typeSize...) | |
| 
 | |
| 		// Type modifier (-1 for default) | |
| 		typeMod := make([]byte, 4) | |
| 		binary.BigEndian.PutUint32(typeMod, 0xFFFFFFFF) // -1 as uint32 | |
| 		msg = append(msg, typeMod...) | |
| 
 | |
| 		// Format (0 for text) | |
| 		format := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(format, 0) | |
| 		msg = append(msg, format...) | |
| 	} | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendDataRow sends a data row message | |
| func (s *PostgreSQLServer) sendDataRow(session *PostgreSQLSession, row []sqltypes.Value) error { | |
| 	msg := make([]byte, 0) | |
| 	msg = append(msg, PG_RESP_DATA_ROW) | |
| 
 | |
| 	// Calculate message length | |
| 	length := 4 + 2 // length + field count | |
| 	for _, value := range row { | |
| 		if value.IsNull() { | |
| 			length += 4 // null value length (-1) | |
| 		} else { | |
| 			valueStr := value.ToString() | |
| 			length += 4 + len(valueStr) // field length + data | |
| 		} | |
| 	} | |
| 
 | |
| 	lengthBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(lengthBytes, uint32(length)) | |
| 	msg = append(msg, lengthBytes...) | |
| 
 | |
| 	// Field count | |
| 	fieldCountBytes := make([]byte, 2) | |
| 	binary.BigEndian.PutUint16(fieldCountBytes, uint16(len(row))) | |
| 	msg = append(msg, fieldCountBytes...) | |
| 
 | |
| 	// Field values | |
| 	for _, value := range row { | |
| 		if value.IsNull() { | |
| 			// Null value | |
| 			nullLength := make([]byte, 4) | |
| 			binary.BigEndian.PutUint32(nullLength, 0xFFFFFFFF) // -1 as uint32 | |
| 			msg = append(msg, nullLength...) | |
| 		} else { | |
| 			valueStr := value.ToString() | |
| 			valueLength := make([]byte, 4) | |
| 			binary.BigEndian.PutUint32(valueLength, uint32(len(valueStr))) | |
| 			msg = append(msg, valueLength...) | |
| 			msg = append(msg, []byte(valueStr)...) | |
| 		} | |
| 	} | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendCommandComplete sends command complete message | |
| func (s *PostgreSQLServer) sendCommandComplete(session *PostgreSQLSession, tag string) error { | |
| 	msg := make([]byte, 0) | |
| 	msg = append(msg, PG_RESP_COMMAND) | |
| 
 | |
| 	length := 4 + len(tag) + 1 | |
| 	lengthBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(lengthBytes, uint32(length)) | |
| 	msg = append(msg, lengthBytes...) | |
| 
 | |
| 	msg = append(msg, []byte(tag)...) | |
| 	msg = append(msg, 0) // null terminator | |
|  | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendParseComplete sends parse complete message | |
| func (s *PostgreSQLServer) sendParseComplete(session *PostgreSQLSession) error { | |
| 	msg := make([]byte, 5) | |
| 	msg[0] = PG_RESP_PARSE_COMPLETE | |
| 	binary.BigEndian.PutUint32(msg[1:5], 4) | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendBindComplete sends bind complete message | |
| func (s *PostgreSQLServer) sendBindComplete(session *PostgreSQLSession) error { | |
| 	msg := make([]byte, 5) | |
| 	msg[0] = PG_RESP_BIND_COMPLETE | |
| 	binary.BigEndian.PutUint32(msg[1:5], 4) | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendCloseComplete sends close complete message | |
| func (s *PostgreSQLServer) sendCloseComplete(session *PostgreSQLSession) error { | |
| 	msg := make([]byte, 5) | |
| 	msg[0] = PG_RESP_CLOSE_COMPLETE | |
| 	binary.BigEndian.PutUint32(msg[1:5], 4) | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // sendError sends an error message | |
| func (s *PostgreSQLServer) sendError(session *PostgreSQLSession, code, message string) error { | |
| 	msg := make([]byte, 0) | |
| 	msg = append(msg, PG_RESP_ERROR) | |
| 
 | |
| 	// Build error fields | |
| 	fields := fmt.Sprintf("S%s\x00C%s\x00M%s\x00\x00", "ERROR", code, message) | |
| 	length := 4 + len(fields) | |
| 
 | |
| 	lengthBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(lengthBytes, uint32(length)) | |
| 	msg = append(msg, lengthBytes...) | |
| 	msg = append(msg, []byte(fields)...) | |
| 
 | |
| 	_, err := session.writer.Write(msg) | |
| 	if err == nil { | |
| 		err = session.writer.Flush() | |
| 	} | |
| 	return err | |
| } | |
| 
 | |
| // getCommandTag generates appropriate command tag for query | |
| func (s *PostgreSQLServer) getCommandTag(query string, rowCount int) string { | |
| 	queryUpper := strings.ToUpper(strings.TrimSpace(query)) | |
| 
 | |
| 	if strings.HasPrefix(queryUpper, "SELECT") { | |
| 		return fmt.Sprintf("SELECT %d", rowCount) | |
| 	} else if strings.HasPrefix(queryUpper, "INSERT") { | |
| 		return fmt.Sprintf("INSERT 0 %d", rowCount) | |
| 	} else if strings.HasPrefix(queryUpper, "UPDATE") { | |
| 		return fmt.Sprintf("UPDATE %d", rowCount) | |
| 	} else if strings.HasPrefix(queryUpper, "DELETE") { | |
| 		return fmt.Sprintf("DELETE %d", rowCount) | |
| 	} else if strings.HasPrefix(queryUpper, "SHOW") { | |
| 		return fmt.Sprintf("SELECT %d", rowCount) | |
| 	} else if strings.HasPrefix(queryUpper, "DESCRIBE") || strings.HasPrefix(queryUpper, "DESC") { | |
| 		return fmt.Sprintf("SELECT %d", rowCount) | |
| 	} | |
| 
 | |
| 	return "SELECT 0" | |
| } | |
| 
 | |
| // getPostgreSQLTypeFromSchema determines PostgreSQL type OID from schema information first, fallback to data | |
| func (s *PostgreSQLServer) getPostgreSQLTypeFromSchema(result *engine.QueryResult, columnName string, colIndex int) uint32 { | |
| 	// Try to get type from schema if database and table are available | |
| 	if result.Database != "" && result.Table != "" { | |
| 		if tableInfo, err := s.sqlEngine.GetCatalog().GetTableInfo(result.Database, result.Table); err == nil { | |
| 			if tableInfo.Schema != nil && tableInfo.Schema.RecordType != nil { | |
| 				// Look for the field in the schema | |
| 				for _, field := range tableInfo.Schema.RecordType.Fields { | |
| 					if field.Name == columnName { | |
| 						return s.mapSchemaTypeToPostgreSQL(field.Type) | |
| 					} | |
| 				} | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	// Handle system columns | |
| 	switch columnName { | |
| 	case "_timestamp_ns": | |
| 		return PG_TYPE_INT8 // PostgreSQL BIGINT for nanosecond timestamps | |
| 	case "_key": | |
| 		return PG_TYPE_BYTEA // PostgreSQL BYTEA for binary keys | |
| 	case "_source": | |
| 		return PG_TYPE_TEXT // PostgreSQL TEXT for source information | |
| 	} | |
| 
 | |
| 	// Fallback to data-based inference if schema is not available | |
| 	return s.getPostgreSQLTypeFromData(result.Columns, result.Rows, colIndex) | |
| } | |
| 
 | |
| // mapSchemaTypeToPostgreSQL maps SeaweedFS schema types to PostgreSQL type OIDs | |
| func (s *PostgreSQLServer) mapSchemaTypeToPostgreSQL(fieldType *schema_pb.Type) uint32 { | |
| 	if fieldType == nil { | |
| 		return PG_TYPE_TEXT | |
| 	} | |
| 
 | |
| 	switch kind := fieldType.Kind.(type) { | |
| 	case *schema_pb.Type_ScalarType: | |
| 		switch kind.ScalarType { | |
| 		case schema_pb.ScalarType_BOOL: | |
| 			return PG_TYPE_BOOL | |
| 		case schema_pb.ScalarType_INT32: | |
| 			return PG_TYPE_INT4 | |
| 		case schema_pb.ScalarType_INT64: | |
| 			return PG_TYPE_INT8 | |
| 		case schema_pb.ScalarType_FLOAT: | |
| 			return PG_TYPE_FLOAT4 | |
| 		case schema_pb.ScalarType_DOUBLE: | |
| 			return PG_TYPE_FLOAT8 | |
| 		case schema_pb.ScalarType_BYTES: | |
| 			return PG_TYPE_BYTEA | |
| 		case schema_pb.ScalarType_STRING: | |
| 			return PG_TYPE_TEXT | |
| 		default: | |
| 			return PG_TYPE_TEXT | |
| 		} | |
| 	case *schema_pb.Type_ListType: | |
| 		// For list types, we'll represent them as JSON text | |
| 		return PG_TYPE_JSONB | |
| 	case *schema_pb.Type_RecordType: | |
| 		// For nested record types, we'll represent them as JSON text | |
| 		return PG_TYPE_JSONB | |
| 	default: | |
| 		return PG_TYPE_TEXT | |
| 	} | |
| } | |
| 
 | |
| // getPostgreSQLTypeFromData determines PostgreSQL type OID from data (legacy fallback method) | |
| func (s *PostgreSQLServer) getPostgreSQLTypeFromData(columns []string, rows [][]sqltypes.Value, colIndex int) uint32 { | |
| 	if len(rows) == 0 || colIndex >= len(rows[0]) { | |
| 		return PG_TYPE_TEXT // Default to text | |
| 	} | |
| 
 | |
| 	// Sample first non-null value to determine type | |
| 	for _, row := range rows { | |
| 		if colIndex < len(row) && !row[colIndex].IsNull() { | |
| 			value := row[colIndex] | |
| 			switch value.Type() { | |
| 			case sqltypes.Int8, sqltypes.Int16, sqltypes.Int32: | |
| 				return PG_TYPE_INT4 | |
| 			case sqltypes.Int64: | |
| 				return PG_TYPE_INT8 | |
| 			case sqltypes.Float32, sqltypes.Float64: | |
| 				return PG_TYPE_FLOAT8 | |
| 			case sqltypes.Bit: | |
| 				return PG_TYPE_BOOL | |
| 			case sqltypes.Timestamp, sqltypes.Datetime: | |
| 				return PG_TYPE_TIMESTAMP | |
| 			default: | |
| 				// Try to infer from string content | |
| 				valueStr := value.ToString() | |
| 				if _, err := strconv.ParseInt(valueStr, 10, 32); err == nil { | |
| 					return PG_TYPE_INT4 | |
| 				} | |
| 				if _, err := strconv.ParseInt(valueStr, 10, 64); err == nil { | |
| 					return PG_TYPE_INT8 | |
| 				} | |
| 				if _, err := strconv.ParseFloat(valueStr, 64); err == nil { | |
| 					return PG_TYPE_FLOAT8 | |
| 				} | |
| 				if valueStr == "true" || valueStr == "false" { | |
| 					return PG_TYPE_BOOL | |
| 				} | |
| 				return PG_TYPE_TEXT | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	return PG_TYPE_TEXT // Default to text | |
| }
 |