feat(tests): add comprehensive test suite, update deps and Go 1.26.1

- Add 20+ tests for utility functions, SharedData, and Model
- Tests cover: LoadTables, LoadTableData, UpdateCell, pagination,
  table inference, focus/blur, empty database, invalid indices
- Update Go to 1.26.1, upgrade all dependencies
- Replace custom Min/Max with Go builtin min/max
- Format all files with goimports
- Add staticcheck to CI workflow
This commit is contained in:
2026-03-13 02:46:03 +00:00
parent 7913b17d74
commit aa4c97c553
15 changed files with 581 additions and 169 deletions

View File

@@ -14,3 +14,6 @@ jobs:
- run: go test -race ./... - run: go test -race ./...
- run: go vet ./... - run: go vet ./...
- run: go build ./... - run: go build ./...
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- run: staticcheck ./...

37
go.mod
View File

@@ -1,32 +1,30 @@
module github.com/taigrr/teaqlite module github.com/taigrr/teaqlite
go 1.26.0 go 1.26.1
require ( require (
github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/fang v0.4.4 github.com/charmbracelet/fang v1.0.0
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
modernc.org/sqlite v1.46.1 modernc.org/sqlite v1.46.1
) )
require ( require (
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect charm.land/lipgloss/v2 v2.0.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250711012602-b1f986320f7e // indirect github.com/charmbracelet/x/exp/charmtone v0.0.0-20260311145557-c83711a11ffa // indirect
github.com/charmbracelet/x/exp/color v0.0.0-20250711012602-b1f986320f7e // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
@@ -34,24 +32,25 @@ require (
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-cobra v1.2.0 // indirect github.com/muesli/mango-cobra v1.3.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/mango-pflag v0.2.0 // indirect
github.com/muesli/roff v0.1.0 // indirect github.com/muesli/roff v0.1.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.27.0 // indirect golang.org/x/text v0.35.0 // indirect
modernc.org/libc v1.67.6 // indirect golang.org/x/tools v0.43.0 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )

91
go.sum
View File

@@ -1,33 +1,31 @@
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k= charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY= github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ=
github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ= github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 h1:J8v4kWJYCaxv1SLhLunN74S+jMteZ1f7Dae99ioq4Bo=
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0= github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188/go.mod h1:FzWNAbe1jEmI+GZljSnlaSA8wJjnNIZhWBLkTsAl6eg=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250711012602-b1f986320f7e h1:sc41kBOnun1OX15Lg05ZB6Ly6AFWnntCYb8jsEDBAPs= github.com/charmbracelet/x/exp/charmtone v0.0.0-20260311145557-c83711a11ffa h1:/hY9CTFQJJ7G5Hu0MFAZTUXV/JO8H8FOIdWKvRA+tTw=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250711012602-b1f986320f7e/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= github.com/charmbracelet/x/exp/charmtone v0.0.0-20260311145557-c83711a11ffa/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY=
github.com/charmbracelet/x/exp/color v0.0.0-20250711012602-b1f986320f7e h1:D0tltuLCSvxMznOpQg7f3MArp8ImU0zALbakI47ffkw=
github.com/charmbracelet/x/exp/color v0.0.0-20250711012602-b1f986320f7e/go.mod h1:hk/GyTELmEgX54pBAOHcFvH8Xed53JWo/g8kJXFo/PI=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@@ -36,12 +34,10 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -63,18 +59,18 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
@@ -90,44 +86,45 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=

View File

@@ -522,20 +522,6 @@ func WrapText(text string, width int) []string {
return lines return lines
} }
func Min(a, b int) int {
if a < b {
return a
}
return b
}
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func InitialModel(db *sql.DB, opts ...Option) *Model { func InitialModel(db *sql.DB, opts ...Option) *Model {
shared := NewSharedData(db) shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil { if err := shared.LoadTables(); err != nil {

428
internal/app/app_test.go Normal file
View File

@@ -0,0 +1,428 @@
package app
import (
"database/sql"
"testing"
_ "modernc.org/sqlite"
)
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
want string
}{
{"short string", "hello", 10, "hello"},
{"exact length", "hello", 5, "hello"},
{"needs truncation", "hello world", 8, "hello..."},
{"empty string", "", 5, ""},
{"min truncation", "abcdef", 4, "a..."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := TruncateString(tt.input, tt.maxLen)
if got != tt.want {
t.Errorf("TruncateString(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
}
})
}
}
func TestWrapText(t *testing.T) {
tests := []struct {
name string
text string
width int
want []string
}{
{"short text", "hello", 20, []string{"hello"}},
{"wrap at word boundary", "hello world foo", 12, []string{"hello world", "foo"}},
{"zero width", "hello", 0, []string{"hello"}},
{"empty text", "", 10, []string{""}},
{"single long word", "abcdefghij", 5, []string{"abcde", "fghij"}},
{"multiple words wrapping", "one two three four", 10, []string{"one two", "three four"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := WrapText(tt.text, tt.width)
if len(got) != len(tt.want) {
t.Errorf("WrapText(%q, %d) returned %d lines, want %d: %v", tt.text, tt.width, len(got), len(tt.want), got)
return
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("WrapText(%q, %d)[%d] = %q, want %q", tt.text, tt.width, i, got[i], tt.want[i])
}
}
})
}
}
func TestNextID(t *testing.T) {
// nextID should return monotonically increasing values
id1 := nextID()
id2 := nextID()
id3 := nextID()
if id2 <= id1 {
t.Errorf("nextID() not monotonically increasing: %d, %d", id1, id2)
}
if id3 <= id2 {
t.Errorf("nextID() not monotonically increasing: %d, %d", id2, id3)
}
}
func TestDefaultAppKeyMap(t *testing.T) {
km := DefaultAppKeyMap()
if len(km.Quit.Keys()) == 0 {
t.Error("Quit keybinding has no keys")
}
if len(km.Suspend.Keys()) == 0 {
t.Error("Suspend keybinding has no keys")
}
if len(km.ToggleHelp.Keys()) == 0 {
t.Error("ToggleHelp keybinding has no keys")
}
}
// createTestDB creates an in-memory SQLite database with test data
func createTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("failed to open test database: %v", err)
}
_, err = db.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
);
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
INSERT INTO users (name, email) VALUES ('Charlie', 'charlie@example.com');
CREATE TABLE products (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
price REAL
);
INSERT INTO products (title, price) VALUES ('Widget', 9.99);
INSERT INTO products (title, price) VALUES ('Gadget', 19.99);
`)
if err != nil {
t.Fatalf("failed to create test data: %v", err)
}
return db
}
func TestNewSharedData(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if shared.DB != db {
t.Error("NewSharedData should store the database reference")
}
if shared.Width != 80 || shared.Height != 24 {
t.Errorf("default dimensions should be 80x24, got %dx%d", shared.Width, shared.Height)
}
}
func TestSharedDataLoadTables(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
if len(shared.Tables) != 2 {
t.Fatalf("expected 2 tables, got %d: %v", len(shared.Tables), shared.Tables)
}
// Tables should be sorted alphabetically
if shared.Tables[0] != "products" || shared.Tables[1] != "users" {
t.Errorf("expected [products, users], got %v", shared.Tables)
}
// FilteredTables should be a copy of Tables
if len(shared.FilteredTables) != len(shared.Tables) {
t.Errorf("FilteredTables length mismatch: %d vs %d", len(shared.FilteredTables), len(shared.Tables))
}
}
func TestSharedDataLoadTableData(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
// Load "users" table (index 1 since sorted alphabetically)
shared.SelectedTable = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() failed: %v", err)
}
if len(shared.Columns) != 3 {
t.Fatalf("expected 3 columns, got %d: %v", len(shared.Columns), shared.Columns)
}
if shared.Columns[0] != "id" || shared.Columns[1] != "name" || shared.Columns[2] != "email" {
t.Errorf("unexpected columns: %v", shared.Columns)
}
if shared.TotalRows != 3 {
t.Errorf("expected 3 total rows, got %d", shared.TotalRows)
}
if len(shared.TableData) != 3 {
t.Fatalf("expected 3 data rows, got %d", len(shared.TableData))
}
// Check primary keys detected
if len(shared.PrimaryKeys) != 1 || shared.PrimaryKeys[0] != "id" {
t.Errorf("expected primary key [id], got %v", shared.PrimaryKeys)
}
// Should not be marked as query result
if shared.IsQueryResult {
t.Error("regular table load should not be marked as query result")
}
}
func TestSharedDataLoadTableDataInvalidIndex(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
shared.SelectedTable = 99
if err := shared.LoadTableData(); err == nil {
t.Error("LoadTableData() should fail with invalid table index")
}
}
func TestSharedDataUpdateCell(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
// Load users table
shared.SelectedTable = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() failed: %v", err)
}
// Update name of first user (row 0, col 1 = name)
if err := shared.UpdateCell(0, 1, "Alicia"); err != nil {
t.Fatalf("UpdateCell() failed: %v", err)
}
// Verify local data updated
if shared.FilteredData[0][1] != "Alicia" {
t.Errorf("FilteredData not updated, got %q", shared.FilteredData[0][1])
}
// Verify database updated
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = 1").Scan(&name)
if err != nil {
t.Fatalf("failed to verify update: %v", err)
}
if name != "Alicia" {
t.Errorf("database not updated, got %q", name)
}
}
func TestSharedDataUpdateCellInvalidIndex(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
shared.SelectedTable = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() failed: %v", err)
}
if err := shared.UpdateCell(99, 0, "value"); err == nil {
t.Error("UpdateCell() should fail with invalid row index")
}
if err := shared.UpdateCell(0, 99, "value"); err == nil {
t.Error("UpdateCell() should fail with invalid column index")
}
}
func TestSharedDataPagination(t *testing.T) {
db := createTestDB(t)
defer db.Close()
// Insert enough rows to test pagination
for i := 0; i < 25; i++ {
_, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)",
"User"+string(rune('A'+i)), "user@example.com")
if err != nil {
t.Fatalf("failed to insert test data: %v", err)
}
}
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
shared.SelectedTable = 1
shared.CurrentPage = 0
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() page 0 failed: %v", err)
}
if shared.TotalRows != 28 { // 3 original + 25 new
t.Errorf("expected 28 total rows, got %d", shared.TotalRows)
}
if len(shared.TableData) != PageSize {
t.Errorf("page 0 should have %d rows, got %d", PageSize, len(shared.TableData))
}
// Load page 2
shared.CurrentPage = 1
if err := shared.LoadTableData(); err != nil {
t.Fatalf("LoadTableData() page 1 failed: %v", err)
}
if len(shared.TableData) != 8 { // 28 - 20 = 8
t.Errorf("page 1 should have 8 rows, got %d", len(shared.TableData))
}
}
func TestInitialModel(t *testing.T) {
db := createTestDB(t)
defer db.Close()
m := InitialModel(db)
if m.Err() != nil {
t.Fatalf("InitialModel() returned error: %v", m.Err())
}
if m.width != 80 || m.height != 24 {
t.Errorf("default dimensions should be 80x24, got %dx%d", m.width, m.height)
}
if !m.Focused() {
t.Error("model should be focused by default")
}
}
func TestInitialModelWithOptions(t *testing.T) {
db := createTestDB(t)
defer db.Close()
km := DefaultAppKeyMap()
m := InitialModel(db, WithKeyMap(km), WithDimensions(120, 40))
if m.Err() != nil {
t.Fatalf("InitialModel() returned error: %v", m.Err())
}
if m.width != 120 || m.height != 40 {
t.Errorf("custom dimensions should be 120x40, got %dx%d", m.width, m.height)
}
}
func TestModelFocusBlur(t *testing.T) {
db := createTestDB(t)
defer db.Close()
m := InitialModel(db)
if !m.Focused() {
t.Error("model should be focused initially")
}
m.Blur()
if m.Focused() {
t.Error("model should not be focused after Blur()")
}
m.Focus()
if !m.Focused() {
t.Error("model should be focused after Focus()")
}
}
func TestInferTableFromQueryResult(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() failed: %v", err)
}
// Set up columns matching the users table
shared.Columns = []string{"id", "name", "email"}
shared.IsQueryResult = true
shared.FilteredData = [][]string{{"1", "Alice", "alice@example.com"}}
tableName, err := shared.inferTableFromQueryResult(0, 0)
if err != nil {
t.Fatalf("inferTableFromQueryResult() failed: %v", err)
}
if tableName != "users" {
t.Errorf("expected 'users', got %q", tableName)
}
// Verify it was cached
if shared.QueryTableName != "users" {
t.Errorf("QueryTableName should be cached as 'users', got %q", shared.QueryTableName)
}
}
func TestGetTableInfo(t *testing.T) {
db := createTestDB(t)
defer db.Close()
shared := NewSharedData(db)
cols, pks, err := shared.getTableInfo("users")
if err != nil {
t.Fatalf("getTableInfo() failed: %v", err)
}
if len(cols) != 3 {
t.Errorf("expected 3 columns, got %d", len(cols))
}
if len(pks) != 1 || pks[0] != "id" {
t.Errorf("expected primary key [id], got %v", pks)
}
}
func TestSharedDataEmptyDatabase(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("failed to open test database: %v", err)
}
defer db.Close()
shared := NewSharedData(db)
if err := shared.LoadTables(); err != nil {
t.Fatalf("LoadTables() on empty db failed: %v", err)
}
if len(shared.Tables) != 0 {
t.Errorf("expected 0 tables in empty db, got %d", len(shared.Tables))
}
}

View File

@@ -4,10 +4,10 @@ import (
"fmt" "fmt"
"time" "time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
) )
type EditCellModel struct { type EditCellModel struct {

View File

@@ -4,17 +4,17 @@ import "github.com/charmbracelet/bubbles/key"
// EditCellKeyMap defines keybindings for the edit cell view // EditCellKeyMap defines keybindings for the edit cell view
type EditCellKeyMap struct { type EditCellKeyMap struct {
Save key.Binding Save key.Binding
Cancel key.Binding Cancel key.Binding
CursorLeft key.Binding CursorLeft key.Binding
CursorRight key.Binding CursorRight key.Binding
WordLeft key.Binding WordLeft key.Binding
WordRight key.Binding WordRight key.Binding
LineStart key.Binding LineStart key.Binding
LineEnd key.Binding LineEnd key.Binding
DeleteWord key.Binding DeleteWord key.Binding
DeleteChar key.Binding DeleteChar key.Binding
ToggleHelp key.Binding ToggleHelp key.Binding
} }
// DefaultEditCellKeyMap returns the default keybindings for edit cell // DefaultEditCellKeyMap returns the default keybindings for edit cell

View File

@@ -418,7 +418,7 @@ func (m *QueryModel) View() string {
content.WriteString("\n") content.WriteString("\n")
// Data rows with scrolling // Data rows with scrolling
visibleCount := Max(1, m.Shared.Height-10) visibleCount := max(1, m.Shared.Height-10)
startIdx := 0 startIdx := 0
// Adjust start index if selected row is out of view // Adjust start index if selected row is out of view
@@ -426,7 +426,7 @@ func (m *QueryModel) View() string {
startIdx = m.selectedRow - visibleCount + 1 startIdx = m.selectedRow - visibleCount + 1
} }
endIdx := Min(len(m.results), startIdx+visibleCount) endIdx := min(len(m.results), startIdx+visibleCount)
for i := range endIdx { for i := range endIdx {
if i < startIdx { if i < startIdx {
@@ -465,4 +465,3 @@ func (m *QueryModel) View() string {
return content.String() return content.String()
} }

View File

@@ -8,25 +8,25 @@ import "github.com/charmbracelet/bubbles/key"
// - G: go to end (single 'G' press) // - G: go to end (single 'G' press)
type QueryKeyMap struct { type QueryKeyMap struct {
// Input mode keys // Input mode keys
Execute key.Binding Execute key.Binding
Escape key.Binding Escape key.Binding
CursorLeft key.Binding CursorLeft key.Binding
CursorRight key.Binding CursorRight key.Binding
WordLeft key.Binding WordLeft key.Binding
WordRight key.Binding WordRight key.Binding
LineStart key.Binding LineStart key.Binding
LineEnd key.Binding LineEnd key.Binding
DeleteWord key.Binding DeleteWord key.Binding
// Results mode keys // Results mode keys
Up key.Binding Up key.Binding
Down key.Binding Down key.Binding
Enter key.Binding Enter key.Binding
EditQuery key.Binding EditQuery key.Binding
GoToStart key.Binding GoToStart key.Binding
GoToEnd key.Binding GoToEnd key.Binding
Back key.Binding Back key.Binding
ToggleHelp key.Binding ToggleHelp key.Binding
} }
// DefaultQueryKeyMap returns the default keybindings for query view // DefaultQueryKeyMap returns the default keybindings for query view

View File

@@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"strings" "strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
) )
type RowDetailModel struct { type RowDetailModel struct {

View File

@@ -5,10 +5,10 @@ import (
"sort" "sort"
"strings" "strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
) )
type TableDataModel struct { type TableDataModel struct {
@@ -409,7 +409,7 @@ func (m *TableDataModel) View() string {
content.WriteString("\n") content.WriteString("\n")
// Show data rows with scrolling within current page // Show data rows with scrolling within current page
visibleCount := Max(1, m.Shared.Height-10) visibleCount := max(1, m.Shared.Height-10)
totalRows := len(m.Shared.FilteredData) totalRows := len(m.Shared.FilteredData)
startIdx := 0 startIdx := 0
@@ -420,7 +420,7 @@ func (m *TableDataModel) View() string {
startIdx = min(startIdx, totalRows-visibleCount) startIdx = min(startIdx, totalRows-visibleCount)
} }
endIdx := Min(totalRows, startIdx+visibleCount) endIdx := min(totalRows, startIdx+visibleCount)
for i := startIdx; i < endIdx; i++ { for i := startIdx; i < endIdx; i++ {
row := m.Shared.FilteredData[i] row := m.Shared.FilteredData[i]

View File

@@ -5,10 +5,10 @@ import (
"sort" "sort"
"strings" "strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
) )
type TableListModel struct { type TableListModel struct {
@@ -356,7 +356,7 @@ func (m *TableListModel) getVisibleCount() int {
if m.searching { if m.searching {
reservedLines += 2 reservedLines += 2
} }
return Max(1, m.Shared.Height-reservedLines) return max(1, m.Shared.Height-reservedLines)
} }
func (m *TableListModel) adjustPage() { func (m *TableListModel) adjustPage() {
@@ -389,7 +389,7 @@ func (m *TableListModel) View() string {
} else { } else {
visibleCount := m.getVisibleCount() visibleCount := m.getVisibleCount()
startIdx := m.currentPage * visibleCount startIdx := m.currentPage * visibleCount
endIdx := Min(startIdx+visibleCount, len(m.Shared.FilteredTables)) endIdx := min(startIdx+visibleCount, len(m.Shared.FilteredTables))
for i := startIdx; i < endIdx; i++ { for i := startIdx; i < endIdx; i++ {
table := m.Shared.FilteredTables[i] table := m.Shared.FilteredTables[i]