mcp接口增加分页

This commit is contained in:
andy committed 2026-09-06 17:57:58 +08:00
1 parent 7243319bbb
commit 7f4adad7b1
16 files changed
+1157 -281

No files matched your search

+196 -67
View File
@@ -233,21 +233,22 @@ FROM public.%s`, quoteIdentifier(table))
}
// SearchPlaceCandidates returns bounded name matches from the fixed fire-safety source tables.
func (p *PostGIS) SearchPlaceCandidates(ctx context.Context, query domain.PlaceSearchQuery) ([]domain.PlaceCandidate, error) {
func (p *PostGIS) SearchPlaceCandidates(ctx context.Context, query domain.PlaceSearchQuery) (domain.Page[domain.PlaceCandidate], error) {
rows, err := p.pool.Query(ctx, placeCandidateSearchSQL,
query.PlaceName,
query.Scope.AllTowns,
query.Scope.AllowedTowns,
query.Limit,
query.Offset,
)
if err != nil {
return nil, err
return domain.Page[domain.PlaceCandidate]{}, err
}
defer rows.Close()
items := make([]domain.PlaceCandidate, 0, query.Limit)
page := domain.Page[domain.PlaceCandidate]{Items: make([]domain.PlaceCandidate, 0, query.Limit)}
for rows.Next() {
var item domain.PlaceCandidate
var totalCount int64
if err := rows.Scan(
&item.PlaceType,
&item.SourceRecordID,
@@ -260,35 +261,65 @@ func (p *PostGIS) SearchPlaceCandidates(ctx context.Context, query domain.PlaceS
&item.Location.Longitude,
&item.Location.Latitude,
&item.LocationKind,
&totalCount,
); err != nil {
return nil, err
rows.Close()
return domain.Page[domain.PlaceCandidate]{}, err
}
items = append(items, item)
page.Items = append(page.Items, item)
page.TotalCount = totalCount
}
return items, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return domain.Page[domain.PlaceCandidate]{}, err
}
rows.Close()
return recoverEmptyOffsetPageTotal(ctx, query.Offset, page, func(ctx context.Context) (domain.Page[domain.PlaceCandidate], error) {
query.Offset = 0
query.Limit = 1
return p.SearchPlaceCandidates(ctx, query)
})
}
// ResolveIncidentContext returns fire grids that cover the supplied point.
func (p *PostGIS) ResolveIncidentContext(ctx context.Context, point domain.Coordinate, scope domain.SpatialScope) ([]domain.IncidentContext, error) {
rows, err := p.pool.Query(ctx, resolveIncidentContextSQL, point.Longitude, point.Latitude, scope.AllTowns, scope.AllowedTowns)
func (p *PostGIS) ResolveIncidentContext(ctx context.Context, query domain.PointQuery) (domain.Page[domain.IncidentContext], error) {
rows, err := p.pool.Query(ctx, resolveIncidentContextSQL,
query.Point.Longitude,
query.Point.Latitude,
query.Scope.AllTowns,
query.Scope.AllowedTowns,
query.Limit,
query.Offset,
)
if err != nil {
return nil, err
return domain.Page[domain.IncidentContext]{}, err
}
defer rows.Close()
items := make([]domain.IncidentContext, 0)
page := domain.Page[domain.IncidentContext]{Items: make([]domain.IncidentContext, 0, query.Limit)}
for rows.Next() {
var item domain.IncidentContext
if err := rows.Scan(&item.GridID, &item.Town, &item.AreaLabel); err != nil {
return nil, err
var totalCount int64
if err := rows.Scan(&item.GridID, &item.Town, &item.AreaLabel, &totalCount); err != nil {
rows.Close()
return domain.Page[domain.IncidentContext]{}, err
}
items = append(items, item)
page.Items = append(page.Items, item)
page.TotalCount = totalCount
}
return items, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return domain.Page[domain.IncidentContext]{}, err
}
rows.Close()
return recoverEmptyOffsetPageTotal(ctx, query.Offset, page, func(ctx context.Context) (domain.Page[domain.IncidentContext], error) {
query.Offset = 0
query.Limit = 1
return p.ResolveIncidentContext(ctx, query)
})
}
// FindNearbyWaterSources returns water-source and storage-pool candidates.
func (p *PostGIS) FindNearbyWaterSources(ctx context.Context, query domain.NearbyQuery) ([]domain.WaterSource, error) {
func (p *PostGIS) FindNearbyWaterSources(ctx context.Context, query domain.NearbyQuery) (domain.Page[domain.WaterSource], error) {
rows, err := p.pool.Query(ctx, nearbyWaterSourcesSQL,
query.Point.Longitude,
query.Point.Latitude,
@@ -296,18 +327,19 @@ func (p *PostGIS) FindNearbyWaterSources(ctx context.Context, query domain.Nearb
query.Scope.AllowedTowns,
query.RadiusMeters,
query.Limit,
query.Offset,
)
if err != nil {
return nil, err
return domain.Page[domain.WaterSource]{}, err
}
defer rows.Close()
items := make([]domain.WaterSource, 0, query.Limit)
page := domain.Page[domain.WaterSource]{Items: make([]domain.WaterSource, 0, query.Limit)}
for rows.Next() {
var item domain.WaterSource
var name, village, resourceType, reportedStatus pgtype.Text
var capacity pgtype.Float8
var sourceTimestamp pgtype.Int8
var totalCount int64
if err := rows.Scan(
&item.Category,
&item.SourceRecordID,
@@ -321,8 +353,10 @@ func (p *PostGIS) FindNearbyWaterSources(ctx context.Context, query domain.Nearb
&resourceType,
&reportedStatus,
&sourceTimestamp,
&totalCount,
); err != nil {
return nil, err
rows.Close()
return domain.Page[domain.WaterSource]{}, err
}
item.Name = optionalText(name)
item.Village = optionalText(village)
@@ -330,13 +364,23 @@ func (p *PostGIS) FindNearbyWaterSources(ctx context.Context, query domain.Nearb
item.ResourceType = optionalText(resourceType)
item.ReportedStatus = optionalText(reportedStatus)
item.SourceTimestampRaw = optionalInt64(sourceTimestamp)
items = append(items, item)
page.Items = append(page.Items, item)
page.TotalCount = totalCount
}
return items, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return domain.Page[domain.WaterSource]{}, err
}
rows.Close()
return recoverEmptyOffsetPageTotal(ctx, query.Offset, page, func(ctx context.Context) (domain.Page[domain.WaterSource], error) {
query.Offset = 0
query.Limit = 1
return p.FindNearbyWaterSources(ctx, query)
})
}
// FindCommandPostCandidates returns nearby check stations and lookout posts.
func (p *PostGIS) FindCommandPostCandidates(ctx context.Context, query domain.NearbyQuery) ([]domain.CommandPostCandidate, error) {
func (p *PostGIS) FindCommandPostCandidates(ctx context.Context, query domain.NearbyQuery) (domain.Page[domain.CommandPostCandidate], error) {
rows, err := p.pool.Query(ctx, commandPostCandidatesSQL,
query.Point.Longitude,
query.Point.Latitude,
@@ -344,16 +388,17 @@ func (p *PostGIS) FindCommandPostCandidates(ctx context.Context, query domain.Ne
query.Scope.AllowedTowns,
query.RadiusMeters,
query.Limit,
query.Offset,
)
if err != nil {
return nil, err
return domain.Page[domain.CommandPostCandidate]{}, err
}
defer rows.Close()
items := make([]domain.CommandPostCandidate, 0, query.Limit)
page := domain.Page[domain.CommandPostCandidate]{Items: make([]domain.CommandPostCandidate, 0, query.Limit)}
for rows.Next() {
var item domain.CommandPostCandidate
var name, village, reportedStatus, managementUnit pgtype.Text
var totalCount int64
if err := rows.Scan(
&item.FacilityType,
&item.SourceRecordID,
@@ -365,20 +410,32 @@ func (p *PostGIS) FindCommandPostCandidates(ctx context.Context, query domain.Ne
&item.DistanceMeters,
&reportedStatus,
&managementUnit,
&totalCount,
); err != nil {
return nil, err
rows.Close()
return domain.Page[domain.CommandPostCandidate]{}, err
}
item.Name = optionalText(name)
item.Village = optionalText(village)
item.ReportedStatus = optionalText(reportedStatus)
item.ManagementUnit = optionalText(managementUnit)
items = append(items, item)
page.Items = append(page.Items, item)
page.TotalCount = totalCount
}
return items, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return domain.Page[domain.CommandPostCandidate]{}, err
}
rows.Close()
return recoverEmptyOffsetPageTotal(ctx, query.Offset, page, func(ctx context.Context) (domain.Page[domain.CommandPostCandidate], error) {
query.Offset = 0
query.Limit = 1
return p.FindCommandPostCandidates(ctx, query)
})
}
// ListNearbyAccessLines returns nearby fire access lines and closest access points.
func (p *PostGIS) ListNearbyAccessLines(ctx context.Context, query domain.NearbyQuery) ([]domain.AccessLine, error) {
func (p *PostGIS) ListNearbyAccessLines(ctx context.Context, query domain.NearbyQuery) (domain.Page[domain.AccessLine], error) {
rows, err := p.pool.Query(ctx, nearbyAccessLinesSQL,
query.Point.Longitude,
query.Point.Latitude,
@@ -386,17 +443,18 @@ func (p *PostGIS) ListNearbyAccessLines(ctx context.Context, query domain.Nearby
query.Scope.AllowedTowns,
query.RadiusMeters,
query.Limit,
query.Offset,
)
if err != nil {
return nil, err
return domain.Page[domain.AccessLine]{}, err
}
defer rows.Close()
items := make([]domain.AccessLine, 0, query.Limit)
page := domain.Page[domain.AccessLine]{Items: make([]domain.AccessLine, 0, query.Limit)}
for rows.Next() {
var item domain.AccessLine
var name, sourceUpdated pgtype.Text
var length pgtype.Float8
var totalCount int64
if err := rows.Scan(
&item.SourceRecordID,
&item.Town,
@@ -406,38 +464,68 @@ func (p *PostGIS) ListNearbyAccessLines(ctx context.Context, query domain.Nearby
&item.NearestPoint.Latitude,
&length,
&sourceUpdated,
&totalCount,
); err != nil {
return nil, err
rows.Close()
return domain.Page[domain.AccessLine]{}, err
}
item.Name = optionalText(name)
item.LengthMeters = optionalFloat64(length)
item.SourceUpdatedRaw = optionalText(sourceUpdated)
items = append(items, item)
page.Items = append(page.Items, item)
page.TotalCount = totalCount
}
return items, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return domain.Page[domain.AccessLine]{}, err
}
rows.Close()
return recoverEmptyOffsetPageTotal(ctx, query.Offset, page, func(ctx context.Context) (domain.Page[domain.AccessLine], error) {
query.Offset = 0
query.Limit = 1
return p.ListNearbyAccessLines(ctx, query)
})
}
// GetResponsibleUnits returns non-personal responsibility details from covering grids.
func (p *PostGIS) GetResponsibleUnits(ctx context.Context, point domain.Coordinate, scope domain.SpatialScope) ([]domain.ResponsibleUnit, error) {
rows, err := p.pool.Query(ctx, responsibleUnitsSQL, point.Longitude, point.Latitude, scope.AllTowns, scope.AllowedTowns)
func (p *PostGIS) GetResponsibleUnits(ctx context.Context, query domain.PointQuery) (domain.Page[domain.ResponsibleUnit], error) {
rows, err := p.pool.Query(ctx, responsibleUnitsSQL,
query.Point.Longitude,
query.Point.Latitude,
query.Scope.AllTowns,
query.Scope.AllowedTowns,
query.Limit,
query.Offset,
)
if err != nil {
return nil, err
return domain.Page[domain.ResponsibleUnit]{}, err
}
defer rows.Close()
items := make([]domain.ResponsibleUnit, 0)
page := domain.Page[domain.ResponsibleUnit]{Items: make([]domain.ResponsibleUnit, 0, query.Limit)}
for rows.Next() {
var item domain.ResponsibleUnit
if err := rows.Scan(&item.GridID, &item.Town, &item.AreaLabel, &item.FireTeam); err != nil {
return nil, err
var totalCount int64
if err := rows.Scan(&item.GridID, &item.Town, &item.AreaLabel, &item.FireTeam, &totalCount); err != nil {
rows.Close()
return domain.Page[domain.ResponsibleUnit]{}, err
}
items = append(items, item)
page.Items = append(page.Items, item)
page.TotalCount = totalCount
}
return items, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return domain.Page[domain.ResponsibleUnit]{}, err
}
rows.Close()
return recoverEmptyOffsetPageTotal(ctx, query.Offset, page, func(ctx context.Context) (domain.Page[domain.ResponsibleUnit], error) {
query.Offset = 0
query.Limit = 1
return p.GetResponsibleUnits(ctx, query)
})
}
// FindNearbyRiskAreas returns nearby cemetery and forest-enterprise polygons.
func (p *PostGIS) FindNearbyRiskAreas(ctx context.Context, query domain.NearbyQuery) ([]domain.RiskArea, error) {
func (p *PostGIS) FindNearbyRiskAreas(ctx context.Context, query domain.NearbyQuery) (domain.Page[domain.RiskArea], error) {
rows, err := p.pool.Query(ctx, nearbyRiskAreasSQL,
query.Point.Longitude,
query.Point.Latitude,
@@ -445,16 +533,17 @@ func (p *PostGIS) FindNearbyRiskAreas(ctx context.Context, query domain.NearbyQu
query.Scope.AllowedTowns,
query.RadiusMeters,
query.Limit,
query.Offset,
)
if err != nil {
return nil, err
return domain.Page[domain.RiskArea]{}, err
}
defer rows.Close()
items := make([]domain.RiskArea, 0, query.Limit)
page := domain.Page[domain.RiskArea]{Items: make([]domain.RiskArea, 0, query.Limit)}
for rows.Next() {
var item domain.RiskArea
var name, village, direction pgtype.Text
var totalCount int64
if err := rows.Scan(
&item.RiskType,
&item.SourceRecordID,
@@ -464,15 +553,44 @@ func (p *PostGIS) FindNearbyRiskAreas(ctx context.Context, query domain.NearbyQu
&direction,
&item.CoversPoint,
&item.DistanceMeters,
&totalCount,
); err != nil {
return nil, err
rows.Close()
return domain.Page[domain.RiskArea]{}, err
}
item.Name = optionalText(name)
item.Village = optionalText(village)
item.Direction = optionalText(direction)
items = append(items, item)
page.Items = append(page.Items, item)
page.TotalCount = totalCount
}
return items, rows.Err()
if err := rows.Err(); err != nil {
rows.Close()
return domain.Page[domain.RiskArea]{}, err
}
rows.Close()
return recoverEmptyOffsetPageTotal(ctx, query.Offset, page, func(ctx context.Context) (domain.Page[domain.RiskArea], error) {
query.Offset = 0
query.Limit = 1
return p.FindNearbyRiskAreas(ctx, query)
})
}
func recoverEmptyOffsetPageTotal[T any](
ctx context.Context,
offset int,
page domain.Page[T],
loadFirstPage func(context.Context) (domain.Page[T], error),
) (domain.Page[T], error) {
if offset <= 0 || len(page.Items) > 0 {
return page, nil
}
firstPage, err := loadFirstPage(ctx)
if err != nil {
return domain.Page[T]{}, err
}
page.TotalCount = firstPage.TotalCount
return page, nil
}
type spatialTableSpec struct {
@@ -727,16 +845,19 @@ SELECT
CASE WHEN match_rank <= 2 THEN 'exact' ELSE 'partial' END,
round(ST_X(location)::numeric, 6)::float8,
round(ST_Y(location)::numeric, 6)::float8,
location_kind
location_kind,
COUNT(*) OVER()::bigint AS total_count
FROM matched
ORDER BY match_rank, town, village, name, place_type, source_record_id
LIMIT $4`
LIMIT $4
OFFSET $5`
const resolveIncidentContextSQL = `
WITH incident AS (
SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom
)
SELECT g.gid::text, LEFT(COALESCE(g.auth, ''), 100), LEFT(COALESCE(g.name, ''), 255)
SELECT g.gid::text, LEFT(COALESCE(g.auth, ''), 100), LEFT(COALESCE(g.name, ''), 255),
COUNT(*) OVER()::bigint AS total_count
FROM public.st_2_fanghuowangge AS g
CROSS JOIN incident AS i
WHERE ($3::boolean OR g.auth = ANY($4::text[]))
@@ -747,7 +868,8 @@ WHERE ($3::boolean OR g.auth = ANY($4::text[]))
AND NOT ST_IsEmpty(g.geom)
AND ST_Covers(g.geom, i.geom)
ORDER BY ST_Area(g.geom::geography), g.gid
LIMIT 20`
LIMIT $5
OFFSET $6`
const nearbyWaterSourcesSQL = `
WITH incident AS (
@@ -806,10 +928,11 @@ WITH incident AS (
SELECT category, source_record_id, name, town, village,
round(longitude::numeric, 6)::float8, round(latitude::numeric, 6)::float8,
round(distance_meters::numeric, 1)::float8, capacity_cubic_meters, resource_type, reported_status,
source_timestamp_raw
source_timestamp_raw, COUNT(*) OVER()::bigint AS total_count
FROM candidates
ORDER BY distance_meters, category, source_record_id
LIMIT $6`
LIMIT $6
OFFSET $7`
const commandPostCandidatesSQL = `
WITH incident AS (
@@ -863,10 +986,12 @@ WITH incident AS (
)
SELECT facility_type, source_record_id, name, town, village,
round(longitude::numeric, 6)::float8, round(latitude::numeric, 6)::float8,
round(distance_meters::numeric, 1)::float8, reported_status, management_unit
round(distance_meters::numeric, 1)::float8, reported_status, management_unit,
COUNT(*) OVER()::bigint AS total_count
FROM candidates
ORDER BY distance_meters, facility_type, source_record_id
LIMIT $6`
LIMIT $6
OFFSET $7`
const nearbyAccessLinesSQL = `
WITH incident AS (
@@ -894,16 +1019,18 @@ WITH incident AS (
SELECT source_record_id, town, name, round(distance_meters::numeric, 1)::float8,
round(ST_X(nearest_point)::numeric, 6)::float8,
round(ST_Y(nearest_point)::numeric, 6)::float8,
length_meters, source_updated_raw
length_meters, source_updated_raw, COUNT(*) OVER()::bigint AS total_count
FROM candidates
ORDER BY distance_meters, source_record_id
LIMIT $6`
LIMIT $6
OFFSET $7`
const responsibleUnitsSQL = `
WITH incident AS (
SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS geom
)
SELECT g.gid::text, LEFT(COALESCE(g.auth, ''), 100), LEFT(COALESCE(g.name, ''), 255), LEFT(COALESCE(g.fhzd, ''), 255)
SELECT g.gid::text, LEFT(COALESCE(g.auth, ''), 100), LEFT(COALESCE(g.name, ''), 255), LEFT(COALESCE(g.fhzd, ''), 255),
COUNT(*) OVER()::bigint AS total_count
FROM public.st_2_fanghuowangge AS g
CROSS JOIN incident AS i
WHERE ($3::boolean OR g.auth = ANY($4::text[]))
@@ -914,7 +1041,8 @@ WHERE ($3::boolean OR g.auth = ANY($4::text[]))
AND NOT ST_IsEmpty(g.geom)
AND ST_Covers(g.geom, i.geom)
ORDER BY ST_Area(g.geom::geography), g.gid
LIMIT 20`
LIMIT $5
OFFSET $6`
const nearbyRiskAreasSQL = `
WITH incident AS (
@@ -963,7 +1091,8 @@ WITH incident AS (
AND ST_DWithin(e.geom::geography, i.geom::geography, $5)
)
SELECT risk_type, source_record_id, name, town, village, direction, covers_point,
round(distance_meters::numeric, 1)::float8
round(distance_meters::numeric, 1)::float8, COUNT(*) OVER()::bigint AS total_count
FROM candidates
ORDER BY covers_point DESC, distance_meters, risk_type, source_record_id
LIMIT $6`
LIMIT $6
OFFSET $7`
+74 -2
View File
@@ -6,6 +6,8 @@ import (
"strings"
"testing"
"time"
"fire-safety-ymd/internal/domain"
)
func TestOpenPostGISDoesNotExposeInvalidDSN(t *testing.T) {
@@ -126,7 +128,7 @@ func TestSpatialQueriesKeepScopeAndBoundsParameterized(t *testing.T) {
}
for _, tt := range nearbyQueries {
query := tt.query
for _, required := range []string{"$3::boolean", "ANY($4::text[])", "$5", "LIMIT $6", "ST_SRID", "ST_IsValid", "ST_IsEmpty", "ST_Expand"} {
for _, required := range []string{"$3::boolean", "ANY($4::text[])", "$5", "LIMIT $6", "OFFSET $7", "COUNT(*) OVER()::bigint", "ST_SRID", "ST_IsValid", "ST_IsEmpty", "ST_Expand"} {
if !strings.Contains(query, required) {
t.Fatalf("%s query lacks %q", tt.name, required)
}
@@ -139,7 +141,7 @@ func TestSpatialQueriesKeepScopeAndBoundsParameterized(t *testing.T) {
}
}
for index, query := range []string{resolveIncidentContextSQL, responsibleUnitsSQL} {
for _, required := range []string{"$3::boolean", "ANY($4::text[])", "ST_SRID", "ST_IsValid", "ST_Covers"} {
for _, required := range []string{"$3::boolean", "ANY($4::text[])", "LIMIT $5", "OFFSET $6", "COUNT(*) OVER()::bigint", "ST_SRID", "ST_IsValid", "ST_IsEmpty", "ST_Covers"} {
if !strings.Contains(query, required) {
t.Fatalf("cover query %d lacks %q", index, required)
}
@@ -156,6 +158,8 @@ func TestPlaceCandidateSearchKeepsTermScopeAndLimitParameterized(t *testing.T) {
"($2::boolean OR",
"ANY($3::text[])",
"LIMIT $4",
"OFFSET $5",
"COUNT(*) OVER()::bigint",
"strpos",
"recorded_point",
"representative_point",
@@ -179,6 +183,74 @@ func TestPlaceCandidateSearchKeepsTermScopeAndLimitParameterized(t *testing.T) {
}
}
func TestSpatialPaginationQueriesUseOneWindowCountAndStableOrdering(t *testing.T) {
tests := []struct {
name string
query string
wantLimit string
wantOffset string
orderBy string
}{
{name: "place candidates", query: placeCandidateSearchSQL, wantLimit: "LIMIT $4", wantOffset: "OFFSET $5", orderBy: "ORDER BY match_rank, town, village, name, place_type, source_record_id"},
{name: "incident context", query: resolveIncidentContextSQL, wantLimit: "LIMIT $5", wantOffset: "OFFSET $6", orderBy: "ORDER BY ST_Area(g.geom::geography), g.gid"},
{name: "water sources", query: nearbyWaterSourcesSQL, wantLimit: "LIMIT $6", wantOffset: "OFFSET $7", orderBy: "ORDER BY distance_meters, category, source_record_id"},
{name: "command posts", query: commandPostCandidatesSQL, wantLimit: "LIMIT $6", wantOffset: "OFFSET $7", orderBy: "ORDER BY distance_meters, facility_type, source_record_id"},
{name: "access lines", query: nearbyAccessLinesSQL, wantLimit: "LIMIT $6", wantOffset: "OFFSET $7", orderBy: "ORDER BY distance_meters, source_record_id"},
{name: "responsible units", query: responsibleUnitsSQL, wantLimit: "LIMIT $5", wantOffset: "OFFSET $6", orderBy: "ORDER BY ST_Area(g.geom::geography), g.gid"},
{name: "risk areas", query: nearbyRiskAreasSQL, wantLimit: "LIMIT $6", wantOffset: "OFFSET $7", orderBy: "ORDER BY covers_point DESC, distance_meters, risk_type, source_record_id"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := strings.Count(tt.query, "COUNT(*) OVER()::bigint"); got != 1 {
t.Fatalf("window count occurrences = %d, want 1", got)
}
orderIndex := strings.Index(tt.query, tt.orderBy)
limitIndex := strings.Index(tt.query, tt.wantLimit)
offsetIndex := strings.Index(tt.query, tt.wantOffset)
if orderIndex < 0 || limitIndex < 0 || offsetIndex < 0 {
t.Fatalf("query lacks stable order or parameterized pagination: order=%d limit=%d offset=%d", orderIndex, limitIndex, offsetIndex)
}
if !(orderIndex < limitIndex && limitIndex < offsetIndex) {
t.Fatalf("pagination order is not ORDER BY, LIMIT, OFFSET")
}
})
}
}
func TestRecoverEmptyOffsetPageTotalProbesOnlyEmptyLaterPages(t *testing.T) {
tests := []struct {
name string
offset int
page domain.Page[string]
wantCalls int
wantTotal int64
}{
{name: "first page empty", offset: 0, page: domain.Page[string]{Items: []string{}}, wantTotal: 0},
{name: "later page has items", offset: 10, page: domain.Page[string]{Items: []string{"item"}, TotalCount: 11}, wantTotal: 11},
{name: "later page empty", offset: 20, page: domain.Page[string]{Items: []string{}}, wantCalls: 1, wantTotal: 12},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
calls := 0
got, err := recoverEmptyOffsetPageTotal(context.Background(), tt.offset, tt.page, func(context.Context) (domain.Page[string], error) {
calls++
return domain.Page[string]{Items: []string{"first"}, TotalCount: 12}, nil
})
if err != nil {
t.Fatalf("recoverEmptyOffsetPageTotal() error = %v", err)
}
if calls != tt.wantCalls {
t.Fatalf("probe calls = %d, want %d", calls, tt.wantCalls)
}
if got.TotalCount != tt.wantTotal {
t.Fatalf("total count = %d, want %d", got.TotalCount, tt.wantTotal)
}
})
}
}
func TestSpatialQueriesDoNotSelectSensitiveContactColumns(t *testing.T) {
queries := strings.Join([]string{
placeCandidateSearchSQL,