diff --git a/.github/workflows/ci.yml b/.github/workflows/test.yml similarity index 81% rename from .github/workflows/ci.yml rename to .github/workflows/test.yml index a78f0e9..cf26d0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: CI +name: Test on: workflow_dispatch: @@ -10,7 +10,7 @@ on: pull_request: jobs: - build: + coverage: runs-on: ubuntu-latest steps: - name: Checkout @@ -19,7 +19,7 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.22.x - - name: Check build + - name: Build run: | go version pwd && ls -l @@ -32,6 +32,21 @@ jobs: --with $MODULE_NAME="." ./k6ext version + - name: Test + if: ${{ github.ref_name == 'main' }} + run: go test -count 1 -coverprofile=coverage.txt ./... + + - name: Upload Coverage + if: ${{ github.ref_name == 'main' }} + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: grafana/xk6-sql + + - name: Generate Go Report Card + if: ${{ github.ref_name == 'main' }} + uses: creekorful/goreportcard-action@v1.0 + test: strategy: fail-fast: false @@ -51,18 +66,3 @@ jobs: which go go version go test -race -timeout 60s ./... - - - name: Coverage Test - if: ${{ matrix.platform == 'ubuntu-latest' && github.ref_name == 'main' }} - run: go test -count 1 -coverprofile=coverage.txt ./... - - - name: Upload Coverage - if: ${{ matrix.platform == 'ubuntu-latest' && github.ref_name == 'main' }} - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - slug: grafana/xk6-sql - - - name: Generate Go Report Card - if: ${{ matrix.platform == 'ubuntu-latest' && github.ref_name == 'main' }} - uses: creekorful/goreportcard-action@v1.0 diff --git a/README.md b/README.md index 35cde0c..b778b5d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![API Reference](https://img.shields.io/badge/API-reference-blue?logo=readme&logoColor=lightgray)](https://sql.x.k6.io) [![GitHub Release](https://img.shields.io/github/v/release/grafana/xk6-sql)](https://github.com/grafana/xk6-sql/releases/) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/xk6-sql)](https://goreportcard.com/report/github.com/grafana/xk6-sql) -[![GitHub Actions](https://github.com/grafana/xk6-sql/actions/workflows/ci.yml/badge.svg)](https://github.com/grafana/xk6-sql/actions/workflows/ci.yml) +[![GitHub Actions](https://github.com/grafana/xk6-sql/actions/workflows/test.yml/badge.svg)](https://github.com/grafana/xk6-sql/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/grafana/xk6-sql/graph/badge.svg?token=DSkK7glKPq)](https://codecov.io/gh/grafana/xk6-sql) # xk6-sql @@ -53,7 +53,7 @@ export default function () { `); console.log(`${result.rowsAffected()} rows inserted`); - let rows = sql.query(db, "SELECT * FROM roster WHERE given_name = $1;", "Peter"); + let rows = db.query("SELECT * FROM roster WHERE given_name = $1;", "Peter"); for (const row of rows) { console.log(`${row.family_name}, ${row.given_name}`); } @@ -78,13 +78,13 @@ export default function () { scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) -time="2024-10-21T14:38:58+02:00" level=info msg="4 rows inserted" source=console -time="2024-10-21T14:38:58+02:00" level=info msg="Pan, Peter" source=console +time="2024-10-21T15:47:50+02:00" level=info msg="4 rows inserted" source=console +time="2024-10-21T15:47:50+02:00" level=info msg="Pan, Peter" source=console data_received........: 0 B 0 B/s data_sent............: 0 B 0 B/s - iteration_duration...: avg=573.77µs min=573.77µs med=573.77µs max=573.77µs p(90)=573.77µs p(95)=573.77µs - iterations...........: 1 967.948327/s + iteration_duration...: avg=371.25µs min=371.25µs med=371.25µs max=371.25µs p(90)=371.25µs p(95)=371.25µs + iterations...........: 1 1061.969082/s running (00m00.0s), 0/1 VUs, 1 complete and 0 interrupted iterations diff --git a/examples/example.js b/examples/example.js index d17d0bc..0e6f316 100644 --- a/examples/example.js +++ b/examples/example.js @@ -32,7 +32,7 @@ export default function () { `); console.log(`${result.rowsAffected()} rows inserted`); - let rows = sql.query(db, "SELECT * FROM roster WHERE given_name = $1;", "Peter"); + let rows = db.query("SELECT * FROM roster WHERE given_name = $1;", "Peter"); for (const row of rows) { console.log(`${row.family_name}, ${row.given_name}`); } diff --git a/examples/example.txt b/examples/example.txt index 2aa95a0..30a7045 100644 --- a/examples/example.txt +++ b/examples/example.txt @@ -12,13 +12,13 @@ scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) -time="2024-10-21T14:54:42+02:00" level=info msg="4 rows inserted" source=console -time="2024-10-21T14:54:42+02:00" level=info msg="Pan, Peter" source=console +time="2024-10-21T15:47:50+02:00" level=info msg="4 rows inserted" source=console +time="2024-10-21T15:47:50+02:00" level=info msg="Pan, Peter" source=console data_received........: 0 B 0 B/s data_sent............: 0 B 0 B/s - iteration_duration...: avg=344.4µs min=344.4µs med=344.4µs max=344.4µs p(90)=344.4µs p(95)=344.4µs - iterations...........: 1 1076.071821/s + iteration_duration...: avg=371.25µs min=371.25µs med=371.25µs max=371.25µs p(90)=371.25µs p(95)=371.25µs + iterations...........: 1 1061.969082/s running (00m00.0s), 0/1 VUs, 1 complete and 0 interrupted iterations diff --git a/index.d.ts b/index.d.ts index c359b8e..f78c69b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,7 +40,7 @@ * `); * console.log(`${result.rowsAffected()} rows inserted`); * - * let rows = sql.query(db, "SELECT * FROM roster WHERE given_name = $1;", "Peter"); + * let rows = db.query("SELECT * FROM roster WHERE given_name = $1;", "Peter"); * for (const row of rows) { * console.log(`${row.family_name}, ${row.given_name}`); * } @@ -159,7 +159,7 @@ export interface Database { * const db = sql.open(driver, "roster_db"); * * export default function () { - * let rows = sql.query(db, "SELECT * FROM roster WHERE given_name = $1;", "Peter"); + * let rows = db.query("SELECT * FROM roster WHERE given_name = $1;", "Peter"); * for (const row of results) { * console.log(`${row.family_name}, ${row.given_name}`); * } @@ -189,7 +189,7 @@ export interface Row { * const db = sql.open(driver, "roster_db"); * * export default function () { - * let rows = sql.query(db, "SELECT * FROM roster WHERE given_name = $1;", "Peter"); + * let rows = db.query("SELECT * FROM roster WHERE given_name = $1;", "Peter"); * for (const row of results) { * console.log(`${row.family_name}, ${row.given_name}`); * } diff --git a/register_test.go b/register_test.go new file mode 100644 index 0000000..64a1acd --- /dev/null +++ b/register_test.go @@ -0,0 +1,15 @@ +package sql + +import ( + "testing" + + "github.com/grafana/xk6-sql/sql" + "github.com/stretchr/testify/require" + "go.k6.io/k6/ext" +) + +func Test_register(t *testing.T) { + t.Parallel() + + require.Contains(t, ext.Get(ext.JSExtension), sql.ImportPath) +} diff --git a/sql/module.go b/sql/module.go index b2cee7c..572d90c 100644 --- a/sql/module.go +++ b/sql/module.go @@ -29,8 +29,7 @@ func (*rootModule) NewModuleInstance(_ modules.VU) modules.Instance { instance.exports.Default = instance instance.exports.Named = map[string]interface{}{ - "open": instance.Open, - "query": instance.Query, + "open": instance.Open, } return instance @@ -51,7 +50,7 @@ type KeyValue map[string]interface{} // open establishes a connection to the specified database type using // the provided connection string. -func (mod *module) Open(driverID sobek.Value, connectionString string) (*sql.DB, error) { +func (mod *module) Open(driverID sobek.Value, connectionString string) (*Database, error) { driverSym, ok := driverID.(*sobek.Symbol) if !ok { return nil, fmt.Errorf("%w: invalid driver parameter type", errUnsupportedDatabase) @@ -67,13 +66,17 @@ func (mod *module) Open(driverID sobek.Value, connectionString string) (*sql.DB, return nil, err } - return db, nil + return &Database{db: db}, nil } -// query executes the provided query string against the database, while -// providing results as a slice of KeyValue instance(s) if available. -func (*module) Query(db *sql.DB, query string, args ...interface{}) ([]KeyValue, error) { - rows, err := db.Query(query, args...) +// Database is a database handle representing a pool of zero or more underlying connections. +type Database struct { + db *sql.DB +} + +// Query executes a query that returns rows, typically a SELECT. +func (dbase *Database) Query(query string, args ...interface{}) ([]KeyValue, error) { + rows, err := dbase.db.Query(query, args...) if err != nil { return nil, err } @@ -114,4 +117,14 @@ func (*module) Query(db *sql.DB, query string, args ...interface{}) ([]KeyValue, return result, nil } +// Exec a query without returning any rows. +func (dbase *Database) Exec(query string, args ...interface{}) (sql.Result, error) { + return dbase.db.Exec(query, args...) +} + +// Close the database and prevents new queries from starting. +func (dbase *Database) Close() error { + return dbase.db.Close() +} + var errUnsupportedDatabase = errors.New("unsupported database") diff --git a/sql/sql_internal_test.go b/sql/sql_internal_test.go new file mode 100644 index 0000000..05765a5 --- /dev/null +++ b/sql/sql_internal_test.go @@ -0,0 +1,28 @@ +package sql + +import ( + "testing" + + "github.com/grafana/sobek" + "github.com/stretchr/testify/require" +) + +func TestOpen(t *testing.T) { //nolint: paralleltest + mod := New().NewModuleInstance(nil).(*module) + + driver := RegisterDriver("ramsql") + require.NotNil(t, driver) + + db, err := mod.Open(driver, "") + + require.NoError(t, err) + require.NotNil(t, db) + + _, err = mod.Open(sobek.New().ToValue("foo"), "testdb") // not a Symbol + + require.Error(t, err) + + _, err = mod.Open(sobek.NewSymbol("ramsql"), "testdb") // not a registered Symbol + + require.Error(t, err) +} diff --git a/sql/testdata/script.js b/sql/testdata/script.js index ca15c56..90d0a15 100644 --- a/sql/testdata/script.js +++ b/sql/testdata/script.js @@ -6,17 +6,17 @@ for (let i = 0; i < 5; i++) { db.exec("INSERT INTO test_table (name, value) VALUES ('name-" + i + "', 'value-" + i + "');"); } -let all_rows = sql.query(db, "SELECT * FROM test_table;"); +let all_rows = db.query("SELECT * FROM test_table;"); if (all_rows.length != 5) { throw new Error("Expected all five rows to be returned; got " + all_rows.length); } -let one_row = sql.query(db, "SELECT * FROM test_table WHERE name = $1;", "name-2"); +let one_row = db.query("SELECT * FROM test_table WHERE name = $1;", "name-2"); if (one_row.length != 1) { throw new Error("Expected single row to be returned; got " + one_row.length); } -let no_rows = sql.query(db, "SELECT * FROM test_table WHERE name = $1;", "bogus-name"); +let no_rows = db.query("SELECT * FROM test_table WHERE name = $1;", "bogus-name"); if (no_rows.length != 0) { throw new Error("Expected no rows to be returned; got " + no_rows.length); } diff --git a/sqltest/sqltest_test.go b/sqltest/sqltest_test.go new file mode 100644 index 0000000..d29e4de --- /dev/null +++ b/sqltest/sqltest_test.go @@ -0,0 +1,22 @@ +package sqltest_test + +import ( + _ "embed" + "testing" + + "github.com/grafana/xk6-sql/sql" + "github.com/grafana/xk6-sql/sqltest" + + _ "github.com/proullon/ramsql/driver" +) + +//go:embed testdata/script.js +var script string + +func TestRunScript(t *testing.T) { + t.Parallel() + + sql.RegisterModule("ramsql") + + sqltest.RunScript(t, "ramsql", "testdb", script) +} diff --git a/sqltest/testdata/script.js b/sqltest/testdata/script.js new file mode 100644 index 0000000..90d0a15 --- /dev/null +++ b/sqltest/testdata/script.js @@ -0,0 +1,24 @@ +const db = sql.open(driver, connection); + +db.exec("CREATE TABLE test_table (id integer PRIMARY KEY AUTOINCREMENT, name varchar NOT NULL, value varchar);"); + +for (let i = 0; i < 5; i++) { + db.exec("INSERT INTO test_table (name, value) VALUES ('name-" + i + "', 'value-" + i + "');"); +} + +let all_rows = db.query("SELECT * FROM test_table;"); +if (all_rows.length != 5) { + throw new Error("Expected all five rows to be returned; got " + all_rows.length); +} + +let one_row = db.query("SELECT * FROM test_table WHERE name = $1;", "name-2"); +if (one_row.length != 1) { + throw new Error("Expected single row to be returned; got " + one_row.length); +} + +let no_rows = db.query("SELECT * FROM test_table WHERE name = $1;", "bogus-name"); +if (no_rows.length != 0) { + throw new Error("Expected no rows to be returned; got " + no_rows.length); +} + +db.close();