17fc813823
M1, M2 and M3 complete * M1, M2 and M3 complete * review changes * docs * docs
602 lines
23 KiB
Go
602 lines
23 KiB
Go
package repository_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"queryorchestration/internal/database/repository"
|
|
"queryorchestration/internal/serviceconfig"
|
|
"queryorchestration/internal/test"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Mutable metadata feature, milestone 1.4: integration tests for the eight
|
|
// sqlc-generated schema CRUD queries against a real Postgres testcontainer.
|
|
//
|
|
// The tests exercise every generated method at least once:
|
|
// - CreateClientMetadataSchema
|
|
// - GetClientMetadataSchema
|
|
// - GetLatestSchemaByName
|
|
// - GetMaxSchemaVersion
|
|
// - SetSchemaStatus
|
|
// - GetSchemaDocumentCount
|
|
// - LockSchemaVersionsForName
|
|
// - ListClientMetadataSchemas (covered by TestListClientMetadataSchemas_Filters)
|
|
//
|
|
// No mocks. All queries run against the shared testcontainer that
|
|
// test.CreateDB stands up. The Postgres container is reused across test
|
|
// runs so each top-level test re-seeds its client rows with a
|
|
// DELETE-then-CreateClient idempotency dance (see the §1.1/§1.3 tests in
|
|
// client_metadata_schemas_migration_test.go for the same pattern).
|
|
|
|
// trivialSchemaJSON is a minimal but valid schema_def. Used throughout
|
|
// these tests when the exact content does not matter.
|
|
const trivialSchemaJSON = `{"type":"object","additionalProperties":false}`
|
|
|
|
// resetClient makes a client row idempotent across reruns of the same test.
|
|
// The Postgres container and per-test database are reused, so leftover rows
|
|
// from a previous run must be cleaned out before CreateClient. The FK on
|
|
// documents.clientid is NOT ON DELETE CASCADE, so we have to delete
|
|
// documents explicitly before the client delete can succeed. Deleting
|
|
// documents also implicitly clears documents.custom_schema_id references,
|
|
// which lets the subsequent cascade through clients -> client_metadata_schemas
|
|
// proceed cleanly.
|
|
func resetClient(t *testing.T, ctx context.Context, cfg *serviceconfig.BaseConfig, clientID, name string) {
|
|
t.Helper()
|
|
pool := cfg.GetDBPool()
|
|
// Order matters: documents first (no cascade from clients), then
|
|
// clients (cascade wipes client_metadata_schemas via the FK from §1.1).
|
|
_, err := pool.Exec(ctx, `DELETE FROM documents WHERE clientId = $1`, clientID)
|
|
require.NoError(t, err, "pre-test DELETE documents for clientID=%q must succeed", clientID)
|
|
_, err = pool.Exec(ctx, `DELETE FROM clients WHERE clientId = $1`, clientID)
|
|
require.NoError(t, err, "pre-test DELETE clients for clientID=%q must succeed", clientID)
|
|
|
|
err = cfg.GetDBQueries().CreateClient(ctx, &repository.CreateClientParams{
|
|
Name: name,
|
|
Clientid: clientID,
|
|
})
|
|
require.NoError(t, err, "CreateClient for clientID=%q must succeed", clientID)
|
|
}
|
|
|
|
// seedSchema inserts a schema row via the generated CreateClientMetadataSchema
|
|
// query and returns the resulting row. Keeps the test bodies uncluttered.
|
|
func seedSchema(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
queries *repository.Queries,
|
|
clientID, name, description string,
|
|
version int32,
|
|
status repository.SchemaStatusType,
|
|
) *repository.ClientMetadataSchema {
|
|
t.Helper()
|
|
desc := description
|
|
row, err := queries.CreateClientMetadataSchema(ctx, &repository.CreateClientMetadataSchemaParams{
|
|
ClientID: clientID,
|
|
Name: name,
|
|
Description: &desc,
|
|
SchemaDef: []byte(trivialSchemaJSON),
|
|
Version: version,
|
|
Status: status,
|
|
CreatedBy: "tester",
|
|
})
|
|
require.NoError(t, err, "seed CreateClientMetadataSchema(%s,%s,v%d) must succeed", clientID, name, version)
|
|
require.NotNil(t, row)
|
|
return row
|
|
}
|
|
|
|
// TestClientMetadataSchemas_CRUD covers the seven non-list queries with one
|
|
// subtest per query. Each subtest is responsible for its own fixture data
|
|
// (no shared schema rows across subtests) so that reordering or running a
|
|
// single subtest in isolation still works.
|
|
func TestClientMetadataSchemas_CRUD(t *testing.T) {
|
|
t.Parallel()
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
ctx := t.Context()
|
|
|
|
cfg := &serviceconfig.BaseConfig{}
|
|
test.CreateDB(t, cfg)
|
|
pool := cfg.GetDBPool()
|
|
require.NotNil(t, pool)
|
|
queries := cfg.GetDBQueries()
|
|
require.NotNil(t, queries)
|
|
|
|
// Two distinct clients so cross-client isolation can be exercised if
|
|
// needed. Each test body picks whichever it needs.
|
|
const (
|
|
clientA = "SQLC_Q_A"
|
|
clientB = "SQLC_Q_B"
|
|
)
|
|
resetClient(t, ctx, cfg, clientA, "sqlc-q-a")
|
|
resetClient(t, ctx, cfg, clientB, "sqlc-q-b")
|
|
|
|
t.Run("create_and_get", func(t *testing.T) {
|
|
// Use a name unique to this subtest so parallel / rerun interactions
|
|
// can't collide with other subtests that seed the same client.
|
|
const schemaName = "create_and_get"
|
|
desc := "unit-test schema"
|
|
params := &repository.CreateClientMetadataSchemaParams{
|
|
ClientID: clientA,
|
|
Name: schemaName,
|
|
Description: &desc,
|
|
SchemaDef: []byte(trivialSchemaJSON),
|
|
Version: 1,
|
|
Status: repository.SchemaStatusTypeActive,
|
|
CreatedBy: "tester",
|
|
}
|
|
created, err := queries.CreateClientMetadataSchema(ctx, params)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, created)
|
|
require.NotEqual(t, uuid.Nil, created.ID, "id must be populated by DEFAULT")
|
|
require.Equal(t, clientA, created.ClientID)
|
|
require.Equal(t, schemaName, created.Name)
|
|
require.NotNil(t, created.Description)
|
|
require.Equal(t, desc, *created.Description)
|
|
require.JSONEq(t, trivialSchemaJSON, string(created.SchemaDef))
|
|
require.Equal(t, int32(1), created.Version)
|
|
require.Equal(t, repository.SchemaStatusTypeActive, created.Status)
|
|
require.True(t, created.CreatedAt.Valid, "created_at must be set by DEFAULT now()")
|
|
require.False(t, created.CreatedAt.Time.IsZero())
|
|
require.Equal(t, "tester", created.CreatedBy)
|
|
|
|
// GetClientMetadataSchema must round-trip the row exactly.
|
|
fetched, err := queries.GetClientMetadataSchema(ctx, created.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fetched)
|
|
require.Equal(t, created.ID, fetched.ID)
|
|
require.Equal(t, created.ClientID, fetched.ClientID)
|
|
require.Equal(t, created.Name, fetched.Name)
|
|
require.NotNil(t, fetched.Description)
|
|
require.Equal(t, *created.Description, *fetched.Description)
|
|
require.JSONEq(t, string(created.SchemaDef), string(fetched.SchemaDef))
|
|
require.Equal(t, created.Version, fetched.Version)
|
|
require.Equal(t, created.Status, fetched.Status)
|
|
require.Equal(t, created.CreatedAt.Time.UnixMicro(), fetched.CreatedAt.Time.UnixMicro())
|
|
require.Equal(t, created.CreatedBy, fetched.CreatedBy)
|
|
})
|
|
|
|
t.Run("get_not_found_returns_err_no_rows", func(t *testing.T) {
|
|
// A random UUID will not collide with any seeded row.
|
|
randomID := uuid.New()
|
|
_, err := queries.GetClientMetadataSchema(ctx, randomID)
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, pgx.ErrNoRows),
|
|
"expected pgx.ErrNoRows, got %T: %v", err, err)
|
|
})
|
|
|
|
t.Run("get_latest_by_name", func(t *testing.T) {
|
|
const schemaName = "get_latest_by_name"
|
|
seedSchema(t, ctx, queries, clientA, schemaName, "v1", 1, repository.SchemaStatusTypeSuperseded)
|
|
seedSchema(t, ctx, queries, clientA, schemaName, "v2", 2, repository.SchemaStatusTypeSuperseded)
|
|
v3 := seedSchema(t, ctx, queries, clientA, schemaName, "v3", 3, repository.SchemaStatusTypeActive)
|
|
|
|
latest, err := queries.GetLatestSchemaByName(ctx, &repository.GetLatestSchemaByNameParams{
|
|
ClientID: clientA,
|
|
Name: schemaName,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, latest)
|
|
require.Equal(t, v3.ID, latest.ID, "GetLatestSchemaByName must return the highest-version row")
|
|
require.Equal(t, int32(3), latest.Version)
|
|
})
|
|
|
|
t.Run("get_max_schema_version", func(t *testing.T) {
|
|
const schemaName = "get_max_schema_version"
|
|
// Invariant the service layer relies on: brand-new (client, name)
|
|
// pair returns 0, not an error. This is what lets the service safely
|
|
// assign v1 to a new lineage on the first write.
|
|
maxVer, err := queries.GetMaxSchemaVersion(ctx, &repository.GetMaxSchemaVersionParams{
|
|
ClientID: clientA,
|
|
Name: schemaName,
|
|
})
|
|
require.NoError(t, err, "GetMaxSchemaVersion on empty lineage must NOT error (COALESCE)")
|
|
require.NotNil(t, maxVer)
|
|
require.Equal(t, int32(0), *maxVer, "empty lineage must return 0")
|
|
|
|
// Non-contiguous versions on purpose — the query must return MAX,
|
|
// not COUNT or the last inserted version.
|
|
seedSchema(t, ctx, queries, clientA, schemaName, "v1", 1, repository.SchemaStatusTypeSuperseded)
|
|
seedSchema(t, ctx, queries, clientA, schemaName, "v2", 2, repository.SchemaStatusTypeSuperseded)
|
|
seedSchema(t, ctx, queries, clientA, schemaName, "v5", 5, repository.SchemaStatusTypeActive)
|
|
|
|
maxVer, err = queries.GetMaxSchemaVersion(ctx, &repository.GetMaxSchemaVersionParams{
|
|
ClientID: clientA,
|
|
Name: schemaName,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, maxVer)
|
|
require.Equal(t, int32(5), *maxVer, "GetMaxSchemaVersion must return the largest existing version")
|
|
})
|
|
|
|
t.Run("set_schema_status", func(t *testing.T) {
|
|
const schemaName = "set_schema_status"
|
|
row := seedSchema(t, ctx, queries, clientA, schemaName, "v1", 1, repository.SchemaStatusTypeActive)
|
|
|
|
// active -> superseded
|
|
err := queries.SetSchemaStatus(ctx, &repository.SetSchemaStatusParams{
|
|
ID: row.ID,
|
|
Status: repository.SchemaStatusTypeSuperseded,
|
|
})
|
|
require.NoError(t, err)
|
|
reloaded, err := queries.GetClientMetadataSchema(ctx, row.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, repository.SchemaStatusTypeSuperseded, reloaded.Status)
|
|
|
|
// superseded -> retired
|
|
err = queries.SetSchemaStatus(ctx, &repository.SetSchemaStatusParams{
|
|
ID: row.ID,
|
|
Status: repository.SchemaStatusTypeRetired,
|
|
})
|
|
require.NoError(t, err)
|
|
reloaded, err = queries.GetClientMetadataSchema(ctx, row.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, repository.SchemaStatusTypeRetired, reloaded.Status)
|
|
})
|
|
|
|
t.Run("get_schema_document_count", func(t *testing.T) {
|
|
const schemaName = "get_schema_document_count"
|
|
schema := seedSchema(t, ctx, queries, clientA, schemaName, "v1", 1, repository.SchemaStatusTypeActive)
|
|
|
|
// No documents bound yet.
|
|
count, err := queries.GetSchemaDocumentCount(ctx, &schema.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(0), count, "unbound schema must report 0 documents")
|
|
|
|
// Insert a first document owned by the SAME client as the schema
|
|
// and bind it. Same-client is required by the composite FK on
|
|
// documents.custom_schema_id (see migration 128 / §1.3 test).
|
|
doc1, err := queries.CreateDocument(ctx, &repository.CreateDocumentParams{
|
|
Clientid: clientA,
|
|
Hash: "sqlc_q_doc_hash_1",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = pool.Exec(ctx,
|
|
`UPDATE documents SET custom_schema_id = $1 WHERE id = $2`,
|
|
schema.ID, doc1)
|
|
require.NoError(t, err)
|
|
|
|
count, err = queries.GetSchemaDocumentCount(ctx, &schema.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), count, "one bound document must report 1")
|
|
|
|
// Bind a second document.
|
|
doc2, err := queries.CreateDocument(ctx, &repository.CreateDocumentParams{
|
|
Clientid: clientA,
|
|
Hash: "sqlc_q_doc_hash_2",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = pool.Exec(ctx,
|
|
`UPDATE documents SET custom_schema_id = $1 WHERE id = $2`,
|
|
schema.ID, doc2)
|
|
require.NoError(t, err)
|
|
|
|
count, err = queries.GetSchemaDocumentCount(ctx, &schema.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), count, "two bound documents must report 2")
|
|
})
|
|
|
|
t.Run("lock_schema_versions_for_name", func(t *testing.T) {
|
|
// This test only proves that LockSchemaVersionsForName returns the
|
|
// expected rows when run inside a transaction. It does NOT exercise
|
|
// the FOR UPDATE serialization semantics — concurrent version
|
|
// creation is proven by the Milestone 1.6 service-layer tests. The
|
|
// value of LockSchemaVersionsForName at this layer is just "give me
|
|
// back every version row of this (client, name) pair, under a
|
|
// transactional lock"; that is what we assert.
|
|
const schemaName = "lock_schema_versions_for_name"
|
|
v1 := seedSchema(t, ctx, queries, clientA, schemaName, "v1", 1, repository.SchemaStatusTypeSuperseded)
|
|
v2 := seedSchema(t, ctx, queries, clientA, schemaName, "v2", 2, repository.SchemaStatusTypeActive)
|
|
|
|
tx, err := pool.Begin(ctx)
|
|
require.NoError(t, err, "pool.Begin must succeed")
|
|
// Always roll back — the seeded rows already exist outside the tx
|
|
// and nothing inside the tx needs to persist.
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
|
|
txQueries := queries.WithTx(tx)
|
|
locked, err := txQueries.LockSchemaVersionsForName(ctx, &repository.LockSchemaVersionsForNameParams{
|
|
ClientID: clientA,
|
|
Name: schemaName,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, locked, 2, "must lock every version row for the (client, name) pair")
|
|
|
|
gotIDs := map[uuid.UUID]bool{}
|
|
for _, row := range locked {
|
|
gotIDs[row.ID] = true
|
|
}
|
|
require.True(t, gotIDs[v1.ID], "locked set must include v1 row")
|
|
require.True(t, gotIDs[v2.ID], "locked set must include v2 row")
|
|
})
|
|
}
|
|
|
|
// TestListClientMetadataSchemas_Filters exercises the filter matrix of
|
|
// ListClientMetadataSchemas. Uses a client pair distinct from the CRUD test
|
|
// (SQLC_LIST_A / SQLC_LIST_B) so the two top-level tests can run in parallel
|
|
// without stepping on each other.
|
|
//
|
|
// Filter semantics asserted:
|
|
// - include_all_versions=false returns only the MAX(version) row per
|
|
// (client_id, name).
|
|
// - include_all_versions=true returns every row.
|
|
// - name_filter uses EXACT match (the generated SQL is
|
|
// `cms.name = $2::varchar`, not ILIKE). See the name_filter_narrows
|
|
// subtest for the assertion.
|
|
// - include_all_statuses=true bypasses the status filter; otherwise
|
|
// status_filter selects exactly one status.
|
|
// - document_count is a correlated subquery on documents.custom_schema_id.
|
|
// - client_id scoping is strict (cross-client isolation).
|
|
// - LIMIT/OFFSET paginate across an ORDER BY (name, version DESC) window.
|
|
func TestListClientMetadataSchemas_Filters(t *testing.T) {
|
|
t.Parallel()
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
ctx := t.Context()
|
|
|
|
cfg := &serviceconfig.BaseConfig{}
|
|
test.CreateDB(t, cfg)
|
|
pool := cfg.GetDBPool()
|
|
require.NotNil(t, pool)
|
|
queries := cfg.GetDBQueries()
|
|
require.NotNil(t, queries)
|
|
|
|
const (
|
|
clientA = "SQLC_LIST_A"
|
|
clientB = "SQLC_LIST_B"
|
|
)
|
|
resetClient(t, ctx, cfg, clientA, "sqlc-list-a")
|
|
resetClient(t, ctx, cfg, clientB, "sqlc-list-b")
|
|
|
|
// Shared fixture on clientA: two alpha versions (v1 superseded, v2 active)
|
|
// and one beta v1 active. Every later subtest that seeds additional rows
|
|
// does so on a DIFFERENT name so it doesn't disturb these.
|
|
alphaV1 := seedSchema(t, ctx, queries, clientA, "alpha", "", 1, repository.SchemaStatusTypeSuperseded)
|
|
alphaV2 := seedSchema(t, ctx, queries, clientA, "alpha", "", 2, repository.SchemaStatusTypeActive)
|
|
betaV1 := seedSchema(t, ctx, queries, clientA, "beta", "", 1, repository.SchemaStatusTypeActive)
|
|
|
|
t.Run("default_filters_returns_latest_active", func(t *testing.T) {
|
|
// include_all_versions=false collapses to MAX(version) per name, so
|
|
// the superseded alpha v1 drops out. Combined with status_filter=active,
|
|
// the result is exactly {alpha v2, beta v1}.
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientA,
|
|
NameFilter: nil,
|
|
IncludeAllStatuses: false,
|
|
StatusFilter: repository.SchemaStatusTypeActive,
|
|
IncludeAllVersions: false,
|
|
OffsetVal: 0,
|
|
LimitVal: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Build a set of (name, version) tuples for assertion.
|
|
type tuple struct {
|
|
name string
|
|
version int32
|
|
}
|
|
got := map[tuple]bool{}
|
|
for _, r := range rows {
|
|
got[tuple{r.Name, r.Version}] = true
|
|
}
|
|
require.Truef(t, got[tuple{"alpha", 2}], "result must contain (alpha, v2); got=%v", got)
|
|
require.Truef(t, got[tuple{"beta", 1}], "result must contain (beta, v1); got=%v", got)
|
|
require.Falsef(t, got[tuple{"alpha", 1}], "result must NOT contain (alpha, v1) when include_all_versions=false; got=%v", got)
|
|
})
|
|
|
|
t.Run("include_all_versions_returns_every_row", func(t *testing.T) {
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientA,
|
|
NameFilter: nil,
|
|
IncludeAllStatuses: true, // don't let status filter confuse this assertion
|
|
StatusFilter: repository.SchemaStatusTypeActive,
|
|
IncludeAllVersions: true,
|
|
OffsetVal: 0,
|
|
LimitVal: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Shared fixture yields 3 rows (alpha v1, alpha v2, beta v1).
|
|
// Other subtests may have added MORE rows to clientA by the time
|
|
// this subtest runs (t.Run children run sequentially inside the
|
|
// top-level test so ordering is deterministic — this subtest runs
|
|
// second, before any seed-adding subtest). Assert the three
|
|
// fixture rows are present rather than an exact length.
|
|
ids := map[uuid.UUID]bool{}
|
|
for _, r := range rows {
|
|
ids[r.ID] = true
|
|
}
|
|
require.True(t, ids[alphaV1.ID], "include_all_versions must include alpha v1")
|
|
require.True(t, ids[alphaV2.ID], "include_all_versions must include alpha v2")
|
|
require.True(t, ids[betaV1.ID], "include_all_versions must include beta v1")
|
|
})
|
|
|
|
t.Run("name_filter_narrows", func(t *testing.T) {
|
|
// The generated SQL uses EXACT match (`cms.name = $2::varchar`),
|
|
// NOT ILIKE. Passing "alpha" must return only alpha rows.
|
|
nameFilter := "alpha"
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientA,
|
|
NameFilter: &nameFilter,
|
|
IncludeAllStatuses: true,
|
|
StatusFilter: repository.SchemaStatusTypeActive,
|
|
IncludeAllVersions: true,
|
|
OffsetVal: 0,
|
|
LimitVal: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, rows, "name_filter=alpha must return at least one row")
|
|
for _, r := range rows {
|
|
require.Equal(t, "alpha", r.Name,
|
|
"name_filter is exact match; every row must have name=alpha, got %q", r.Name)
|
|
}
|
|
})
|
|
|
|
t.Run("status_filter_superseded", func(t *testing.T) {
|
|
// Seed one additional row with status=superseded on a name that no
|
|
// other subtest touches, so we can assert status filtering cleanly.
|
|
gammaV1 := seedSchema(t, ctx, queries, clientA, "gamma", "", 1, repository.SchemaStatusTypeSuperseded)
|
|
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientA,
|
|
NameFilter: nil,
|
|
IncludeAllStatuses: false,
|
|
StatusFilter: repository.SchemaStatusTypeSuperseded,
|
|
IncludeAllVersions: true, // look at every row, filter by status only
|
|
OffsetVal: 0,
|
|
LimitVal: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
sawGamma := false
|
|
for _, r := range rows {
|
|
require.Equal(t, repository.SchemaStatusTypeSuperseded, r.Status,
|
|
"status_filter=superseded must not return %s rows", r.Status)
|
|
if r.ID == gammaV1.ID {
|
|
sawGamma = true
|
|
}
|
|
}
|
|
require.True(t, sawGamma, "result must include the seeded gamma superseded row")
|
|
})
|
|
|
|
t.Run("include_all_statuses_returns_all", func(t *testing.T) {
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientA,
|
|
NameFilter: nil,
|
|
IncludeAllStatuses: true,
|
|
StatusFilter: repository.SchemaStatusTypeActive, // ignored
|
|
IncludeAllVersions: true,
|
|
OffsetVal: 0,
|
|
LimitVal: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// The shared-fixture rows plus the gamma row from the previous
|
|
// subtest must all come back regardless of status.
|
|
seenStatuses := map[repository.SchemaStatusType]bool{}
|
|
for _, r := range rows {
|
|
seenStatuses[r.Status] = true
|
|
}
|
|
require.True(t, seenStatuses[repository.SchemaStatusTypeActive], "result must include active rows")
|
|
require.True(t, seenStatuses[repository.SchemaStatusTypeSuperseded], "result must include superseded rows")
|
|
})
|
|
|
|
t.Run("document_count_is_populated", func(t *testing.T) {
|
|
// Fresh schema on clientB with two bound documents. The correlated
|
|
// subquery must report DocumentCount=2.
|
|
schemaB := seedSchema(t, ctx, queries, clientB, "docbound", "", 1, repository.SchemaStatusTypeActive)
|
|
|
|
doc1, err := queries.CreateDocument(ctx, &repository.CreateDocumentParams{
|
|
Clientid: clientB,
|
|
Hash: "sqlc_list_doc_hash_1",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = pool.Exec(ctx,
|
|
`UPDATE documents SET custom_schema_id = $1 WHERE id = $2`,
|
|
schemaB.ID, doc1)
|
|
require.NoError(t, err)
|
|
|
|
doc2, err := queries.CreateDocument(ctx, &repository.CreateDocumentParams{
|
|
Clientid: clientB,
|
|
Hash: "sqlc_list_doc_hash_2",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = pool.Exec(ctx,
|
|
`UPDATE documents SET custom_schema_id = $1 WHERE id = $2`,
|
|
schemaB.ID, doc2)
|
|
require.NoError(t, err)
|
|
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientB,
|
|
NameFilter: nil,
|
|
IncludeAllStatuses: true,
|
|
StatusFilter: repository.SchemaStatusTypeActive,
|
|
IncludeAllVersions: true,
|
|
OffsetVal: 0,
|
|
LimitVal: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, rows, 1, "clientB fixture has exactly one schema")
|
|
require.Equal(t, schemaB.ID, rows[0].ID)
|
|
require.Equal(t, int64(2), rows[0].DocumentCount,
|
|
"DocumentCount must reflect bound documents via correlated subquery")
|
|
})
|
|
|
|
t.Run("cross_client_isolation", func(t *testing.T) {
|
|
// Listing for clientA must not leak any clientB rows — and we know
|
|
// clientB now has a schema from the previous subtest.
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientA,
|
|
NameFilter: nil,
|
|
IncludeAllStatuses: true,
|
|
StatusFilter: repository.SchemaStatusTypeActive,
|
|
IncludeAllVersions: true,
|
|
OffsetVal: 0,
|
|
LimitVal: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, rows)
|
|
for _, r := range rows {
|
|
require.Equal(t, clientA, r.ClientID,
|
|
"cross-client isolation: clientA query must not leak %s rows", r.ClientID)
|
|
}
|
|
})
|
|
|
|
t.Run("pagination_limit_offset", func(t *testing.T) {
|
|
// Seed 5 schemas with distinct names on a NEW client so the
|
|
// pagination assertions don't have to account for shared-fixture
|
|
// rows accumulated by the earlier subtests. Uses a third client id
|
|
// to stay isolated.
|
|
const clientC = "SQLC_LIST_C"
|
|
resetClient(t, ctx, cfg, clientC, "sqlc-list-c")
|
|
|
|
names := []string{"p1_apple", "p2_banana", "p3_cherry", "p4_date", "p5_elderberry"}
|
|
for _, n := range names {
|
|
seedSchema(t, ctx, queries, clientC, n, "", 1, repository.SchemaStatusTypeActive)
|
|
}
|
|
|
|
fetch := func(offset, limit int64) []*repository.ListClientMetadataSchemasRow {
|
|
rows, err := queries.ListClientMetadataSchemas(ctx, &repository.ListClientMetadataSchemasParams{
|
|
ClientID: clientC,
|
|
NameFilter: nil,
|
|
IncludeAllStatuses: true,
|
|
StatusFilter: repository.SchemaStatusTypeActive,
|
|
IncludeAllVersions: true,
|
|
OffsetVal: offset,
|
|
LimitVal: limit,
|
|
})
|
|
require.NoError(t, err)
|
|
return rows
|
|
}
|
|
|
|
page1 := fetch(0, 2)
|
|
require.Len(t, page1, 2)
|
|
page2 := fetch(2, 2)
|
|
require.Len(t, page2, 2)
|
|
page3 := fetch(4, 2)
|
|
require.Len(t, page3, 1)
|
|
|
|
// Combined across the three calls we must see every seeded name
|
|
// exactly once — no duplicates, no gaps.
|
|
seen := map[string]int{}
|
|
for _, r := range page1 {
|
|
seen[r.Name]++
|
|
}
|
|
for _, r := range page2 {
|
|
seen[r.Name]++
|
|
}
|
|
for _, r := range page3 {
|
|
seen[r.Name]++
|
|
}
|
|
require.Len(t, seen, 5, "pagination must cover all 5 distinct schemas")
|
|
for _, n := range names {
|
|
require.Equal(t, 1, seen[n], "name %q must appear exactly once across pages", n)
|
|
}
|
|
})
|
|
}
|