Skip to content

Commit a78b61d

Browse files
committed
Related orders
1 parent 3854f3c commit a78b61d

5 files changed

Lines changed: 359 additions & 278 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66
* Support for Computed Relationships (table-returning functions as virtual relationships)
77
* Support filtering embedded resources by FK column name (e.g. `client_id.id=eq.1`)
8+
* Related orders: order parent rows by columns in to-one embedded resources (e.g. `order=clients(name).desc`)
89

910
### Fixes
1011
* Fixed serialization bugs for composite and unrecognized types

TODO.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,15 @@
4242
* [x] range headers
4343
* [ ] Admin
4444
* [ ] Projects
45-
* [ ] Related orders
45+
* [x] Related orders
4646
* [ ] Related limit and offset
47+
* [ ] Null filtering on embedded resources (&relation=is.null)
48+
* [ ] Spread to-many relationships (correlated arrays)
49+
* [ ] Spread join table columns (junction table fields in M2M spread)
50+
* [ ] OR filtering across embedded resources (or=(rel.not.is.null,...))
51+
* [ ] Overriding FK relationships with computed functions
52+
* [ ] Recursive relationships (self-referential computed functions)
53+
* [ ] Chained views embedding (views of views)
4754
* [x] hints for rels
4855
* [ ] embeds with views (this is not easy)
4956
* [ ] variadic function

database/querybuilder.go

