Compare commits

..

1 Commits

Author SHA1 Message Date
Hein
fd77385dd6 feat(handler): enhance FetchRowNumber support in handlers
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -26m2s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -25m39s
Build , Vet Test, and Lint / Lint Code (push) Successful in -25m42s
Build , Vet Test, and Lint / Build (push) Successful in -25m55s
Tests / Integration Tests (push) Failing after -26m29s
Tests / Unit Tests (push) Successful in -26m17s
* Implement FetchRowNumber handling in multiple handlers
* Improve error logging for missing rows with filters
* Set row numbers correctly based on FetchRowNumber
2026-02-10 17:42:27 +02:00
3 changed files with 178 additions and 24 deletions

View File

@@ -433,7 +433,18 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
// Execute query to get row number // Execute query to get row number
var result RowNumResult var result RowNumResult
if err := rowNumQuery.Scan(ctx, &result); err != nil { if err := rowNumQuery.Scan(ctx, &result); err != nil {
if err != sql.ErrNoRows { if err == sql.ErrNoRows {
// Build filter description for error message
filterInfo := fmt.Sprintf("filters: %d", len(options.Filters))
if len(options.CustomOperators) > 0 {
customOps := make([]string, 0, len(options.CustomOperators))
for _, op := range options.CustomOperators {
customOps = append(customOps, op.SQL)
}
filterInfo += fmt.Sprintf(", custom operators: [%s]", strings.Join(customOps, "; "))
}
logger.Warn("No row found for primary key %s=%s with %s", pkName, *options.FetchRowNumber, filterInfo)
} else {
logger.Warn("Error fetching row number: %v", err) logger.Warn("Error fetching row number: %v", err)
} }
} else { } else {
@@ -499,7 +510,11 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
// When FetchRowNumber is used, we only return 1 record // When FetchRowNumber is used, we only return 1 record
if options.FetchRowNumber != nil && *options.FetchRowNumber != "" { if options.FetchRowNumber != nil && *options.FetchRowNumber != "" {
count = 1 count = 1
// Don't use limit/offset when fetching specific record // Set the fetched row number on the record
if rowNumber != nil {
logger.Debug("FetchRowNumber: Setting row number %d on record", *rowNumber)
h.setRowNumbersOnRecords(result, int(*rowNumber-1)) // -1 because setRowNumbersOnRecords adds 1
}
} else { } else {
if options.Limit != nil { if options.Limit != nil {
limit = *options.Limit limit = *options.Limit

View File

@@ -549,8 +549,30 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
} }
} }
// If ID is provided, filter by ID // Handle FetchRowNumber before applying ID filter
if id != "" { // This must happen before the query to get the row position, then filter by PK
var fetchedRowNumber *int64
var fetchRowNumberPKValue string
if options.FetchRowNumber != nil && *options.FetchRowNumber != "" {
pkName := reflection.GetPrimaryKeyName(model)
fetchRowNumberPKValue = *options.FetchRowNumber
logger.Debug("FetchRowNumber: Fetching row number for PK %s = %s", pkName, fetchRowNumberPKValue)
rowNum, err := h.FetchRowNumber(ctx, tableName, pkName, fetchRowNumberPKValue, options, model)
if err != nil {
logger.Error("Failed to fetch row number: %v", err)
h.sendError(w, http.StatusBadRequest, "fetch_rownumber_error", "Failed to fetch row number", err)
return
}
fetchedRowNumber = &rowNum
logger.Debug("FetchRowNumber: Row number %d for PK %s = %s", rowNum, pkName, fetchRowNumberPKValue)
// Now filter the main query to this specific primary key
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), fetchRowNumberPKValue)
} else if id != "" {
// If ID is provided (and not FetchRowNumber), filter by ID
pkName := reflection.GetPrimaryKeyName(model) pkName := reflection.GetPrimaryKeyName(model)
logger.Debug("Filtering by ID=%s: %s", pkName, id) logger.Debug("Filtering by ID=%s: %s", pkName, id)
@@ -730,7 +752,14 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
} }
// Set row numbers on each record if the model has a RowNumber field // Set row numbers on each record if the model has a RowNumber field
h.setRowNumbersOnRecords(modelPtr, offset) // If FetchRowNumber was used, set the fetched row number instead of offset-based
if fetchedRowNumber != nil {
// FetchRowNumber: set the actual row position on the record
logger.Debug("FetchRowNumber: Setting row number %d on record", *fetchedRowNumber)
h.setRowNumbersOnRecords(modelPtr, int(*fetchedRowNumber-1)) // -1 because setRowNumbersOnRecords adds 1
} else {
h.setRowNumbersOnRecords(modelPtr, offset)
}
metadata := &common.Metadata{ metadata := &common.Metadata{
Total: int64(total), Total: int64(total),
@@ -740,21 +769,10 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
Offset: offset, Offset: offset,
} }
// Fetch row number for a specific record if requested // If FetchRowNumber was used, also set it in metadata
if options.FetchRowNumber != nil && *options.FetchRowNumber != "" { if fetchedRowNumber != nil {
pkName := reflection.GetPrimaryKeyName(model) metadata.RowNumber = fetchedRowNumber
pkValue := *options.FetchRowNumber logger.Debug("FetchRowNumber: Row number %d set in metadata", *fetchedRowNumber)
logger.Debug("Fetching row number for specific PK %s = %s", pkName, pkValue)
rowNum, err := h.FetchRowNumber(ctx, tableName, pkName, pkValue, options, model)
if err != nil {
logger.Warn("Failed to fetch row number: %v", err)
// Don't fail the entire request, just log the warning
} else {
metadata.RowNumber = &rowNum
logger.Debug("Row number for PK %s: %d", pkValue, rowNum)
}
} }
// Execute AfterRead hooks // Execute AfterRead hooks
@@ -2651,13 +2669,19 @@ func (h *Handler) FetchRowNumber(ctx context.Context, tableName string, pkName s
var result []struct { var result []struct {
RN int64 `bun:"rn"` RN int64 `bun:"rn"`
} }
logger.Debug("[FetchRowNumber] BEFORE Query call - about to execute raw query")
err := h.db.Query(ctx, &result, queryStr, pkValue) err := h.db.Query(ctx, &result, queryStr, pkValue)
logger.Debug("[FetchRowNumber] AFTER Query call - query completed with %d results, err: %v", len(result), err)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to fetch row number: %w", err) return 0, fmt.Errorf("failed to fetch row number: %w", err)
} }
if len(result) == 0 { if len(result) == 0 {
return 0, fmt.Errorf("no row found for primary key %s", pkValue) whereInfo := "none"
if whereSQL != "" {
whereInfo = whereSQL
}
return 0, fmt.Errorf("no row found for primary key %s=%s with active filters: %s", pkName, pkValue, whereInfo)
} }
return result[0].RN, nil return result[0].RN, nil

