diff --git a/pkg/resolvespec/handler.go b/pkg/resolvespec/handler.go index 0b1251f..4511331 100644 --- a/pkg/resolvespec/handler.go +++ b/pkg/resolvespec/handler.go @@ -433,7 +433,18 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st // Execute query to get row number var result RowNumResult 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) } } 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 if options.FetchRowNumber != nil && *options.FetchRowNumber != "" { 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 { if options.Limit != nil { limit = *options.Limit diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index 68cc8be..58be0a2 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -549,8 +549,30 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st } } - // If ID is provided, filter by ID - if id != "" { + // Handle FetchRowNumber before applying ID filter + // 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) 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 - 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{ Total: int64(total), @@ -740,21 +769,10 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st Offset: offset, } - // Fetch row number for a specific record if requested - if options.FetchRowNumber != nil && *options.FetchRowNumber != "" { - pkName := reflection.GetPrimaryKeyName(model) - pkValue := *options.FetchRowNumber - - 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) - } + // If FetchRowNumber was used, also set it in metadata + if fetchedRowNumber != nil { + metadata.RowNumber = fetchedRowNumber + logger.Debug("FetchRowNumber: Row number %d set in metadata", *fetchedRowNumber) } // Execute AfterRead hooks @@ -2651,13 +2669,19 @@ func (h *Handler) FetchRowNumber(ctx context.Context, tableName string, pkName s var result []struct { RN int64 `bun:"rn"` } + logger.Debug("[FetchRowNumber] BEFORE Query call - about to execute raw query") 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 { return 0, fmt.Errorf("failed to fetch row number: %w", err) } 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 diff --git a/pkg/websocketspec/handler.go b/pkg/websocketspec/handler.go index f04995d..bb2e695 100644 --- a/pkg/websocketspec/handler.go +++ b/pkg/websocketspec/handler.go @@ -210,10 +210,14 @@ func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContex var metadata map[string]interface{} var err error - if hookCtx.ID != "" { - // Read single record by ID + // Check if FetchRowNumber is specified (treat as single record read) + 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) metadata = map[string]interface{}{"total": 1} + // The row number is already set on the record itself via setRowNumbersOnRecords } else { // Read multiple records data, metadata, err = h.readMultiple(hookCtx) @@ -510,10 +514,29 @@ func (h *Handler) notifySubscribers(schema, entity string, operation OperationTy // CRUD operation implementations 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) // Add ID filter - pkName := reflection.GetPrimaryKeyName(hookCtx.Model) query = query.Where(fmt.Sprintf("%s = ?", pkName), hookCtx.ID) // Apply columns @@ -533,6 +556,12 @@ func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) { 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 } @@ -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 func (h *Handler) Shutdown() { h.connManager.Shutdown()