Skip to content

Commit b28b494

Browse files
authored
Merge pull request #195 from MattiasMTS/ms/integration-test-sqlite
test: integration tests for `sqlite`
2 parents 6effc08 + 771e150 commit b28b494

File tree

5 files changed

+309
-27
lines changed

5 files changed

+309
-27
lines changed

dbee/adapters/sqlite.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ func (s *SQLite) Connect(url string) (core.Driver, error) {
5151
}
5252

5353
return &sqliteDriver{
54-
c: builders.NewClient(db),
54+
c: builders.NewClient(db),
55+
currentDatabase: path,
5556
}, nil
5657
}
5758

dbee/adapters/sqlite_driver.go

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,52 @@ import (
77
"github.com/kndndrj/nvim-dbee/dbee/core/builders"
88
)
99

10-
var _ core.Driver = (*sqliteDriver)(nil)
10+
var (
11+
_ core.Driver = (*sqliteDriver)(nil)
12+
_ core.DatabaseSwitcher = (*sqliteDriver)(nil)
13+
)
1114

1215
type sqliteDriver struct {
13-
c *builders.Client
16+
c *builders.Client
17+
currentDatabase string
1418
}
1519

16-
func (c *sqliteDriver) Query(ctx context.Context, query string) (core.ResultStream, error) {
20+
func (d *sqliteDriver) Query(ctx context.Context, query string) (core.ResultStream, error) {
1721
// run query, fallback to affected rows
18-
return c.c.QueryUntilNotEmpty(ctx, query, "select changes() as 'Rows Affected'")
22+
return d.c.QueryUntilNotEmpty(ctx, query, "select changes() as 'Rows Affected'")
1923
}
2024

21-
func (c *sqliteDriver) Columns(opts *core.TableOptions) ([]*core.Column, error) {
22-
return c.c.ColumnsFromQuery("SELECT name, type FROM pragma_table_info('%s')", opts.Table)
25+
func (d *sqliteDriver) Columns(opts *core.TableOptions) ([]*core.Column, error) {
26+
return d.c.ColumnsFromQuery("SELECT name, type FROM pragma_table_info('%s')", opts.Table)
2327
}
2428

25-
func (c *sqliteDriver) Structure() ([]*core.Structure, error) {
26-
query := `SELECT name FROM sqlite_schema WHERE type ='table'`
29+
func (d *sqliteDriver) Structure() ([]*core.Structure, error) {
30+
// sqlite is single schema structure, so we hardcode the name of it.
31+
query := "SELECT 'sqlite_schema' as schema, name, type FROM sqlite_schema"
2732

28-
rows, err := c.Query(context.TODO(), query)
33+
rows, err := d.Query(context.Background(), query)
2934
if err != nil {
3035
return nil, err
3136
}
3237

33-
var schema []*core.Structure
34-
for rows.HasNext() {
35-
row, err := rows.Next()
36-
if err != nil {
37-
return nil, err
38+
decodeStructureType := func(typ string) core.StructureType {
39+
switch typ {
40+
case "table":
41+
return core.StructureTypeTable
42+
case "view":
43+
return core.StructureTypeView
44+
default:
45+
return core.StructureTypeNone
3846
}
39-
40-
// We know for a fact there is only one string field (see query above)
41-
table := row[0].(string)
42-
schema = append(schema, &core.Structure{
43-
Name: table,
44-
Schema: "",
45-
Type: core.StructureTypeTable,
46-
})
4747
}
48-
49-
return schema, nil
48+
return core.GetGenericStructure(rows, decodeStructureType)
5049
}
5150

52-
func (c *sqliteDriver) Close() {
53-
c.c.Close()
51+
func (d *sqliteDriver) Close() { d.c.Close() }
52+
53+
func (d *sqliteDriver) ListDatabases() (string, []string, error) {
54+
return d.currentDatabase, []string{"not supported yet"}, nil
5455
}
56+
57+
// SelectDatabase is a no-op, added to make the UI more pleasent.
58+
func (d *sqliteDriver) SelectDatabase(name string) error { return nil }
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package integration
2+
3+
import (
4+
"context"
5+
"log"
6+
"testing"
7+
8+
"github.com/kndndrj/nvim-dbee/dbee/core"
9+
th "github.com/kndndrj/nvim-dbee/dbee/tests/testhelpers"
10+
"github.com/stretchr/testify/assert"
11+
tsuite "github.com/stretchr/testify/suite"
12+
tc "github.com/testcontainers/testcontainers-go"
13+
)
14+
15+
// SQLiteTestSuite is the test suite for the sqlite adapter.
16+
type SQLiteTestSuite struct {
17+
tsuite.Suite
18+
ctr *th.SQLiteContainer
19+
ctx context.Context
20+
d *core.Connection
21+
}
22+
23+
func TestSQLiteTestSuite(t *testing.T) {
24+
tsuite.Run(t, new(SQLiteTestSuite))
25+
}
26+
27+
func (suite *SQLiteTestSuite) SetupSuite() {
28+
suite.ctx = context.Background()
29+
tempDir := suite.T().TempDir()
30+
31+
params := &core.ConnectionParams{ID: "test-sqlite", Name: "test-sqlite"}
32+
ctr, err := th.NewSQLiteContainer(suite.ctx, params, tempDir)
33+
if err != nil {
34+
log.Fatal(err)
35+
}
36+
37+
suite.ctr, suite.d = ctr, ctr.Driver
38+
}
39+
40+
func (suite *SQLiteTestSuite) TeardownSuite() {
41+
tc.CleanupContainer(suite.T(), suite.ctr)
42+
}
43+
44+
func (suite *SQLiteTestSuite) TestShouldErrorInvalidQuery() {
45+
t := suite.T()
46+
47+
want := "syntax error"
48+
49+
call := suite.d.Execute("invalid sql", func(cs core.CallState, c *core.Call) {
50+
if cs == core.CallStateExecutingFailed {
51+
assert.ErrorContains(t, c.Err(), want)
52+
}
53+
})
54+
assert.NotNil(t, call)
55+
}
56+
57+
func (suite *SQLiteTestSuite) TestShouldCancelQuery() {
58+
t := suite.T()
59+
want := []core.CallState{core.CallStateExecuting, core.CallStateCanceled}
60+
61+
_, got, err := th.GetResultWithCancel(t, suite.d, "SELECT 1")
62+
assert.NoError(t, err)
63+
64+
assert.Equal(t, want, got)
65+
}
66+
67+
func (suite *SQLiteTestSuite) TestShouldReturnManyRows() {
68+
t := suite.T()
69+
70+
wantStates := []core.CallState{
71+
core.CallStateExecuting, core.CallStateRetrieving, core.CallStateArchived,
72+
}
73+
wantCols := []string{"id", "username"}
74+
wantRows := []core.Row{
75+
{int64(1), "john_doe"},
76+
{int64(2), "jane_smith"},
77+
{int64(3), "bob_wilson"},
78+
}
79+
80+
query := "SELECT id, username FROM test_table"
81+
82+
gotRows, gotCols, gotStates, err := th.GetResult(t, suite.d, query)
83+
assert.NoError(t, err)
84+
85+
assert.ElementsMatch(t, wantCols, gotCols)
86+
assert.ElementsMatch(t, wantStates, gotStates)
87+
assert.Equal(t, wantRows, gotRows)
88+
}
89+
90+
func (suite *SQLiteTestSuite) TestShouldReturnOneRow() {
91+
t := suite.T()
92+
93+
wantStates := []core.CallState{
94+
core.CallStateExecuting, core.CallStateRetrieving, core.CallStateArchived,
95+
}
96+
wantCols := []string{"id", "username"}
97+
wantRows := []core.Row{{int64(2), "jane_smith"}}
98+
99+
query := "SELECT id, username FROM test_view"
100+
101+
gotRows, gotCols, gotStates, err := th.GetResult(t, suite.d, query)
102+
assert.NoError(t, err)
103+
104+
assert.ElementsMatch(t, wantCols, gotCols)
105+
assert.ElementsMatch(t, wantStates, gotStates)
106+
assert.Equal(t, wantRows, gotRows)
107+
}
108+
109+
func (suite *SQLiteTestSuite) TestShouldReturnStructure() {
110+
t := suite.T()
111+
112+
var (
113+
wantSchema = "sqlite_schema"
114+
wantSomeTable = "test_table"
115+
wantSomeView = "test_view"
116+
)
117+
118+
structure, err := suite.d.GetStructure()
119+
assert.NoError(t, err)
120+
121+
gotSchemas := th.GetSchemas(t, structure)
122+
assert.Contains(t, gotSchemas, wantSchema)
123+
124+
gotTables := th.GetModels(t, structure, core.StructureTypeTable)
125+
assert.Contains(t, gotTables, wantSomeTable)
126+
127+
gotViews := th.GetModels(t, structure, core.StructureTypeView)
128+
assert.Contains(t, gotViews, wantSomeView)
129+
}
130+
131+
func (suite *SQLiteTestSuite) TestShouldReturnColumns() {
132+
t := suite.T()
133+
134+
want := []*core.Column{
135+
{Name: "id", Type: "INTEGER"},
136+
{Name: "username", Type: "TEXT"},
137+
{Name: "email", Type: "TEXT"},
138+
}
139+
140+
got, err := suite.d.GetColumns(&core.TableOptions{
141+
Table: "test_table",
142+
Schema: "sqlite_schema",
143+
Materialization: core.StructureTypeTable,
144+
})
145+
146+
assert.NoError(t, err)
147+
assert.Equal(t, want, got)
148+
}
149+
150+
func (suite *SQLiteTestSuite) TestShouldNoOperationSwitchDatabase() {
151+
t := suite.T()
152+
153+
driver, err := suite.ctr.NewDriver(&core.ConnectionParams{
154+
ID: "test-sqlite-2",
155+
Name: "test-sqlite-2",
156+
})
157+
assert.NoError(t, err)
158+
159+
err = driver.SelectDatabase("no-op")
160+
assert.Nil(t, err)
161+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CREATE TABLE IF NOT EXISTS test_table (
2+
id INTEGER PRIMARY KEY,
3+
username TEXT,
4+
email TEXT
5+
);
6+
7+
INSERT INTO test_table (id, username, email) VALUES
8+
(1, 'john_doe', 'john@example.com'),
9+
(2, 'jane_smith', 'jane@example.com'),
10+
(3, 'bob_wilson', 'bob@example.com');
11+
12+
CREATE VIEW IF NOT EXISTS test_view AS
13+
SELECT id, username, email
14+
FROM test_table
15+
WHERE id = 2;
16+

dbee/tests/testhelpers/sqlite.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package testhelpers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"path/filepath"
7+
"strings"
8+
"time"
9+
10+
"github.com/docker/docker/api/types/container"
11+
"github.com/kndndrj/nvim-dbee/dbee/adapters"
12+
"github.com/kndndrj/nvim-dbee/dbee/core"
13+
tc "github.com/testcontainers/testcontainers-go"
14+
"github.com/testcontainers/testcontainers-go/wait"
15+
)
16+
17+
type SQLiteContainer struct {
18+
tc.Container
19+
ConnURL string
20+
Driver *core.Connection
21+
TempDir string
22+
}
23+
24+
// NewSQLiteContainer creates a new sqlite container with
25+
// default adapter and connection. The params.URL is overwritten.
26+
// It uses a temporary directory (usually the test suite tempDir) to store the db file.
27+
// The tmpDir is then mounted to the container and all the dependencies are installed
28+
// in the container file, while still being able to connect to the db file in the host.
29+
func NewSQLiteContainer(ctx context.Context, params *core.ConnectionParams, tmpDir string) (*SQLiteContainer, error) {
30+
seedFile, err := GetTestDataFile("sqlite_seed.sql")
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
dbName, containerDBPath := "test.db", "/container/db"
36+
entrypointCmd := []string{
37+
"apk add sqlite=3.48.0-r0",
38+
fmt.Sprintf("sqlite3 %s/%s < %s", containerDBPath, dbName, seedFile.Name()),
39+
"echo 'ready'",
40+
"tail -f /dev/null", // hack to keep the container running indefinitely
41+
}
42+
43+
req := tc.ContainerRequest{
44+
Image: "alpine:3.21",
45+
Files: []tc.ContainerFile{
46+
{
47+
Reader: seedFile,
48+
ContainerFilePath: seedFile.Name(),
49+
FileMode: 0o755,
50+
},
51+
},
52+
HostConfigModifier: func(hc *container.HostConfig) {
53+
hc.Binds = append(hc.Binds, fmt.Sprintf("%s:%s", tmpDir, containerDBPath))
54+
},
55+
Cmd: []string{"sh", "-c", strings.Join(entrypointCmd, " && ")},
56+
WaitingFor: wait.ForLog("ready").WithStartupTimeout(5 * time.Second),
57+
}
58+
59+
ctr, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
60+
ContainerRequest: req,
61+
ProviderType: GetContainerProvider(),
62+
Started: true,
63+
})
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
if params.Type == "" {
69+
params.Type = "sqlite"
70+
}
71+
72+
connURL := filepath.Join(tmpDir, dbName)
73+
if params.URL == "" {
74+
params.URL = connURL
75+
}
76+
77+
driver, err := adapters.NewConnection(params)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
return &SQLiteContainer{
83+
Container: ctr,
84+
ConnURL: connURL,
85+
Driver: driver,
86+
TempDir: tmpDir,
87+
}, nil
88+
}
89+
90+
// NewDriver helper function to create a new driver with the connection URL.
91+
func (p *SQLiteContainer) NewDriver(params *core.ConnectionParams) (*core.Connection, error) {
92+
if params.URL == "" {
93+
params.URL = p.ConnURL
94+
}
95+
if params.Type == "" {
96+
params.Type = "sqlite"
97+
}
98+
99+
return adapters.NewConnection(params)
100+
}

0 commit comments

Comments
 (0)