View File

@@ -210,10 +210,14 @@ func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContex
var metadata map[string]interface{} var metadata map[string]interface{}
var err error var err error
if hookCtx.ID != "" { // Check if FetchRowNumber is specified (treat as single record read)
// Read single record by ID isFetchRowNumber := hookCtx.Options != nil && hookCtx.Options.FetchRowNumber != nil && *hookCtx.Options.FetchRowNumber != ""
if hookCtx.ID != "" || isFetchRowNumber {
// Read single record by ID or FetchRowNumber
data, err = h.readByID(hookCtx) data, err = h.readByID(hookCtx)
metadata = map[string]interface{}{"total": 1} metadata = map[string]interface{}{"total": 1}
// The row number is already set on the record itself via setRowNumbersOnRecords
} else { } else {
// Read multiple records // Read multiple records
data, metadata, err = h.readMultiple(hookCtx) data, metadata, err = h.readMultiple(hookCtx)
@@ -510,10 +514,29 @@ func (h *Handler) notifySubscribers(schema, entity string, operation OperationTy
// CRUD operation implementations // CRUD operation implementations
func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) { func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) {
// Handle FetchRowNumber before building query
var fetchedRowNumber *int64
pkName := reflection.GetPrimaryKeyName(hookCtx.Model)
if hookCtx.Options != nil && hookCtx.Options.FetchRowNumber != nil && *hookCtx.Options.FetchRowNumber != "" {
fetchRowNumberPKValue := *hookCtx.Options.FetchRowNumber
logger.Debug("[WebSocketSpec] FetchRowNumber: Fetching row number for PK %s = %s", pkName, fetchRowNumberPKValue)
rowNum, err := h.FetchRowNumber(hookCtx.Context, hookCtx.TableName, pkName, fetchRowNumberPKValue, hookCtx.Options, hookCtx.Model)
if err != nil {
return nil, fmt.Errorf("failed to fetch row number: %w", err)
}
fetchedRowNumber = &rowNum
logger.Debug("[WebSocketSpec] FetchRowNumber: Row number %d for PK %s = %s", rowNum, pkName, fetchRowNumberPKValue)
// Override ID with FetchRowNumber value
hookCtx.ID = fetchRowNumberPKValue
}
query := h.db.NewSelect().Model(hookCtx.ModelPtr).Table(hookCtx.TableName) query := h.db.NewSelect().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
// Add ID filter // Add ID filter
pkName := reflection.GetPrimaryKeyName(hookCtx.Model)
query = query.Where(fmt.Sprintf("%s = ?", pkName), hookCtx.ID) query = query.Where(fmt.Sprintf("%s = ?", pkName), hookCtx.ID)
// Apply columns // Apply columns
@@ -533,6 +556,12 @@ func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) {
return nil, fmt.Errorf("failed to read record: %w", err) return nil, fmt.Errorf("failed to read record: %w", err)
} }
// Set the fetched row number on the record if FetchRowNumber was used
if fetchedRowNumber != nil {
logger.Debug("[WebSocketSpec] FetchRowNumber: Setting row number %d on record", *fetchedRowNumber)
h.setRowNumbersOnRecords(hookCtx.ModelPtr, int(*fetchedRowNumber-1)) // -1 because setRowNumbersOnRecords adds 1
}
return hookCtx.ModelPtr, nil return hookCtx.ModelPtr, nil
} }
@@ -841,6 +870,92 @@ func (h *Handler) getOperatorSQL(operator string) string {
} }
} }
// FetchRowNumber calculates the row number of a specific record based on sorting and filtering
// Returns the 1-based row number of the record with the given primary key value
func (h *Handler) FetchRowNumber(ctx context.Context, tableName string, pkName string, pkValue string, options *common.RequestOptions, model interface{}) (int64, error) {
defer func() {
if r := recover(); r != nil {
logger.Error("[WebSocketSpec] Panic during FetchRowNumber: %v", r)
}
}()
// Build the sort order SQL
sortSQL := ""
if options != nil && len(options.Sort) > 0 {
sortParts := make([]string, 0, len(options.Sort))
for _, sort := range options.Sort {
if sort.Column == "" {
continue
}
direction := "ASC"
if strings.EqualFold(sort.Direction, "desc") {
direction = "DESC"
}
sortParts = append(sortParts, fmt.Sprintf("%s %s", sort.Column, direction))
}
sortSQL = strings.Join(sortParts, ", ")
} else {
// Default sort by primary key
sortSQL = fmt.Sprintf("%s ASC", pkName)
}
// Build WHERE clause from filters
whereSQL := ""
var whereArgs []interface{}
if options != nil && len(options.Filters) > 0 {
var conditions []string
for _, filter := range options.Filters {
operatorSQL := h.getOperatorSQL(filter.Operator)
conditions = append(conditions, fmt.Sprintf("%s.%s %s ?", tableName, filter.Column, operatorSQL))
whereArgs = append(whereArgs, filter.Value)
}
if len(conditions) > 0 {
whereSQL = "WHERE " + strings.Join(conditions, " AND ")
}
}
// Build the final query with parameterized PK value
queryStr := fmt.Sprintf(`
SELECT search.rn
FROM (
SELECT %[1]s.%[2]s,
ROW_NUMBER() OVER(ORDER BY %[3]s) AS rn
FROM %[1]s
%[4]s
) search
WHERE search.%[2]s = ?
`,
tableName, // [1] - table name
pkName, // [2] - primary key column name
sortSQL, // [3] - sort order SQL
whereSQL, // [4] - WHERE clause
)
logger.Debug("[WebSocketSpec] FetchRowNumber query: %s, pkValue: %s", queryStr, pkValue)
// Append PK value to whereArgs
whereArgs = append(whereArgs, pkValue)
// Execute the raw query with parameterized PK value
var result []struct {
RN int64 `bun:"rn"`
}
err := h.db.Query(ctx, &result, queryStr, whereArgs...)
if err != nil {
return 0, fmt.Errorf("failed to fetch row number: %w", err)
}
if len(result) == 0 {
whereInfo := "none"
if whereSQL != "" {
whereInfo = whereSQL
}
return 0, fmt.Errorf("no row found for primary key %s=%s with active filters: %s", pkName, pkValue, whereInfo)
}
return result[0].RN, nil
}
// Shutdown gracefully shuts down the handler // Shutdown gracefully shuts down the handler
func (h *Handler) Shutdown() { func (h *Handler) Shutdown() {
h.connManager.Shutdown() h.connManager.Shutdown()