Skip to content

Commit c82433f

Browse files
committed
add changelog, update coverage, revert function sig, add doc
1 parent a001152 commit c82433f

File tree

8 files changed

+125
-36
lines changed

8 files changed

+125
-36
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Canonical reference for changes, improvements, and bugfixes for mql.
44

55
## Next
6+
* feat: add support for table column mapping by @dlclark in [[PNR](https://github.com/hashicorp/mql/pull/45)]
67

78
## 0.1.4 (2024/05/14)
89

README.md

+34-2
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Example:
106106

107107

108108

109-
### Mapping column names
109+
### Mapping field names
110110

111111
You can also provide an optional map from query column identifiers to model
112112
field names via
@@ -118,7 +118,7 @@ Example
118118
usage:
119119

120120
``` Go
121-
type User {
121+
type User struct {
122122
FullName string
123123
}
124124

@@ -137,6 +137,38 @@ if err != nil {
137137
}
138138
```
139139

140+
### Mapping column names
141+
You can also provide an optional map from model field names to output column
142+
names via
143+
[WithTableColumnMap(...)](https://pkg.go.dev/github.com/hashicorp/mql#WithTableColumnMap)
144+
if needed.
145+
146+
Example
147+
[WithTableColumnMap(...)](https://pkg.go.dev/github.com/hashicorp/mql#WithTableColumnMap)
148+
usage:
149+
150+
``` Go
151+
type User struct {
152+
FullName string
153+
}
154+
155+
// map the field name FullName to column "u.fullname"
156+
tableColumnMap := map[string]string{
157+
"fullname": "u.fullname",
158+
}
159+
160+
w, err := mql.Parse(
161+
`FullName="alice"`,
162+
User{},
163+
mql.WithTableColumnMap(tableColumnMap))
164+
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
fmt.Print(w.Condition) // prints u.fullname=?
170+
```
171+
140172
### Ignoring fields
141173

142174
If your model (Go struct) has fields you don't want users searching then you can

coverage/coverage.html

+62-20
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@
5757

5858
<option value="file0">github.com/hashicorp/mql/common.go (100.0%)</option>
5959

60-
<option value="file1">github.com/hashicorp/mql/expr.go (100.0%)</option>
60+
<option value="file1">github.com/hashicorp/mql/expr.go (97.9%)</option>
6161

6262
<option value="file2">github.com/hashicorp/mql/lex.go (100.0%)</option>
6363

6464
<option value="file3">github.com/hashicorp/mql/mql.go (98.2%)</option>
6565

6666
<option value="file4">github.com/hashicorp/mql/options.go (100.0%)</option>
6767

68-
<option value="file5">github.com/hashicorp/mql/parser.go (93.4%)</option>
68+
<option value="file5">github.com/hashicorp/mql/parser.go (93.6%)</option>
6969

7070
<option value="file6">github.com/hashicorp/mql/stack.go (100.0%)</option>
7171

@@ -224,6 +224,16 @@
224224
if err != nil </span><span class="cov8" title="1">{
225225
return nil, fmt.Errorf("%s: %q in %s: %w", op, *e.value, e.String(), ErrInvalidParameter)
226226
}</span>
227+
228+
<span class="cov8" title="1">opts, err := getOpts(opt...)
229+
if err != nil </span><span class="cov0" title="0">{
230+
return nil, fmt.Errorf("%s: %w", op, err)
231+
}</span>
232+
<span class="cov8" title="1">if n, ok := opts.withTableColumnMap[columnName]; ok </span><span class="cov8" title="1">{
233+
// override our column name with the mapped column name
234+
columnName = n
235+
}</span>
236+
227237
<span class="cov8" title="1">if validator.typ == "time" </span><span class="cov8" title="1">{
228238
columnName = fmt.Sprintf("%s::date", columnName)
229239
}</span>
@@ -251,9 +261,7 @@
251261
func newLogicalOp(s string) (logicalOp, error) <span class="cov8" title="1">{
252262
const op = "newLogicalOp"
253263
switch logicalOp(s) </span>{
254-
case
255-
andOp,
256-
orOp:<span class="cov8" title="1">
264+
case andOp, orOp:<span class="cov8" title="1">
257265
return logicalOp(s), nil</span>
258266
default:<span class="cov8" title="1">
259267
return "", fmt.Errorf("%s: %w %q", op, ErrInvalidLogicalOp, s)</span>
@@ -758,9 +766,9 @@
758766
if err != nil </span><span class="cov8" title="1">{
759767
return nil, fmt.Errorf("%s: %w", op, err)
760768
}</span>
761-
<span class="cov8" title="1">switch </span>{
762-
case opts.withValidateConvertColumn == v.column &amp;&amp; !isNil(opts.withValidateConvertFn):<span class="cov8" title="1">
763-
return opts.withValidateConvertFn(v.column, v.comparisonOp, v.value)</span>
769+
<span class="cov8" title="1">switch validateConvertFn, ok := opts.withValidateConvertFns[v.column]; </span>{
770+
case ok &amp;&amp; !isNil(validateConvertFn):<span class="cov8" title="1">
771+
return validateConvertFn(v.column, v.comparisonOp, v.value)</span>
764772
default:<span class="cov8" title="1">
765773
columnName := strings.ToLower(v.column)
766774
if n, ok := opts.withColumnMap[columnName]; ok </span><span class="cov8" title="1">{
@@ -812,20 +820,22 @@
812820
)
813821

814822
type options struct {
815-
withSkipWhitespace bool
816-
withColumnMap map[string]string
817-
withValidateConvertFn ValidateConvertFunc
818-
withValidateConvertColumn string
819-
withIgnoredFields []string
820-
withPgPlaceholder bool
823+
withSkipWhitespace bool
824+
withColumnMap map[string]string
825+
withValidateConvertFns map[string]ValidateConvertFunc
826+
withIgnoredFields []string
827+
withPgPlaceholder bool
828+
withTableColumnMap map[string]string // map of model field names to their table.column name
821829
}
822830

823831
// Option - how options are passed as args
824832
type Option func(*options) error
825833

826834
func getDefaultOptions() options <span class="cov8" title="1">{
827835
return options{
828-
withColumnMap: make(map[string]string),
836+
withColumnMap: make(map[string]string),
837+
withValidateConvertFns: make(map[string]ValidateConvertFunc),
838+
withTableColumnMap: make(map[string]string),
829839
}
830840
}</span>
831841

@@ -848,8 +858,8 @@
848858
}</span>
849859
}
850860

851-
// WithColumnMap provides an optional map of columns from a column in the user
852-
// provided query to a column in the database model
861+
// WithColumnMap provides an optional map of columns from the user
862+
// provided query to a field in the given model
853863
func WithColumnMap(m map[string]string) Option <span class="cov8" title="1">{
854864
return func(o *options) error </span><span class="cov8" title="1">{
855865
if !isNil(m) </span><span class="cov8" title="1">{
@@ -871,8 +881,10 @@
871881
return func(o *options) error </span><span class="cov8" title="1">{
872882
switch </span>{
873883
case fieldName != "" &amp;&amp; !isNil(fn):<span class="cov8" title="1">
874-
o.withValidateConvertFn = fn
875-
o.withValidateConvertColumn = fieldName</span>
884+
if _, exists := o.withValidateConvertFns[fieldName]; exists </span><span class="cov8" title="1">{
885+
return fmt.Errorf("%s: duplicated convert: %w", op, ErrInvalidParameter)
886+
}</span>
887+
<span class="cov8" title="1">o.withValidateConvertFns[fieldName] = fn</span>
876888
case fieldName == "" &amp;&amp; !isNil(fn):<span class="cov8" title="1">
877889
return fmt.Errorf("%s: missing field name: %w", op, ErrInvalidParameter)</span>
878890
case fieldName != "" &amp;&amp; isNil(fn):<span class="cov8" title="1">
@@ -902,6 +914,28 @@
902914
return nil
903915
}</span>
904916
}
917+
918+
// WithTableColumnMap provides an optional map of columns from the
919+
// model to the table.column name in the generated where clause
920+
//
921+
// For example, if you need to map the language field name to something
922+
// more complex in your SQL statement then you can use this map:
923+
//
924+
// WithTableColumnMap(map[string]string{"language":"preferences-&gt;&gt;'language'"})
925+
//
926+
// In the example above we're mapping "language" field to a json field in
927+
// the "preferences" column. A user can say `language="blah"` and the
928+
// mql-created SQL where clause will contain `preferences-&gt;&gt;'language'="blah"`
929+
//
930+
// The field names in the keys to the map should always be lower case.
931+
func WithTableColumnMap(m map[string]string) Option <span class="cov8" title="1">{
932+
return func(o *options) error </span><span class="cov8" title="1">{
933+
if !isNil(m) </span><span class="cov8" title="1">{
934+
o.withTableColumnMap = m
935+
}</span>
936+
<span class="cov8" title="1">return nil</span>
937+
}
938+
}
905939
</pre>
906940

907941
<pre class="file" id="file5" style="display: none">// Copyright (c) HashiCorp, Inc.
@@ -969,7 +1003,15 @@
9691003
return nil, fmt.Errorf("%s: %w before right side expression in: %q", op, ErrMissingLogicalOp, p.raw)</span>
9701004
// finally, assign the right expr
9711005
case logicExpr.rightExpr == nil:<span class="cov8" title="1">
972-
logicExpr.rightExpr = e.leftExpr
1006+
if e.rightExpr != nil </span><span class="cov8" title="1">{
1007+
// if e.rightExpr isn't nil, then we've got a complete
1008+
// expr (left + op + right) and we need to assign this to
1009+
// our rightExpr
1010+
logicExpr.rightExpr = e
1011+
break TkLoop</span>
1012+
}
1013+
// otherwise, we need to assign the left side of e
1014+
<span class="cov8" title="1">logicExpr.rightExpr = e.leftExpr
9731015
break TkLoop</span>
9741016
}
9751017
case stringToken, numberToken, symbolToken:<span class="cov8" title="1">

coverage/coverage.svg

+1-1
Loading

expr.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (e *comparisonExpr) isComplete() bool {
7777

7878
// defaultValidateConvert will validate the comparison expr value, and then convert the
7979
// expr to its SQL equivalence.
80-
func defaultValidateConvert(columnName string, comparisonOp ComparisonOp, columnValue *string, validator validator, opts options) (*WhereClause, error) {
80+
func defaultValidateConvert(columnName string, comparisonOp ComparisonOp, columnValue *string, validator validator, opt ...Option) (*WhereClause, error) {
8181
const op = "mql.(comparisonExpr).convertToSql"
8282
switch {
8383
case columnName == "":
@@ -103,10 +103,14 @@ func defaultValidateConvert(columnName string, comparisonOp ComparisonOp, column
103103
if err != nil {
104104
return nil, fmt.Errorf("%s: %q in %s: %w", op, *e.value, e.String(), ErrInvalidParameter)
105105
}
106-
newCol, ok := opts.withTableColumnMap[columnName]
107-
if ok {
106+
107+
opts, err := getOpts(opt...)
108+
if err != nil {
109+
return nil, fmt.Errorf("%s: %w", op, err)
110+
}
111+
if n, ok := opts.withTableColumnMap[columnName]; ok {
108112
// override our column name with the mapped column name
109-
columnName = newCol
113+
columnName = n
110114
}
111115

112116
if validator.typ == "time" {

expr_test.go

+7-8
Original file line numberDiff line numberDiff line change
@@ -93,45 +93,44 @@ func Test_defaultValidateConvert(t *testing.T) {
9393
t.Parallel()
9494
fValidators, err := fieldValidators(reflect.ValueOf(testModel{}))
9595
require.NoError(t, err)
96-
opts := getDefaultOptions()
9796
t.Run("missing-column", func(t *testing.T) {
98-
e, err := defaultValidateConvert("", EqualOp, pointer("alice"), fValidators["name"], opts)
97+
e, err := defaultValidateConvert("", EqualOp, pointer("alice"), fValidators["name"])
9998
require.Error(t, err)
10099
assert.Empty(t, e)
101100
assert.ErrorIs(t, err, ErrMissingColumn)
102101
assert.ErrorContains(t, err, "missing column")
103102
})
104103
t.Run("missing-comparison-op", func(t *testing.T) {
105-
e, err := defaultValidateConvert("name", "", pointer("alice"), fValidators["name"], opts)
104+
e, err := defaultValidateConvert("name", "", pointer("alice"), fValidators["name"])
106105
require.Error(t, err)
107106
assert.Empty(t, e)
108107
assert.ErrorIs(t, err, ErrMissingComparisonOp)
109108
assert.ErrorContains(t, err, "missing comparison operator")
110109
})
111110
t.Run("missing-value", func(t *testing.T) {
112-
e, err := defaultValidateConvert("name", EqualOp, nil, fValidators["name"], opts)
111+
e, err := defaultValidateConvert("name", EqualOp, nil, fValidators["name"])
113112
require.Error(t, err)
114113
assert.Empty(t, e)
115114
assert.ErrorIs(t, err, ErrMissingComparisonValue)
116115
assert.ErrorContains(t, err, "missing comparison value")
117116
})
118117
t.Run("missing-validator-func", func(t *testing.T) {
119-
e, err := defaultValidateConvert("name", EqualOp, pointer("alice"), validator{typ: "string"}, opts)
118+
e, err := defaultValidateConvert("name", EqualOp, pointer("alice"), validator{typ: "string"})
120119
require.Error(t, err)
121120
assert.Empty(t, e)
122121
assert.ErrorIs(t, err, ErrInvalidParameter)
123122
assert.ErrorContains(t, err, "missing validator function")
124123
})
125124
t.Run("missing-validator-typ", func(t *testing.T) {
126-
e, err := defaultValidateConvert("name", EqualOp, pointer("alice"), validator{fn: fValidators["name"].fn}, opts)
125+
e, err := defaultValidateConvert("name", EqualOp, pointer("alice"), validator{fn: fValidators["name"].fn})
127126
require.Error(t, err)
128127
assert.Empty(t, e)
129128
assert.ErrorIs(t, err, ErrInvalidParameter)
130129
assert.ErrorContains(t, err, "missing validator type")
131130
})
132131
t.Run("success-with-table-override", func(t *testing.T) {
133-
opts.withTableColumnMap["name"] = "users.name"
134-
e, err := defaultValidateConvert("name", EqualOp, pointer("alice"), validator{fn: fValidators["name"].fn, typ: "default"}, opts)
132+
e, err := defaultValidateConvert("name", EqualOp, pointer("alice"), validator{fn: fValidators["name"].fn, typ: "default"},
133+
WithTableColumnMap(map[string]string{"name": "users.name"}))
135134
assert.Empty(t, err)
136135
assert.NotEmpty(t, e)
137136
assert.Equal(t, "users.name=?", e.Condition, "condition")

mql.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func exprToWhereClause(e expr, fValidators map[string]validator, opt ...Option)
8787
}
8888
return nil, fmt.Errorf("%s: %w %q %s", op, ErrInvalidColumn, columnName, cols)
8989
}
90-
w, err := defaultValidateConvert(columnName, v.comparisonOp, v.value, validator, opts)
90+
w, err := defaultValidateConvert(columnName, v.comparisonOp, v.value, validator, opt...)
9191
if err != nil {
9292
return nil, fmt.Errorf("%s: %w", op, err)
9393
}

options.go

+11
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ func WithPgPlaceholders() Option {
105105

106106
// WithTableColumnMap provides an optional map of columns from the
107107
// model to the table.column name in the generated where clause
108+
//
109+
// For example, if you need to map the language field name to something
110+
// more complex in your SQL statement then you can use this map:
111+
//
112+
// WithTableColumnMap(map[string]string{"language":"preferences->>'language'"})
113+
//
114+
// In the example above we're mapping "language" field to a json field in
115+
// the "preferences" column. A user can say `language="blah"` and the
116+
// mql-created SQL where clause will contain `preferences->>'language'="blah"`
117+
//
118+
// The field names in the keys to the map should always be lower case.
108119
func WithTableColumnMap(m map[string]string) Option {
109120
return func(o *options) error {
110121
if !isNil(m) {

0 commit comments

Comments
 (0)