Skip to content

Conversation

@plutov
Copy link
Owner

@plutov plutov commented Aug 3, 2025

Summary by CodeRabbit

  • New Features

    • Added comprehensive support for managing surveys and survey sessions, including creation, updating, retrieval, and deletion of surveys, questions, answers, and sessions.
    • Introduced new SQL queries and methods for handling survey data, session tracking, and webhook responses.
    • Enhanced testing capabilities with new autogenerated mocks for database and storage interfaces.
    • Added utility to decode UUID strings to support data handling.
  • Refactor

    • Improved storage implementation by switching from raw SQL to generated, type-safe query methods for better maintainability and reliability.
  • Documentation

    • Simplified deployment instructions for backend, frontend, and database services in the documentation.
  • Chores

    • Added configuration for automated mock generation to streamline testing and development workflows.

@coderabbitai
Copy link

coderabbitai bot commented Aug 3, 2025

Walkthrough

This update significantly refactors and expands the backend data access layer for surveys. It introduces new, strongly-typed SQL queries and corresponding Go methods for survey and session management, replaces hand-written mocks with auto-generated ones using mockery, and updates the storage implementation to leverage the new query interfaces. Supporting utilities and documentation are also updated for consistency and maintainability.

Changes

Cohort / File(s) Change Summary
SQL Query Expansion & Go Bindings
api/pkg/db/queries/surveys.sql, api/pkg/db/surveys.sql.go
Added comprehensive SQL queries and generated Go methods for survey, question, answer, and session CRUD, status updates, paginated retrieval, and webhook response storage. Introduced new types and parameter/result structs for strong typing.
Storage Layer Refactor
api/pkg/storage/postgres.go
Refactored storage methods to use generated query interfaces instead of raw SQL. Enhanced error handling, switched to typed parameters/results, and improved maintainability.
Mock Generation Modernization
api/mocks/answer.go, api/mocks/interface.go, api/pkg/db/mocks_test.go, api/pkg/storage/mocks_test.go, api/pkg/types/mocks_test.go
Deleted old, hand-written or outdated mocks. Added new, mockery-generated mocks for interfaces in db, storage, and types packages, supporting modern testify patterns and improved testability.
Mockery Configuration & Makefile
api/.mockery.yml, api/Makefile
Added mockery configuration file to standardize mock generation. Simplified Makefile target for mock generation to rely on config defaults. Added sqlc target to run SQL code generation.
DB Utility Enhancement
api/pkg/db/extend.go
Added DecodeUUID function for converting UUID strings to pgtype.UUID, complementing existing encoding utilities.
Documentation Update
README.md
Simplified the deployment section by removing detailed packaging/deployment info, listing only the core services.

Sequence Diagram(s)

sequenceDiagram
    participant Service as Storage Layer (Postgres)
    participant Queries as db.Queries (Generated)
    participant DB as Database

    Service->>Queries: Call method (e.g., UpdateSurvey)
    Queries->>DB: Execute SQL with typed params
    DB-->>Queries: Return result/rows
    Queries-->>Service: Return typed result/error
    Service-->>Caller: Return mapped domain object/error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

In the warren where data flows deep,
Mocks are reborn, and the queries now leap!
SQLs are typed, and the storage is neat,
With mockery's magic, our tests are complete.
The rabbits all cheer, as the backend refines—
For code that's this tidy, the future's divine!
🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/sqlc-all-queries

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🔭 Outside diff range comments (1)
api/pkg/storage/postgres.go (1)

445-461: Critical: SQL injection vulnerability with string interpolation

The GetSurveySessionsWithAnswers method constructs SQL queries using string formatting with user-provided sort parameters. This creates a SQL injection vulnerability. The filter.SortBy and filter.Order values are directly interpolated into the query without validation.

