internal/database/database_test.go

package database

import (
	"net/url"
	"reflect"
	"strings"
	"testing"
	"time"
)

func TestBuildQueryString(t *testing.T) {
	tests := []struct {
		name      string
		query     EventQuery
		total     bool
		wantQuery string
		wantArgs  []any
	}{
		{
			name:      "empty query total",
			query:     EventQuery{},
			total:     true,
			wantQuery: "SELECT COUNT(*) FROM honeypot_events",
			wantArgs:  []any{},
		},
		{
			name:      "empty query non-total",
			query:     EventQuery{},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",
			wantArgs:  []any{},
		},
		{
			name: "with IDs filter",
			query: EventQuery{
				IDs: []string{"1", "2", "3"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE id IN (?, ?, ?)",
			wantArgs:  []any{"1", "2", "3"},
		},
		{
			name: "with time range",
			query: EventQuery{
				TimeStart: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
				TimeEnd:   time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE time >= ? AND time <= ?",
			wantArgs:  []any{"2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z"},
		},
		{
			name: "with remote addresses",
			query: EventQuery{
				RemoteAddrs: []string{"192.168.1.1", "10.0.0.1"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_ip_int = ? OR remote_ip_int = ?)",
			wantArgs:  []any{uint32(3232235777), uint32(167772161)},
		},
		{
			name: "with remote addresses negation",
			query: EventQuery{
				RemoteAddrs: []string{"192.168.1.1", "!10.0.0.1"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_ip_int = ?) AND (remote_ip_int != ?)",
			wantArgs:  []any{uint32(3232235777), uint32(167772161)},
		},
		{
			name: "with destination ports",
			query: EventQuery{
				DstPorts: []string{"80", "443", "8000-8080", "!22"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (dst_port = ? OR dst_port = ? OR dst_port BETWEEN ? AND ?) AND (dst_port != ?)",
			wantArgs:  []any{80, 443, 8000, 8080, 22},
		},
		{
			name: "with destination ports 2",
			query: EventQuery{
				DstPorts: []string{"!80", "443"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (dst_port = ?) AND (dst_port != ?)",
			wantArgs:  []any{443, 80},
		},
		{
			name: "with type filter",
			query: EventQuery{
				Type: []string{"ssh", "http"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE type IN (?, ?)",
			wantArgs:  []any{"ssh", "http"},
		},
		{
			name: "with event filter",
			query: EventQuery{
				Event: []string{"login", "!logout"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE event IN (?) AND event NOT IN (?)",
			wantArgs:  []any{"login", "logout"},
		},
		{
			name: "with custom columns",
			query: EventQuery{
				Columns: []string{"id", "time", "type"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type FROM honeypot_events",
			wantArgs:  []any{},
		},
		{
			name: "with invalid columns filtered out",
			query: EventQuery{
				Columns: []string{"id", "time", "invalid_col", "type"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type FROM honeypot_events",
			wantArgs:  []any{},
		},
		{
			name: "with all valid columns",
			query: EventQuery{
				Columns: []string{"id", "time", "type", "event", "remote_addr", "remote_port", "dst_port", "fields"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",
			wantArgs:  []any{},
		},
		{
			name: "with JSON field filter",
			query: EventQuery{
				FieldFilters: map[string][]string{"username": {"admin"}},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?)",
			wantArgs:  []any{"$.username", "admin"},
		},
		{
			name: "with JSON field filter multiple values",
			query: EventQuery{
				FieldFilters: map[string][]string{"username": {"admin", "user", "guest"}},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?, ?, ?)",
			wantArgs:  []any{"$.username", "admin", "user", "guest"},
		},
		{
			name: "with JSON field filter negation",
			query: EventQuery{
				FieldFilters: map[string][]string{"username": {"admin", "!user"}},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?) AND json_extract_string(fields, ?) NOT IN (?)",
			wantArgs:  []any{"$.username", "admin", "$.username", "user"},
		},
		{
			name: "with multiple JSON field filters",
			query: EventQuery{
				FieldFilters: map[string][]string{"username": {"admin"}, "status": {"active"}},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?) AND json_extract_string(fields, ?) IN (?)",
			wantArgs:  []any{"$.username", "admin", "$.status", "active"},
		},
		{
			name: "with field exists",
			query: EventQuery{
				FieldExists: []string{"username", "password"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract(fields, ?) IS NOT NULL AND json_extract(fields, ?) IS NOT NULL",
			wantArgs:  []any{"$.username", "$.password"},
		},
		{
			name: "with order direction",
			query: EventQuery{
				OrderDirection: "ASC",
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events ORDER BY time ASC",
			wantArgs:  []any{},
		},
		{
			name: "with limit",
			query: EventQuery{
				Limit: 10,
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events LIMIT 10",
			wantArgs:  []any{},
		},
		{
			name: "with offset",
			query: EventQuery{
				Offset: 20,
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events OFFSET 20",
			wantArgs:  []any{},
		},
		{
			name: "with limit and offset",
			query: EventQuery{
				Limit:  10,
				Offset: 20,
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events LIMIT 10 OFFSET 20",
			wantArgs:  []any{},
		},
		{
			name: "complex query with all filters",
			query: EventQuery{
				IDs:            []string{"1", "2"},
				TimeStart:      time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
				TimeEnd:        time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
				RemoteAddrs:    []string{"192.168.1.1", "!10.0.0.1"},
				DstPorts:       []string{"80", "443"},
				Type:           []string{"http"},
				Event:          []string{"request"},
				FieldFilters:   map[string][]string{"username": {"admin"}},
				FieldExists:    []string{"session_id"},
				Columns:        []string{"id", "time", "type"},
				OrderDirection: "DESC",
				Limit:          50,
				Offset:         100,
			},
			total:     false,
			wantQuery: "SELECT id, time, type FROM honeypot_events WHERE time >= ? AND time <= ? AND id IN (?, ?) AND (remote_ip_int = ?) AND (remote_ip_int != ?) AND (dst_port = ? OR dst_port = ?) AND type IN (?) AND event IN (?) AND json_extract_string(fields, ?) IN (?) AND json_extract(fields, ?) IS NOT NULL ORDER BY time DESC LIMIT 50 OFFSET 100",
			wantArgs:  []any{"2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z", "1", "2", uint32(3232235777), uint32(167772161), 80, 443, "http", "request", "$.username", "admin", "$.session_id"},
		},
		{
			name: "complex query total",
			query: EventQuery{
				IDs:            []string{"1", "2"},
				TimeStart:      time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
				TimeEnd:        time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
				RemoteAddrs:    []string{"192.168.1.1"},
				DstPorts:       []string{"80"},
				Type:           []string{"http"},
				Event:          []string{"request"},
				FieldFilters:   map[string][]string{"username": {"admin"}},
				FieldExists:    []string{"session_id"},
				OrderDirection: "DESC",
				Limit:          50,
				Offset:         100,
			},
			total:     true,
			wantQuery: "SELECT COUNT(*) FROM honeypot_events WHERE time >= ? AND time <= ? AND id IN (?, ?) AND (remote_ip_int = ?) AND (dst_port = ?) AND type IN (?) AND event IN (?) AND json_extract_string(fields, ?) IN (?) AND json_extract(fields, ?) IS NOT NULL",
			wantArgs:  []any{"2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z", "1", "2", uint32(3232235777), 80, "http", "request", "$.username", "admin", "$.session_id"},
		},
		{
			name: "empty arrays don't add clauses",
			query: EventQuery{
				IDs:         []string{},
				RemoteAddrs: []string{},
				DstPorts:    []string{},
				Type:        []string{},
				Event:       []string{},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",
			wantArgs:  []any{},
		},
		{
			name: "single negation value",
			query: EventQuery{
				RemoteAddrs: []string{"!192.168.1.1"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_ip_int != ?)",
			wantArgs:  []any{uint32(3232235777)},
		},
		{
			name: "negation with single character ignored",
			query: EventQuery{
				RemoteAddrs: []string{"!"},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_addr = ?)",
			wantArgs:  []any{"!"},
		},
		{
			name: "zero limit and offset ignored",
			query: EventQuery{
				Limit:  0,
				Offset: 0,
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",
			wantArgs:  []any{},
		},
		{
			name: "domain with wildcards",
			query: EventQuery{
				Domains: []string{"*.censys-scanner.com", "google.*", "!*.evil.com"},
			},
			total:     false,
			wantQuery: "SELECT honeypot_events.id, honeypot_events.time, honeypot_events.type, honeypot_events.event, honeypot_events.remote_addr, honeypot_events.remote_port, honeypot_events.dst_port, honeypot_events.fields, ips.country, ips.city, ips.latitude, ips.longitude FROM honeypot_events JOIN ips ON honeypot_events.remote_addr = ips.ip WHERE (ips.domain GLOB ? OR ips.domain GLOB ?) AND ips.domain NOT GLOB ?",
			wantArgs:  []any{"*.censys-scanner.com", "google.*", "*.evil.com"},
		},
		{
			name: "with JSON field wildcard",
			query: EventQuery{
				FieldFilters: map[string][]string{"uri": {"/v1/*"}},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) GLOB ?",
			wantArgs:  []any{"$.uri", "/v1/*"},
		},
		{
			name: "with complex JSON field filters",
			query: EventQuery{
				FieldFilters: map[string][]string{"uri": {"/v1/*", "/v2/*", "!/v1/private/*"}},
			},
			total:     false,
			wantQuery: "SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (json_extract_string(fields, ?) GLOB ? OR json_extract_string(fields, ?) GLOB ?) AND json_extract_string(fields, ?) NOT GLOB ?",
			wantArgs:  []any{"$.uri", "/v1/*", "$.uri", "/v2/*", "$.uri", "/v1/private/*"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotQuery, gotArgs := buildQueryString(tt.query, tt.total)
			assertEqualSQL(t, gotQuery, gotArgs, tt.wantQuery, tt.wantArgs)
		})
	}
}

func TestParseEventQuery(t *testing.T) {
	db := &Database{}
	tests := []struct {
		name string // description of this test case
		// Named input parameters for target function.
		query   url.Values
		want    EventQuery
		wantErr bool
	}{
		{
			name:  "empty query",
			query: url.Values{},
			want: EventQuery{
				Limit:          100,
				Offset:         0,
				OrderDirection: "desc",
				IDs:            nil,
				RemoteAddrs:    nil,
				RemotePorts:    nil,
				DstPorts:       nil,
				Event:          nil,
				Type:           nil,
				FieldFilters:   map[string][]string{},
				FieldExists:    []string{},
				Columns:        nil,
			},
		},
		{
			name: "basic parameters",
			query: url.Values{
				"limit":           []string{"50"},
				"offset":          []string{"10"},
				"order_direction": []string{"asc"},
			},
			want: EventQuery{
				Limit:          50,
				Offset:         10,
				OrderDirection: "asc",
				IDs:            nil,
				RemoteAddrs:    nil,
				RemotePorts:    nil,
				DstPorts:       nil,
				Event:          nil,
				Type:           nil,
				FieldFilters:   map[string][]string{},
				FieldExists:    []string{},
				Columns:        nil,
			},
		},
		{
			name: "csv parameters",
			query: url.Values{
				"id":          []string{"1", "2", "3"},
				"remote_addr": []string{"1.1.1.1", "2.2.2.2"},
				"dst_port":    []string{"80", "443"},
				"type":        []string{"http", "ssh"},
				"event":       []string{"login", "logout"},
			},
			want: EventQuery{
				Limit:          100,
				Offset:         0,
				OrderDirection: "desc",
				IDs:            []string{"1", "2", "3"},
				RemoteAddrs:    []string{"1.1.1.1", "2.2.2.2"},
				RemotePorts:    nil,
				DstPorts:       []string{"80", "443"},
				Event:          []string{"login", "logout"},
				Type:           []string{"http", "ssh"},
				FieldFilters:   map[string][]string{},
				FieldExists:    []string{},
				Columns:        nil,
			},
		},
		{
			name: "time parameters",
			query: url.Values{
				"time_start": []string{"2024-01-01 00:00:00"},
				"time_end":   []string{"2024-01-01 23:59:59"},
			},
			want: EventQuery{
				Limit:          100,
				Offset:         0,
				OrderDirection: "desc",
				IDs:            nil,
				RemoteAddrs:    nil,
				RemotePorts:    nil,
				DstPorts:       nil,
				Event:          nil,
				Type:           nil,
				FieldFilters:   map[string][]string{},
				FieldExists:    []string{},
				Columns:        nil,
				TimeStart:      time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
				TimeEnd:        time.Date(2024, 1, 1, 23, 59, 59, 0, time.UTC),
			},
		},
		{
			name: "field parameters",
			query: url.Values{
				"f:user":     []string{"admin"},
				"f:pass":     []string{"1234"},
				"fe:session": []string{""},
			},
			want: EventQuery{
				Limit:          100,
				Offset:         0,
				OrderDirection: "desc",
				IDs:            nil,
				RemoteAddrs:    nil,
				RemotePorts:    nil,
				DstPorts:       nil,
				Event:          nil,
				Type:           nil,
				FieldFilters:   map[string][]string{"user": {"admin"}, "pass": {"1234"}},
				FieldExists:    []string{"session"},
				Columns:        nil,
			},
		},
		{
			name: "columns parameter",
			query: url.Values{
				"columns": []string{"id", "time", "type"},
			},
			want: EventQuery{
				Limit:          100,
				Offset:         0,
				OrderDirection: "desc",
				IDs:            nil,
				RemoteAddrs:    nil,
				RemotePorts:    nil,
				DstPorts:       nil,
				Event:          nil,
				Type:           nil,
				FieldFilters:   map[string][]string{},
				FieldExists:    []string{},
				Columns:        []string{"id", "time", "type"},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, gotErr := db.parseEventQuery(tt.query)
			if (gotErr != nil) != tt.wantErr {
				t.Errorf("parseEventQuery() error = %v, wantErr %v", gotErr, tt.wantErr)
				return
			}
			if tt.wantErr {
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("parseEventQuery() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_addPortClauses(t *testing.T) {
	tests := []struct {
		name        string
		values      []string
		wantClauses []string
		wantArgs    []any
	}{
		{
			name:        "empty values",
			values:      []string{},
			wantClauses: nil,
			wantArgs:    nil,
		},
		{
			name:        "single port",
			values:      []string{"80"},
			wantClauses: []string{"(dst_port = ?)"},
			wantArgs:    []any{80},
		},
		{
			name:        "multiple ports",
			values:      []string{"80", "443"},
			wantClauses: []string{"(dst_port = ? OR dst_port = ?)"},
			wantArgs:    []any{80, 443},
		},
		{
			name:        "port range",
			values:      []string{"8000-8080"},
			wantClauses: []string{"(dst_port BETWEEN ? AND ?)"},
			wantArgs:    []any{8000, 8080},
		},
		{
			name:        "negated port",
			values:      []string{"!22"},
			wantClauses: []string{"(dst_port != ?)"},
			wantArgs:    []any{22},
		},
		{
			name:        "negated range",
			values:      []string{"!1024-2048"},
			wantClauses: []string{"(dst_port NOT BETWEEN ? AND ?)"},
			wantArgs:    []any{1024, 2048},
		},
		{
			name:        "mixed include and exclude",
			values:      []string{"!80", "443"},
			wantClauses: []string{"(dst_port = ?)", "(dst_port != ?)"},
			wantArgs:    []any{443, 80},
		},
		{
			name:        "complex mixed",
			values:      []string{"80", "443-445", "!22", "!3000-3001"},
			wantClauses: []string{"(dst_port = ? OR dst_port BETWEEN ? AND ?)", "(dst_port != ? AND dst_port NOT BETWEEN ? AND ?)"},
			wantArgs:    []any{80, 443, 445, 22, 3000, 3001},
		},
		{
			name:        "invalid port ignored",
			values:      []string{"abc", "80"},
			wantClauses: []string{"(dst_port = ?)"},
			wantArgs:    []any{80},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var gotClauses []string
			var gotArgs []any
			addPortClauses(&gotClauses, &gotArgs, tt.values, "dst_port")
			if !reflect.DeepEqual(gotClauses, tt.wantClauses) {
				t.Errorf("addPortClauses() clauses = %v, want %v", gotClauses, tt.wantClauses)
			}
			if !reflect.DeepEqual(gotArgs, tt.wantArgs) {
				t.Errorf("addPortClauses() args = %v, want %v", gotArgs, tt.wantArgs)
			}
		})
	}
}

type sqlClause struct {
	query string
	args  []any
}

func parseSQL(query string, args []any) (base string, clauses []sqlClause, tail string) {
	query = strings.Join(strings.Fields(query), " ")

	whereIdx := strings.Index(query, " WHERE ")
	if whereIdx == -1 {
		// No WHERE, but might have ORDER BY etc.
		tailIdx := -1
		for _, kw := range []string{" ORDER BY ", " LIMIT ", " OFFSET "} {
			idx := strings.Index(query, kw)
			if idx != -1 && (tailIdx == -1 || idx < tailIdx) {
				tailIdx = idx
			}
		}
		if tailIdx == -1 {
			return query, nil, ""
		}
		return query[:tailIdx], nil, query[tailIdx:]
	}

	base = query[:whereIdx]
	afterWhere := query[whereIdx+7:] // len(" WHERE ")

	tailIdx := -1
	for _, kw := range []string{" ORDER BY ", " LIMIT ", " OFFSET "} {
		idx := strings.Index(afterWhere, kw)
		if idx != -1 && (tailIdx == -1 || idx < tailIdx) {
			tailIdx = idx
		}
	}

	var wherePart string
	if tailIdx != -1 {
		wherePart = afterWhere[:tailIdx]
		tail = afterWhere[tailIdx:]
	} else {
		wherePart = afterWhere
	}

	// Split wherePart into clauses
	rawClauses := splitWhereClauses(wherePart)

	argIdx := 0
	for _, rc := range rawClauses {
		placeholders := strings.Count(rc, "?")
		if argIdx+placeholders > len(args) {
			clauses = append(clauses, sqlClause{query: rc, args: nil})
			continue
		}
		clauses = append(clauses, sqlClause{query: rc, args: args[argIdx : argIdx+placeholders]})
		argIdx += placeholders
	}

	return base, clauses, tail
}

func splitWhereClauses(wherePart string) []string {
	var clauses []string
	var current strings.Builder
	parenLevel := 0

	chars := []rune(wherePart)
	for i := 0; i < len(chars); i++ {
		c := chars[i]
		if c == '(' {
			parenLevel++
			current.WriteRune(c)
		} else if c == ')' {
			parenLevel--
			current.WriteRune(c)
		} else if parenLevel == 0 && i+5 <= len(chars) && string(chars[i:i+5]) == " AND " {
			clauses = append(clauses, strings.TrimSpace(current.String()))
			current.Reset()
			i += 4 // Skip " AND"
		} else {
			current.WriteRune(c)
		}
	}
	if current.Len() > 0 {
		clauses = append(clauses, strings.TrimSpace(current.String()))
	}
	return clauses
}

func assertEqualSQL(t *testing.T, gotQuery string, gotArgs []any, wantQuery string, wantArgs []any) {
	t.Helper()

	gotBase, gotClauses, gotTail := parseSQL(gotQuery, gotArgs)
	wantBase, wantClauses, wantTail := parseSQL(wantQuery, wantArgs)

	if gotBase != wantBase {
		t.Errorf("SQL base mismatch:\ngot:  %s\nwant: %s", gotBase, wantBase)
		return
	}
	if gotTail != wantTail {
		t.Errorf("SQL tail mismatch:\ngot:  %s\nwant: %s", gotTail, wantTail)
		return
	}

	if len(gotClauses) != len(wantClauses) {
		t.Errorf("SQL WHERE clauses count mismatch: got %d, want %d\ngot query: %s\nwant query: %s", len(gotClauses), len(wantClauses), gotQuery, wantQuery)
		return
	}

	// Match clauses
	visited := make([]bool, len(gotClauses))
	for _, wantC := range wantClauses {
		found := false
		for i, gotC := range gotClauses {
			if !visited[i] && gotC.query == wantC.query && reflect.DeepEqual(gotC.args, wantC.args) {
				visited[i] = true
				found = true
				break
			}
		}
		if !found {
			t.Errorf("Missing expected WHERE clause: %s with args %v\nGot query: %s\nGot args: %v", wantC.query, wantC.args, gotQuery, gotArgs)
			return
		}
	}
}