Skip to content

Commit 791e945

Browse files
committed
support Select with WhereAny condition
Signed-off-by: Yan Zhu <hackzhuyan@gmail.com>
1 parent f218b18 commit 791e945

7 files changed

Lines changed: 335 additions & 147 deletions

File tree

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,101 @@ They can also be created based on a list of Conditions:
233233
}).Delete()
234234
ovs.Transact(ops...)
235235

236+
### Querying with Select
237+
238+
The `Select` operation is used to build an OVSDB `select` query. Unlike the `List` operation, which returns results directly from the cache, `Select` only generates an `ovsdb.Operation`. You must pass this operation to `Transact` to execute it against the database, and then use a helper function like `GetSelectResults` to parse the reply.
239+
240+
The core workflow is: `WhereXxx(...).Select() -> Transact(...) -> GetSelectResults(...)`
241+
242+
#### Selecting Rows
243+
244+
**Select All Rows from a Table:**
245+
246+
To select all rows from a table, you can call `Where()` with a zero-value instance of the model struct (e.g., `&MyLogicalSwitch{}`). This sets the table context for the `Select` operation without adding any conditions.
247+
248+
```go
249+
// var ovs client.Client
250+
// var ctx context.Context
251+
// var allSwitches []MyLogicalSwitch
252+
// 1. Generate Op: Where(&MyLogicalSwitch{}) specifies the table
253+
selectOp, err := ovs.Where(&MyLogicalSwitch{}).Select()
254+
// ...
255+
// 2. Execute transaction
256+
reply, err := ovs.Transact(ctx, selectOp...)
257+
// ...
258+
// 3. Parse result
259+
err = ovs.GetSelectResults(reply, &allSwitches)
260+
// ...
261+
```
262+
263+
**Select by Index:**
264+
265+
You can also use `Where()` with a model instance that has indexed fields populated. This will create a condition to find matching rows based on the first available index.
266+
267+
```go
268+
// Assuming "Name" is an indexed field
269+
ls := &MyLogicalSwitch{Name: "switch1"}
270+
var results []MyLogicalSwitch
271+
// 1. Generate Op
272+
selectOp, err := ovs.Where(ls).Select()
273+
// ...
274+
// 2. Transact and parse
275+
reply, err := ovs.Transact(ctx, selectOp...)
276+
err = ovs.GetSelectResults(reply, &results)
277+
// ...
278+
```
279+
280+
**Select with Conditions (AND):**
281+
282+
Use `WhereAll()` to set the table context and one or more filter conditions. All conditions are combined with an `AND` operator.
283+
284+
```go
285+
var specificSwitches []MyLogicalSwitch
286+
lsModel := &MyLogicalSwitch{}
287+
cond1 := model.Condition{Field: &lsModel.Name, Function: ovsdb.ConditionEqual, Value: "sw1"}
288+
cond2 := model.Condition{Field: &lsModel.Ports, Function: ovsdb.ConditionIncludes, Value: "some_port_uuid"}
289+
290+
// 1. Generate Op: Use WhereAll for one or more AND conditions
291+
selectOp, err := ovs.WhereAll(lsModel, cond1, cond2).Select()
292+
// ...
293+
// 2. Transact and parse
294+
reply, err := ovs.Transact(ctx, selectOp...)
295+
err = ovs.GetSelectResults(reply, &specificSwitches)
296+
// ...
297+
```
298+
299+
**Select with Conditions (OR):**
300+
301+
Use `WhereAny()` to set the table context and one or more filter conditions. All conditions are combined with an `OR` operator.
302+
303+
```go
304+
var specificSwitches []MyLogicalSwitch
305+
lsModel := &MyLogicalSwitch{}
306+
cond1 := model.Condition{Field: &lsModel.Name, Function: ovsdb.ConditionEqual, Value: "sw1"}
307+
cond2 := model.Condition{Field: &lsModel.Ports, Function: ovsdb.ConditionIncludes, Value: "some_port_uuid"}
308+
309+
// 1. Generate Op: Use WhereAny for one or more AND conditions
310+
selectOp, err := ovs.WhereAny(lsModel, cond1, cond2).Select()
311+
// ...
312+
// 2. Transact and parse
313+
reply, err := ovs.Transact(ctx, selectOp...)
314+
err = ovs.GetSelectResults(reply, &specificSwitches)
315+
// ...
316+
```
317+
318+
#### Selecting Specific Columns
319+
320+
By default, `Select` queries all columns of a table. You can pass column names to the `Select` method to retrieve only specific columns. The `_uuid` column is always included in the result.
321+
322+
```go
323+
// Selects only the "name" and "ports" columns
324+
selectOp, err := ovs.Where(&MyLogicalSwitch{Name: "sw1"}).Select("name", "ports")
325+
```
326+
327+
#### Limitations
328+
329+
- `WhereCache(...).Select()`: **Not supported**. Cache-based predicate functions cannot be translated into OVSDB query conditions. Calling `Select` after `WhereCache` will return an error.
330+
236331
## Monitor for updates
237332

