Added rs:new and rs:query

This commit is contained in:
Leon Mika 2025-05-17 11:11:04 +10:00
parent cb908ec4eb
commit 6bf721873b
11 changed files with 424 additions and 19 deletions

View file

@ -162,9 +162,12 @@ func main() {
commandController := commandctrl.NewCommandController(inputHistoryService,
cmdpacks.StandardCommands{
ReadController: tableReadController,
WriteController: tableWriteController,
ExportController: exportController,
TableService: tableService,
State: state,
ReadController: tableReadController,
WriteController: tableWriteController,
ExportController: exportController,
KeyBindingController: keyBindingController,
},
)
commandController.AddCommandLookupExtension(scriptController)

6
go.mod
View file

@ -1,8 +1,8 @@
module github.com/lmika/dynamo-browse
go 1.22
go 1.24
toolchain go1.22.0
toolchain go1.24.0
require (
github.com/alecthomas/participle/v2 v2.1.1
@ -117,5 +117,5 @@ require (
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 // indirect
ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 // indirect
)

4
go.sum
View file

@ -438,3 +438,7 @@ ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281 h1:/M7phiv/0XVp3wKkOxEnGQysf8+R
ucl.lmika.dev v0.0.0-20240504013531-0dc9fd3c3281/go.mod h1:T6V4jIUxlWvMTgn4J752VDHNA8iyVrEX6v98EvDj8G4=
ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8 h1:vWttdW8GJWcTUQeJFbQHqCHJDLFWQ9nccUTx/lW2v8s=
ucl.lmika.dev v0.0.0-20250306030053-ad6d002a22e8/go.mod h1:FMP2ncSu4UxfvB0iA2zlebwL+1UPCARdyYNOrmi86A4=
ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2 h1:cvguOoQ0HVgLKbHH17ZHvAUFht6HXApLi0o8JOdaaNU=
ucl.lmika.dev v0.0.0-20250515115457-27b6cc0b92e2/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
ucl.lmika.dev v0.0.0-20250517003439-109be33d1495 h1:r46r+7T59Drm+in7TEWKCZfFYIM0ZyZ26QjHAbj8Lto=
ucl.lmika.dev v0.0.0-20250517003439-109be33d1495/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=

View file

@ -0,0 +1,123 @@
package cmdpacks
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/pkg/errors"
"ucl.lmika.dev/repl"
"ucl.lmika.dev/ucl"
)
type rsModule struct {
tableService *tables.Service
state *controllers.State
}
var rsNewDoc = repl.Doc{
Brief: "Creates a new, empty result set",
}
func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (any, error) {
return &ResultSetProxy{
RS: &models.ResultSet{},
}, nil
}
var rsQueryDoc = repl.Doc{
Brief: "Runs a query and returns the results as a result-set",
Usage: "QUERY [ARGS] [-table NAME]",
Args: []repl.ArgDoc{
{Name: "query", Brief: "Query expression to run"},
{Name: "args", Brief: "Hash of argument values to substitute into the query"},
{Name: "-table", Brief: "Optional table name to use for the query"},
},
Detailed: `
If no table is specified, then the value of @table will be used. If this is unavailable,
the command will return an error.
`,
}
func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
var expr string
if err := args.Bind(&expr); err != nil {
return nil, err
}
q, err := queryexpr.Parse(expr)
if err != nil {
return nil, err
}
if args.NArgs() > 0 {
var queryArgs ucl.Hashable
if err := args.Bind(&queryArgs); err != nil {
return nil, err
}
queryNames := map[string]string{}
queryValues := map[string]types.AttributeValue{}
queryArgs.Each(func(k string, v ucl.Object) error {
if v == nil {
return nil
}
queryNames[k] = v.String()
switch v.(type) {
case ucl.StringObject:
queryValues[k] = &types.AttributeValueMemberS{Value: v.String()}
case ucl.IntObject:
queryValues[k] = &types.AttributeValueMemberN{Value: v.String()}
// TODO: other types
}
return nil
})
q = q.WithNameParams(queryNames).WithValueParams(queryValues)
}
var tableInfo *models.TableInfo
if args.HasSwitch("table") {
var tblName string
if err := args.BindSwitch("table", &tblName); err != nil {
return nil, err
}
tableInfo, err = rs.tableService.Describe(ctx, tblName)
if err != nil {
return nil, err
}
} else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil {
tableInfo = currRs.TableInfo
} else {
return nil, errors.New("no table specified")
}
newResultSet, err := rs.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil)
if err != nil {
return nil, err
}
return &ResultSetProxy{
RS: newResultSet,
}, nil
}
func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module {
m := &rsModule{
tableService: tableService,
state: state,
}
return ucl.Module{
Name: "rs",
Builtins: map[string]ucl.BuiltinHandler{
"new": m.rsNew,
"query": m.rsQuery,
},
}
}

