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.