* Introduce `logic_operator` field to combine filters with OR logic.
* Implement grouping for consecutive OR filters to ensure proper SQL precedence.
* Add support for custom SQL operators in filter conditions.
* Enhance `fetch_row_number` functionality to return specific record with its position.
* Update tests to cover new filter logic and grouping behavior.
Features Implemented:
1. OR Logic Filter Support (SearchOr)
- Added to resolvespec, restheadspec, and websocketspec
- Consecutive OR filters are automatically grouped with parentheses
- Prevents SQL logic errors: (A OR B OR C) AND D instead of A OR B OR C AND D
2. CustomOperators
- Allows arbitrary SQL conditions in resolvespec
- Properly integrated with filter logic
3. FetchRowNumber
- Uses SQL window functions: ROW_NUMBER() OVER (ORDER BY ...)
- Returns only the specific record (not all records)
- Available in resolvespec and restheadspec
- Perfect for "What's my rank?" queries
4. RowNumber Field Auto-Population
- Now available in all three packages: resolvespec, restheadspec, and websocketspec
- Uses simple offset-based math: offset + index + 1
- Automatically populates RowNumber int64 field if it exists on models
- Perfect for displaying paginated lists with sequential numbering
12 KiB
ResolveSpec Query Features Examples
This document provides examples of using the advanced query features in ResolveSpec, including OR logic filters, Custom Operators, and FetchRowNumber.
OR Logic in Filters (SearchOr)
Basic OR Filter Example
Find all users with status "active" OR "pending":
POST /users
{
"operation": "read",
"options": {
"filters": [
{
"column": "status",
"operator": "eq",
"value": "active"
},
{
"column": "status",
"operator": "eq",
"value": "pending",
"logic_operator": "OR"
}
]
}
}
Combined AND/OR Filters
Find users with (status="active" OR status="pending") AND age >= 18:
{
"operation": "read",
"options": {
"filters": [
{
"column": "status",
"operator": "eq",
"value": "active"
},
{
"column": "status",
"operator": "eq",
"value": "pending",
"logic_operator": "OR"
},
{
"column": "age",
"operator": "gte",
"value": 18
}
]
}
}
SQL Generated: WHERE (status = 'active' OR status = 'pending') AND age >= 18
Important Notes:
- By default, filters use AND logic
- Consecutive filters with
"logic_operator": "OR"are automatically grouped with parentheses - This grouping ensures OR conditions don't interfere with AND conditions
- You don't need to specify
"logic_operator": "AND"as it's the default
Multiple OR Groups
You can have multiple separate OR groups:
{
"operation": "read",
"options": {
"filters": [
{
"column": "status",
"operator": "eq",
"value": "active"
},
{
"column": "status",
"operator": "eq",
"value": "pending",
"logic_operator": "OR"
},
{
"column": "priority",
"operator": "eq",
"value": "high"
},
{
"column": "priority",
"operator": "eq",
"value": "urgent",
"logic_operator": "OR"
}
]
}
}
SQL Generated: WHERE (status = 'active' OR status = 'pending') AND (priority = 'high' OR priority = 'urgent')
Custom Operators
Simple Custom SQL Condition
Filter by email domain using custom SQL:
{
"operation": "read",
"options": {
"customOperators": [
{
"name": "company_emails",
"sql": "email LIKE '%@company.com'"
}
]
}
}
Multiple Custom Operators
Combine multiple custom SQL conditions:
{
"operation": "read",
"options": {
"customOperators": [
{
"name": "recent_active",
"sql": "last_login > NOW() - INTERVAL '30 days'"
},
{
"name": "high_score",
"sql": "score > 1000"
}
]
}
}
Complex Custom Operator
Use complex SQL expressions:
{
"operation": "read",
"options": {
"customOperators": [
{
"name": "priority_users",
"sql": "(subscription = 'premium' AND points > 500) OR (subscription = 'enterprise')"
}
]
}
}
Combining Custom Operators with Regular Filters
Mix custom operators with standard filters:
{
"operation": "read",
"options": {
"filters": [
{
"column": "country",
"operator": "eq",
"value": "USA"
}
],
"customOperators": [
{
"name": "active_last_month",
"sql": "last_activity > NOW() - INTERVAL '1 month'"
}
]
}
}
Row Numbers
Two Ways to Get Row Numbers
There are two different features for row numbers:
fetch_row_number- Get the position of ONE specific record in a sorted/filtered setRowNumberfield in models - Automatically number all records in the response
1. FetchRowNumber - Get Position of Specific Record
Get the rank/position of a specific user in a leaderboard. Important: When fetch_row_number is specified, the response contains ONLY that specific record, not all records.
{
"operation": "read",
"options": {
"sort": [
{
"column": "score",
"direction": "desc"
}
],
"fetch_row_number": "12345"
}
}
Response - Contains ONLY the specified user:
{
"success": true,
"data": {
"id": 12345,
"name": "Alice Smith",
"score": 9850,
"level": 42
},
"metadata": {
"total": 10000,
"count": 1,
"filtered": 10000,
"row_number": 42
}
}
Result: User "12345" is ranked #42 out of 10,000 users. The response includes only Alice's data, not the other 9,999 users.
Row Number with Filters
Find position within a filtered subset (e.g., "What's my rank in my country?"):
{
"operation": "read",
"options": {
"filters": [
{
"column": "country",
"operator": "eq",
"value": "USA"
},
{
"column": "status",
"operator": "eq",
"value": "active"
}
],
"sort": [
{
"column": "score",
"direction": "desc"
}
],
"fetch_row_number": "12345"
}
}
Response:
{
"success": true,
"data": {
"id": 12345,
"name": "Bob Johnson",
"country": "USA",
"score": 7200,
"status": "active"
},
"metadata": {
"total": 2500,
"count": 1,
"filtered": 2500,
"row_number": 156
}
}
Result: Bob is ranked #156 out of 2,500 active USA users. Only Bob's record is returned.
2. RowNumber Field - Auto-Number All Records
If your model has a RowNumber int64 field, restheadspec will automatically populate it for paginated results.
Model Definition:
type Player struct {
ID int64 `json:"id"`
Name string `json:"name"`
Score int64 `json:"score"`
RowNumber int64 `json:"row_number"` // Will be auto-populated
}
Request (with pagination):
{
"operation": "read",
"options": {
"sort": [{"column": "score", "direction": "desc"}],
"limit": 10,
"offset": 20
}
}
Response - RowNumber automatically set:
{
"success": true,
"data": [
{
"id": 456,
"name": "Player21",
"score": 8900,
"row_number": 21
},
{
"id": 789,
"name": "Player22",
"score": 8850,
"row_number": 22
},
{
"id": 123,
"name": "Player23",
"score": 8800,
"row_number": 23
}
// ... records 24-30 ...
]
}
How It Works:
row_number = offset + index + 1(1-based)- With offset=20, first record gets row_number=21
- With offset=20, second record gets row_number=22
- Perfect for displaying "Rank" in paginated tables
Use Case: Displaying leaderboards with rank numbers:
Rank | Player | Score
-----|-----------|-------
21 | Player21 | 8900
22 | Player22 | 8850
23 | Player23 | 8800
Note: This feature is available in all three packages: resolvespec, restheadspec, and websocketspec.
When to Use Each Feature
| Feature | Use Case | Returns | Performance |
|---|---|---|---|
fetch_row_number |
"What's my rank?" | 1 record with position | Fast - 1 record |
RowNumber field |
"Show top 10 with ranks" | Many records numbered | Fast - simple math |
Combined Example - Full Leaderboard UI:
// Request 1: Get current user's rank
const userRank = await api.read({
fetch_row_number: currentUserId,
sort: [{column: "score", direction: "desc"}]
});
// Returns: {id: 123, name: "You", score: 7500, row_number: 156}
// Request 2: Get top 10 with rank numbers
const top10 = await api.read({
sort: [{column: "score", direction: "desc"}],
limit: 10,
offset: 0
});
// Returns: [{row_number: 1, ...}, {row_number: 2, ...}, ...]
// Display:
// "Your Rank: #156"
// "Top Players:"
// "#1 - Alice - 9999"
// "#2 - Bob - 9876"
// ...
Complete Example: Advanced Query
Combine all features for a complex query:
{
"operation": "read",
"options": {
"columns": ["id", "name", "email", "score", "status"],
"filters": [
{
"column": "status",
"operator": "eq",
"value": "active"
},
{
"column": "status",
"operator": "eq",
"value": "trial",
"logic_operator": "OR"
},
{
"column": "score",
"operator": "gte",
"value": 100
}
],
"customOperators": [
{
"name": "recent_activity",
"sql": "last_login > NOW() - INTERVAL '7 days'"
},
{
"name": "verified_email",
"sql": "email_verified = true"
}
],
"sort": [
{
"column": "score",
"direction": "desc"
},
{
"column": "created_at",
"direction": "asc"
}
],
"fetch_row_number": "12345",
"limit": 50,
"offset": 0
}
}
This query:
- Selects specific columns
- Filters for users with status "active" OR "trial"
- AND score >= 100
- Applies custom SQL conditions for recent activity and verified emails
- Sorts by score (descending) then creation date (ascending)
- Returns the row number of user "12345" in this filtered/sorted set
- Returns 50 records starting from the first one
Use Cases
1. Leaderboards - Get Current User's Rank
Get the current user's position and data (returns only their record):
{
"operation": "read",
"options": {
"filters": [
{
"column": "game_id",
"operator": "eq",
"value": "game123"
}
],
"sort": [
{
"column": "score",
"direction": "desc"
}
],
"fetch_row_number": "current_user_id"
}
}
Tip: For full leaderboards, make two requests:
- One with
fetch_row_numberto get user's rank - One with
limitandoffsetto get top players list
2. Multi-Status Search
{
"operation": "read",
"options": {
"filters": [
{
"column": "order_status",
"operator": "eq",
"value": "pending"
},
{
"column": "order_status",
"operator": "eq",
"value": "processing",
"logic_operator": "OR"
},
{
"column": "order_status",
"operator": "eq",
"value": "shipped",
"logic_operator": "OR"
}
]
}
}
3. Advanced Date Filtering
{
"operation": "read",
"options": {
"customOperators": [
{
"name": "this_month",
"sql": "created_at >= DATE_TRUNC('month', CURRENT_DATE)"
},
{
"name": "business_hours",
"sql": "EXTRACT(HOUR FROM created_at) BETWEEN 9 AND 17"
}
]
}
}
Security Considerations
Warning: Custom operators allow raw SQL, which can be a security risk if not properly handled:
- Never directly interpolate user input into custom operator SQL
- Always validate and sanitize custom operator SQL on the backend
- Consider using a whitelist of allowed custom operators
- Use prepared statements or parameterized queries when possible
- Implement proper authorization checks before executing queries
Example of safe custom operator handling in Go:
// Whitelist of allowed custom operators
allowedOperators := map[string]string{
"recent_week": "created_at > NOW() - INTERVAL '7 days'",
"active_users": "status = 'active' AND last_login > NOW() - INTERVAL '30 days'",
"premium_only": "subscription_level = 'premium'",
}
// Validate custom operators from request
for _, op := range req.Options.CustomOperators {
if sql, ok := allowedOperators[op.Name]; ok {
op.SQL = sql // Use whitelisted SQL
} else {
return errors.New("custom operator not allowed: " + op.Name)
}
}