Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Group lines under `Added` / `Changed` / `Fixed` / `Removed`. Append a PR link

### Fixed

- TPC-B fails fast with a named missing query/section instead of running a silent noop iteration when a custom SQL file omits required statements. ([#145](https://github.com/stroppy-io/stroppy/pull/145))
- TPC-C treats absent customer, warehouse, item, and stock rows as transaction errors, propagates rollback failures instead of reporting an unknown outcome as success, and fails population-validation checks whose aggregate queries error. ([#145](https://github.com/stroppy-io/stroppy/pull/145))
- `stroppy version` now reports the real build version for Docker images (pushed tag), nightly artifacts (`nightly-<short-sha>`), and release archives (release tag), instead of the generic `0.0.0` fallback. ([#144](https://github.com/stroppy-io/stroppy/pull/144))
- Workload help and shell completion expose typed flags accurately, including explicit booleans and contextual defaults, and SQL override positionals reach registered workload bindings. ([#128](https://github.com/stroppy-io/stroppy/pull/128))
- Typed run parameters and SQL sources keep CLI-over-environment precedence, config env names reject case-only collisions, shared driver pool settings remain active alongside driver-specific settings, and invalid pool fields fail clearly instead of being ignored. ([#128](https://github.com/stroppy-io/stroppy/pull/128))
Expand Down
108 changes: 108 additions & 0 deletions internal/workloads/tpcb/sql_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package tpcb

import (
"strings"
"testing"

"github.com/stroppy-io/stroppy/pkg/bench"
)

// tpcbSQL assembles a minimal TPC-B SQL document with both required setup
// sections and the five named transaction queries. missing names one
// (section, query) pair to omit so a test can assert validateSQL names it.
func tpcbSQL(missing struct{ section, query string }) string {
var b strings.Builder

if missing.section != "drop_schema" {
b.WriteString("--+ drop_schema\n--=\nDROP TABLE t;\n")
}

if missing.section != "create_schema" {
b.WriteString("--+ create_schema\n--=\nCREATE TABLE t (a INT);\n")
}

b.WriteString("--+ workload_tx_tpcb\n")

for _, q := range requiredTxQueries {
if missing.section == q.section && missing.query == q.query {
continue
}

b.WriteString("--= " + q.query + "\nSELECT 1;\n")
}

return b.String()
}

func TestValidateSQLAcceptsCompleteFile(t *testing.T) {
if err := validateSQL(bench.ParseSQL(tpcbSQL(struct{ section, query string }{}))); err != nil {
t.Fatalf("validateSQL(complete) = %v, want nil", err)
}
}

func TestValidateSQLNamesEveryMissingRequiredQuery(t *testing.T) {
for _, q := range requiredTxQueries {
t.Run(q.query, func(t *testing.T) {
err := validateSQL(bench.ParseSQL(tpcbSQL(q)))
if err == nil {
t.Fatalf("validateSQL missing %s/%s = nil, want error", q.section, q.query)
}

if !strings.Contains(err.Error(), q.section+"/"+q.query) {
t.Fatalf("validateSQL error %q does not name %s/%s", err, q.section, q.query)
}
})
}
}

func TestValidateSQLNamesMissingRequiredSection(t *testing.T) {
for _, name := range requiredSetupSections {
t.Run(name, func(t *testing.T) {
err := validateSQL(bench.ParseSQL(tpcbSQL(struct{ section, query string }{section: name})))
if err == nil {
t.Fatalf("validateSQL missing %q = nil, want error", name)
}

if !strings.Contains(err.Error(), name) {
t.Fatalf("validateSQL error %q does not name section %q", err, name)
}
})
}
}

// tpcbSQLWithEmptyQuery assembles a TPC-B document whose named query for empty
// is declared with a `--= name` marker but no statement body.
func tpcbSQLWithEmptyQuery(empty struct{ section, query string }) string {
var b strings.Builder

b.WriteString("--+ drop_schema\n--=\nDROP TABLE t;\n")
b.WriteString("--+ create_schema\n--=\nCREATE TABLE t (a INT);\n")
b.WriteString("--+ workload_tx_tpcb\n")

for _, q := range requiredTxQueries {
b.WriteString("--= " + q.query + "\n")

if empty.section == q.section && empty.query == q.query {
continue // declared but empty: the next marker follows immediately
}

b.WriteString("SELECT 1;\n")
}

return b.String()
}

func TestValidateSQLRejectsEmptyQueryBody(t *testing.T) {
for _, q := range requiredTxQueries {
t.Run(q.query, func(t *testing.T) {
err := validateSQL(bench.ParseSQL(tpcbSQLWithEmptyQuery(q)))
if err == nil {
t.Fatalf("validateSQL empty %s/%s = nil, want error", q.section, q.query)
}

if !strings.Contains(err.Error(), "empty query "+q.section+"/"+q.query) {
t.Fatalf("validateSQL error %q does not name empty %s/%s", err, q.section, q.query)
}
})
}
}
52 changes: 52 additions & 0 deletions internal/workloads/tpcb/tpcb.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"math/rand/v2"
"strings"
"sync"
"sync/atomic"

Expand All @@ -19,6 +20,28 @@ import (

var errAccountNotFound = errors.New("tpc-b: account not found")

// requiredTxQueries are the named transaction queries the measured iteration
// depends on. Every custom SQL file must provide them; a missing one would
// otherwise parse as an empty statement and run as a silent noop.
var requiredTxQueries = []struct{ section, query string }{
{"workload_tx_tpcb", "update_account"},
{"workload_tx_tpcb", "get_balance"},
{"workload_tx_tpcb", "update_teller"},
{"workload_tx_tpcb", "update_branch"},
{"workload_tx_tpcb", "insert_history"},
}

// requiredSetupSections are the schema sections the setup steps execute. They are
// present in every dialect (unlike the pg/mysql-only index/fk/analyze sections,
// which legitimately no-op on picodata/ydb).
var requiredSetupSections = []string{"drop_schema", "create_schema"}

var (
errMissingSection = errors.New("tpc-b: missing section")
errMissingQuery = errors.New("tpc-b: missing query")
errEmptyQuery = errors.New("tpc-b: empty query")
)

const (
preset = "tpcb"

Expand Down Expand Up @@ -71,6 +94,10 @@ func (w *workload) Setup(ctx context.Context, b *bench.Bench) error {
w.iso = resolveIsolation(w.driverType, w.iso)
w.sql = mustLoadSQL(w.driverType, w.sqlFile)

if err := validateSQL(w.sql); err != nil {
return err
}

runSection := func(name string) error {
for _, q := range w.sql.Section(name) {
if err := b.Exec(ctx, q, nil); err != nil {
Expand Down Expand Up @@ -218,6 +245,31 @@ func mustLoadSQL(dt bench.DriverTypeName, override string) *bench.SQL {
return s
}

// validateSQL asserts the schema sections and named transaction queries the
// workload needs are present before measured execution, so a custom SQL file
// that omits TPC-B statements fails with a named missing query/section instead
// of degrading into successful noop iterations.
func validateSQL(sql *bench.SQL) error {
for _, name := range requiredSetupSections {
if len(sql.Section(name)) == 0 {
return fmt.Errorf("%w %q", errMissingSection, name)
}
}

for _, q := range requiredTxQueries {
body, ok := sql.Query(q.section, q.query)
if !ok {
return fmt.Errorf("%w %s/%s", errMissingQuery, q.section, q.query)
}

if strings.TrimSpace(body) == "" {
return fmt.Errorf("%w %s/%s", errEmptyQuery, q.section, q.query)
}
}

return nil
}

// --- per-VU tx-time generators ---

type vuState struct {
Expand Down
Loading