This method should be migrated to sqlc like the other queries, or at minimum, validate the sort parameters against a whitelist.

 func (p *Postgres) GetSurveySessionsWithAnswers(surveyUUID string, filter *types.SurveySessionsFilter) ([]types.SurveySession, int, error) {
+	// Validate sort parameters to prevent SQL injection
+	validSortColumns := map[string]bool{
+		"created_at": true,
+		"completed_at": true,
+		"status": true,
+	}
+	validOrders := map[string]bool{
+		"ASC": true,
+		"DESC": true,
+	}
+	
+	if !validSortColumns[filter.SortBy] {
+		return nil, 0, fmt.Errorf("invalid sort column: %s", filter.SortBy)
+	}
+	if !validOrders[filter.Order] {
+		return nil, 0, fmt.Errorf("invalid sort order: %s", filter.Order)
+	}
+	
 	query := fmt.Sprintf(`WITH limited_sessions AS (
🧹 Nitpick comments (6)
README.md (1)

288-290: Bullet list feels redundant and loses the actionable detail the previous version provided

The three bullets don’t add information beyond the sentence on Line 286. Either remove them or enrich each item with the concrete deployment hints that were deleted (e.g., container names, default ports, links to Dockerfiles).

api/pkg/db/queries/surveys.sql (2)

108-121: Review complex CTE query for performance.

The GetSurveySessionsWithAnswers query uses a CTE with multiple LEFT JOINs. While comprehensive, this could be expensive for large datasets.

Consider the performance implications and potential for N+1-like issues. Monitor query execution plans in production and consider:

  1. Adding appropriate indexes
  2. Breaking into separate queries if performance becomes an issue
  3. Implementing result caching for frequently accessed data
-- Potential performance indexes to consider:
-- CREATE INDEX idx_surveys_sessions_survey_created ON surveys_sessions(survey_id, created_at DESC);
-- CREATE INDEX idx_surveys_answers_session ON surveys_answers(session_id);
-- CREATE INDEX idx_surveys_webhook_responses_session ON surveys_webhook_responses(session_id);

12-19: Ensure composite index on surveys_sessions(survey_id, status) for optimal performance

I wasn’t able to find an existing index on surveys_sessions(survey_id, status) in the SQL or Go code—please double-check your migration files or schema to confirm it’s present. If it’s missing, add:

-- Index to speed up correlated subqueries in GetSurveys
CREATE INDEX idx_surveys_sessions_survey_status 
  ON surveys_sessions(survey_id, status);
api/pkg/db/surveys.sql.go (2)

85-88: Rename generic parameter name for clarity

The parameter Column2 in DeleteSurveyQuestionsNotInListParams is generic and doesn't convey its purpose. Consider renaming it to QuestionIDs or KeepQuestionIDs for better readability.


223-225: Consider using more descriptive parameter names in SQL

The auto-generated parameter names like Uuid_2 are not very descriptive. Consider updating the SQL query parameter names to be more specific (e.g., $session_uuid and $survey_uuid) which would result in better generated Go code.

Also applies to: 572-576

api/pkg/storage/postgres.go (1)

314-325: Simplify conditional logic

The else block is unnecessary after a return statement.

 	if newStatus == types.SurveySessionStatus_Completed {
 		return p.queries.UpdateSurveySessionStatusCompleted(p.ctx, db.UpdateSurveySessionStatusCompletedParams{
 			Status: db.NullSurveysSessionsStatus{Valid: true, SurveysSessionsStatus: db.SurveysSessionsStatus(newStatus)},
 			Uuid:   uuid,
 		})
-	} else {
-		return p.queries.UpdateSurveySessionStatus(p.ctx, db.UpdateSurveySessionStatusParams{
-			Status: db.NullSurveysSessionsStatus{Valid: true, SurveysSessionsStatus: db.SurveysSessionsStatus(newStatus)},
-			Uuid:   uuid,
-		})
 	}
+	
+	return p.queries.UpdateSurveySessionStatus(p.ctx, db.UpdateSurveySessionStatusParams{
+		Status: db.NullSurveysSessionsStatus{Valid: true, SurveysSessionsStatus: db.SurveysSessionsStatus(newStatus)},
+		Uuid:   uuid,
+	})
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05060db and 4188263.

📒 Files selected for processing (12)
  • README.md (1 hunks)
  • api/.mockery.yml (1 hunks)
  • api/Makefile (1 hunks)
  • api/mocks/answer.go (0 hunks)
  • api/mocks/interface.go (0 hunks)
  • api/pkg/db/extend.go (1 hunks)
  • api/pkg/db/mocks_test.go (1 hunks)
  • api/pkg/db/queries/surveys.sql (1 hunks)
  • api/pkg/db/surveys.sql.go (1 hunks)
  • api/pkg/storage/mocks_test.go (1 hunks)
  • api/pkg/storage/postgres.go (4 hunks)
  • api/pkg/types/mocks_test.go (1 hunks)
💤 Files with no reviewable changes (2)
  • api/mocks/interface.go
  • api/mocks/answer.go
🔇 Additional comments (10)
api/pkg/db/extend.go (1)

26-30: LGTM! Clean UUID decoding utility.

The DecodeUUID function provides a clean abstraction for converting UUID strings to pgtype.UUID. The implementation correctly delegates to pgtype.UUID.Scan() for proper validation and parsing, maintaining consistency with the existing EncodeUUID function.

api/.mockery.yml (1)

1-18: Excellent centralized mock configuration.

The mockery configuration follows best practices by:

  • Centralizing mock generation settings in a single file
  • Using the testify template for comprehensive mock functionality
  • Applying consistent naming conventions (mocks_test.go)
  • Enabling recursive processing for all interfaces
  • Including proper formatting with goimports

This approach significantly improves maintainability compared to scattered manual mock configurations.

api/Makefile (1)

9-9: Good simplification leveraging centralized configuration.

Removing the explicit mockery flags in favor of the .mockery.yml configuration file improves maintainability and follows the DRY principle. The mock generation parameters are now centrally managed and version-controlled.

api/pkg/types/mocks_test.go (1)

1-145: Auto-generated mock looks correct.

This auto-generated mock file follows standard mockery + testify patterns correctly:

  • Proper mock embedding and constructor
  • Type-safe fluent API for method expectations
  • Appropriate panic handling for missing return values
  • Clean separation of concerns with typed call structs

Since this is generated code, the main verification is that the generation process worked correctly, which it has.

api/pkg/db/queries/surveys.sql (4)

7-10: LGTM! Clean update query with proper parameterization.

The UpdateSurvey query correctly uses parameterized queries to prevent SQL injection and follows a standard UPDATE pattern. All survey fields are updateable via parameters.


55-59: Good use of subquery for referential integrity.

The CreateSurveySession query correctly uses a subquery to resolve the survey UUID to its ID, maintaining referential integrity while accepting UUID parameters.


61-64: LGTM! Proper timestamp handling for completion.

The UpdateSurveySessionStatusCompleted query correctly sets both status and completion timestamp atomically using NOW().


37-40: Ensure Empty Slice Behavior Is Intentional

The code in api/pkg/storage/postgres.go allocates questionIds from the length of survey.Config.Questions.Questions, so when there are no questions it passes an empty []string{} into the DeleteSurveyQuestionsNotInList call. In PostgreSQL:

  • question_id != ALL(ARRAY[]::text[]) is always true (vacuous truth), so the DELETE will remove all rows for that survey.
  • That matches the semantic “no questions remain → delete all existing questions.”

No adjustments are required unless you want different behavior on empty lists. If you do, you could:

• Skip calling DeleteSurveyQuestionsNotInList when len(questionIds) == 0 to preserve existing rows
• Switch to question_id = ANY($2::text[]) with an empty array check if your intent is “delete none”

Otherwise, this is correct.

api/pkg/storage/mocks_test.go (1)

1-1306: Auto-generated mocks look good

The mockery-generated mocks follow the standard testify template and include all necessary methods for MockInterface and MockFileInterface. The expecter pattern and cleanup functions are properly implemented.

api/pkg/db/mocks_test.go (1)

1-279: Auto-generated database mocks are well-structured

The DBTX mock properly handles variadic arguments and context parameters for database operations. The implementation correctly manages both fixed and variable argument scenarios.

Comment on lines 130 to 133
-- name: StoreWebhookResponse :exec
INSERT INTO surveys_webhook_responses
(created_at, session_id, response_status, response)
VALUES ($1, $2, $3, $4);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify webhook response data handling.

The StoreWebhookResponse query looks correct but ensure the calling code properly handles potential large response payloads and validates the response_status values.


🏁 Script executed:

#!/bin/bash
# Check webhook response handling and any size limits
echo "Searching for webhook response handling..."
rg -A 10 -B 5 "StoreWebhookResponse|webhook.*response"

Length of output: 12553


Enforce payload size limit & status code validation for webhook responses

The call in api/pkg/surveys/sessions.go currently uses io.ReadAll(resp.Body) without any cap and stores the raw resp.StatusCode directly. To prevent unbounded memory use and ensure only valid statuses are recorded, please:

  • In api/pkg/surveys/sessions.go around the io.ReadAll(resp.Body) call, wrap resp.Body in an io.LimitReader (e.g. io.LimitReader(resp.Body, maxPayloadBytes)) and define a reasonable maximum (e.g. 1 MB).
  • After reading, if the payload exceeds your limit, decide whether to truncate, reject, or archive it externally instead of storing the full string.
  • Validate resp.StatusCode before passing it to StoreWebhookResponse (e.g. ensure it’s within 100–599, map unexpected codes to an error state, or enforce a whitelist of expected codes).
🤖 Prompt for AI Agents
In api/pkg/surveys/sessions.go near the io.ReadAll(resp.Body) call, wrap
resp.Body with io.LimitReader using a defined maxPayloadBytes (e.g., 1 MB) to
cap the read size. After reading, implement logic to handle payloads exceeding
this limit by truncating, rejecting, or archiving externally instead of storing
the full content. Also, validate resp.StatusCode before passing it to
StoreWebhookResponse to ensure it falls within the 100–599 range or matches an
allowed set; map any invalid codes to an error state or handle accordingly. This
prevents unbounded memory use and ensures only valid status codes are stored.

Comment on lines 54 to 77
const createSurveySession = `-- name: CreateSurveySession :one
INSERT INTO surveys_sessions
(status, survey_id, ip_addr)
VALUES ($1, (SELECT s.id FROM surveys s WHERE s.uuid = $2), $3)
RETURNING id, uuid
`

type CreateSurveySessionParams struct {
Status NullSurveysSessionsStatus
Uuid pgtype.UUID
IpAddr pgtype.Text
}

type CreateSurveySessionRow struct {
ID int32
Uuid pgtype.UUID
}

func (q *Queries) CreateSurveySession(ctx context.Context, arg CreateSurveySessionParams) (CreateSurveySessionRow, error) {
row := q.db.QueryRow(ctx, createSurveySession, arg.Status, arg.Uuid, arg.IpAddr)
var i CreateSurveySessionRow
err := row.Scan(&i.ID, &i.Uuid)
return i, err
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider adding index on surveys.uuid for subquery performance

The CreateSurveySession query uses a subquery to fetch survey ID by UUID. This operation would benefit from an index on surveys.uuid if not already present.


🏁 Script executed:

#!/bin/bash
# Check if there's an index on surveys.uuid column
rg -A 5 "CREATE.*INDEX.*surveys.*uuid" --glob "migrations/*.sql"

Length of output: 215


I wasn’t able to find any SQL files under the migrations/ folder with your first search. Let’s broaden the search to locate your migrations directory and then scan all .sql files for any index on a uuid column:


🏁 Script executed:

#!/bin/bash
# 1. Locate any migrations folders in the repo:
echo "🔍 Looking for migrations directories:"
find . -type d -name migrations

# 2. Search all SQL files for index creation on uuid columns:
echo -e "\n🔍 Searching for CREATE INDEX statements referencing 'uuid':"
rg -Hn "CREATE\s+INDEX" --glob "*.sql" | grep -i uuid || echo "No CREATE INDEX on uuid found in .sql files"

Length of output: 433


Add missing index on surveys.uuid for subquery performance

To speed up the lookup in CreateSurveySession—which does
(SELECT s.id FROM surveys s WHERE s.uuid = $2)—please add an index on the uuid column:

• Create a new migration under api/migrations, for example:
20250801T120000_create_surveys_uuid_index.sql

-- Add index on surveys.uuid to optimize session-creation subquery
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_surveys_uuid
    ON surveys (uuid);

• This ensures the subquery in
api/pkg/db/surveys.sql.go:CreateSurveySession
benefits from index lookups rather than full table scans.

🤖 Prompt for AI Agents
In api/pkg/db/surveys.sql.go around lines 54 to 77, the subquery in
CreateSurveySession uses surveys.uuid without an index, causing slow lookups. To
fix this, add a new migration SQL file under api/migrations named like
20250801T120000_create_surveys_uuid_index.sql that creates a concurrent index on
the surveys.uuid column. This will optimize the subquery by enabling index
lookups instead of full table scans.

Comment on lines 179 to 235
func (p *Postgres) GetSurveyByField(field string, value interface{}) (*types.Survey, error) {
query := fmt.Sprintf(`SELECT
s.id, s.uuid, s.created_at,
s.parse_status, s.delivery_status,
s.error_log, s.name, s.config, s.url_slug
FROM surveys AS s
WHERE s.%s=$1;`, field)

row := p.conn.QueryRow(p.ctx, query, value)
survey := &types.Survey{}
err := row.Scan(&survey.ID, &survey.UUID, &survey.CreatedAt,
&survey.ParseStatus, &survey.DeliveryStatus, &survey.ErrorLog,
&survey.Name, &survey.Config, &survey.URLSlug)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
var survey *types.Survey
var err error

return nil, err
switch field {
case "uuid":
uuid, err := db.DecodeUUID(value.(string))
if err != nil {
return nil, fmt.Errorf("failed to decode UUID: %w", err)
}
row, err := p.queries.GetSurveyByUUID(p.ctx, uuid)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
survey = &types.Survey{
ID: int64(row.ID),
UUID: db.EncodeUUID(row.Uuid),
CreatedAt: row.CreatedAt.Time,
ParseStatus: types.SurveyParseStatus(row.ParseStatus.SurveyParseStatuses),
DeliveryStatus: types.SurveyDeliveryStatus(row.DeliveryStatus.SurveyDeliveryStatuses),
ErrorLog: row.ErrorLog.String,
Name: row.Name,
URLSlug: row.UrlSlug,
}
if err := json.Unmarshal(row.Config, &survey.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
}
case "url_slug":
row, err := p.queries.GetSurveyByURLSlug(p.ctx, value.(string))
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
survey = &types.Survey{
ID: int64(row.ID),
UUID: db.EncodeUUID(row.Uuid),
CreatedAt: row.CreatedAt.Time,
ParseStatus: types.SurveyParseStatus(row.ParseStatus.SurveyParseStatuses),
DeliveryStatus: types.SurveyDeliveryStatus(row.DeliveryStatus.SurveyDeliveryStatuses),
ErrorLog: row.ErrorLog.String,
Name: row.Name,
URLSlug: row.UrlSlug,
}
if err := json.Unmarshal(row.Config, &survey.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
}
default:
return nil, fmt.Errorf("unsupported field: %s", field)
}

return survey, nil
return survey, err
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor duplicated survey construction logic

The survey construction code is duplicated between the uuid and url_slug cases. Consider extracting this into a helper function to improve maintainability.

 func (p *Postgres) GetSurveyByField(field string, value interface{}) (*types.Survey, error) {
 	var survey *types.Survey
 	var err error
+	
+	// Helper function to construct survey from row data
+	constructSurvey := func(id int32, uuid pgtype.UUID, createdAt pgtype.Timestamp, 
+		parseStatus db.NullSurveyParseStatuses, deliveryStatus db.NullSurveyDeliveryStatuses,
+		errorLog pgtype.Text, name string, urlSlug string, config []byte) (*types.Survey, error) {
+		s := &types.Survey{
+			ID:             int64(id),
+			UUID:           db.EncodeUUID(uuid),
+			CreatedAt:      createdAt.Time,
+			ParseStatus:    types.SurveyParseStatus(parseStatus.SurveyParseStatuses),
+			DeliveryStatus: types.SurveyDeliveryStatus(deliveryStatus.SurveyDeliveryStatuses),
+			ErrorLog:       errorLog.String,
+			Name:           name,
+			URLSlug:        urlSlug,
+		}
+		if err := json.Unmarshal(config, &s.Config); err != nil {
+			return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
+		}
+		return s, nil
+	}
 
 	switch field {
 	case "uuid":
 		uuid, err := db.DecodeUUID(value.(string))
 		if err != nil {
 			return nil, fmt.Errorf("failed to decode UUID: %w", err)
 		}
 		row, err := p.queries.GetSurveyByUUID(p.ctx, uuid)
 		if err != nil {
 			if err == sql.ErrNoRows {
 				return nil, nil
 			}
 			return nil, err
 		}
-		survey = &types.Survey{
-			ID:             int64(row.ID),
-			UUID:           db.EncodeUUID(row.Uuid),
-			CreatedAt:      row.CreatedAt.Time,
-			ParseStatus:    types.SurveyParseStatus(row.ParseStatus.SurveyParseStatuses),
-			DeliveryStatus: types.SurveyDeliveryStatus(row.DeliveryStatus.SurveyDeliveryStatuses),
-			ErrorLog:       row.ErrorLog.String,
-			Name:           row.Name,
-			URLSlug:        row.UrlSlug,
-		}
-		if err := json.Unmarshal(row.Config, &survey.Config); err != nil {
-			return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
-		}
+		survey, err = constructSurvey(row.ID, row.Uuid, row.CreatedAt, row.ParseStatus, 
+			row.DeliveryStatus, row.ErrorLog, row.Name, row.UrlSlug, row.Config)
 	case "url_slug":
 		row, err := p.queries.GetSurveyByURLSlug(p.ctx, value.(string))
 		if err != nil {
 			if err == sql.ErrNoRows {
 				return nil, nil
 			}
 			return nil, err
 		}
-		survey = &types.Survey{
-			ID:             int64(row.ID),
-			UUID:           db.EncodeUUID(row.Uuid),
-			CreatedAt:      row.CreatedAt.Time,
-			ParseStatus:    types.SurveyParseStatus(row.ParseStatus.SurveyParseStatuses),
-			DeliveryStatus: types.SurveyDeliveryStatus(row.DeliveryStatus.SurveyDeliveryStatuses),
-			ErrorLog:       row.ErrorLog.String,
-			Name:           row.Name,
-			URLSlug:        row.UrlSlug,
-		}
-		if err := json.Unmarshal(row.Config, &survey.Config); err != nil {
-			return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
-		}
+		survey, err = constructSurvey(row.ID, row.Uuid, row.CreatedAt, row.ParseStatus, 
+			row.DeliveryStatus, row.ErrorLog, row.Name, row.UrlSlug, row.Config)
 	default:
 		return nil, fmt.Errorf("unsupported field: %s", field)
 	}
 
 	return survey, err
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (p *Postgres) GetSurveyByField(field string, value interface{}) (*types.Survey, error) {
query := fmt.Sprintf(`SELECT
s.id, s.uuid, s.created_at,
s.parse_status, s.delivery_status,
s.error_log, s.name, s.config, s.url_slug
FROM surveys AS s
WHERE s.%s=$1;`, field)
row := p.conn.QueryRow(p.ctx, query, value)
survey := &types.Survey{}
err := row.Scan(&survey.ID, &survey.UUID, &survey.CreatedAt,
&survey.ParseStatus, &survey.DeliveryStatus, &survey.ErrorLog,
&survey.Name, &survey.Config, &survey.URLSlug)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
var survey *types.Survey
var err error
return nil, err
switch field {
case "uuid":
uuid, err := db.DecodeUUID(value.(string))
if err != nil {
return nil, fmt.Errorf("failed to decode UUID: %w", err)
}
row, err := p.queries.GetSurveyByUUID(p.ctx, uuid)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
survey = &types.Survey{
ID: int64(row.ID),
UUID: db.EncodeUUID(row.Uuid),
CreatedAt: row.CreatedAt.Time,
ParseStatus: types.SurveyParseStatus(row.ParseStatus.SurveyParseStatuses),
DeliveryStatus: types.SurveyDeliveryStatus(row.DeliveryStatus.SurveyDeliveryStatuses),
ErrorLog: row.ErrorLog.String,
Name: row.Name,
URLSlug: row.UrlSlug,
}
if err := json.Unmarshal(row.Config, &survey.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
}
case "url_slug":
row, err := p.queries.GetSurveyByURLSlug(p.ctx, value.(string))
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
survey = &types.Survey{
ID: int64(row.ID),
UUID: db.EncodeUUID(row.Uuid),
CreatedAt: row.CreatedAt.Time,
ParseStatus: types.SurveyParseStatus(row.ParseStatus.SurveyParseStatuses),
DeliveryStatus: types.SurveyDeliveryStatus(row.DeliveryStatus.SurveyDeliveryStatuses),
ErrorLog: row.ErrorLog.String,
Name: row.Name,
URLSlug: row.UrlSlug,
}
if err := json.Unmarshal(row.Config, &survey.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
}
default:
return nil, fmt.Errorf("unsupported field: %s", field)
}
return survey, nil
return survey, err
}
func (p *Postgres) GetSurveyByField(field string, value interface{}) (*types.Survey, error) {
var survey *types.Survey
var err error
// Helper function to construct survey from row data
constructSurvey := func(id int32, uuid pgtype.UUID, createdAt pgtype.Timestamp,
parseStatus db.NullSurveyParseStatuses, deliveryStatus db.NullSurveyDeliveryStatuses,
errorLog pgtype.Text, name string, urlSlug string, config []byte,
) (*types.Survey, error) {
s := &types.Survey{
ID: int64(id),
UUID: db.EncodeUUID(uuid),
CreatedAt: createdAt.Time,
ParseStatus: types.SurveyParseStatus(parseStatus.SurveyParseStatuses),
DeliveryStatus: types.SurveyDeliveryStatus(deliveryStatus.SurveyDeliveryStatuses),
ErrorLog: errorLog.String,
Name: name,
URLSlug: urlSlug,
}
if err := json.Unmarshal(config, &s.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal survey config: %w", err)
}
return s, nil
}
switch field {
case "uuid":
uuid, err := db.DecodeUUID(value.(string))
if err != nil {
return nil, fmt.Errorf("failed to decode UUID: %w", err)
}
row, err := p.queries.GetSurveyByUUID(p.ctx, uuid)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
survey, err = constructSurvey(
row.ID, row.Uuid, row.CreatedAt,
row.ParseStatus, row.DeliveryStatus,
row.ErrorLog, row.Name, row.UrlSlug, row.Config,
)
case "url_slug":
row, err := p.queries.GetSurveyByURLSlug(p.ctx, value.(string))
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
survey, err = constructSurvey(
row.ID, row.Uuid, row.CreatedAt,
row.ParseStatus, row.DeliveryStatus,
row.ErrorLog, row.Name, row.UrlSlug, row.Config,
)
default:
return nil, fmt.Errorf("unsupported field: %s", field)
}
return survey, err
}
🤖 Prompt for AI Agents
In api/pkg/storage/postgres.go between lines 179 and 235, the code that
constructs the Survey struct from the database row is duplicated in both the
"uuid" and "url_slug" cases. To fix this, extract the survey construction logic
into a separate helper function that takes the database row as input and returns
a *types.Survey and error. Then call this helper function in both cases to
reduce duplication and improve maintainability.

@plutov plutov merged commit d55c0b0 into main Aug 4, 2025
3 of 4 checks passed
@plutov plutov deleted the chore/sqlc-all-queries branch August 4, 2025 20:08
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
api/pkg/db/queries/surveys.sql (3)

52-66: Add index on surveys.uuid for query performance

This query would benefit from an index on the surveys.uuid column for optimal performance.


106-117: Add index on surveys.uuid for subquery performance

The subquery (SELECT s.id FROM surveys s WHERE s.uuid = $2) requires an index on surveys.uuid for optimal performance.


253-255: Validate webhook response data before storage

Ensure proper validation of response_status values and response payload size limits in the application layer.

api/pkg/db/surveys.sql.go (1)

54-84: Performance concern inherited from SQL query

This function executes a subquery to find the survey ID. As mentioned in the SQL review, this requires an index on surveys.uuid.

🧹 Nitpick comments (8)
api/pkg/db/queries/surveys.sql (5)

20-50: Optimize GetSurveys query with conditional aggregation

The current implementation uses two subqueries that execute for each survey row, which can be inefficient with many surveys. Consider using a LEFT JOIN with conditional aggregation for better performance.

-- name: GetSurveys :many
SELECT
    s.id,
    s.uuid,
    s.created_at,
    s.parse_status,
    s.delivery_status,
    s.error_log,
    s.name,
    s.config,
    s.url_slug,
-    (
-        SELECT
-            COUNT(*)
-        FROM
-            surveys_sessions ss
-        WHERE
-            ss.survey_id = s.id
-            AND ss.status = $1) AS sessions_count_in_progress,
-    (
-        SELECT
-            COUNT(*)
-        FROM
-            surveys_sessions ss
-        WHERE
-            ss.survey_id = s.id
-            AND ss.status = $2) AS sessions_count_completed
+    COALESCE(SUM(CASE WHEN ss.status = $1 THEN 1 ELSE 0 END), 0) AS sessions_count_in_progress,
+    COALESCE(SUM(CASE WHEN ss.status = $2 THEN 1 ELSE 0 END), 0) AS sessions_count_completed
FROM
    surveys AS s
+    LEFT JOIN surveys_sessions ss ON ss.survey_id = s.id
+GROUP BY
+    s.id, s.uuid, s.created_at, s.parse_status, s.delivery_status, s.error_log, s.name, s.config, s.url_slug
ORDER BY
    s.created_at DESC;

68-82: Add index on surveys.url_slug for query performance

Similar to the UUID query, this query would benefit from an index on the surveys.url_slug column.

Create a migration to add the index:

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_surveys_url_slug
    ON surveys (url_slug);

154-166: Consider index on ip_addr for frequent lookups

If IP-based session lookups are frequent, consider adding an index on surveys_sessions.ip_addr to improve query performance.

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_surveys_sessions_ip_addr
    ON surveys_sessions (survey_id, ip_addr);

168-185: Add index on surveys_sessions.uuid for subquery performance

The subquery in the WHERE clause would benefit from an index on surveys_sessions.uuid.

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_surveys_sessions_uuid
    ON surveys_sessions (uuid);

187-205: Optimize UpsertSurveyQuestionAnswer with JOIN instead of subqueries

The two subqueries in the VALUES clause execute separately and could be optimized using a single query with JOINs.

Consider refactoring to use a CTE or rewriting the application logic to pass the IDs directly if they're already known, which would eliminate the subqueries entirely.

api/pkg/db/surveys.sql.go (3)

92-95: Improve parameter naming for clarity

The parameter Column2 is auto-generated but unclear. Consider using sqlc's @rename comment in the SQL file to give it a more descriptive name like QuestionIDs.

In the SQL file, update the query:

-- name: DeleteSurveyQuestionsNotInList :exec
DELETE FROM surveys_questions
WHERE survey_id = $1
    AND question_id != ALL ($2::text[]); -- @rename question_ids

256-259: Clarify parameter names for better readability

The parameters Uuid and Uuid_2 are unclear. Use sqlc's parameter naming feature to make them more descriptive.

In the SQL file, update the query to use named parameters:

-- name: GetSurveySession :one
SELECT
    ss.id,
    ss.uuid,
    ss.created_at,
    ss.status,
    s.uuid AS survey_uuid
FROM
    surveys_sessions AS ss
    INNER JOIN surveys AS s ON s.id = ss.survey_id
WHERE
    ss.uuid = @session_uuid
    AND s.uuid = @survey_uuid;

694-698: Improve parameter naming for UpsertSurveyQuestionAnswer

Similar to other functions, Uuid and Uuid_2 should be renamed to SessionUuid and QuestionUuid for clarity.

Update the SQL query to use named parameters:

-- name: UpsertSurveyQuestionAnswer :exec
INSERT INTO surveys_answers (session_id, question_id, answer)
    VALUES ((
            SELECT
                ss.id
            FROM
                surveys_sessions ss
            WHERE
                ss.uuid = @session_uuid), (
                SELECT
                    sq.id
                FROM
                    surveys_questions sq
                WHERE
                    sq.uuid = @question_uuid), @answer)
    ON CONFLICT (session_id,
        question_id)
    DO UPDATE SET
        answer = EXCLUDED.answer;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4188263 and b011d53.

📒 Files selected for processing (4)
  • api/Makefile (1 hunks)
  • api/pkg/db/queries/surveys.sql (1 hunks)
  • api/pkg/db/surveys.sql.go (1 hunks)
  • api/pkg/storage/postgres.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/Makefile
  • api/pkg/storage/postgres.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants