diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bec3e49..82479fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,3 +27,14 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.out fail_ci_if_error: false + + # The example (testdata/example) is a separate module with its own dependencies. It is checked + # but deliberately excluded from coverage, so it runs as its own job. + example: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + with: + cache: true + - run: mise run example diff --git a/mise.toml b/mise.toml index f80f915..588861b 100644 --- a/mise.toml +++ b/mise.toml @@ -37,3 +37,12 @@ run = 'go test -coverpkg=./... -covermode=atomic -coverprofile=coverage.out ./.. [tasks.check] description = "Full gate: fmt, build, vet, lint, deadcode, test" depends = ["fmt", "build", "vet", "lint", "deadcode", "test"] + +# The example is a separate module (testdata/example, joined via go.work) with its own deps +# (modernc.org/sqlite, samber/lo). It is checked but deliberately kept out of coverage, so it +# runs as its own task/CI job rather than under `check`/`cover`. Formatting is already covered by +# the root `fmt` task (gofmt -l . recurses into testdata/example). +[tasks.example] +description = "Check the example module (build, vet, lint, deadcode, test -race); no coverage" +dir = "testdata/example" +run = 'go build ./... && go vet ./... && golangci-lint run ./... && deadcode -test ./... && go test -race ./...' diff --git a/e2e_test.go b/snapshot_test.go similarity index 67% rename from e2e_test.go rename to snapshot_test.go index 42901ed..5a4edc8 100644 --- a/e2e_test.go +++ b/snapshot_test.go @@ -12,21 +12,23 @@ import ( "github.com/mpyw/bisql/dialect" ) -// TestE2E exercises one comprehensive template that uses every directive kind at once — a CTE -// bind, a recursive @include of reusable fragments, an if/elseif/else branch, an IN-list bind, a -// PostgreSQL array bind, a tuple (row) IN, a /*%for*/ loop, an order-by conditional, and a /*^ */ -// inline literal — rendered across all four dialects to lock placeholder numbering. +// TestSnapshots exercises one comprehensive template that uses every directive kind at once — a +// CTE bind, a recursive @include of reusable fragments, an if/elseif/else branch, an IN-list +// bind, a PostgreSQL array bind, a tuple (row) IN, a /*%for*/ loop, an order-by conditional, and +// a /*^ */ inline literal — rendered across all four dialects to lock placeholder numbering. // -// All fixtures are flat files under testdata/e2e, named by the template and (per case) the case: +// The template and its fragments live under testdata/; every generated artifact lives under +// testdata/snapshots/, named by the template and (per case) the case: // -// all_in_one.sql the template (@include-ing all_in_one.scope.sql, which in turn -// @include-s all_in_one.active.sql) -// all_in_one.expanded.sql every @include resolved (dialect-independent) -// all_in_one..prepared.sql parameterized SQL (placeholder form) -// all_in_one..embedded.sql values-embedded SQL (when checked) +// testdata/all_in_one.sql the template (@include-s _all_in_one.scope.sql, +// which in turn @include-s _all_in_one.active.sql) +// testdata/_all_in_one.{scope,active}.sql reusable fragments (underscore-prefixed by convention) +// testdata/snapshots/all_in_one.expanded.sql every @include resolved (dialect-independent) +// testdata/snapshots/all_in_one..prepared.sql parameterized SQL (placeholder form) +// testdata/snapshots/all_in_one..embedded.sql values-embedded SQL (when checked) // -// Regenerate with: go test -run TestE2E -update -var update = flag.Bool("update", false, "update golden files in testdata/") +// Regenerate with: go test -run TestSnapshots -update +var update = flag.Bool("update", false, "update golden files in testdata/snapshots") func checkGolden(t *testing.T, path, got string) { t.Helper() @@ -38,23 +40,23 @@ func checkGolden(t *testing.T, path, got string) { } want, err := os.ReadFile(path) if err != nil { - t.Fatalf("read golden %s: %v (regenerate with: go test -run TestE2E -update)", path, err) + t.Fatalf("read golden %s: %v (regenerate with: go test -run TestSnapshots -update)", path, err) } if got != string(want) { t.Errorf("%s mismatch\n--- got ---\n%s\n--- want ---\n%s", path, got, want) } } -func TestE2E(t *testing.T) { - const dir = "testdata/e2e" - fsys := os.DirFS(dir) +func TestSnapshots(t *testing.T) { + fsys := os.DirFS("testdata") + snap := func(name string) string { return filepath.Join("testdata", "snapshots", name) } // Expanded snapshot: every @include resolved, still two-way, independent of dialect. expanded, err := bisql.ExpandFile(fsys, "all_in_one.sql") if err != nil { t.Fatalf("expand: %v", err) } - checkGolden(t, filepath.Join(dir, "all_in_one.expanded.sql"), expanded) + checkGolden(t, snap("all_in_one.expanded.sql"), expanded) // The full case supplies every optional predicate; rendered under all four dialects it locks // the placeholder numbering. pg_array adds the PostgreSQL-only array bind; the pending/else @@ -109,9 +111,9 @@ func TestE2E(t *testing.T) { if !reflect.DeepEqual(stmt.Args, c.args) { t.Errorf("Args\n got: %#v\nwant: %#v", stmt.Args, c.args) } - checkGolden(t, filepath.Join(dir, "all_in_one."+c.name+".prepared.sql"), stmt.SQL) + checkGolden(t, snap("all_in_one."+c.name+".prepared.sql"), stmt.SQL) if c.embedded { - checkGolden(t, filepath.Join(dir, "all_in_one."+c.name+".embedded.sql"), stmt.SQLWithArgs()) + checkGolden(t, snap("all_in_one."+c.name+".embedded.sql"), stmt.SQLWithArgs()) } }) } diff --git a/testdata/e2e/all_in_one.active.sql b/testdata/_all_in_one.active.sql similarity index 100% rename from testdata/e2e/all_in_one.active.sql rename to testdata/_all_in_one.active.sql diff --git a/testdata/e2e/all_in_one.scope.sql b/testdata/_all_in_one.scope.sql similarity index 65% rename from testdata/e2e/all_in_one.scope.sql rename to testdata/_all_in_one.scope.sql index 1031593..1b7d9fe 100644 --- a/testdata/e2e/all_in_one.scope.sql +++ b/testdata/_all_in_one.scope.sql @@ -1,2 +1,2 @@ -/*%! @include all_in_one.active.sql */ +/*%! @include _all_in_one.active.sql */ /*%if departmentId != null*/and u.department_id = /*departmentId*/0/*%end*/ diff --git a/testdata/e2e/all_in_one.sql b/testdata/all_in_one.sql similarity index 93% rename from testdata/e2e/all_in_one.sql rename to testdata/all_in_one.sql index 559f833..7a67817 100644 --- a/testdata/e2e/all_in_one.sql +++ b/testdata/all_in_one.sql @@ -5,7 +5,7 @@ select u.id, u.name from users u join active_depts d on d.id = u.department_id where 1 = 1 -/*%! @include all_in_one.scope.sql */ +/*%! @include _all_in_one.scope.sql */ /*%if ageBand == 'adult'*/ and u.age >= 18/*%elseif ageBand == 'senior'*/ and u.age >= 65/*%else*/ and u.age >= 0/*%end*/ /*%if ids != null*/and u.id in /*ids*/(0)/*%end*/ /*%if tags != null*/and u.tags && /*tags*/'{}'::text[]/*%end*/ diff --git a/testdata/example/src/app/infrastructure/query/query.go b/testdata/example/src/app/infrastructure/query/query.go index be53373..24a5fee 100644 --- a/testdata/example/src/app/infrastructure/query/query.go +++ b/testdata/example/src/app/infrastructure/query/query.go @@ -1,6 +1,7 @@ // Package query implements the app/query ports over a *sql.DB, building each statement from the // embedded bisql templates (SQLite dialect). The .sql templates live under sql/, organized by -// domain, with reusable fragments under a fragment/ subdirectory pulled in via @include. +// domain; reusable fragments are underscore-prefixed (_name.sql) by convention and pulled in via +// @include. The embed uses the all: prefix so those _-prefixed files are included. package query import ( @@ -16,7 +17,7 @@ import ( port "github.com/mpyw/bisql/example/src/app/query" ) -//go:embed sql +//go:embed all:sql var embedded embed.FS // templates is the query tree rooted at sql/, so a name like "users/search.sql" resolves and its diff --git a/testdata/example/src/app/infrastructure/query/sql/audit_logs/fragment/since.sql b/testdata/example/src/app/infrastructure/query/sql/audit_logs/_since.sql similarity index 100% rename from testdata/example/src/app/infrastructure/query/sql/audit_logs/fragment/since.sql rename to testdata/example/src/app/infrastructure/query/sql/audit_logs/_since.sql diff --git a/testdata/example/src/app/infrastructure/query/sql/audit_logs/fragment/window.sql b/testdata/example/src/app/infrastructure/query/sql/audit_logs/_window.sql similarity index 59% rename from testdata/example/src/app/infrastructure/query/sql/audit_logs/fragment/window.sql rename to testdata/example/src/app/infrastructure/query/sql/audit_logs/_window.sql index b28530d..f3da1db 100644 --- a/testdata/example/src/app/infrastructure/query/sql/audit_logs/fragment/window.sql +++ b/testdata/example/src/app/infrastructure/query/sql/audit_logs/_window.sql @@ -1,2 +1,2 @@ -/*%! @include audit_logs/fragment/since.sql */ +/*%! @include audit_logs/_since.sql */ /*%if until != null*/and a.created_at < /*until*/'2026-01-01'/*%end*/ diff --git a/testdata/example/src/app/infrastructure/query/sql/audit_logs/activity-report.sql b/testdata/example/src/app/infrastructure/query/sql/audit_logs/activity-report.sql index 6551558..c1b8d14 100644 --- a/testdata/example/src/app/infrastructure/query/sql/audit_logs/activity-report.sql +++ b/testdata/example/src/app/infrastructure/query/sql/audit_logs/activity-report.sql @@ -11,7 +11,7 @@ from audit_logs a join users u on u.id = a.user_id join departments d on d.id = u.department_id where 1 = 1 -/*%! @include audit_logs/fragment/window.sql */ +/*%! @include audit_logs/_window.sql */ /*%if actions != null*/and a.action in /*actions*/('login')/*%end*/ group by d.name order by events desc, d.name diff --git a/testdata/example/src/app/infrastructure/query/sql/users/fragment/active.sql b/testdata/example/src/app/infrastructure/query/sql/users/_active.sql similarity index 100% rename from testdata/example/src/app/infrastructure/query/sql/users/fragment/active.sql rename to testdata/example/src/app/infrastructure/query/sql/users/_active.sql diff --git a/testdata/example/src/app/infrastructure/query/sql/users/fragment/scope.sql b/testdata/example/src/app/infrastructure/query/sql/users/_scope.sql similarity index 88% rename from testdata/example/src/app/infrastructure/query/sql/users/fragment/scope.sql rename to testdata/example/src/app/infrastructure/query/sql/users/_scope.sql index 1b56b33..81f8f60 100644 --- a/testdata/example/src/app/infrastructure/query/sql/users/fragment/scope.sql +++ b/testdata/example/src/app/infrastructure/query/sql/users/_scope.sql @@ -1,5 +1,5 @@ -- Row-visibility scope shared by every users query: an optional active-only filter (its own -- reusable fragment) and an optional single-department filter. Both are conjunctions, so the -- scope contributes nothing when neither is requested. -/*%! @include users/fragment/active.sql */ +/*%! @include users/_active.sql */ /*%if departmentId != null*/and u.department_id = /*departmentId*/0/*%end*/ diff --git a/testdata/example/src/app/infrastructure/query/sql/users/search.sql b/testdata/example/src/app/infrastructure/query/sql/users/search.sql index d794500..a2e3b04 100644 --- a/testdata/example/src/app/infrastructure/query/sql/users/search.sql +++ b/testdata/example/src/app/infrastructure/query/sql/users/search.sql @@ -22,7 +22,7 @@ select from users u /*%if withDepartment*/join departments d on d.id = u.department_id/*%end*/ where 1 = 1 -/*%! @include users/fragment/scope.sql */ +/*%! @include users/_scope.sql */ /*%if ageBand == 'adult'*/ and u.age >= 18/*%elseif ageBand == 'senior'*/ and u.age >= 65/*%else*/ and u.age >= 0/*%end*/ /*%if q != null*/and u.name like /*q*/'%alice%'/*%end*/ /*%if departmentIds != null*/and u.department_id in /*departmentIds*/(0)/*%end*/ diff --git a/testdata/e2e/all_in_one.expanded.sql b/testdata/snapshots/all_in_one.expanded.sql similarity index 100% rename from testdata/e2e/all_in_one.expanded.sql rename to testdata/snapshots/all_in_one.expanded.sql diff --git a/testdata/e2e/all_in_one.mysql_full.embedded.sql b/testdata/snapshots/all_in_one.mysql_full.embedded.sql similarity index 100% rename from testdata/e2e/all_in_one.mysql_full.embedded.sql rename to testdata/snapshots/all_in_one.mysql_full.embedded.sql diff --git a/testdata/e2e/all_in_one.mysql_full.prepared.sql b/testdata/snapshots/all_in_one.mysql_full.prepared.sql similarity index 100% rename from testdata/e2e/all_in_one.mysql_full.prepared.sql rename to testdata/snapshots/all_in_one.mysql_full.prepared.sql diff --git a/testdata/e2e/all_in_one.mysql_pending.embedded.sql b/testdata/snapshots/all_in_one.mysql_pending.embedded.sql similarity index 100% rename from testdata/e2e/all_in_one.mysql_pending.embedded.sql rename to testdata/snapshots/all_in_one.mysql_pending.embedded.sql diff --git a/testdata/e2e/all_in_one.mysql_pending.prepared.sql b/testdata/snapshots/all_in_one.mysql_pending.prepared.sql similarity index 100% rename from testdata/e2e/all_in_one.mysql_pending.prepared.sql rename to testdata/snapshots/all_in_one.mysql_pending.prepared.sql diff --git a/testdata/e2e/all_in_one.oracle_full.prepared.sql b/testdata/snapshots/all_in_one.oracle_full.prepared.sql similarity index 100% rename from testdata/e2e/all_in_one.oracle_full.prepared.sql rename to testdata/snapshots/all_in_one.oracle_full.prepared.sql diff --git a/testdata/e2e/all_in_one.pg_array.embedded.sql b/testdata/snapshots/all_in_one.pg_array.embedded.sql similarity index 100% rename from testdata/e2e/all_in_one.pg_array.embedded.sql rename to testdata/snapshots/all_in_one.pg_array.embedded.sql diff --git a/testdata/e2e/all_in_one.pg_array.prepared.sql b/testdata/snapshots/all_in_one.pg_array.prepared.sql similarity index 100% rename from testdata/e2e/all_in_one.pg_array.prepared.sql rename to testdata/snapshots/all_in_one.pg_array.prepared.sql diff --git a/testdata/e2e/all_in_one.pg_else.prepared.sql b/testdata/snapshots/all_in_one.pg_else.prepared.sql similarity index 100% rename from testdata/e2e/all_in_one.pg_else.prepared.sql rename to testdata/snapshots/all_in_one.pg_else.prepared.sql diff --git a/testdata/e2e/all_in_one.postgres_full.embedded.sql b/testdata/snapshots/all_in_one.postgres_full.embedded.sql similarity index 100% rename from testdata/e2e/all_in_one.postgres_full.embedded.sql rename to testdata/snapshots/all_in_one.postgres_full.embedded.sql diff --git a/testdata/e2e/all_in_one.postgres_full.prepared.sql b/testdata/snapshots/all_in_one.postgres_full.prepared.sql similarity index 100% rename from testdata/e2e/all_in_one.postgres_full.prepared.sql rename to testdata/snapshots/all_in_one.postgres_full.prepared.sql diff --git a/testdata/e2e/all_in_one.sqlserver_full.prepared.sql b/testdata/snapshots/all_in_one.sqlserver_full.prepared.sql similarity index 100% rename from testdata/e2e/all_in_one.sqlserver_full.prepared.sql rename to testdata/snapshots/all_in_one.sqlserver_full.prepared.sql