tiki/ruki/executor_test.go
2026-04-08 00:34:28 -04:00

3740 lines
98 KiB
Go

package ruki
import (
"fmt"
"strings"
"testing"
"time"
"github.com/boolean-maybe/tiki/task"
)
func newTestExecutor() *Executor {
return NewExecutor(testSchema{}, func() string { return "alice" }, ExecutorRuntime{Mode: ExecutorRuntimeCLI})
}
func testDate(m time.Month, d int) time.Time {
return time.Date(2026, m, d, 0, 0, 0, 0, time.UTC)
}
func makeTasks() []*task.Task {
return []*task.Task{
{
ID: "TIKI-000001", Title: "Setup CI", Status: "ready", Type: "story",
Priority: 2, Tags: []string{"infra"}, Assignee: "alice",
Due: testDate(4, 10), CreatedAt: testDate(3, 1),
},
{
ID: "TIKI-000002", Title: "Fix login bug", Status: "in_progress", Type: "bug",
Priority: 1, Tags: []string{"bug", "frontend"}, Assignee: "bob",
Due: testDate(4, 5), DependsOn: []string{"TIKI-000001"},
CreatedAt: testDate(3, 2),
},
{
ID: "TIKI-000003", Title: "Write docs", Status: "done", Type: "story",
Priority: 3, Tags: []string{"docs"}, Assignee: "alice",
Due: testDate(4, 15), Points: 5, CreatedAt: testDate(3, 3),
},
{
ID: "TIKI-000004", Title: "Plan sprint", Status: "backlog", Type: "spike",
Priority: 2, Tags: []string{}, Assignee: "",
CreatedAt: testDate(3, 4),
},
}
}
// --- nil guards ---
func TestExecuteNilStatement(t *testing.T) {
e := newTestExecutor()
_, err := e.Execute(nil, nil)
if err == nil || !strings.Contains(err.Error(), "nil statement") {
t.Fatalf("expected 'nil statement' error, got: %v", err)
}
}
func TestNewExecutorNilUserFunc(t *testing.T) {
e := NewExecutor(testSchema{}, nil, ExecutorRuntime{Mode: ExecutorRuntimeCLI})
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", Assignee: ""},
}
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "assignee"},
Op: "=",
Right: &FunctionCall{Name: "user", Args: nil},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1 task (empty assignee matches empty user()), got %d", len(result.Select.Tasks))
}
}
// --- basic select ---
func TestExecuteSelectAll(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement("select")
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Select == nil {
t.Fatal("expected Select result")
}
if len(result.Select.Tasks) != 4 {
t.Fatalf("expected 4 tasks, got %d", len(result.Select.Tasks))
}
if result.Select.Fields != nil {
t.Fatalf("expected nil Fields, got %v", result.Select.Fields)
}
}
func TestExecuteSelectWithFields(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement("select title, status")
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Fields) != 2 {
t.Fatalf("expected 2 fields, got %d", len(result.Select.Fields))
}
if result.Select.Fields[0] != "title" || result.Select.Fields[1] != "status" {
t.Fatalf("unexpected fields: %v", result.Select.Fields)
}
if len(result.Select.Tasks) != 4 {
t.Fatalf("expected 4 tasks, got %d", len(result.Select.Tasks))
}
}
// --- WHERE filtering ---
func TestExecuteSelectWhere(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
tests := []struct {
name string
input string
wantCount int
wantIDs []string
}{
{
"status equals", `select where status = "done"`,
1, []string{"TIKI-000003"},
},
{
"priority less than", `select where priority <= 2`,
3, []string{"TIKI-000001", "TIKI-000002", "TIKI-000004"},
},
{
"and condition", `select where status = "ready" and priority = 2`,
1, []string{"TIKI-000001"},
},
{
"or condition", `select where status = "done" or status = "backlog"`,
2, []string{"TIKI-000003", "TIKI-000004"},
},
{
"not condition", `select where not status = "done"`,
3, []string{"TIKI-000001", "TIKI-000002", "TIKI-000004"},
},
{
"in list", `select where status in ["done", "backlog"]`,
2, []string{"TIKI-000003", "TIKI-000004"},
},
{
"not in list", `select where status not in ["done", "backlog"]`,
2, []string{"TIKI-000001", "TIKI-000002"},
},
{
"value in tags", `select where "bug" in tags`,
1, []string{"TIKI-000002"},
},
{
"is empty", `select where assignee is empty`,
1, []string{"TIKI-000004"},
},
{
"is not empty", `select where assignee is not empty`,
3, []string{"TIKI-000001", "TIKI-000002", "TIKI-000003"},
},
{
"tags is empty", `select where tags is empty`,
1, []string{"TIKI-000004"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != tt.wantCount {
ids := make([]string, len(result.Select.Tasks))
for i, tk := range result.Select.Tasks {
ids[i] = tk.ID
}
t.Fatalf("expected %d tasks, got %d: %v", tt.wantCount, len(result.Select.Tasks), ids)
}
for i, wantID := range tt.wantIDs {
if result.Select.Tasks[i].ID != wantID {
t.Errorf("task[%d].ID = %q, want %q", i, result.Select.Tasks[i].ID, wantID)
}
}
})
}
}
// --- ORDER BY ---
func TestExecuteSelectOrderBy(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
tests := []struct {
name string
input string
wantIDs []string
}{
{
"order by priority asc",
"select order by priority",
[]string{"TIKI-000002", "TIKI-000001", "TIKI-000004", "TIKI-000003"},
},
{
"order by priority desc",
"select order by priority desc",
[]string{"TIKI-000003", "TIKI-000001", "TIKI-000004", "TIKI-000002"},
},
{
"order by title asc",
"select order by title",
[]string{"TIKI-000002", "TIKI-000004", "TIKI-000001", "TIKI-000003"},
},
{
"order by due",
"select order by due",
[]string{"TIKI-000004", "TIKI-000002", "TIKI-000001", "TIKI-000003"},
},
{
"multi-field sort",
"select order by priority, createdAt",
[]string{"TIKI-000002", "TIKI-000001", "TIKI-000004", "TIKI-000003"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != len(tt.wantIDs) {
t.Fatalf("expected %d tasks, got %d", len(tt.wantIDs), len(result.Select.Tasks))
}
for i, wantID := range tt.wantIDs {
if result.Select.Tasks[i].ID != wantID {
t.Errorf("task[%d].ID = %q, want %q", i, result.Select.Tasks[i].ID, wantID)
}
}
})
}
}
func TestExecuteSelectNoOrderByPreservesInputOrder(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-CCC", Title: "C", Status: "ready", Priority: 1},
{ID: "TIKI-AAA", Title: "A", Status: "ready", Priority: 1},
{ID: "TIKI-BBB", Title: "B", Status: "ready", Priority: 1},
}
stmt, err := p.ParseStatement("select")
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
wantIDs := []string{"TIKI-CCC", "TIKI-AAA", "TIKI-BBB"}
for i, wantID := range wantIDs {
if result.Select.Tasks[i].ID != wantID {
t.Errorf("task[%d].ID = %q, want %q — input order not preserved", i, result.Select.Tasks[i].ID, wantID)
}
}
}
// --- enum normalization ---
func TestExecuteEnumNormalization(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-A", Title: "A", Status: "done", Type: "story"},
{ID: "TIKI-B", Title: "B", Status: "in_progress", Type: "bug"},
{ID: "TIKI-C", Title: "C", Status: "ready", Type: "story"},
}
tests := []struct {
name string
input string
wantCount int
wantIDs []string
}{
{
"status literal exact", `select where status = "done"`,
1, []string{"TIKI-A"},
},
{
"status alias todo->ready", `select where status = "todo"`,
1, []string{"TIKI-C"},
},
{
"status alias in progress", `select where status = "in progress"`,
1, []string{"TIKI-B"},
},
{
"type alias feature->story", `select where type = "feature"`,
2, []string{"TIKI-A", "TIKI-C"},
},
{
"type alias task->story", `select where type = "task"`,
2, []string{"TIKI-A", "TIKI-C"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != tt.wantCount {
ids := make([]string, len(result.Select.Tasks))
for i, tk := range result.Select.Tasks {
ids[i] = tk.ID
}
t.Fatalf("expected %d tasks, got %d: %v", tt.wantCount, len(result.Select.Tasks), ids)
}
for i, wantID := range tt.wantIDs {
if result.Select.Tasks[i].ID != wantID {
t.Errorf("task[%d].ID = %q, want %q", i, result.Select.Tasks[i].ID, wantID)
}
}
})
}
}
// --- ID case-insensitive comparison ---
func TestExecuteIDCaseInsensitive(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`select where id = "tiki-000001"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(result.Select.Tasks))
}
if result.Select.Tasks[0].ID != "TIKI-000001" {
t.Fatalf("expected TIKI-000001, got %s", result.Select.Tasks[0].ID)
}
}
// --- list set equality ---
func TestExecuteListSetEquality(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tests := []struct {
name string
tasks []*task.Task
input string
wantCount int
}{
{
"order-insensitive match",
[]*task.Task{
{ID: "T1", Title: "x", Status: "ready", Tags: []string{"a", "b"}},
},
`select where tags = ["b", "a"]`,
1,
},
{
"multiplicity matters — different lengths",
[]*task.Task{
{ID: "T1", Title: "x", Status: "ready", Tags: []string{"a"}},
},
`select where tags = ["a", "b"]`,
0,
},
{
"multiplicity matters — duplicate vs single",
[]*task.Task{
{ID: "T1", Title: "x", Status: "ready", Tags: []string{"a", "a"}},
},
`select where tags = ["a"]`,
0,
},
{
"empty list equality",
[]*task.Task{
{ID: "T1", Title: "x", Status: "ready", Tags: []string{}},
},
`select where tags = []`,
1,
},
{
"nil tags equals empty list",
[]*task.Task{
{ID: "T1", Title: "x", Status: "ready"},
},
`select where tags = []`,
1,
},
{
"list inequality",
[]*task.Task{
{ID: "T1", Title: "x", Status: "ready", Tags: []string{"a"}},
},
`select where tags != ["a", "b"]`,
1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tt.tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != tt.wantCount {
t.Fatalf("expected %d tasks, got %d", tt.wantCount, len(result.Select.Tasks))
}
})
}
}
// --- comparison matrix ---
func TestExecuteComparisonMatrix(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{
ID: "TIKI-A", Title: "Alpha", Status: "done", Type: "bug",
Priority: 5, Points: 3, Due: testDate(6, 15),
CreatedAt: testDate(1, 1), UpdatedAt: testDate(3, 1),
},
}
tests := []struct {
name string
input string
want bool
}{
// string =, !=
{"string eq match", `select where title = "Alpha"`, true},
{"string eq no match", `select where title = "Beta"`, false},
{"string neq", `select where title != "Beta"`, true},
// int =, !=, <, >, <=, >=
{"int eq", `select where priority = 5`, true},
{"int neq", `select where priority != 5`, false},
{"int lt", `select where priority < 10`, true},
{"int gt", `select where priority > 10`, false},
{"int lte", `select where priority <= 5`, true},
{"int gte", `select where priority >= 5`, true},
// date ordering
{"date lt", `select where due < 2026-07-01`, true},
{"date gt", `select where due > 2026-07-01`, false},
{"date eq", `select where due = 2026-06-15`, true},
// timestamp field-to-field
{"timestamp lt field", `select where createdAt < updatedAt`, true},
{"timestamp eq self", `select where createdAt = createdAt`, true},
// status =, !=
{"status eq", `select where status = "done"`, true},
{"status neq", `select where status != "done"`, false},
// type =, !=
{"type eq", `select where type = "bug"`, true},
{"type neq", `select where type != "bug"`, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
got := len(result.Select.Tasks) > 0
if got != tt.want {
t.Fatalf("expected match=%v, got %v", tt.want, got)
}
})
}
}
// --- functions ---
func TestExecuteInSubstring(t *testing.T) {
e := newTestExecutor()
tasks := makeTasks()
tests := []struct {
name string
query string
wantIDs []string
}{
{"match", `select where "bug" in title`, []string{"TIKI-000002"}},
{"negated", `select where "bug" not in title`, []string{"TIKI-000001", "TIKI-000003", "TIKI-000004"}},
{"assignee", `select where "ali" in assignee`, []string{"TIKI-000001", "TIKI-000003"}},
{"no match", `select where "xyz" in title`, nil},
{"empty needle", `select where "" in title`, []string{"TIKI-000001", "TIKI-000002", "TIKI-000003", "TIKI-000004"}},
{"case sensitive", `select where "BUG" in title`, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := newTestParser().ParseStatement(tt.query)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
got := make([]string, len(result.Select.Tasks))
for i, tk := range result.Select.Tasks {
got[i] = tk.ID
}
if len(got) != len(tt.wantIDs) {
t.Fatalf("expected %v, got %v", tt.wantIDs, got)
}
for i := range got {
if got[i] != tt.wantIDs[i] {
t.Fatalf("expected %v, got %v", tt.wantIDs, got)
}
}
})
}
}
func TestExecuteUser(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`select where assignee = user()`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 2 {
t.Fatalf("expected 2 tasks assigned to alice, got %d", len(result.Select.Tasks))
}
}
func TestExecuteCount(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`select where count(select where status = "done") >= 1`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// count(done) = 1 which is >= 1, so the condition is true for every task
if len(result.Select.Tasks) != 4 {
t.Fatalf("expected 4 tasks, got %d", len(result.Select.Tasks))
}
}
func TestExecuteNextDate(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{
ID: "T1", Title: "Daily", Status: "ready",
Recurrence: task.RecurrenceDaily,
},
{
ID: "T2", Title: "No recurrence", Status: "ready",
Recurrence: task.RecurrenceNone,
},
}
stmt, err := p.ParseStatement(`select where next_date(recurrence) is not empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 || result.Select.Tasks[0].ID != "T1" {
t.Fatalf("expected T1, got %v", result.Select.Tasks)
}
}
func TestExecuteBlocks(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks() // TIKI-000002 depends on TIKI-000001
stmt, err := p.ParseStatement(`select where id in blocks("TIKI-000001")`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 || result.Select.Tasks[0].ID != "TIKI-000002" {
t.Fatalf("expected TIKI-000002, got %v", result.Select.Tasks)
}
}
// --- call() phase-1 rejection ---
func TestExecuteCallRejected(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`select where call("echo hello") = "hello"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
_, err = e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for call()")
}
if !strings.Contains(err.Error(), "call()") {
t.Fatalf("expected error mentioning call(), got: %v", err)
}
}
// --- CREATE execution ---
func TestExecuteCreateBasic(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
stmt, err := p.ParseStatement(`create title="Fix login" priority=2 status="ready"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, nil)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Create == nil {
t.Fatal("expected Create result")
}
tk := result.Create.Task
if tk.Title != "Fix login" {
t.Errorf("title = %q, want %q", tk.Title, "Fix login")
}
if tk.Priority != 2 {
t.Errorf("priority = %d, want 2", tk.Priority)
}
if tk.Status != "ready" {
t.Errorf("status = %q, want %q", tk.Status, "ready")
}
}
func TestExecuteCreateWithUser(t *testing.T) {
e := newTestExecutor() // userFunc returns "alice"
p := newTestParser()
stmt, err := p.ParseStatement(`create title="test" assignee=user()`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, nil)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Create.Task.Assignee != "alice" {
t.Errorf("assignee = %q, want %q", result.Create.Task.Assignee, "alice")
}
}
func TestExecuteCreateEnumNormalization(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
stmt, err := p.ParseStatement(`create title="test" status="todo" type="feature"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, nil)
if err != nil {
t.Fatalf("execute: %v", err)
}
tk := result.Create.Task
if tk.Status != "ready" {
t.Errorf("status = %q, want normalized %q", tk.Status, "ready")
}
if tk.Type != "story" {
t.Errorf("type = %q, want normalized %q", tk.Type, "story")
}
}
func TestExecuteCreateImmutableFieldRejected(t *testing.T) {
e := newTestExecutor()
for _, field := range []string{"id", "createdBy", "createdAt", "updatedAt"} {
t.Run(field, func(t *testing.T) {
stmt := &Statement{
Create: &CreateStmt{
Assignments: []Assignment{
{Field: "title", Value: &StringLiteral{Value: "x"}},
{Field: field, Value: &StringLiteral{Value: "test"}},
},
},
}
_, err := e.Execute(stmt, nil)
if err == nil {
t.Fatal("expected error for immutable field")
}
if !strings.Contains(err.Error(), "immutable") {
t.Errorf("expected immutable error, got: %v", err)
}
})
}
}
func TestExecuteCreateEmptyTitleRejected(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
stmt, err := p.ParseStatement(`create title=""`)
if err != nil {
t.Fatalf("parse: %v", err)
}
_, err = e.Execute(stmt, nil)
if err == nil {
t.Fatal("expected error for empty title")
}
if !strings.Contains(err.Error(), "empty") {
t.Errorf("expected empty error, got: %v", err)
}
}
func TestExecuteCreateExprError(t *testing.T) {
e := newTestExecutor()
stmt := &Statement{
Create: &CreateStmt{
Assignments: []Assignment{
{Field: "title", Value: &QualifiedRef{Qualifier: "old", Name: "title"}},
},
},
}
_, err := e.Execute(stmt, nil)
if err == nil {
t.Fatal("expected error from eval expression")
}
}
func TestExecuteCreateListField(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
stmt, err := p.ParseStatement(`create title="test" tags=["a","b"]`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, nil)
if err != nil {
t.Fatalf("execute: %v", err)
}
tags := result.Create.Task.Tags
if len(tags) != 2 || tags[0] != "a" || tags[1] != "b" {
t.Errorf("tags = %v, want [a b]", tags)
}
}
func TestExecuteCreateDateField(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
stmt, err := p.ParseStatement(`create title="test" due=2026-06-01`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, nil)
if err != nil {
t.Fatalf("execute: %v", err)
}
due := result.Create.Task.Due
if due.Year() != 2026 || due.Month() != 6 || due.Day() != 1 {
t.Errorf("due = %v, want 2026-06-01", due)
}
}
func TestExecuteCreatePriorityOutOfRange(t *testing.T) {
e := newTestExecutor()
for _, prio := range []int{0, 99, -1} {
t.Run(fmt.Sprintf("priority=%d", prio), func(t *testing.T) {
stmt := &Statement{
Create: &CreateStmt{
Assignments: []Assignment{
{Field: "title", Value: &StringLiteral{Value: "x"}},
{Field: "priority", Value: &IntLiteral{Value: prio}},
},
},
}
_, err := e.Execute(stmt, nil)
if err == nil {
t.Fatal("expected error for out-of-range priority")
}
})
}
}
func TestExecuteCreateEmptyTasks(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
stmt, err := p.ParseStatement(`create title="test"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, []*task.Task{})
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Create.Task.Title != "test" {
t.Errorf("title = %q, want %q", result.Create.Task.Title, "test")
}
}
func TestExecuteCreateWithTemplate(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
template := &task.Task{
Tags: []string{"idea"},
Priority: 7,
Status: "ready",
Type: "story",
}
stmt, err := p.ParseStatement(`create title="x" tags=tags+["new"]`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, nil, ExecutionInput{CreateTemplate: template})
if err != nil {
t.Fatalf("execute: %v", err)
}
tk := result.Create.Task
// tags should be template's ["idea"] + ["new"]
if len(tk.Tags) != 2 || tk.Tags[0] != "idea" || tk.Tags[1] != "new" {
t.Errorf("tags = %v, want [idea new]", tk.Tags)
}
// priority should be preserved from template (not set by assignment)
if tk.Priority != 7 {
t.Errorf("priority = %d, want 7 (template default)", tk.Priority)
}
}
func TestExecuteCreateWithoutTemplate(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
stmt, err := p.ParseStatement(`create title="x" priority=3`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, nil)
if err != nil {
t.Fatalf("execute: %v", err)
}
tk := result.Create.Task
if tk.Title != "x" {
t.Errorf("title = %q, want %q", tk.Title, "x")
}
if tk.Priority != 3 {
t.Errorf("priority = %d, want 3", tk.Priority)
}
// unset fields should be zero-valued
if tk.Points != 0 {
t.Errorf("points = %d, want 0 (zero-value)", tk.Points)
}
}
// --- DELETE execution ---
func TestExecuteDeleteBasic(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`delete where id = "TIKI-000001"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Delete == nil {
t.Fatal("expected Delete result")
}
if len(result.Delete.Deleted) != 1 {
t.Fatalf("expected 1 deleted, got %d", len(result.Delete.Deleted))
}
if result.Delete.Deleted[0].ID != "TIKI-000001" {
t.Errorf("deleted ID = %q, want TIKI-000001", result.Delete.Deleted[0].ID)
}
}
func TestExecuteDeleteMultipleMatches(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`delete where type = "story"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// TIKI-000001 and TIKI-000003 are stories
if len(result.Delete.Deleted) != 2 {
t.Fatalf("expected 2 deleted, got %d", len(result.Delete.Deleted))
}
}
func TestExecuteDeleteNoMatches(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`delete where id = "NONEXISTENT"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Delete.Deleted) != 0 {
t.Fatalf("expected 0 deleted, got %d", len(result.Delete.Deleted))
}
}
func TestExecuteDeleteWhereError(t *testing.T) {
e := newTestExecutor()
tasks := makeTasks()
stmt := &Statement{
Delete: &DeleteStmt{
Where: &CompareExpr{
Left: &QualifiedRef{Qualifier: "old", Name: "status"},
Op: "=",
Right: &StringLiteral{Value: "done"},
},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error from WHERE evaluation")
}
}
// --- quantifier ---
func TestExecuteQuantifier(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
tests := []struct {
name string
input string
wantCount int
}{
{
"any — dep not done",
`select where dependsOn any status != "done"`,
1, // TIKI-000002 depends on TIKI-000001 (status=ready, not done)
},
{
"all — all deps done (vacuously true for no deps)",
`select where dependsOn all status = "done"`,
3, // TIKI-000001, TIKI-000003, TIKI-000004 (no deps = vacuous truth)
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != tt.wantCount {
ids := make([]string, len(result.Select.Tasks))
for i, tk := range result.Select.Tasks {
ids[i] = tk.ID
}
t.Fatalf("expected %d tasks, got %d: %v", tt.wantCount, len(result.Select.Tasks), ids)
}
})
}
}
// --- date arithmetic in WHERE ---
func TestExecuteDateArithmetic(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "T1", Title: "Soon", Status: "ready", Due: testDate(4, 5)},
{ID: "T2", Title: "Later", Status: "ready", Due: testDate(5, 1)},
}
stmt, err := p.ParseStatement(`select where due <= 2026-04-01 + 7day`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 || result.Select.Tasks[0].ID != "T1" {
t.Fatalf("expected T1, got %v", result.Select.Tasks)
}
}
// --- qualified ref rejection ---
func TestExecuteQualifiedRefRejected(t *testing.T) {
e := newTestExecutor()
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &QualifiedRef{Qualifier: "old", Name: "status"},
Op: "=",
Right: &StringLiteral{Value: "done"},
},
},
}
_, err := e.Execute(stmt, makeTasks())
if err == nil {
t.Fatal("expected error for qualified ref")
}
if !strings.Contains(err.Error(), "qualified") {
t.Fatalf("expected qualified ref error, got: %v", err)
}
}
// --- stable sort test ---
func TestExecuteSortStable(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "T1", Title: "First", Status: "ready", Priority: 1},
{ID: "T2", Title: "Second", Status: "ready", Priority: 1},
{ID: "T3", Title: "Third", Status: "ready", Priority: 1},
{ID: "T4", Title: "Fourth", Status: "ready", Priority: 2},
}
stmt, err := p.ParseStatement("select order by priority")
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// priority=1 tasks should preserve input order: T1, T2, T3
wantIDs := []string{"T1", "T2", "T3", "T4"}
for i, wantID := range wantIDs {
if result.Select.Tasks[i].ID != wantID {
t.Errorf("task[%d].ID = %q, want %q", i, result.Select.Tasks[i].ID, wantID)
}
}
}
// --- empty statement ---
func TestExecuteEmptyStatement(t *testing.T) {
e := newTestExecutor()
_, err := e.Execute(&Statement{}, nil)
if err == nil || !strings.Contains(err.Error(), "empty statement") {
t.Fatalf("expected 'empty statement' error, got: %v", err)
}
}
// --- compareWithNil ---
func TestExecuteCompareWithNil(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", Assignee: ""},
{ID: "T2", Title: "y", Status: "ready", Assignee: "bob"},
}
tests := []struct {
name string
op string
wantIDs []string
}{
{"field = empty", "=", []string{"T1"}},
{"field != empty", "!=", []string{"T2"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "assignee"},
Op: tt.op,
Right: &EmptyLiteral{},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != len(tt.wantIDs) {
t.Fatalf("expected %d tasks, got %d", len(tt.wantIDs), len(result.Select.Tasks))
}
for i, wantID := range tt.wantIDs {
if result.Select.Tasks[i].ID != wantID {
t.Errorf("task[%d].ID = %q, want %q", i, result.Select.Tasks[i].ID, wantID)
}
}
})
}
// ordering op with nil returns false, no error
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "assignee"},
Op: "<",
Right: &EmptyLiteral{},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 0 {
t.Fatalf("expected 0 tasks for < nil, got %d", len(result.Select.Tasks))
}
}
// --- duration comparison ---
func TestExecuteDurationComparison(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
tests := []struct {
name string
op string
l, r int
want bool
}{
{"eq true", "=", 2, 2, true},
{"eq false", "=", 1, 2, false},
{"neq", "!=", 1, 2, true},
{"lt", "<", 1, 2, true},
{"gt", ">", 2, 1, true},
{"lte", "<=", 2, 2, true},
{"gte", ">=", 3, 2, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &DurationLiteral{Value: tt.l, Unit: "day"},
Op: tt.op,
Right: &DurationLiteral{Value: tt.r, Unit: "day"},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
got := len(result.Select.Tasks) > 0
if got != tt.want {
t.Fatalf("expected %v, got %v", tt.want, got)
}
})
}
}
// --- subtractValues ---
func TestExecuteSubtractValues(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", Priority: 10, Due: testDate(6, 15)},
}
tests := []struct {
name string
left Expr
right Expr
op string
cmp Expr
want bool
}{
{
"int - int",
&BinaryExpr{Op: "-", Left: &FieldRef{Name: "priority"}, Right: &IntLiteral{Value: 5}},
nil, "=", &IntLiteral{Value: 5}, true,
},
{
"date - duration",
&BinaryExpr{Op: "-", Left: &FieldRef{Name: "due"}, Right: &DurationLiteral{Value: 1, Unit: "day"}},
nil, "=", &DateLiteral{Value: testDate(6, 14)}, true,
},
{
"date - date yields duration",
&BinaryExpr{
Op: "-",
Left: &DateLiteral{Value: testDate(6, 17)},
Right: &DateLiteral{Value: testDate(6, 15)},
},
nil, "=", &DurationLiteral{Value: 2, Unit: "day"}, true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{Left: tt.left, Op: tt.op, Right: tt.cmp},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
got := len(result.Select.Tasks) > 0
if got != tt.want {
t.Fatalf("expected %v, got %v", tt.want, got)
}
})
}
}
// --- addValues additional branches ---
func TestExecuteAddValuesIntAndString(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", Priority: 3},
}
// int + int
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &BinaryExpr{Op: "+", Left: &IntLiteral{Value: 2}, Right: &IntLiteral{Value: 3}},
Op: "=",
Right: &IntLiteral{Value: 5},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1, got %d", len(result.Select.Tasks))
}
// string + string
stmt2 := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &BinaryExpr{Op: "+", Left: &StringLiteral{Value: "hello"}, Right: &StringLiteral{Value: " world"}},
Op: "=",
Right: &StringLiteral{Value: "hello world"},
},
},
}
result, err = e.Execute(stmt2, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1, got %d", len(result.Select.Tasks))
}
}
// --- sorting by status, type, recurrence, and nil fields ---
func TestExecuteSortByStatusTypeRecurrence(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "a", Status: "done", Type: "bug", Recurrence: "0 0 * * *"},
{ID: "T2", Title: "b", Status: "backlog", Type: "story", Recurrence: ""},
{ID: "T3", Title: "c", Status: "ready", Type: "epic", Recurrence: "0 0 1 * *"},
}
tests := []struct {
name string
field string
wantIDs []string
}{
{"by status", "status", []string{"T2", "T1", "T3"}},
{"by type", "type", []string{"T1", "T3", "T2"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := &Statement{
Select: &SelectStmt{
OrderBy: []OrderByClause{{Field: tt.field}},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
for i, wantID := range tt.wantIDs {
if result.Select.Tasks[i].ID != wantID {
t.Errorf("task[%d].ID = %q, want %q", i, result.Select.Tasks[i].ID, wantID)
}
}
})
}
}
// --- extractField additional branches ---
func TestExtractFieldAllFields(t *testing.T) {
tk := &task.Task{
ID: "T1", Title: "hi", Description: "desc", Status: "ready",
Type: "bug", Priority: 1, Points: 3, Tags: []string{"a"},
DependsOn: []string{"T2"}, Due: testDate(1, 1),
Recurrence: task.RecurrenceDaily, Assignee: "bob",
CreatedBy: "alice", CreatedAt: testDate(1, 1), UpdatedAt: testDate(2, 1),
}
fields := []string{
"id", "title", "description", "status", "type", "priority",
"points", "tags", "dependsOn", "due", "recurrence", "assignee",
"createdBy", "createdAt", "updatedAt",
}
for _, f := range fields {
v := extractField(tk, f)
if v == nil {
t.Errorf("extractField(%q) returned nil", f)
}
}
if v := extractField(tk, "nonexistent"); v != nil {
t.Errorf("extractField(nonexistent) should be nil, got %v", v)
}
}
// --- isZeroValue full coverage ---
func TestIsZeroValue(t *testing.T) {
tests := []struct {
name string
val interface{}
want bool
}{
{"nil", nil, true},
{"empty string", "", true},
{"non-empty string", "x", false},
{"zero int", 0, true},
{"non-zero int", 1, false},
{"zero time", time.Time{}, true},
{"non-zero time", testDate(1, 1), false},
{"zero duration", time.Duration(0), true},
{"non-zero duration", time.Hour, false},
{"false bool", false, true},
{"true bool", true, false},
{"empty status", task.Status(""), true},
{"non-empty status", task.Status("done"), false},
{"empty type", task.Type(""), true},
{"non-empty type", task.Type("bug"), false},
{"empty recurrence", task.Recurrence(""), true},
{"non-empty recurrence", task.RecurrenceDaily, false},
{"empty list", []interface{}{}, true},
{"non-empty list", []interface{}{"a"}, false},
{"unknown type", struct{}{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isZeroValue(tt.val); got != tt.want {
t.Errorf("isZeroValue(%v) = %v, want %v", tt.val, got, tt.want)
}
})
}
}
// --- durationToTimeDelta full coverage ---
func TestDurationLiteralUnknownUnitError(t *testing.T) {
e := &Executor{}
tasks := []*task.Task{{ID: "TIKI-AAA001", Title: "test"}}
// unknown unit should produce an error, not silently default to days
stmt, err := newTestParser().ParseStatement(`select where due > 2026-01-01 + 1day`)
if err != nil {
t.Fatal(err)
}
// manually inject an unknown unit into the AST
cmp, ok := stmt.Select.Where.(*CompareExpr)
if !ok {
t.Fatal("expected *CompareExpr")
}
add, ok := cmp.Right.(*BinaryExpr)
if !ok {
t.Fatal("expected *BinaryExpr")
}
add.Right = &DurationLiteral{Value: 1, Unit: "bogus"}
_, err = e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for unknown duration unit, got nil")
}
}
// --- normalizeToString coverage ---
func TestNormalizeToString(t *testing.T) {
tests := []struct {
name string
val interface{}
want string
}{
{"string", "hello", "hello"},
{"status", task.Status("done"), "done"},
{"type", task.Type("bug"), "bug"},
{"recurrence", task.Recurrence("0 0 * * *"), "0 0 * * *"},
{"int", 42, "42"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeToString(tt.val); got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
// --- compareForSort nil handling and all type branches ---
func TestCompareForSort(t *testing.T) {
tests := []struct {
name string
a, b interface{}
want int
}{
{"nil nil", nil, nil, 0},
{"nil left", nil, 1, -1},
{"nil right", 1, nil, 1},
{"int lt", 1, 2, -1},
{"int eq", 2, 2, 0},
{"int gt", 3, 2, 1},
{"string", "a", "b", -1},
{"status", task.Status("a"), task.Status("b"), -1},
{"type", task.Type("a"), task.Type("b"), -1},
{"time before", testDate(1, 1), testDate(2, 1), -1},
{"time equal", testDate(1, 1), testDate(1, 1), 0},
{"time after", testDate(3, 1), testDate(2, 1), 1},
{"recurrence", task.Recurrence("a"), task.Recurrence("b"), -1},
{"duration lt", time.Hour, 2 * time.Hour, -1},
{"duration eq", time.Hour, time.Hour, 0},
{"duration gt", 2 * time.Hour, time.Hour, 1},
{"fallback", struct{ x int }{1}, struct{ x int }{2}, -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := compareForSort(tt.a, tt.b)
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
// --- toInt non-int branch ---
func TestToInt(t *testing.T) {
if v, ok := toInt(42); !ok || v != 42 {
t.Errorf("expected (42, true), got (%d, %v)", v, ok)
}
if _, ok := toInt("not int"); ok {
t.Error("expected false for string")
}
}
// --- count with nil where (counts all) ---
func TestExecuteCountNoWhere(t *testing.T) {
e := newTestExecutor()
tasks := makeTasks()
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{
Name: "count",
Args: []Expr{&SubQuery{Where: nil}},
},
Op: "=",
Right: &IntLiteral{Value: 4},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 4 {
t.Fatalf("expected 4, got %d", len(result.Select.Tasks))
}
}
// --- ID != comparison ---
func TestExecuteIDNotEqual(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-A", Title: "a", Status: "ready"},
{ID: "TIKI-B", Title: "b", Status: "ready"},
}
stmt, err := p.ParseStatement(`select where id != "tiki-a"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 || result.Select.Tasks[0].ID != "TIKI-B" {
t.Fatalf("expected TIKI-B only, got %v", result.Select.Tasks)
}
}
// --- recurrence field comparison ---
func TestExecuteRecurrenceComparison(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", Recurrence: task.RecurrenceDaily},
{ID: "T2", Title: "y", Status: "ready", Recurrence: task.RecurrenceNone},
}
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "recurrence"},
Op: "=",
Right: &FieldRef{Name: "recurrence"},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 2 {
t.Fatalf("expected 2, got %d", len(result.Select.Tasks))
}
}
// --- type normalization with unknown value fallback ---
func TestExecuteNormalizeFallback(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "unknown_status_xyz", Type: "unknown_type_xyz"},
}
// status with unknown value passes through unchanged
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "status"},
Op: "=",
Right: &StringLiteral{Value: "unknown_status_xyz"},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1, got %d", len(result.Select.Tasks))
}
// type with unknown value passes through unchanged
stmt2 := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "type"},
Op: "=",
Right: &StringLiteral{Value: "unknown_type_xyz"},
},
},
}
result, err = e.Execute(stmt2, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1, got %d", len(result.Select.Tasks))
}
}
// --- time comparison all operators ---
func TestExecuteTimeComparisonAllOps(t *testing.T) {
e := newTestExecutor()
d1 := testDate(3, 1)
d2 := testDate(6, 1)
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
tests := []struct {
name string
op string
l, r time.Time
want bool
}{
{"eq", "=", d1, d1, true},
{"neq", "!=", d1, d2, true},
{"lt", "<", d1, d2, true},
{"gt", ">", d2, d1, true},
{"lte", "<=", d1, d1, true},
{"gte", ">=", d1, d1, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &DateLiteral{Value: tt.l},
Op: tt.op,
Right: &DateLiteral{Value: tt.r},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
got := len(result.Select.Tasks) > 0
if got != tt.want {
t.Fatalf("expected %v, got %v", tt.want, got)
}
})
}
}
// --- error propagation tests ---
func TestExecuteErrorPropagation(t *testing.T) {
e := newTestExecutor()
tasks := makeTasks()
badExpr := &QualifiedRef{Qualifier: "old", Name: "status"}
tests := []struct {
name string
stmt *Statement
}{
{
"evalCompare left error",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{Left: badExpr, Op: "=", Right: &StringLiteral{Value: "x"}},
}},
},
{
"evalCompare right error",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{Left: &StringLiteral{Value: "x"}, Op: "=", Right: badExpr},
}},
},
{
"evalIsEmpty error",
&Statement{Select: &SelectStmt{
Where: &IsEmptyExpr{Expr: badExpr},
}},
},
{
"evalIn value error",
&Statement{Select: &SelectStmt{
Where: &InExpr{Value: badExpr, Collection: &FieldRef{Name: "tags"}},
}},
},
{
"evalIn collection error",
&Statement{Select: &SelectStmt{
Where: &InExpr{Value: &StringLiteral{Value: "x"}, Collection: badExpr},
}},
},
{
"evalQuantifier expr error",
&Statement{Select: &SelectStmt{
Where: &QuantifierExpr{Expr: badExpr, Kind: "any", Condition: &CompareExpr{Left: &IntLiteral{Value: 1}, Op: "=", Right: &IntLiteral{Value: 1}}},
}},
},
{
"evalBinaryExpr left error",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &BinaryExpr{Op: "+", Left: badExpr, Right: &IntLiteral{Value: 1}},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}},
},
{
"evalBinaryExpr right error",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &BinaryExpr{Op: "+", Left: &IntLiteral{Value: 1}, Right: badExpr},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}},
},
{
"evalListLiteral element error",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &ListLiteral{Elements: []Expr{badExpr}},
Op: "=",
Right: &ListLiteral{Elements: []Expr{&StringLiteral{Value: "x"}}},
},
}},
},
{
"unknown function",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{Name: "nonexistent", Args: nil},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}},
},
{
"subquery not in count",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &SubQuery{Where: nil},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}},
},
{
"binary expr unknown op",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &BinaryExpr{Op: "*", Left: &IntLiteral{Value: 1}, Right: &IntLiteral{Value: 1}},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}},
},
{
"add type mismatch",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &BinaryExpr{Op: "+", Left: &IntLiteral{Value: 1}, Right: &StringLiteral{Value: "x"}},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}},
},
{
"subtract type mismatch",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &BinaryExpr{Op: "-", Left: &IntLiteral{Value: 1}, Right: &StringLiteral{Value: "x"}},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}},
},
{
"not condition inner error",
&Statement{Select: &SelectStmt{
Where: &NotCondition{Inner: &CompareExpr{Left: badExpr, Op: "=", Right: &StringLiteral{Value: "x"}}},
}},
},
{
"binary condition left error",
&Statement{Select: &SelectStmt{
Where: &BinaryCondition{
Op: "and",
Left: &CompareExpr{Left: badExpr, Op: "=", Right: &StringLiteral{Value: "x"}},
Right: &CompareExpr{Left: &IntLiteral{Value: 1}, Op: "=", Right: &IntLiteral{Value: 1}},
},
}},
},
{
"next_date arg error",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{Name: "next_date", Args: []Expr{badExpr}},
Op: "=",
Right: &DateLiteral{Value: testDate(1, 1)},
},
}},
},
{
"blocks arg error",
&Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{Name: "blocks", Args: []Expr{badExpr}},
Op: "=",
Right: &ListLiteral{Elements: nil},
},
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := e.Execute(tt.stmt, tasks)
if err == nil {
t.Fatal("expected error")
}
})
}
}
// --- in substring with literal collection ---
func TestExecuteInSubstringLiteral(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
stmt := &Statement{
Select: &SelectStmt{
Where: &InExpr{
Value: &StringLiteral{Value: "x"},
Collection: &StringLiteral{Value: "not a list"},
},
},
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// "x" is not a substring of "not a list" → no match
if len(result.Select.Tasks) != 0 {
t.Fatalf("expected 0 tasks, got %d", len(result.Select.Tasks))
}
}
// --- in fail-fast for non-list/non-string runtime values (hand-built AST) ---
func TestExecuteInNonListNonString(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
t.Run("int collection", func(t *testing.T) {
stmt := &Statement{
Select: &SelectStmt{
Where: &InExpr{
Value: &IntLiteral{Value: 1},
Collection: &IntLiteral{Value: 42},
},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "not a list or string") {
t.Fatalf("expected 'not a list or string' error, got: %v", err)
}
})
t.Run("string collection non-string value", func(t *testing.T) {
stmt := &Statement{
Select: &SelectStmt{
Where: &InExpr{
Value: &IntLiteral{Value: 1},
Collection: &StringLiteral{Value: "abc"},
},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "substring check requires string value") {
t.Fatalf("expected 'substring check requires string value' error, got: %v", err)
}
})
}
// --- quantifier with non-list expression ---
func TestExecuteQuantifierNonList(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
stmt := &Statement{
Select: &SelectStmt{
Where: &QuantifierExpr{
Expr: &StringLiteral{Value: "not a list"},
Kind: "any",
Condition: &CompareExpr{Left: &IntLiteral{Value: 1}, Op: "=", Right: &IntLiteral{Value: 1}},
},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "not a list") {
t.Fatalf("expected 'not a list' error, got: %v", err)
}
}
// --- next_date with non-recurrence value ---
func TestExecuteNextDateNonRecurrence(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{Name: "next_date", Args: []Expr{&StringLiteral{Value: "not recurrence"}}},
Op: "=",
Right: &DateLiteral{Value: testDate(1, 1)},
},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "recurrence") {
t.Fatalf("expected recurrence error, got: %v", err)
}
}
// --- count non-subquery arg ---
func TestExecuteCountNonSubquery(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
stmt := &Statement{
Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{Name: "count", Args: []Expr{&IntLiteral{Value: 1}}},
Op: "=",
Right: &IntLiteral{Value: 1},
},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "subquery") {
t.Fatalf("expected subquery error, got: %v", err)
}
}
// --- quantifier condition error propagation ---
func TestExecuteQuantifierConditionError(t *testing.T) {
e := newTestExecutor()
badCond := &CompareExpr{Left: &QualifiedRef{Qualifier: "old", Name: "status"}, Op: "=", Right: &StringLiteral{Value: "x"}}
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", DependsOn: []string{"T2"}},
{ID: "T2", Title: "y", Status: "done"},
}
// any with error in condition
stmt := &Statement{Select: &SelectStmt{
Where: &QuantifierExpr{Expr: &FieldRef{Name: "dependsOn"}, Kind: "any", Condition: badCond},
}}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error from quantifier any")
}
// all with error in condition
stmt2 := &Statement{Select: &SelectStmt{
Where: &QuantifierExpr{Expr: &FieldRef{Name: "dependsOn"}, Kind: "all", Condition: badCond},
}}
_, err = e.Execute(stmt2, tasks)
if err == nil {
t.Fatal("expected error from quantifier all")
}
// unknown quantifier kind
stmt3 := &Statement{Select: &SelectStmt{
Where: &QuantifierExpr{
Expr: &FieldRef{Name: "dependsOn"}, Kind: "none",
Condition: &CompareExpr{Left: &IntLiteral{Value: 1}, Op: "=", Right: &IntLiteral{Value: 1}},
},
}}
_, err = e.Execute(stmt3, tasks)
if err == nil || !strings.Contains(err.Error(), "unknown quantifier") {
t.Fatalf("expected unknown quantifier error, got: %v", err)
}
}
// --- count subquery condition error ---
func TestExecuteCountSubqueryError(t *testing.T) {
e := newTestExecutor()
tasks := makeTasks()
stmt := &Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{Name: "count", Args: []Expr{
&SubQuery{Where: &CompareExpr{
Left: &QualifiedRef{Qualifier: "old", Name: "status"}, Op: "=", Right: &StringLiteral{Value: "x"},
}},
}},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error from count subquery")
}
}
// --- resolveComparisonType right-side field ---
func TestExecuteResolveComparisonTypeRightSide(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "TIKI-A", Title: "x", Status: "ready"},
}
// literal on left, field on right — resolveComparisonType checks right side
stmt := &Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &StringLiteral{Value: "tiki-a"},
Op: "=",
Right: &FieldRef{Name: "id"},
},
}}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1, got %d", len(result.Select.Tasks))
}
// literal on left, status field on right
stmt2 := &Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &StringLiteral{Value: "todo"},
Op: "=",
Right: &FieldRef{Name: "status"},
},
}}
result, err = e.Execute(stmt2, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1 (todo->ready), got %d", len(result.Select.Tasks))
}
// literal on left, type field on right
stmt3 := &Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &StringLiteral{Value: "feature"},
Op: "!=",
Right: &FieldRef{Name: "type"},
},
}}
result, err = e.Execute(stmt3, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// status is "ready", type is "" — "feature" normalizes to "story", "" doesn't normalize → not equal
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1, got %d", len(result.Select.Tasks))
}
}
// --- exprFieldType with unknown field ---
func TestExprFieldType(t *testing.T) {
e := newTestExecutor()
// non-FieldRef returns -1
if got := e.exprFieldType(&StringLiteral{Value: "x"}); got != -1 {
t.Errorf("expected -1 for StringLiteral, got %d", got)
}
// unknown field returns -1
if got := e.exprFieldType(&FieldRef{Name: "nonexistent"}); got != -1 {
t.Errorf("expected -1 for unknown field, got %d", got)
}
// known field returns its type
if got := e.exprFieldType(&FieldRef{Name: "status"}); got != ValueStatus {
t.Errorf("expected ValueStatus, got %d", got)
}
}
// --- unknown condition type ---
type fakeCondition struct{}
func (*fakeCondition) conditionNode() {}
func TestExecuteUnknownConditionType(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
stmt := &Statement{Select: &SelectStmt{Where: &fakeCondition{}}}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "unknown condition type") {
t.Fatalf("expected unknown condition type error, got: %v", err)
}
}
// --- unknown binary condition operator ---
func TestExecuteUnknownBinaryConditionOp(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
stmt := &Statement{Select: &SelectStmt{
Where: &BinaryCondition{
Op: "xor",
Left: &CompareExpr{Left: &IntLiteral{Value: 1}, Op: "=", Right: &IntLiteral{Value: 1}},
Right: &CompareExpr{Left: &IntLiteral{Value: 1}, Op: "=", Right: &IntLiteral{Value: 1}},
},
}}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "unknown binary operator") {
t.Fatalf("expected unknown binary operator error, got: %v", err)
}
}
// --- unknown expression type ---
type fakeExpr struct{}
func (*fakeExpr) exprNode() {}
func TestExecuteUnknownExprType(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
stmt := &Statement{Select: &SelectStmt{
Where: &CompareExpr{Left: &fakeExpr{}, Op: "=", Right: &IntLiteral{Value: 1}},
}}
_, err := e.Execute(stmt, tasks)
if err == nil || !strings.Contains(err.Error(), "unknown expression type") {
t.Fatalf("expected unknown expression type error, got: %v", err)
}
}
// --- blocks returning empty list ---
func TestExecuteBlocksNoBlockers(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready"},
{ID: "T2", Title: "y", Status: "ready"},
}
stmt := &Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &FunctionCall{Name: "blocks", Args: []Expr{&FieldRef{Name: "id"}}},
Op: "=",
Right: &ListLiteral{Elements: nil},
},
}}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// no task depends on any other, so blocks() returns [] for all → all match
if len(result.Select.Tasks) != 2 {
t.Fatalf("expected 2, got %d", len(result.Select.Tasks))
}
}
// --- now() function ---
func TestExecuteNow(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", Due: testDate(12, 31)},
}
stmt := &Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "due"},
Op: ">",
Right: &FunctionCall{Name: "now", Args: nil},
},
}}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// 2026-12-31 should be after now
if len(result.Select.Tasks) != 1 {
t.Fatalf("expected 1, got %d", len(result.Select.Tasks))
}
}
// --- sort by status and type (covers compareForSort branches already
// tested via TestCompareForSort, but exercises them through sortTasks) ---
func TestExecuteSortByStatus(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "a", Status: "ready"},
{ID: "T2", Title: "b", Status: "done"},
{ID: "T3", Title: "c", Status: "backlog"},
}
stmt := &Statement{Select: &SelectStmt{
OrderBy: []OrderByClause{{Field: "status", Desc: true}},
}}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// desc: ready > done > backlog
if result.Select.Tasks[0].ID != "T1" {
t.Errorf("expected T1 first (ready), got %s", result.Select.Tasks[0].ID)
}
}
// --- compareValues unsupported type fallback ---
func TestExecuteCompareUnsupportedType(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready"}}
// DurationLiteral on left compared with int on right — type mismatch
stmt := &Statement{Select: &SelectStmt{
Where: &CompareExpr{
Left: &DurationLiteral{Value: 1, Unit: "day"},
Op: "=",
Right: &IntLiteral{Value: 1},
},
}}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected type mismatch error")
}
}
// --- comparison helper error branches ---
func TestComparisonHelperErrors(t *testing.T) {
// all comparison helpers reject unknown operators
if _, err := compareStrings("a", "b", "<"); err == nil {
t.Error("compareStrings should reject <")
}
if _, err := compareStringsCI("a", "b", "<"); err == nil {
t.Error("compareStringsCI should reject <")
}
if _, err := compareBools(true, false, "<"); err == nil {
t.Error("compareBools should reject <")
}
if _, err := compareIntValues(1, 2, "~"); err == nil {
t.Error("compareIntValues should reject ~")
}
if _, err := compareTimes(time.Now(), time.Now(), "~"); err == nil {
t.Error("compareTimes should reject ~")
}
if _, err := compareDurations(time.Hour, time.Hour, "~"); err == nil {
t.Error("compareDurations should reject ~")
}
if _, err := compareListEquality(nil, nil, "<"); err == nil {
t.Error("compareListEquality should reject <")
}
}
// --- sortedMultisetEqual with same-length but different elements ---
func TestSortedMultisetEqualMismatch(t *testing.T) {
a := []interface{}{"x", "y"}
b := []interface{}{"x", "z"}
if sortedMultisetEqual(a, b) {
t.Error("expected false for different elements")
}
}
// --- compareValues with task.Status/Type falling through (non-field context) ---
func TestCompareValuesStatusTypeDirect(t *testing.T) {
e := newTestExecutor()
// status value vs string without FieldRef context — falls through to task.Status case
ok, err := e.compareValues(task.Status("done"), "done", "=", &StringLiteral{Value: "done"}, &StringLiteral{Value: "done"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("expected status done = done")
}
// type value vs string
ok, err = e.compareValues(task.Type("bug"), "bug", "=", &StringLiteral{Value: "bug"}, &StringLiteral{Value: "bug"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("expected type bug = bug")
}
// int vs non-int
_, err = e.compareValues(42, "not int", "=", &IntLiteral{Value: 42}, &StringLiteral{Value: "not int"})
if err == nil {
t.Error("expected error for int vs string")
}
// time vs non-time
_, err = e.compareValues(time.Now(), "not time", "=", &DateLiteral{Value: time.Now()}, &StringLiteral{Value: "not time"})
if err == nil {
t.Error("expected error for time vs string")
}
// unsupported type
_, err = e.compareValues(struct{}{}, struct{}{}, "=", &IntLiteral{Value: 1}, &IntLiteral{Value: 1})
if err == nil {
t.Error("expected error for unsupported type")
}
}
// --- evalQuantifier: all where one ref fails condition ---
func TestExecuteQuantifierAllFailing(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", DependsOn: []string{"T2", "T3"}},
{ID: "T2", Title: "y", Status: "done"},
{ID: "T3", Title: "z", Status: "ready"},
}
stmt, err := newTestParser().ParseStatement(`select where dependsOn all status = "done"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// T1 depends on T2(done) and T3(ready) — not all done
// T2 has no deps → vacuous truth
// T3 has no deps → vacuous truth
wantCount := 2
if len(result.Select.Tasks) != wantCount {
ids := make([]string, len(result.Select.Tasks))
for i, tk := range result.Select.Tasks {
ids[i] = tk.ID
}
t.Fatalf("expected %d tasks, got %d: %v", wantCount, len(result.Select.Tasks), ids)
}
}
func TestExecuteQuantifierAllPassing(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready", DependsOn: []string{"T2", "T3"}},
{ID: "T2", Title: "y", Status: "done"},
{ID: "T3", Title: "z", Status: "done"},
}
stmt, err := newTestParser().ParseStatement(`select where dependsOn all status = "done"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
// all 3: T1 deps are T2+T3 both done, T2+T3 have no deps (vacuous)
if len(result.Select.Tasks) != 3 {
ids := make([]string, len(result.Select.Tasks))
for i, tk := range result.Select.Tasks {
ids[i] = tk.ID
}
t.Fatalf("expected 3 tasks, got %d: %v", len(result.Select.Tasks), ids)
}
}
// --- UPDATE execution ---
func TestExecuteUpdateSingleField(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "old title", Status: "ready"},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set title="new title"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update == nil {
t.Fatal("expected Update result")
}
if len(result.Update.Updated) != 1 {
t.Fatalf("expected 1 updated task, got %d", len(result.Update.Updated))
}
if result.Update.Updated[0].Title != "new title" {
t.Errorf("expected title 'new title', got %q", result.Update.Updated[0].Title)
}
}
func TestExecuteUpdateMultipleFields(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Priority: 3},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set status="done" priority=1`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if u.Status != "done" {
t.Errorf("expected status 'done', got %q", u.Status)
}
if u.Priority != 1 {
t.Errorf("expected priority 1, got %d", u.Priority)
}
}
func TestExecuteUpdateMatchesMultipleTasks(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "T1", Title: "a", Status: "ready", Priority: 1},
{ID: "T2", Title: "b", Status: "ready", Priority: 2},
{ID: "T3", Title: "c", Status: "done", Priority: 3},
}
stmt, err := p.ParseStatement(`update where status = "ready" set status="done"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Update.Updated) != 2 {
t.Fatalf("expected 2 updated, got %d", len(result.Update.Updated))
}
for _, u := range result.Update.Updated {
if u.Status != "done" {
t.Errorf("task %s status = %q, want done", u.ID, u.Status)
}
}
}
func TestExecuteUpdateMatchesNoTasks(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`update where id = "NONEXISTENT" set title="x"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Update.Updated) != 0 {
t.Fatalf("expected 0 updated, got %d", len(result.Update.Updated))
}
}
func TestExecuteUpdateWithComplexWhere(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := makeTasks()
stmt, err := p.ParseStatement(`update where priority < 3 and "bug" in tags set status="done"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Update.Updated) != 1 {
t.Fatalf("expected 1 updated, got %d", len(result.Update.Updated))
}
if result.Update.Updated[0].ID != "TIKI-000002" {
t.Errorf("expected TIKI-000002, got %s", result.Update.Updated[0].ID)
}
}
func TestExecuteUpdateWithFieldReference(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", CreatedBy: "alice", Assignee: ""},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set assignee=createdBy`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Assignee != "alice" {
t.Errorf("expected assignee 'alice', got %q", result.Update.Updated[0].Assignee)
}
}
func TestExecuteUpdateWithFunction(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Recurrence: task.RecurrenceDaily},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set due=next_date(recurrence)`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Due.IsZero() {
t.Error("expected non-zero due date after next_date()")
}
}
func TestExecuteUpdateListField(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Tags: []string{"old"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set tags=["a","b"]`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.Tags) != 2 || u.Tags[0] != "a" || u.Tags[1] != "b" {
t.Errorf("expected tags [a b], got %v", u.Tags)
}
}
func TestExecuteUpdateListPlusList(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Tags: []string{"old"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set tags=tags+["new"]`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.Tags) != 2 || u.Tags[0] != "old" || u.Tags[1] != "new" {
t.Errorf("expected tags [old new], got %v", u.Tags)
}
}
func TestExecuteUpdateListPlusElement(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Tags: []string{"old"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set tags=tags+"new"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.Tags) != 2 || u.Tags[0] != "old" || u.Tags[1] != "new" {
t.Errorf("expected tags [old new], got %v", u.Tags)
}
}
func TestExecuteUpdateListMinusList(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Tags: []string{"old", "keep"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set tags=tags-["old"]`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.Tags) != 1 || u.Tags[0] != "keep" {
t.Errorf("expected tags [keep], got %v", u.Tags)
}
}
func TestExecuteUpdateListMinusElement(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Tags: []string{"old", "keep"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set tags=tags-"old"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.Tags) != 1 || u.Tags[0] != "keep" {
t.Errorf("expected tags [keep], got %v", u.Tags)
}
}
func TestExecuteUpdateListMinusDuplicates(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Tags: []string{"old", "old", "keep"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set tags=tags-"old"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.Tags) != 1 || u.Tags[0] != "keep" {
t.Errorf("expected tags [keep], got %v", u.Tags)
}
}
func TestExecuteUpdateDependsOnPlusElement(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", DependsOn: []string{}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set dependsOn=dependsOn+"TIKI-Y"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.DependsOn) != 1 || u.DependsOn[0] != "TIKI-Y" {
t.Errorf("expected dependsOn [TIKI-Y], got %v", u.DependsOn)
}
}
func TestExecuteUpdateDependsOnPlusList(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", DependsOn: []string{"TIKI-Z"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set dependsOn=dependsOn+["TIKI-Y"]`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.DependsOn) != 2 || u.DependsOn[0] != "TIKI-Z" || u.DependsOn[1] != "TIKI-Y" {
t.Errorf("expected dependsOn [TIKI-Z TIKI-Y], got %v", u.DependsOn)
}
}
func TestExecuteUpdateDependsOnMinusElement(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", DependsOn: []string{"TIKI-Y", "TIKI-Z"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set dependsOn=dependsOn-"TIKI-Y"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.DependsOn) != 1 || u.DependsOn[0] != "TIKI-Z" {
t.Errorf("expected dependsOn [TIKI-Z], got %v", u.DependsOn)
}
}
func TestExecuteUpdateDependsOnMinusList(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", DependsOn: []string{"TIKI-Y", "TIKI-Z"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set dependsOn=dependsOn-["TIKI-Y"]`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
u := result.Update.Updated[0]
if len(u.DependsOn) != 1 || u.DependsOn[0] != "TIKI-Z" {
t.Errorf("expected dependsOn [TIKI-Z], got %v", u.DependsOn)
}
}
func TestExecuteUpdateTagsToEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Tags: []string{"a", "b"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set tags=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Tags != nil {
t.Errorf("expected nil tags, got %v", result.Update.Updated[0].Tags)
}
}
func TestExecuteUpdateStringToEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Assignee: "alice"},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set assignee=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Assignee != "" {
t.Errorf("expected empty assignee, got %q", result.Update.Updated[0].Assignee)
}
}
func TestExecuteUpdateDateToEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Due: testDate(6, 1)},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set due=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if !result.Update.Updated[0].Due.IsZero() {
t.Errorf("expected zero due, got %v", result.Update.Updated[0].Due)
}
}
func TestExecuteUpdateConstrainedFieldsRejectEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tests := []struct {
name string
input string
}{
{"title", `update where id = "T1" set title=empty`},
{"priority", `update where id = "T1" set priority=empty`},
{"status", `update where id = "T1" set status=empty`},
{"type", `update where id = "T1" set type=empty`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready", Type: "bug", Priority: 2}}
stmt, err := p.ParseStatement(tt.input)
if err != nil {
t.Fatalf("parse: %v", err)
}
_, err = e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for empty on constrained field")
}
if !strings.Contains(err.Error(), "empty") {
t.Errorf("expected error mentioning empty, got: %v", err)
}
})
}
}
func TestExecuteUpdateImmutableFieldRejected(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready"},
}
fields := []string{"id", "createdBy", "createdAt", "updatedAt"}
for _, field := range fields {
t.Run(field, func(t *testing.T) {
stmt := &Statement{
Update: &UpdateStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "id"}, Op: "=", Right: &StringLiteral{Value: "TIKI-000001"},
},
Set: []Assignment{{Field: field, Value: &StringLiteral{Value: "test"}}},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for immutable field")
}
if !strings.Contains(err.Error(), "immutable") {
t.Errorf("expected immutable error, got: %v", err)
}
})
}
}
func TestExecuteUpdateEnumNormalization(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "done"},
}
// "todo" is an alias for "ready" in the test schema
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set status="todo"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Status != "ready" {
t.Errorf("expected canonical 'ready', got %q", result.Update.Updated[0].Status)
}
}
func TestExecuteUpdateOriginalUnmodified(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "old", Status: "ready"},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set title="new"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
_, err = e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if tasks[0].Title != "old" {
t.Errorf("original task was mutated: title = %q, want 'old'", tasks[0].Title)
}
}
// --- list arithmetic (addValues/subtractValues) ---
func TestAddValuesList(t *testing.T) {
tests := []struct {
name string
left []interface{}
right interface{}
want []interface{}
}{
{"list + list", []interface{}{"a"}, []interface{}{"b"}, []interface{}{"a", "b"}},
{"list + element", []interface{}{"a"}, "b", []interface{}{"a", "b"}},
{"empty + list", []interface{}{}, []interface{}{"a"}, []interface{}{"a"}},
{"list + empty list", []interface{}{"a"}, []interface{}{}, []interface{}{"a"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := addValues(tt.left, tt.right)
if err != nil {
t.Fatalf("addValues error: %v", err)
}
got, ok := result.([]interface{})
if !ok {
t.Fatalf("expected []interface{}, got %T", result)
}
if len(got) != len(tt.want) {
t.Fatalf("expected %d elements, got %d: %v", len(tt.want), len(got), got)
}
for i := range tt.want {
if normalizeToString(got[i]) != normalizeToString(tt.want[i]) {
t.Errorf("element %d: got %v, want %v", i, got[i], tt.want[i])
}
}
})
}
}
func TestSubtractValuesList(t *testing.T) {
tests := []struct {
name string
left []interface{}
right interface{}
want []interface{}
}{
{"list - list", []interface{}{"a", "b", "c"}, []interface{}{"b"}, []interface{}{"a", "c"}},
{"list - element", []interface{}{"a", "b"}, "a", []interface{}{"b"}},
{"remove all occurrences", []interface{}{"a", "a", "b"}, "a", []interface{}{"b"}},
{"remove nothing", []interface{}{"a", "b"}, "c", []interface{}{"a", "b"}},
{"remove all", []interface{}{"a"}, "a", []interface{}{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := subtractValues(tt.left, tt.right)
if err != nil {
t.Fatalf("subtractValues error: %v", err)
}
got, ok := result.([]interface{})
if !ok {
t.Fatalf("expected []interface{}, got %T", result)
}
if len(got) != len(tt.want) {
t.Fatalf("expected %d elements, got %d: %v", len(tt.want), len(got), got)
}
for i := range tt.want {
if normalizeToString(got[i]) != normalizeToString(tt.want[i]) {
t.Errorf("element %d: got %v, want %v", i, got[i], tt.want[i])
}
}
})
}
}
func TestExecuteUpdateRecurrenceToEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Recurrence: task.RecurrenceDaily},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set recurrence=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Recurrence != "" {
t.Errorf("expected empty recurrence, got %q", result.Update.Updated[0].Recurrence)
}
}
func TestExecuteUpdatePointsToEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Points: 5},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set points=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Points != 0 {
t.Errorf("expected 0 points, got %d", result.Update.Updated[0].Points)
}
}
func TestExecuteUpdateUnknownField(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready"},
}
stmt := &Statement{
Update: &UpdateStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "id"}, Op: "=", Right: &StringLiteral{Value: "T1"},
},
Set: []Assignment{{Field: "nonexistent", Value: &StringLiteral{Value: "x"}}},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for unknown field")
}
if !strings.Contains(err.Error(), "unknown field") {
t.Errorf("expected 'unknown field' error, got: %v", err)
}
}
func TestExecuteUpdateTypeNormalization(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Type: "bug"},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set type="feature"`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Type != "story" {
t.Errorf("expected normalized type 'story', got %q", result.Update.Updated[0].Type)
}
}
func TestExecuteUpdateDescriptionToEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", Description: "some desc"},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set description=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].Description != "" {
t.Errorf("expected empty description, got %q", result.Update.Updated[0].Description)
}
}
func TestExecuteUpdateTitleToEmptyRejected(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "old title", Status: "ready"},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set title=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
_, err = e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for title=empty")
}
if !strings.Contains(err.Error(), "empty") {
t.Errorf("expected error mentioning empty, got: %v", err)
}
}
func TestExecuteUpdateDependsOnToEmpty(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
tasks := []*task.Task{
{ID: "TIKI-000001", Title: "x", Status: "ready", DependsOn: []string{"T2"}},
}
stmt, err := p.ParseStatement(`update where id = "TIKI-000001" set dependsOn=empty`)
if err != nil {
t.Fatalf("parse: %v", err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Update.Updated[0].DependsOn != nil {
t.Errorf("expected nil dependsOn, got %v", result.Update.Updated[0].DependsOn)
}
}
// --- executeUpdate error branches ---
func TestExecuteUpdateWhereError(t *testing.T) {
e := newTestExecutor()
tasks := makeTasks()
// WHERE clause with a bad expr triggers filterTasks error
stmt := &Statement{
Update: &UpdateStmt{
Where: &CompareExpr{
Left: &QualifiedRef{Qualifier: "old", Name: "status"},
Op: "=",
Right: &StringLiteral{Value: "done"},
},
Set: []Assignment{{Field: "title", Value: &StringLiteral{Value: "x"}}},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error from WHERE evaluation")
}
}
func TestExecuteUpdateEvalExprError(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{
{ID: "T1", Title: "x", Status: "ready"},
}
// assignment value that fails evalExpr
stmt := &Statement{
Update: &UpdateStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "id"}, Op: "=", Right: &StringLiteral{Value: "T1"},
},
Set: []Assignment{{Field: "title", Value: &QualifiedRef{Qualifier: "old", Name: "title"}}},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error from evalExpr in assignment")
}
}
// --- setField type mismatch branches ---
func TestSetFieldTypeMismatches(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Type: "bug", Priority: 2}
tests := []struct {
name string
field string
val interface{}
}{
{"title non-string", "title", 42},
{"description non-string", "description", 42},
{"status non-string", "status", 42},
{"type non-string", "type", 42},
{"priority non-int", "priority", "abc"},
{"points non-int", "points", "abc"},
{"due non-time", "due", "abc"},
{"recurrence non-string", "recurrence", 42},
{"assignee non-string", "assignee", 42},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := e.setField(tk, tt.field, tt.val)
if err == nil {
t.Fatalf("expected error for %s with %T", tt.field, tt.val)
}
})
}
}
func TestSetFieldStatusAsTaskStatus(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
// passing task.Status value directly (not string)
err := e.setField(tk, "status", task.Status("done"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Status != "done" {
t.Errorf("expected status done, got %q", tk.Status)
}
}
func TestSetFieldTypeAsTaskType(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Type: "bug"}
// passing task.Type value directly (not string)
err := e.setField(tk, "type", task.Type("story"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Type != "story" {
t.Errorf("expected type story, got %q", tk.Type)
}
}
func TestSetFieldUnknownStatusError(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
err := e.setField(tk, "status", "nonexistent_status")
if err == nil || !strings.Contains(err.Error(), "unknown status") {
t.Fatalf("expected unknown status error, got: %v", err)
}
}
func TestSetFieldUnknownTypeError(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Type: "bug"}
err := e.setField(tk, "type", "nonexistent_type")
if err == nil || !strings.Contains(err.Error(), "unknown type") {
t.Fatalf("expected unknown type error, got: %v", err)
}
}
func TestSetFieldDescriptionToEmpty(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Description: "some desc"}
err := e.setField(tk, "description", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Description != "" {
t.Errorf("expected empty description, got %q", tk.Description)
}
}
func TestSetFieldPointsToEmpty(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Points: 5}
err := e.setField(tk, "points", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Points != 0 {
t.Errorf("expected 0 points, got %d", tk.Points)
}
}
func TestSetFieldRecurrenceFromString(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
err := e.setField(tk, "recurrence", "0 0 * * *")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Recurrence != task.Recurrence("0 0 * * *") {
t.Errorf("expected recurrence '0 0 * * *', got %q", tk.Recurrence)
}
}
func TestSetFieldRecurrenceFromTaskRecurrence(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
err := e.setField(tk, "recurrence", task.RecurrenceDaily)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Recurrence != task.RecurrenceDaily {
t.Errorf("expected daily recurrence, got %q", tk.Recurrence)
}
}
func TestToStringSliceNonList(t *testing.T) {
result := toStringSlice("not a list")
if result != nil {
t.Errorf("expected nil for non-list input, got %v", result)
}
}
func TestExecuteUpdatePriorityOutOfRange(t *testing.T) {
e := newTestExecutor()
tests := []struct {
name string
val int
}{
{"too low", 0},
{"too high", 99},
{"negative", -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready", Priority: 2}}
stmt := &Statement{
Update: &UpdateStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "id"}, Op: "=", Right: &StringLiteral{Value: "T1"},
},
Set: []Assignment{{Field: "priority", Value: &IntLiteral{Value: tt.val}}},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for out-of-range priority")
}
if !strings.Contains(err.Error(), "priority value out of range") {
t.Errorf("expected range error, got: %v", err)
}
})
}
}
func TestExecuteUpdatePriorityValidRange(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
for _, prio := range []int{1, 3, 5} {
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready", Priority: 2}}
stmt, err := p.ParseStatement(fmt.Sprintf(`update where id = "T1" set priority=%d`, prio))
if err != nil {
t.Fatalf("parse priority=%d: %v", prio, err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute priority=%d: %v", prio, err)
}
if result.Update.Updated[0].Priority != prio {
t.Errorf("expected priority %d, got %d", prio, result.Update.Updated[0].Priority)
}
}
}
func TestExecuteUpdatePointsOutOfRange(t *testing.T) {
e := newTestExecutor()
tests := []struct {
name string
val int
}{
{"negative", -1},
{"exceeds max", 999},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready", Points: 3}}
stmt := &Statement{
Update: &UpdateStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "id"}, Op: "=", Right: &StringLiteral{Value: "T1"},
},
Set: []Assignment{{Field: "points", Value: &IntLiteral{Value: tt.val}}},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for invalid points value")
}
if !strings.Contains(err.Error(), "points value out of range") {
t.Errorf("expected points range error, got: %v", err)
}
})
}
}
func TestExecuteUpdatePointsValidValues(t *testing.T) {
e := newTestExecutor()
p := newTestParser()
for _, pts := range []int{0, 1, 5, 10} {
tasks := []*task.Task{{ID: "T1", Title: "x", Status: "ready", Points: 3}}
stmt, err := p.ParseStatement(fmt.Sprintf(`update where id = "T1" set points=%d`, pts))
if err != nil {
t.Fatalf("parse points=%d: %v", pts, err)
}
result, err := e.Execute(stmt, tasks)
if err != nil {
t.Fatalf("execute points=%d: %v", pts, err)
}
if result.Update.Updated[0].Points != pts {
t.Errorf("expected points %d, got %d", pts, result.Update.Updated[0].Points)
}
}
}
func TestExecuteUpdateTitleWhitespaceRejected(t *testing.T) {
e := newTestExecutor()
tasks := []*task.Task{{ID: "T1", Title: "old", Status: "ready"}}
stmt := &Statement{
Update: &UpdateStmt{
Where: &CompareExpr{
Left: &FieldRef{Name: "id"}, Op: "=", Right: &StringLiteral{Value: "T1"},
},
Set: []Assignment{{Field: "title", Value: &StringLiteral{Value: " "}}},
},
}
_, err := e.Execute(stmt, tasks)
if err == nil {
t.Fatal("expected error for whitespace-only title")
}
if !strings.Contains(err.Error(), "empty") {
t.Errorf("expected empty error, got: %v", err)
}
}
func TestSetFieldDescriptionString(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
err := e.setField(tk, "description", "new desc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Description != "new desc" {
t.Errorf("expected 'new desc', got %q", tk.Description)
}
}
func TestSetFieldPointsInt(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
err := e.setField(tk, "points", 8)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Points != 8 {
t.Errorf("expected 8, got %d", tk.Points)
}
}
// --- bool comparison coverage ---
func TestCompareBoolsEqual(t *testing.T) {
ok, err := compareBools(true, true, "=")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("true = true should be true")
}
ok, err = compareBools(true, false, "=")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok {
t.Error("true = false should be false")
}
}
func TestCompareBoolsNotEqual(t *testing.T) {
ok, err := compareBools(true, false, "!=")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("true != false should be true")
}
ok, err = compareBools(true, true, "!=")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok {
t.Error("true != true should be false")
}
}
func TestCompareBoolsUnsupportedOp(t *testing.T) {
_, err := compareBools(true, false, "<")
if err == nil {
t.Fatal("expected error for unsupported bool operator")
}
if !strings.Contains(err.Error(), "not supported for bool") {
t.Fatalf("expected 'not supported for bool' error, got: %v", err)
}
}
func TestCompareValues_BoolDispatch(t *testing.T) {
e := newTestExecutor()
// both sides are bool — should dispatch to compareBools
ok, err := e.compareValues(true, false, "!=", &IntLiteral{Value: 1}, &IntLiteral{Value: 0})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Error("true != false should be true via compareValues")
}
}
// --- evalID in plugin runtime ---
func TestExecuteEvalIDPluginRuntime(t *testing.T) {
p := newTestParser()
e := NewExecutor(testSchema{}, func() string { return "alice" }, ExecutorRuntime{Mode: ExecutorRuntimePlugin})
tasks := makeTasks()
// id() returns the selected task ID as a constant; comparing it to a
// matching literal yields true for all tasks (global predicate).
validated, err := p.ParseAndValidateStatement(`select where id() = "TIKI-000001"`, ExecutorRuntimePlugin)
if err != nil {
t.Fatalf("validate: %v", err)
}
result, err := e.Execute(validated, tasks, ExecutionInput{SelectedTaskID: "TIKI-000001"})
if err != nil {
t.Fatalf("execute: %v", err)
}
if result.Select == nil {
t.Fatal("expected Select result")
}
// id() = "TIKI-000001" is always true when selected task is TIKI-000001
if len(result.Select.Tasks) != 4 {
t.Fatalf("expected 4 tasks (global true predicate), got %d", len(result.Select.Tasks))
}
// compare id() against each task's own id field to select only the matching task
validated2, err := p.ParseAndValidateStatement(`select where id = id()`, ExecutorRuntimePlugin)
if err != nil {
t.Fatalf("validate: %v", err)
}
result2, err := e.Execute(validated2, tasks, ExecutionInput{SelectedTaskID: "TIKI-000001"})
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result2.Select.Tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(result2.Select.Tasks))
}
if result2.Select.Tasks[0].ID != "TIKI-000001" {
t.Errorf("expected TIKI-000001, got %s", result2.Select.Tasks[0].ID)
}
}
func TestExecuteEvalIDPluginRuntimeNoMatch(t *testing.T) {
p := newTestParser()
e := NewExecutor(testSchema{}, func() string { return "alice" }, ExecutorRuntime{Mode: ExecutorRuntimePlugin})
tasks := makeTasks()
// id() returns "TIKI-000002", compare with literal "TIKI-000001" → always false
validated, err := p.ParseAndValidateStatement(`select where id() = "TIKI-000001"`, ExecutorRuntimePlugin)
if err != nil {
t.Fatalf("validate: %v", err)
}
result, err := e.Execute(validated, tasks, ExecutionInput{SelectedTaskID: "TIKI-000002"})
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(result.Select.Tasks) != 0 {
t.Fatalf("expected 0 tasks, got %d", len(result.Select.Tasks))
}
}
// --- setField type mismatches for description, status, type with unusual value types ---
func TestSetFieldDescriptionNonString(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Description: "old"}
err := e.setField(tk, "description", 42)
if err == nil {
t.Fatal("expected error for non-string description")
}
if !strings.Contains(err.Error(), "description must be a string") {
t.Errorf("expected 'description must be a string' error, got: %v", err)
}
}
func TestSetFieldStatusWithTaskStatusValue(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
// pass task.Status("done") directly, not a string
err := e.setField(tk, "status", task.Status("done"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Status != "done" {
t.Errorf("expected status 'done', got %q", tk.Status)
}
}
func TestSetFieldTypeWithTaskTypeValue(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Type: "bug"}
// pass task.Type("story") directly, not a string
err := e.setField(tk, "type", task.Type("story"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tk.Type != "story" {
t.Errorf("expected type 'story', got %q", tk.Type)
}
}
func TestSetFieldStatusWithIntValue(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
err := e.setField(tk, "status", 42)
if err == nil {
t.Fatal("expected error for int status")
}
if !strings.Contains(err.Error(), "status must be a string") {
t.Errorf("expected 'status must be a string' error, got: %v", err)
}
}
func TestSetFieldTypeWithIntValue(t *testing.T) {
e := newTestExecutor()
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Type: "bug"}
err := e.setField(tk, "type", 42)
if err == nil {
t.Fatal("expected error for int type")
}
if !strings.Contains(err.Error(), "type must be a string") {
t.Errorf("expected 'type must be a string' error, got: %v", err)
}
}
// --- Execute with unsupported statement type ---
func TestExecuteUnsupportedStatementType(t *testing.T) {
e := newTestExecutor()
_, err := e.Execute("not a statement", nil)
if err == nil {
t.Fatal("expected error for unsupported statement type")
}
if !strings.Contains(err.Error(), "unsupported statement type") {
t.Errorf("expected 'unsupported statement type' error, got: %v", err)
}
}