Lines changed: 98 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ type QueryBuilder interface {
2020
// Join containts information to create a join relationship.
2121
// It is composed while building the select clause.
2222
type Join struct {
23-
name string // join name, crafted to be unique
24-
fields string // fields to be selected in the related table (eg. a, b::text, c AS C)
25-
nested string // nested joins
26-
inner bool // is a inner join
27-
rel *Relationship // base relationship
28-
relLabel string // label for the relationship, taken from the select clause
29-
relName string // relation/function name as it appears in the query
23+
name string // join name, crafted to be unique
24+
fields string // fields to be selected in the related table (eg. a, b::text, c AS C)
25+
nested string // nested joins
26+
inner bool // is a inner join
27+
rel *Relationship // base relationship
28+
relLabel string // label for the relationship, taken from the select clause
29+
relName string // relation/function name as it appears in the query
30+
selectFields []SelectField // inner select fields (for related order validation)
3031
}
3132

3233
type BuildError struct {
@@ -120,7 +121,7 @@ func labelWithNumber(table string, num int) string {
120121
return table + "_" + strconv.Itoa(num)
121122
}
122123

123-
func selectForJoinClause(join Join, label string, parts *QueryParts, stack BuildStack) (sel string) {
124+
func selectForJoinClause(join Join, label string, parts *QueryParts, stack BuildStack) (sel string, err error) {
124125
rel := join.rel
125126
sel = " SELECT " + join.fields
126127
_, t1 := splitTableName(rel.RelatedTable)
@@ -166,7 +167,10 @@ func selectForJoinClause(join Join, label string, parts *QueryParts, stack Build
166167
if wc != "" {
167168
sel += " WHERE " + wc
168169
}
169-
oc := orderClause(table, schema, label1, parts.orderFields, stack.info)
170+
oc, err := orderClause(table, schema, label1, stack.level+1, parts.orderFields, join.selectFields, stack.info)
171+
if err != nil {
172+
return "", err
173+
}
170174
if oc != "" {
171175
sel += " ORDER BY " + oc
172176
}
@@ -231,13 +235,16 @@ func selectForJoinClause(join Join, label string, parts *QueryParts, stack Build
231235
if whereClause != "" {
232236
sel += " AND " + whereClause
233237
}
234-
orderClause := orderClause(table, schema, label1, parts.orderFields, stack.info)
235-
if orderClause != "" {
236-
sel += " ORDER BY " + orderClause
238+
oc, err := orderClause(table, schema, label1, stack.level+1, parts.orderFields, join.selectFields, stack.info)
239+
if err != nil {
240+
return "", err
241+
}
242+
if oc != "" {
243+
sel += " ORDER BY " + oc
237244
}
238245
}
239246
}
240-
return sel
247+
return sel, nil
241248
}
242249

243250
func findRelationship(table, relation, fk, schema string, info *SchemaInfo) (rel *Relationship, err error) {
@@ -360,7 +367,7 @@ func selectClause(table, schema, label string, parts *QueryParts, stack BuildSta
360367
if err != nil {
361368
return "", "", nil, err
362369
}
363-
joinSeq = append(joinSeq, Join{joinName, sc, j, sfield.relation.inner, frel, sfield.label, sfield.relation.name})
370+
joinSeq = append(joinSeq, Join{joinName, sc, j, sfield.relation.inner, frel, sfield.label, sfield.relation.name, sfield.relation.fields})
364371
} else {
365372
if i != 0 {
366373
selClause += ", "
@@ -388,7 +395,10 @@ func selectClause(table, schema, label string, parts *QueryParts, stack BuildSta
388395
if len(joinSeq) > 0 {
389396
for _, join := range joinSeq {
390397
relName := join.name
391-
selectForJoin := selectForJoinClause(join, label, parts, stack)
398+
selectForJoin, err := selectForJoinClause(join, label, parts, stack)
399+
if err != nil {
400+
return "", "", nil, err
401+
}
392402
if join.inner {
393403
joins += " INNER"
394404
} else {
@@ -466,25 +476,41 @@ func groupByClause(table, schema string, parts *QueryParts, info *SchemaInfo) st
466476
return ""
467477
}
468478

469-
func orderClause(table, schema, label string, orderFields []OrderField, info *SchemaInfo) string {
479+
func orderClause(table, schema, label string, level int, orderFields []OrderField,
480+
selectFields []SelectField, info *SchemaInfo) (string, error) {
470481
var order string
471482
for _, o := range orderFields {
472483
if o.field.tablename != table {
473-
// skip where filters for other tables
484+
// skip order fields for other tables
474485
continue
475486
}
476487
if order != "" {
477488
order += ", "
478489
}
479490
var fieldname string
480-
if label == "" {
491+
if o.relation != "" {
492+
// Related order: order by a column in a to-one related table
493+
matchedLabel, err := validateRelatedOrder(table, schema, o, selectFields, info)
494+
if err != nil {
495+
return "", err
496+
}
497+
joinAlias := quote(labelWithNumber(table+"_"+matchedLabel, level+1))
498+
fieldname = joinAlias + "." + quote(o.field.name)
499+
if o.field.jsonPath != "" {
500+
fieldname = "(" + fieldname + o.field.jsonPath + ")"
501+
}
502+
} else if label == "" {
481503
fieldname = _stq(o.field.name, schema, table)
504+
if o.field.jsonPath != "" {
505+
fieldname = "(" + toJson(table, schema, o.field.name, fieldname, info) +
506+
o.field.jsonPath + ")"
507+
}
482508
} else {
483509
fieldname = label + "." + quote(o.field.name)
484-
}
485-
if o.field.jsonPath != "" {
486-
fieldname = "(" + toJson(table, schema, o.field.name, fieldname, info) +
487-
o.field.jsonPath + ")"
510+
if o.field.jsonPath != "" {
511+
fieldname = "(" + toJson(table, schema, o.field.name, fieldname, info) +
512+
o.field.jsonPath + ")"
513+
}
488514
}
489515
order += fieldname
490516
if o.descending {
@@ -498,7 +524,44 @@ func orderClause(table, schema, label string, orderFields []OrderField, info *Sc
498524
}
499525
}
500526
}
501-
return order
527+
return order, nil
528+
}
529+
530+
// validateRelatedOrder checks that a related order references an embedded to-one relationship.
531+
// Returns the matched label (used for join alias construction) or an error.
532+
func validateRelatedOrder(table, schema string, o OrderField, selectFields []SelectField, info *SchemaInfo) (string, error) {
533+
for _, sf := range selectFields {
534+
if sf.relation == nil {
535+
continue
536+
}
537+
sfLabel := sf.label
538+
if sfLabel == "" {
539+
sfLabel = sf.relation.name
540+
}
541+
if sfLabel == o.relation || sf.relation.name == o.relation {
542+
// Found matching embedded resource - check relationship type
543+
parentTable := sf.relation.parent
544+
if parentTable == "" {
545+
parentTable = table
546+
}
547+
rel, err := findRelationship(parentTable, sf.relation.name, sf.relation.fk, schema, info)
548+
if err != nil {
549+
return "", err
550+
}
551+
switch rel.Type {
552+
case M2O, O2O:
553+
// OK
554+
case O2M, M2M:
555+
return "", &BuildError{"A related order on '" + o.relation + "' is not possible"}
556+
case Computed:
557+
if rel.ReturnIsSet {
558+
return "", &BuildError{"A related order on '" + o.relation + "' is not possible"}
559+
}
560+
}
561+
return sfLabel, nil
562+
}
563+
}
564+
return "", &BuildError{"'" + o.relation + "' is not an embedded resource in this request"}
502565
}
503566

504567
func appendValue(where, value string, valueList []any, nmarker int) (string, []any, int) {
@@ -968,7 +1031,10 @@ func (CommonBuilder) BuildExecute(name string, record Record, parts *QueryParts,
9681031
for i := range parts.orderFields {
9691032
parts.orderFields[i].field.tablename = "t"
9701033
}
971-
orderClause := orderClause("t", "", "", parts.orderFields, info)
1034+
orderClause, err := orderClause("t", "", "", 0, parts.orderFields, parts.selectFields, info)
1035+
if err != nil {
1036+
return "", nil, err
1037+
}
9721038
query = "SELECT " + selectClause
9731039
from := "FROM " + _sq(name, schema) + "(" + pairs + ") t "
9741040

@@ -988,7 +1054,10 @@ func (DirectQueryBuilder) BuildSelect(table string, parts *QueryParts, options *
9881054
}
9891055
whereClause, valueList := whereClause(table, schema, "", parts.whereConditionsTree, 0, stack)
9901056
groupByClause := groupByClause(table, schema, parts, info)
991-
orderClause := orderClause(table, schema, "", parts.orderFields, info)
1057+
orderClause, err := orderClause(table, schema, "", 0, parts.orderFields, parts.selectFields, info)
1058+
if err != nil {
1059+
return "", nil, err
1060+
}
9921061

9931062
query := "SELECT " + selectClause
9941063
from := "FROM " + _sq(table, schema)
@@ -1013,7 +1082,10 @@ func (QueryWithJSON) BuildSelect(table string, parts *QueryParts, options *Query
10131082
}
10141083
whereClause, valueList := whereClause(table, schema, "", parts.whereConditionsTree, 0, stack)
10151084
groupByClause := groupByClause(table, schema, parts, info)
1016-
orderClause := orderClause(table, schema, "", parts.orderFields, info)
1085+
orderClause, err := orderClause(table, schema, "", 0, parts.orderFields, parts.selectFields, info)
1086+
if err != nil {
1087+
return "", nil, err
1088+
}
10171089

10181090
query := "SELECT "
10191091
if selectClause == "*" {

database/requestparser.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type SelectRelation struct {
5252

5353
type OrderField struct {
5454
field Field
55+
relation string // for related orders: the relation/label name (empty for regular orders)
5556
descending bool
5657
invertNulls bool
5758
}
@@ -604,7 +605,7 @@ func (p *PostgRestParser) parseConflicts(s string) ([]string, error) {
604605
func (p *PostgRestParser) parseOrderCondition(table, o string) (fields []OrderField, err error) {
605606
var value1, value2 string
606607
p.reset()
607-
p.scan(o, ".,", "->>", "->")
608+
p.scan(o, ".,()", "->>", "->")
608609

609610
token := p.lookAhead()
610611
if token == "" {
@@ -615,7 +616,25 @@ func (p *PostgRestParser) parseOrderCondition(table, o string) (fields []OrderFi
615616
if err != nil {
616617
return nil, err
617618
}
619+
620+
// Check for related order syntax: relation(field)
621+
var relation string
622+
if p.lookAhead() == "(" {
623+
p.next() // consume (
624+
relation = field.name
625+
field, err = p.field(false, false)
626+
if err != nil {
627+
return nil, err
628+
}
629+
if p.lookAhead() != ")" {
630+
return nil, &ParseError{"')' expected"}
631+
}
632+
p.next() // consume )
633+
}
634+
618635
field.tablename = table
636+
value1 = ""
637+
value2 = ""
619638
if p.lookAhead() == "." {
620639
p.next()
621640
value1 = p.next()
@@ -643,7 +662,7 @@ func (p *PostgRestParser) parseOrderCondition(table, o string) (fields []OrderFi
643662
invertNulls = true
644663
}
645664
fields = append(fields,
646-
OrderField{field: field, descending: descending, invertNulls: invertNulls})
665+
OrderField{field: field, relation: relation, descending: descending, invertNulls: invertNulls})
647666
if p.lookAhead() != "," {
648667
break
649668
}

0 commit comments

Comments
 (0)