238333
You can also register a notification handler to get notified every time an element is added, deleted or updated from the database.

client/api.go

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -617,19 +617,17 @@ func newConditionalAPI(cache *cache.TableCache, cond Conditional, logger *logr.L
617617
// Select generates the OVSDB select operation based on the conditions previously set
618618
// using Where, WhereAll, or WhereCache.
619619
// It determines the target table and columns from the condition context.
620-
// Returns an error if called after WhereAny, as OVSDB select only supports
621-
// a single set of ANDed conditions.
620+
// Returns an error if called after WhereCache.
621+
// If used with WhereAny, it will generate one select operation per condition.
622622
func (a api) Select(columns ...string) ([]ovsdb.Operation, error) {
623623
// Select now requires a condition to be set via WhereXxx first.
624624
if a.cond == nil {
625-
return nil, fmt.Errorf("Select called on API with no condition set (use Where or WhereAll first)")
625+
return nil, fmt.Errorf("Select called on API with no condition set (use Where, WhereAll, or WhereAny first)")
626626
}
627627

628628
if _, ok := a.cond.(*predicateConditional); ok {
629629
// Prevent select based on cache predicate function which cannot be translated
630630
return nil, fmt.Errorf("cannot generate OVSDB select operation from a cache predicate function (WhereCache)")
631-
} else if a.cond.IsDisjunction() { // Check if condition is OR (from WhereAny)
632-
return nil, fmt.Errorf("Select cannot be used with WhereAny conditions, use separate Select calls or Transact")
633631
}
634632

635633
// Get table name directly from the condition
@@ -644,22 +642,6 @@ func (a api) Select(columns ...string) ([]ovsdb.Operation, error) {
644642
return nil, fmt.Errorf("failed to generate conditions for select: %w", err)
645643
}
646644

647-
// OVSDB Select only supports one set of conditions (implicitly ANDed).
648-
// The IsDisjunction check above handles WhereAny. This check is a safeguard
649-
// in case a future Conditional type could generate multiple lists without
650-
// being a disjunction.
651-
if len(ovsdbConditionsList) > 1 {
652-
return nil, fmt.Errorf("internal error: condition generated multiple condition lists unexpectedly for Select")
653-
}
654-
655-
var whereClause []ovsdb.Condition
656-
if len(ovsdbConditionsList) == 1 {
657-
whereClause = ovsdbConditionsList[0]
658-
} else {
659-
// No conditions specified or generated, select all rows
660-
whereClause = []ovsdb.Condition{}
661-
}
662-
663645
// Determine columns to select
664646
if a.cache == nil || !a.cache.DatabaseModel().Valid() {
665647
return nil, fmt.Errorf("database model/schema info not available for select")
@@ -699,12 +681,22 @@ func (a api) Select(columns ...string) ([]ovsdb.Operation, error) {
699681
}
700682
}
701683

702-
selectOp := ovsdb.Operation{
703-
Op: ovsdb.OperationSelect,
704-
Table: tableName,
705-
Where: whereClause,
706-
Columns: columnsToSelect,
684+
// If no conditions were generated (e.g. select all), create a single
685+
// operation with an empty where clause which selects all rows.
686+
if len(ovsdbConditionsList) == 0 {
687+
ovsdbConditionsList = append(ovsdbConditionsList, []ovsdb.Condition{})
707688
}
708689

709-
return []ovsdb.Operation{selectOp}, nil
690+
operations := make([]ovsdb.Operation, 0, len(ovsdbConditionsList))
691+
for _, whereClause := range ovsdbConditionsList {
692+
selectOp := ovsdb.Operation{
693+
Op: ovsdb.OperationSelect,
694+
Table: tableName,
695+
Where: whereClause,
696+
Columns: columnsToSelect,
697+
}
698+
operations = append(operations, selectOp)
699+
}
700+
701+
return operations, nil
710702
}

client/client.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ type Client interface {
6565
NewMonitor(...MonitorOption) *Monitor
6666
CurrentEndpoint() string
6767
API
68-
// ParseSelectResult parses the result of a Select operation into the target slice.
68+
// GetSelectResults parses the result of Select operations into the target slice.
6969
// targetSlice must be a non-nil pointer to a slice of models (structs or pointers to structs).
70-
ParseSelectResult(result ovsdb.OperationResult, targetSlice interface{}) error
70+
GetSelectResults(results []ovsdb.OperationResult, targetSlice interface{}) error
7171
}
7272

7373
type bufferedUpdate struct {
@@ -1475,14 +1475,21 @@ func (o *ovsdbClient) WhereCache(predicate any) ConditionalAPI {
14751475
return o.primaryDB().api.WhereCache(predicate)
14761476
}
14771477

1478-
// ParseSelectResult parses the result of a Select operation into the target slice.
1479-
func (o *ovsdbClient) ParseSelectResult(result ovsdb.OperationResult, targetSlice interface{}) error {
1480-
if result.Error != "" {
1481-
details := ""
1482-
if result.Details != "" {
1483-
details = ": " + result.Details
1478+
// GetSelectResults parses the result of a Select operation into the target slice.
1479+
func (o *ovsdbClient) GetSelectResults(results []ovsdb.OperationResult, targetSlice interface{}) error {
1480+
var rows []ovsdb.Row
1481+
for _, result := range results {
1482+
rows = append(rows, result.Rows...)
1483+
}
1484+
exist := make(map[ovsdb.UUID]struct{})
1485+
var uniqRows []ovsdb.Row
1486+
for _, row := range rows {
1487+
uuid := row["_uuid"].(ovsdb.UUID)
1488+
if _, ok := exist[uuid]; ok {
1489+
continue
14841490
}
1485-
return fmt.Errorf("select operation failed: %s%s", result.Error, details)
1491+
uniqRows = append(uniqRows, row)
1492+
exist[uuid] = struct{}{}
14861493
}
14871494

14881495
_, err := validateParseTargetSlice(targetSlice)
@@ -1498,10 +1505,10 @@ func (o *ovsdbClient) ParseSelectResult(result ovsdb.OperationResult, targetSlic
14981505
return fmt.Errorf("database model for '%s' is not valid (client may not be connected or schema mismatch)", o.primaryDBName)
14991506
}
15001507

1501-
return mapSelectResultToSlice(result.Rows, targetSlice, db.model)
1508+
return mapSelectResultToSlice(uniqRows, targetSlice, db.model)
15021509
}
15031510

1504-
// validateParseTargetSlice validates the 'targetSlice' argument for ParseSelectResult.
1511+
// validateParseTargetSlice validates the 'targetSlice' argument for GetSelectResults.
15051512
// It ensures 'targetSlice' is a non-nil pointer to a slice whose elements are
15061513
// structs or pointers to structs that implement model.Model.
15071514
// It returns the underlying struct type of the slice elements.

0 commit comments

Comments
 (0)