View file

@ -0,0 +1,80 @@
package cmdpacks_test
import (
"fmt"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
"github.com/stretchr/testify/assert"
"testing"
)
func TestModRS_New(t *testing.T) {
svc := newService(t)
rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:new`)
assert.NoError(t, err)
assert.IsType(t, rsProxy, &cmdpacks.ResultSetProxy{})
}
func TestModRS_Query(t *testing.T) {
tests := []struct {
descr string
cmd string
wantRows []int
}{
{
descr: "query with pk 1",
cmd: `rs:query 'pk="abc"' -table service-test-data`,
wantRows: []int{0, 1},
},
{
descr: "query with pk 2",
cmd: `rs:query 'pk="bbb"' -table service-test-data`,
wantRows: []int{2},
},
{
descr: "query with sk 1",
cmd: `rs:query 'sk="222"' -table service-test-data`,
wantRows: []int{1},
},
{
descr: "query with args 1",
cmd: `rs:query 'pk=$v' [v:'abc'] -table service-test-data`,
wantRows: []int{0, 1},
},
{
descr: "query with args 2",
cmd: `rs:query ':k=$v' [k:'pk' v:'abc'] -table service-test-data`,
wantRows: []int{0, 1},
},
{
descr: "query with args 3",
cmd: `rs:query ':k=$v' [k:'beta' v:1231] -table service-test-data`,
wantRows: []int{1},
},
{
descr: "query with args with no table set",
cmd: `rs:query ':k=$v' [k:'beta' v:1231]`,
wantRows: []int{1},
},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
svc := newService(t)
res, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd)
assert.NoError(t, err)
rs := res.(*cmdpacks.ResultSetProxy).RS
assert.Len(t, rs.Items(), len(tt.wantRows))
for i, rowIndex := range tt.wantRows {
for key, want := range testData[0].Data[rowIndex] {
have, ok := rs.Items()[i].AttributeValueAsString(key)
assert.True(t, ok)
assert.Equal(t, fmt.Sprint(want), have)
}
}
})
}
}

View file

@ -0,0 +1,7 @@
package cmdpacks
import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
type ResultSetProxy struct {
RS *models.ResultSet
}

View file

@ -6,12 +6,15 @@ import (
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/pkg/errors"
"ucl.lmika.dev/repl"
"ucl.lmika.dev/ucl"
)
type StandardCommands struct {
TableService *tables.Service
State *controllers.State
ReadController *controllers.TableReadController
WriteController *controllers.TableWriteController
ExportController *controllers.ExportController
@ -358,6 +361,12 @@ func (sc StandardCommands) cmdRebind(ctx context.Context, args ucl.CallArgs) (an
return nil, nil
}
func (sc StandardCommands) InstOptions() []ucl.InstOption {
return []ucl.InstOption{
ucl.WithModule(moduleRS(sc.TableService, sc.State)),
}
}
func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) {
ucl.SetBuiltin("quit", sc.cmdQuit)
ucl.SetBuiltin("table", sc.cmdTable)

View file

@ -0,0 +1,143 @@
package cmdpacks_test
import (
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
"github.com/lmika/dynamo-browse/test/testdynamo"
"github.com/lmika/dynamo-browse/test/testworkspace"
bus "github.com/lmika/events"
"github.com/stretchr/testify/assert"
"testing"
)
func TestStdCmds_Mark(t *testing.T) {
tests := []struct {
descr string
cmd string
wantMarks []bool
}{
{descr: "mark default", cmd: "mark", wantMarks: []bool{true, true, true}},
{descr: "mark all", cmd: "mark all", wantMarks: []bool{true, true, true}},
{descr: "mark none", cmd: "mark none", wantMarks: []bool{false, false, false}},
{descr: "mark where", cmd: `mark -where 'sk="222"'`, wantMarks: []bool{false, true, false}},
{descr: "mark toggle", cmd: "mark ; mark toggle", wantMarks: []bool{false, false, false}},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
svc := newService(t)
_, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd)
assert.NoError(t, err)
for i, want := range tt.wantMarks {
assert.Equal(t, want, svc.State.ResultSet().Marked(i))
}
})
}
}
type services struct {
CommandController *commandctrl.CommandController
SelItemIndex int
State *controllers.State
}
func newService(t *testing.T) *services {
ws := testworkspace.New(t)
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
settingStore := settingstore.New(ws)
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
inputHistoryService := inputhistory.New(inputHistoryStore)
client := testdynamo.SetupTestTable(t, testData)
provider := dynamo.NewProvider(client)
service := tables.NewService(provider, settingStore)
eventBus := bus.New()
state := controllers.NewState()
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
readController := controllers.NewTableReadController(
state,
service,
workspaceService,
itemRendererService,
jobsController,
inputHistoryService,
eventBus,
pasteboardprovider.NilProvider{},
nil,
"service-test-data",
)
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
settingsController := controllers.NewSettingsController(settingStore, eventBus)
columnsController := controllers.NewColumnsController(readController, eventBus)
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
_ = settingsController
commandController := commandctrl.NewCommandController(inputHistoryService,
cmdpacks.StandardCommands{
State: state,
TableService: service,
ReadController: readController,
WriteController: writeController,
ExportController: exportController,
},
)
s := &services{
State: state,
CommandController: commandController,
}
commandController.SetUIStateProvider(s)
readController.Init()
return s
}
func (s *services) SelectedItemIndex() int {
return s.SelItemIndex
}
var testData = []testdynamo.TestData{
{
TableName: "service-test-data",
Data: []map[string]interface{}{
{
"pk": "abc",
"sk": "111",
"alpha": "This is some value",
},
{
"pk": "abc",
"sk": "222",
"alpha": "This is another some value",
"beta": 1231,
},
{
"pk": "bbb",
"sk": "131",
"beta": 2468,
"gamma": "foobar",
},
},
},
}

View file

@ -43,11 +43,17 @@ func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) *Co
msgChan: make(chan tea.Msg),
interactive: true,
}
cc.uclInst = ucl.New(
options := []ucl.InstOption{
ucl.WithOut(ucl.LineHandler(cc.printLine)),
ucl.WithModule(builtins.OS()),
ucl.WithModule(builtins.FS(nil)),
)
}
for _, pkg := range pkgs {
options = append(options, pkg.InstOptions()...)
}
cc.uclInst = ucl.New(options...)
for _, pkg := range pkgs {
pkg.ConfigureUCL(cc.uclInst)
@ -126,26 +132,20 @@ func (c *CommandController) execute(ctx ExecContext, commandInput string) tea.Ms
return events.Error(errors.New("command currently running"))
}
/*
res, err := c.uclInst.Eval(context.Background(), commandInput)
if err != nil {
return events.Error(err)
}
if teaMsg, ok := res.(teaMsgWrapper); ok {
return teaMsg.msg
}
*/
return nil
}
func (c *CommandController) ExecuteAndWait(ctx context.Context, commandInput string) (any, error) {
return c.uclInst.Eval(ctx, commandInput)
}
func (c *CommandController) cmdLooper() {
ctx := context.WithValue(context.Background(), commandCtlKey, c)
for {
select {
case cmdChan := <-c.cmdChan:
res, err := c.uclInst.Eval(ctx, cmdChan.cmd)
res, err := c.ExecuteAndWait(ctx, cmdChan.cmd)
if err != nil {
c.postMessage(events.Error(err))
} else if res != nil {

View file

@ -3,5 +3,6 @@ package commandctrl
import "ucl.lmika.dev/ucl"
type CommandPack interface {
InstOptions() []ucl.InstOption
ConfigureUCL(ucl *ucl.Inst)
}

View file

@ -8,6 +8,7 @@ import (
"github.com/pkg/errors"
"math/big"
"strconv"
"strings"
)
type exprValue interface {
@ -62,6 +63,14 @@ func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error)
case *types.AttributeValueMemberS:
return stringExprValue(xVal.Value), nil
case *types.AttributeValueMemberN:
if !strings.Contains(xVal.Value, ".") {
iVal, err := strconv.ParseInt(xVal.Value, 10, 64)
if err != nil {
return nil, err
}
return int64ExprValue(iVal), nil
}
xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven)
if err != nil {
return nil, err
@ -139,6 +148,32 @@ func (s int64ExprValue) typeName() string {
return "N"
}
type bigIntExprValue struct {
num *big.Int
}
func (i bigIntExprValue) asGoValue() any {
return i.num
}
func (i bigIntExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberN{Value: i.num.String()}
}
func (i bigIntExprValue) asInt() int64 {
return i.num.Int64()
}
func (i bigIntExprValue) asBigFloat() *big.Float {
var f big.Float
f.SetInt64(i.num.Int64())
return &f
}
func (s bigIntExprValue) typeName() string {
return "N"
}
type bigNumExprValue struct {
num *big.Float
}