Skip to main content
Version: 5.1.2

Extensibility

Stroppy uses a driver registry pattern. Adding a database target means implementing the Go driver interface, registering it, and adding the driver type to the shared config schema.

Stroppy currently registers six drivers:

DriverPackageNotes
PostgreSQLpkg/driver/postgrespgx pool, COPY for native InsertSpec, transactions.
MySQLpkg/driver/mysqldatabase/sql, multi-row inserts, transactions.
Picodatapkg/driver/picodataPostgreSQL-wire SQL path, no transactions.
YDBpkg/driver/ydbYDB SQL and native BulkUpsert.
Nooppkg/driver/noopDrains generators and discards I/O for framework overhead tests.
CSVpkg/driver/csvEmits InsertSpec rows to CSV files.

Driver Interface

Every driver implements pkg/driver.Driver:

type Driver interface {
InsertSpec(ctx context.Context, spec *dgproto.InsertSpec) (*stats.Query, error)
RunQuery(ctx context.Context, sql string, args map[string]any) (*QueryResult, error)
Begin(ctx context.Context, isolation stroppy.TxIsolationLevel) (Tx, error)
Teardown(ctx context.Context) error
}

Transactions return pkg/driver.Tx:

type Tx interface {
RunQuery(ctx context.Context, sql string, args map[string]any) (*QueryResult, error)
Commit(ctx context.Context) error
Rollback(ctx context.Context) error
Isolation() stroppy.TxIsolationLevel
}

The TypeScript layer calls these through DriverX.exec(), DriverX.insertSpec(), DriverX.begin(), and DriverX.beginTx().

InsertSpec Load Path

The current load path is relational InsertSpec. TypeScript builds an InsertSpec with Rel.table(...), serializes it, and sends it to Driver.InsertSpec.

Driver implementations usually follow this pattern:

  1. Validate spec and method support.
  2. Build a deterministic datagen runtime with runtime.NewRuntime(spec).
  3. If spec.parallelism.workers is greater than 1, split work with common.RunParallelByWorkers.
  4. Stream rows into the database using spec.GetMethod().
  5. Return *stats.Query with elapsed time and row count.

Example shape from SQL drivers:

func (d *Driver) InsertSpec(ctx context.Context, spec *dgproto.InsertSpec) (*stats.Query, error) {
if spec == nil {
return nil, fmt.Errorf("%w: nil spec", runtime.ErrInvalidSpec)
}

workers := int(spec.GetParallelism().GetWorkers())
if workers <= 1 {
return d.insertSpecSingle(ctx, spec)
}

return d.insertSpecParallel(ctx, spec, workers)
}

Method mapping is driver-specific:

Insert methodTypical implementation
NATIVEPostgreSQL COPY, YDB BulkUpsert, CSV output, Noop drain, or a driver-native fast path.
PLAIN_BULKMulti-row INSERT batches.
PLAIN_QUERYPer-row INSERT path, often implemented as batch size 1.

If a method is not supported, return driver.ErrInsertSpecNotImplemented or a driver-specific unsupported-method error.

Query Execution

RunQuery receives SQL with Stroppy's named :param placeholders and an argument map. The driver is responsible for converting placeholders to the native dialect and returning a cursor-like Rows implementation.

Drivers based on database/sql can use the shared sqldriver package:

func (d *Driver) RunQuery(
ctx context.Context,
sqlStr string,
args map[string]any,
) (*driver.QueryResult, error) {
return sqldriver.RunQuery(ctx, d.db, wrapRows, d.dialect, d.logger, sqlStr, args)
}

The dialect supplies placeholder formatting and value conversion. PostgreSQL-like dialects use $1, $2; MySQL uses ?.

Transactions

Drivers that support transactions implement Begin. Drivers that do not support transactions should return a clear error for normal isolation levels.

Special levels:

LevelRuntime behavior
CONNECTION_ONLY (conn)Driver acquires a dedicated connection without issuing BEGIN; Commit/Rollback release the connection.
NONE (none)The xk6 bridge wraps pool-level RunQuery; Commit/Rollback are no-ops.

Picodata workloads usually use TX_ISOLATION=none because the Picodata driver does not implement database transactions.

Adding a Driver

1. Add the driver type

Add the enum value to proto/stroppy/config.proto:

enum DriverType {
DRIVER_TYPE_UNSPECIFIED = 0;
DRIVER_TYPE_POSTGRES = 1;
DRIVER_TYPE_MYSQL = 2;
DRIVER_TYPE_PICODATA = 3;
DRIVER_TYPE_YDB = 4;
DRIVER_TYPE_NOOP = 5;
DRIVER_TYPE_CSV = 6;
DRIVER_TYPE_MYDB = 7;
}

Regenerate code:

make proto

2. Create the package

pkg/driver/mydb/
├── driver.go
├── run_query.go
├── insert_spec.go
└── tx.go

3. Register the driver

Register in the package init():

func init() {
driver.RegisterDriver(
stroppy.DriverConfig_DRIVER_TYPE_MYDB,
func(ctx context.Context, opts driver.Options) (driver.Driver, error) {
return NewDriver(ctx, opts)
},
)
}

The constructor receives driver.Options:

type Options struct {
DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
Logger *zap.Logger
Config *stroppy.DriverConfig
}

Use opts.DialFunc where possible so k6 network metrics still work.

4. Import it into the xk6 module

Add a blank import in cmd/xk6air/instance.go so the init() registration runs:

import (
_ "github.com/stroppy-io/stroppy/pkg/driver/mydb"
)

5. Add CLI and TypeScript names

Update the user-facing mappings so declareDriverSetup() and -D driverType=mydb understand the new string name:

  • internal/static/helpers.ts driver type union and map.
  • internal/runner/driver_preset.go if you want a short -d preset.
  • proto/stroppy/run.proto comments for config-file docs if the type is public.

6. Build and test

make build
./build/stroppy probe my_workload.ts --drivers --envs --steps
./build/stroppy run my_workload.ts -D driverType=mydb -D url=mydb://localhost/db

Registry Behavior

The dispatcher is intentionally small. Drivers self-register at import time, and Dispatch selects a constructor from the enum value in DriverConfig.

var registry = map[stroppy.DriverConfig_DriverType]driverConstructor{}

func RegisterDriver(driverType stroppy.DriverConfig_DriverType, constructor driverConstructor) {
registry[driverType] = constructor
}

func Dispatch(ctx context.Context, opts Options) (Driver, error) {
drvType := opts.Config.GetDriverType()
if constructor, ok := registry[drvType]; ok {
return constructor(ctx, opts)
}
return nil, fmt.Errorf("driver type '%s': %w", drvType.String(), ErrNoRegisteredDriver)
}

Adding a driver should not require dispatcher changes beyond importing the package.

Reference Implementations

Use existing drivers as templates:

DriverFile to studyWhy
PostgreSQLpkg/driver/postgres/insert_spec.goNative COPY and parallel InsertSpec.
MySQLpkg/driver/mysql/insert_spec.godatabase/sql plus shared bulk insert path.
YDBpkg/driver/ydb/insert_spec.goNative BulkUpsert and SQL fallback.
CSVpkg/driver/csvSink driver that implements InsertSpec without SQL query execution.
Nooppkg/driver/noopMinimal driver for overhead measurement.