0636 — The client takes the pool
Edit this pageContext. [0633] left the setup at three steps: build a pool, wrap it in a datastore, hand the datastore to NewClient. The middle step carries no decision. PostgresDatastore is {Pool, Schema}, and 73 of the 75 call sites in the repo build one for the sole purpose of passing it to NewClient — the playground’s thirteen scenarios do it identically, and scenario 01’s own header already charged it against the API as a concept the user holds before any domain code. Nothing in the Go-on-Postgres ecosystem asks for an intermediate wrapper: river.NewClient(riverpgxv5.New(pool), cfg) names its driver adapter because River supports more than one driver, and pgxpool.New(ctx, url) is a single step. Vulkan has one driver and one schema field, so the wrapper is a step with nothing in it.
Decision. NewClient(ctx context.Context, pool *pgxpool.Pool, cfg *ClientConfig) builds the datastore itself, and the two-step pool -> client shape is the only one the doc site teaches. ClientConfig gains Schema, passed through untouched to PostgresDatastoreConfig, which stays the single owner of the vulkan default and the lowercase-identifier rule — the client re-spells neither. vulkan.InTransaction(ctx, ds, fn) — the one other public verb that took a datastore — becomes the method client.InTransaction(ctx, fn); leaving it free would have made the transactional-produce sample read vulkan.InTransaction(ctx, client.Datastore(), fn), which is worse than what it replaced. Client.Datastore() returns the built handle for the paths that legitimately need it: a lab constructing controllers directly, and otelvulkan.NewExporter(ds, cfg). That accessor is what makes the fold safe rather than merely shorter — with the client owning the schema, a caller building a second datastore beside it could set a different one, and schemalab and the CLI both pass a schema in two places today. One client is one datastore, so there is nothing to diverge. NewPostgresDatastore stays exported for the programs that work at the datastore layer rather than through the client — tools/compat and the dozen labs whose scenarios drive controllers directly.
Consequences. NewClient grows a ctx and now performs I/O — the construction ping moves inside it — so the “it does not connect” contract is gone; one constructor verifying connectivity replaces two, and the caller still owns defer pool.Close(). The doc site drops the datastore import from its first program: a quickstart imports pgxpool and vulkan. The CLI collapses to one connection helper: openDatastore is gone, openClient returns (client, close, err) and takes the log level its caller wants — ERROR for the twenty-two one-shot commands, INFO for manager run, whose log stream is its output — and the commands that need the handle take client.Datastore(). The cost is that Client carries a public method returning a pgx-typed handle the docs do not mention — an escape hatch, deliberately unadvertised, and the pgx commitment it exposes was already public through PostgresDatastore.Pool and vulkan.Tx. Rejected: keeping NewClient(ds, cfg) beside the new form — two constructors for one assembly, and the ds-taking one is exactly the step being removed; putting Schema validation on ClientConfig — a second read path for a rule PostgresDatastoreConfig.Validate already owns; leaving labs to build their own datastore beside the client — that is the divergence this record exists to close.