Repository: rm-hull/gps-routes-api
Pull request #40: Specify whether to truncate text in search results
Author: login: rm-hull
Created: 2025-04-19T14:05:12Z | Merged: 2025-04-19T14:51:24Z



Diff:
diff --git a/db/query_builder.go b/db/query_builder.go
index 973468c38..0dee28fd0 100644
--- a/db/query_builder.go
+++ b/db/query_builder.go
@@ -10,23 +10,25 @@ import (
 )
 
 type QueryBuilder struct {
-	criteria       *request.SearchRequest
-	selectPart     string
-	whereClauses   []string
-	excludedFacets map[string]struct{}
-	arrayFields    map[string]struct{}
-	params         []any
-	orderBy        string
-	groupBy        string
-	limit          string
-	offset         string
+	criteria        *request.SearchRequest
+	selectPart      string
+	whereClauses    []string
+	excludedFacets  map[string]struct{}
+	arrayFields     map[string]struct{}
+	params          []any
+	orderBy         string
+	groupBy         string
+	limit           string
+	offset          string
+	truncatedFields map[string]int
 }
 
 func NewQueryBuilder(selectPart string, criteria *request.SearchRequest) *QueryBuilder {
 	qb := &QueryBuilder{
-		selectPart: selectPart,
-		criteria:   criteria,
-		params:     make([]any, 0),
+		selectPart:      selectPart,
+		criteria:        criteria,
+		params:          make([]any, 0),
+		truncatedFields: make(map[string]int, 0),
 	}
 
 	return qb.applyWhereConditions()
@@ -93,6 +95,11 @@ func (qb *QueryBuilder) WithArrayFields(arrayFields ...string) *QueryBuilder {
 	return qb
 }
 
+func (qb *QueryBuilder) WithTruncatedField(fieldName string, maxLength int) *QueryBuilder {
+	qb.truncatedFields[fieldName] = maxLength
+	return qb
+}
+
 func (qb *QueryBuilder) Build() (string, []interface{}) {
 
 	if qb.criteria.Facets != nil {
@@ -121,11 +128,24 @@ func (qb *QueryBuilder) Build() (string, []interface{}) {
 		whereClause = "WHERE " + strings.Join(qb.whereClauses, " AND ")
 	}
 
-	filteredParts := removeEmptyStrings([]string{qb.selectPart, whereClause, qb.groupBy, qb.orderBy, qb.offset, qb.limit})
+	selectPart := qb.replaceTruncatedFields(qb.selectPart)
+	filteredParts := removeEmptyStrings(selectPart, whereClause, qb.groupBy, qb.orderBy, qb.offset, qb.limit)
 	return strings.Join(filteredParts, " "), qb.params
 }
 
-func removeEmptyStrings(slice []string) []string {
+func (qb *QueryBuilder) replaceTruncatedFields(query string) string {
+	for fieldName, maxLength := range qb.truncatedFields {
+		query = strings.ReplaceAll(query, fieldName, truncate(fieldName, maxLength))
+	}
+	return query
+}
+
+func truncate(fieldName string, maxLength int) string {
+	return fmt.Sprintf("LEFT(%s, %d) || CASE WHEN LENGTH(%s) > %d THEN '…' ELSE '' END AS %s",
+		fieldName, maxLength, fieldName, maxLength, fieldName)
+}
+
+func removeEmptyStrings(slice ...string) []string {
 	var result []string
 	for _, str := range slice {
 		if str != "" {
diff --git a/db/query_builder_test.go b/db/query_builder_test.go
index 405a040d5..65cc382dc 100644
--- a/db/query_builder_test.go
+++ b/db/query_builder_test.go
@@ -125,6 +125,14 @@ func TestQueryBuilder_Build(t *testing.T) {
 				params: []interface{}{int32(20)},
 			},
 		},
+		{
+			name: "with truncated fields",
+			qb:   NewQueryBuilder("SELECT description FROM routes", &request.SearchRequest{}).WithTruncatedField("description", 100),
+			want: Result{
+				sql:    "SELECT LEFT(description, 100) || CASE WHEN LENGTH(description) > 100 THEN '…' ELSE '' END AS description FROM routes",
+				params: []interface{}{},
+			},
+		},
 		{
 			name: "with all",
 			qb: NewQueryBuilder("SELECT * FROM routes", &request.SearchRequest{}).
diff --git a/models/request/search_request.go b/models/request/search_request.go
index 9fd249a3f..2bda4ec74 100644
--- a/models/request/search_request.go
+++ b/models/request/search_request.go
@@ -18,6 +18,8 @@ type SearchRequest struct {
 	Facets map[string][]string `json:"facets,omitempty"`
 
 	Nearby *Nearby `json:"nearby,omitempty"`
+
+	TruncateText bool `json:"truncateText,omitempty"`
 }
 
 type Nearby struct {
diff --git a/repositories/postgres.go b/repositories/postgres.go
index 3c9582c33..fde70dacd 100644
--- a/repositories/postgres.go
+++ b/repositories/postgres.go
@@ -276,15 +276,10 @@ func (repo *PostgresDbRepository) SearchHits(ctx context.Context, criteria *requ
 
 	selectPart := `
 		SELECT
-			object_id, ref,
-			CASE
-				WHEN LENGTH(title) > 50 THEN SUBSTRING(title, 1, 49) || '…'
-				ELSE title
-			END AS title,
-			CASE
-				WHEN LENGTH(description) > 150 THEN SUBSTRING(description, 1, 149) || '…'
-				ELSE description
-			END AS description,
+			object_id,
+			ref,
+			title,
+			description,
 			headline_image_url,
 			ST_X(_geoloc) AS longitude,
 			ST_Y(_geoloc) AS latitude,
@@ -301,12 +296,18 @@ func (repo *PostgresDbRepository) SearchHits(ctx context.Context, criteria *requ
 		sortField = "ts_rank_cd(search_vector, to_tsquery($1), 32) DESC"
 	}
 
-	query, params := db.NewQueryBuilder(selectPart, criteria).
+	qb := db.NewQueryBuilder(selectPart, criteria).
 		WithArrayFields(DEFAULT_ARRAY_FIELDS...).
 		WithOrderBy(sortField).
 		WithOffset(criteria.Offset).
-		WithLimit(criteria.Limit).
-		Build()
+		WithLimit(criteria.Limit)
+
+	if criteria.TruncateText {
+		qb.WithTruncatedField("title", 50)
+		qb.WithTruncatedField("description", 150)
+	}
+
+	query, params := qb.Build()
 
 	rows, err := repo.pool.Query(ctx, query, params...)
 	if err != nil {


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @rm-hull, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces a new feature that allows the caller to specify whether the text in search results should be truncated. A new `TruncateText` field is added to the `SearchRequest` struct in `models/request/search_request.go`. The `SearchHits` function in `repositories/postgres.go` is modified to conditionally truncate the title and description fields based on the value of the `TruncateText` field in the `SearchRequest`.

### Highlights
* **New Feature: Truncate Text**: Adds the ability to truncate the title and description fields in search results based on a new `TruncateText` flag in the search request.
* **Conditional Truncation Logic**: Implements conditional logic in the `SearchHits` function to truncate text fields in the database query based on the `TruncateText` flag.

### Changelog
* **models/request/search_request.go**
  * Added `TruncateText` field to the `SearchRequest` struct.
* **repositories/postgres.go**
  * Modified `SearchHits` function to conditionally truncate title and description based on the `TruncateText` flag in the request.
  * Introduced a `titleDescription` variable to hold the SQL fragment for selecting title and description, either truncated or not.
  * Used `fmt.Sprintf` to construct the `selectPart` query with the appropriate `titleDescription` fragment.


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A query's tale,
> Truncate or reveal,
> Data's choice is made.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T14:05:19Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This pull request introduces a new feature to truncate text in search results based on a boolean flag in the search request. The implementation involves modifying the `SearchRequest` struct and updating the SQL query in the `SearchHits` function to conditionally truncate the title and description fields. Overall, the changes seem reasonable, but there are a few areas that could be improved.

### Summary of Findings
* **SQL Injection Risk**: The current implementation uses `fmt.Sprintf` to construct the SQL query, which can be vulnerable to SQL injection if the `titleDescription` variable is not properly sanitized. While the current code only uses internal values, it's a good practice to avoid `fmt.Sprintf` for building SQL queries to prevent potential security risks in the future.
* **Code Duplication**: The `CASE WHEN` logic for truncating the title and description is duplicated within the `titleDescription` variable. This duplication can make the code harder to maintain and understand. Consider extracting this logic into a separate function or using a more concise way to build the SQL fragment.
* **Readability**: The use of a ternary-like operator with a string assignment for `titleDescription` can be less readable than a more explicit `if/else` block, especially for developers not familiar with Go's idioms.

### Merge Readiness
The pull request introduces a useful feature, but it's important to address the potential SQL injection risk and improve code maintainability before merging. I recommend addressing the `critical` and `high` severity issues before merging. I am unable to directly approve this pull request, and recommend that others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T14:05:49Z
user: login: gemini-code-assist[bot]
body: ## Code Review
The pull request introduces a new feature to truncate text in search results, which enhances the user experience by preventing long text fields from cluttering the search interface. The implementation involves adding a `TruncateText` field to the `SearchRequest` model, a `WithTruncatedField` method to the `QueryBuilder` struct, and logic in the `PostgresDbRepository` to apply the truncation based on the `TruncateText` flag. Overall, the changes seem well-structured and address the intended functionality.

### Summary of Findings
* **Potential SQL Injection Vulnerability**: The `truncate` function in `db/query_builder.go` constructs a SQL fragment using `fmt.Sprintf`, which could be vulnerable to SQL injection if `fieldName` is not properly sanitized. Although the code appears to use internal values for `fieldName`, it's crucial to ensure that no external input can influence this value to prevent potential security risks. I did not add a review comment for this issue due to the review settings.
* **Inconsistent Naming**: The `removeEmptyStrings` function in `db/query_builder.go` accepts a variadic slice of strings (`...string`), but the test case in `db/query_builder_test.go` does not include a test case for this function. I did not add a review comment for this issue due to the review settings.

### Merge Readiness
The pull request is well-structured and introduces a useful feature. However, due to the potential SQL injection vulnerability in the `truncate` function, I recommend addressing this issue before merging. I am unable to directly approve the pull request, and users should have others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T14:48:33Z

Review comments:
diff_hunk: @@ -274,22 +274,28 @@ func (repo *PostgresDbRepository) FacetCounts(ctx context.Context, criteria *req
 
 func (repo *PostgresDbRepository) SearchHits(ctx context.Context, criteria *request.SearchRequest) (*[]domain.RouteSummary, error) {
 
-	selectPart := `
-		SELECT
-			object_id, ref,
+	titleDescription := "title, description"
+	if criteria.TruncateText {
+		titleDescription = `
 			CASE
 				WHEN LENGTH(title) > 50 THEN SUBSTRING(title, 1, 49) || '…'
 				ELSE title
 			END AS title,
 			CASE
 				WHEN LENGTH(description) > 150 THEN SUBSTRING(description, 1, 149) || '…'
 				ELSE description
-			END AS description,
+			END AS description`
+	}
+
+	selectPart := fmt.Sprintf(`
+		SELECT
+			object_id, ref,
+			%s,
 			headline_image_url,
 			ST_X(_geoloc) AS longitude,
 			ST_Y(_geoloc) AS latitude,
 			distance_km
-		FROM routes`
+		FROM routes`, titleDescription)
path: repositories/postgres.go
user: login: gemini-code-assist[bot]
body: ![critical](https://img.shields.io/badge/priority-critical-red)

Using `fmt.Sprintf` to construct the SQL query can introduce a potential SQL injection vulnerability if the `titleDescription` variable is not properly sanitized. While the current code only uses internal values, it's a good practice to avoid `fmt.Sprintf` for building SQL queries to prevent potential security risks in the future. Consider using a query builder library or parameterized queries instead.
created_at: 2025-04-19T14:05:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,22 +274,28 @@ func (repo *PostgresDbRepository) FacetCounts(ctx context.Context, criteria *req
 
 func (repo *PostgresDbRepository) SearchHits(ctx context.Context, criteria *request.SearchRequest) (*[]domain.RouteSummary, error) {
 
-	selectPart := `
-		SELECT
-			object_id, ref,
+	titleDescription := "title, description"
+	if criteria.TruncateText {
+		titleDescription = `
 			CASE
 				WHEN LENGTH(title) > 50 THEN SUBSTRING(title, 1, 49) || '…'
 				ELSE title
 			END AS title,
 			CASE
 				WHEN LENGTH(description) > 150 THEN SUBSTRING(description, 1, 149) || '…'
 				ELSE description
-			END AS description,
+			END AS description`
path: repositories/postgres.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

This approach of constructing the SQL query using string concatenation and conditional logic can be prone to errors and harder to maintain. Consider using a more structured approach, such as a query builder library or parameterized queries, to avoid potential SQL injection vulnerabilities and improve readability.

Alternatively, you could construct the `CASE WHEN` clauses dynamically and safely using `fmt.Sprintf` with carefully controlled inputs, but using a query builder is generally preferred.
created_at: 2025-04-19T14:05:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,22 +274,28 @@ func (repo *PostgresDbRepository) FacetCounts(ctx context.Context, criteria *req
 
 func (repo *PostgresDbRepository) SearchHits(ctx context.Context, criteria *request.SearchRequest) (*[]domain.RouteSummary, error) {
 
-	selectPart := `
-		SELECT
-			object_id, ref,
+	titleDescription := "title, description"
+	if criteria.TruncateText {
+		titleDescription = `
 			CASE
 				WHEN LENGTH(title) > 50 THEN SUBSTRING(title, 1, 49) || '…'
 				ELSE title
 			END AS title,
 			CASE
 				WHEN LENGTH(description) > 150 THEN SUBSTRING(description, 1, 149) || '…'
 				ELSE description
-			END AS description,
+			END AS description`
+	}
path: repositories/postgres.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The use of a ternary-like operator with a string assignment for `titleDescription` can be less readable than a more explicit `if/else` block, especially for developers not familiar with Go's idioms. Consider using a more traditional `if/else` block for improved clarity.

```suggestion
titleDescription := "title, description"
if criteria.TruncateText {
	titleDescription = `
			CASE
				WHEN LENGTH(title) > 50 THEN SUBSTRING(title, 1, 49) || '…'
				ELSE title
			END AS title,
			CASE
				WHEN LENGTH(description) > 150 THEN SUBSTRING(description, 1, 149) || '…'
				ELSE description
			END AS description`
}
```
created_at: 2025-04-19T14:05:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,22 +274,28 @@ func (repo *PostgresDbRepository) FacetCounts(ctx context.Context, criteria *req
 
 func (repo *PostgresDbRepository) SearchHits(ctx context.Context, criteria *request.SearchRequest) (*[]domain.RouteSummary, error) {
 
-	selectPart := `
-		SELECT
-			object_id, ref,
+	titleDescription := "title, description"
+	if criteria.TruncateText {
+		titleDescription = `
 			CASE
 				WHEN LENGTH(title) > 50 THEN SUBSTRING(title, 1, 49) || '…'
 				ELSE title
 			END AS title,
 			CASE
 				WHEN LENGTH(description) > 150 THEN SUBSTRING(description, 1, 149) || '…'
 				ELSE description
-			END AS description,
+			END AS description`
+	}
path: repositories/postgres.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Duplicating the `CASE WHEN` logic for both title and description increases the risk of inconsistencies and makes future modifications more difficult. Consider creating a helper function to generate the `CASE WHEN` clause, ensuring consistency and reducing code duplication.

```go
titleDescription := "title, description"
if criteria.TruncateText {
	truncateClause := func(field string, maxLength int) string {
		return fmt.Sprintf("CASE WHEN LENGTH(%s) > %d THEN SUBSTRING(%s, 1, %d) || '…' ELSE %s END AS %s", field, maxLength, field, maxLength-1, field, field)
	}

	titleDescription = fmt.Sprintf("%s, %s", truncateClause("title", 50), truncateClause("description", 150))
}
```
created_at: 2025-04-19T14:05:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,11 +128,24 @@ func (qb *QueryBuilder) Build() (string, []interface{}) {
 		whereClause = "WHERE " + strings.Join(qb.whereClauses, " AND ")
 	}
 
-	filteredParts := removeEmptyStrings([]string{qb.selectPart, whereClause, qb.groupBy, qb.orderBy, qb.offset, qb.limit})
+	selectPart := qb.replaceTruncatedFields(qb.selectPart)
+	filteredParts := removeEmptyStrings(selectPart, whereClause, qb.groupBy, qb.orderBy, qb.offset, qb.limit)
 	return strings.Join(filteredParts, " "), qb.params
 }
 
-func removeEmptyStrings(slice []string) []string {
+func (qb *QueryBuilder) replaceTruncatedFields(query string) string {
+	for fieldName, maxLength := range qb.truncatedFields {
+		query = strings.ReplaceAll(query, fieldName, truncate(fieldName, maxLength))
+	}
+	return query
+}
+
+func truncate(fieldName string, maxLength int) string {
+	return fmt.Sprintf("LEFT(%s, %d) || CASE WHEN LENGTH(%s) > %d THEN '…' ELSE '' END AS %s",
+		fieldName, maxLength, fieldName, maxLength, fieldName)
+}
path: db/query_builder.go
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The `truncate` function constructs a SQL fragment using `fmt.Sprintf`, which could be vulnerable to SQL injection if `fieldName` is not properly sanitized. Although the code appears to use internal values for `fieldName`, it's crucial to ensure that no external input can influence this value to prevent potential security risks. Consider using parameterized queries or a more robust sanitization method to mitigate this risk.
created_at: 2025-04-19T14:48:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,11 +128,24 @@ func (qb *QueryBuilder) Build() (string, []interface{}) {
 		whereClause = "WHERE " + strings.Join(qb.whereClauses, " AND ")
 	}
 
-	filteredParts := removeEmptyStrings([]string{qb.selectPart, whereClause, qb.groupBy, qb.orderBy, qb.offset, qb.limit})
+	selectPart := qb.replaceTruncatedFields(qb.selectPart)
+	filteredParts := removeEmptyStrings(selectPart, whereClause, qb.groupBy, qb.orderBy, qb.offset, qb.limit)
 	return strings.Join(filteredParts, " "), qb.params
 }
 
-func removeEmptyStrings(slice []string) []string {
+func (qb *QueryBuilder) replaceTruncatedFields(query string) string {
+	for fieldName, maxLength := range qb.truncatedFields {
+		query = strings.ReplaceAll(query, fieldName, truncate(fieldName, maxLength))
+	}
+	return query
+}
+
+func truncate(fieldName string, maxLength int) string {
+	return fmt.Sprintf("LEFT(%s, %d) || CASE WHEN LENGTH(%s) > %d THEN '…' ELSE '' END AS %s",
+		fieldName, maxLength, fieldName, maxLength, fieldName)
+}
+
+func removeEmptyStrings(slice ...string) []string {
path: db/query_builder.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The function `removeEmptyStrings` is using a variadic parameter `slice ...string`. It would be helpful to add a test case in `db/query_builder_test.go` to test this function directly, to ensure it handles different combinations of empty and non-empty strings correctly.
created_at: 2025-04-19T14:48:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -276,15 +276,10 @@ func (repo *PostgresDbRepository) SearchHits(ctx context.Context, criteria *requ
 
 	selectPart := `
 		SELECT
-			object_id, ref,
-			CASE
-				WHEN LENGTH(title) > 50 THEN SUBSTRING(title, 1, 49) || '…'
-				ELSE title
-			END AS title,
-			CASE
-				WHEN LENGTH(description) > 150 THEN SUBSTRING(description, 1, 149) || '…'
-				ELSE description
-			END AS description,
path: repositories/postgres.go
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The original `selectPart` included logic to truncate the `title` and `description` fields directly in the SQL query. This logic has been removed in favor of the new `TruncateText` feature. Ensure that the new feature provides equivalent functionality and performance.
created_at: 2025-04-19T14:48:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-19T14:06:11Z
body: <table><tr><th><th>Tests</th><th>Passed ✅</th><th>Skipped</th><th>Failed</th></tr><tr><td>JUnit Test Report</td><td>37 ran</td><td>37 passed</td><td>0 skipped</td><td>0 failed</td></tr></table>

<table><tr><th>Test</th><th>Result</th></tr><tr><td colspan="2"><strong>JUnit Test Report</strong></td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/empty_query</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/single_word_query</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/multiple_word_query</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/bounding_box</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/nearest</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_where_clause</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_multiple_where_clauses</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_group_by</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_order_by</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_limit</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_no_limit</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_offset</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_truncated_fields</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_all</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_facets</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_empty_facets</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_excluded_facets</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build/with_array_fields</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/db.TestQueryBuilder_Build</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestToWSG84/London_-_Trafalgar_Square</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestToWSG84/Edinburgh_Castle</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestToWSG84/Cardiff_Castle</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestToWSG84/Belfast_City_Hall</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestToWSG84/Zero_values</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestToWSG84/Nil_values</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestToWSG84</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestNewNamesApi</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestFind_Success</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestFind_NoResults</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestFind_Error</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestFind_InvalidJSON</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestFind_ErrorResponse</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/services/osdatahub.TestFind_RequestError</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/tests.TestHealthEndpoint</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/tests.TestMetricsEndpoint</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/tests.TestImportData</td><td>✅ passed</td></tr><tr><td>com/rm-hull/gps-routes-api/tests.TestDbPing</td><td>✅ passed</td></tr></table>

<!-- Summary comment for ["JUnit Test Report"] by mikepenz/action-junit-report -->
author_association: NONE
user: login: coveralls
created_at: 2025-04-19T14:07:22Z
body: ## Pull Request Test Coverage Report for [Build 14550177794](https://coveralls.io/builds/73298663)


### Details

* **20** of **32**   **(62.5%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.7%**) to **45.231%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [repositories/postgres.go](https://coveralls.io/builds/73298663/source?filename=repositories%2Fpostgres.go#L279) | 0 | 12 | 0.0%
<!-- | **Total:** | **20** | **32** | **62.5%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/73298663/badge)](https://coveralls.io/builds/73298663) |
| :-- | --: |
| Change from base [Build 14540360812](https://coveralls.io/builds/73293498): |  0.7% |
| Covered Lines: | 569 |
| Relevant Lines: | 1258 |

---
##### 💛  - [Coveralls](https://coveralls.io)

author_association: NONE
user: login: rm-hull
created_at: 2025-04-19T14:48:03Z
body: /gemini review
author_association: OWNER