Files
zeekayandhanzo-dev 633b692511
CI/CD / containment (push) Failing after 7s
Hanzo CI/CD / cicd (push) Successful in 19s
CI/CD / gate (push) Successful in 19s
CI/CD / image (push) Skipped
CI/CD / rollout (push) Skipped
CI/CD / reach (push) Skipped
CI/CD / fanout (push) Skipped
CI/CD / receipt (push) Skipped
the store prologue that 33 apps copied, and the one that got it wrong
`type Store` is declared 40 times under apps/ and 35 of those are structurally
IDENTICAL — `struct { db *sql.DB }`. That number is a trap and I am not acting
on it: in Go, git.Store and bot.Store are different types because they are in
different packages, each carries its own app's schema in its own methods, and
"consolidating" a one-field handle wrapper would produce `type Store struct{
shared.DB }` — a rename that moves no code and removes no duplication. 35 of 40
share a shape; ~0 share a MEANING. Optimising that number would be pure damage.

The duplication the number was pointing at is one level down, in the
CONSTRUCTOR, and it is exact. 33 stores open themselves with byte-identical
code modulo their own name:

	db, err := cek.Open(namespace.System(), "<app>", dir)
	if err != nil { return nil, fmt.Errorf("open <app> store: %w", err) }
	sqlpool.Single(db)

Those three lines are a PAIR that nothing paired. sqlpool.Single's own doc says
a two-statement read-modify-write (tracker's per-project issue number, agents'
MAX(seq)+1) is atomic ONLY because no second connection can interleave — so the
cap is a correctness requirement, and it was a separate call every caller had to
remember. 33 remembered.

apps/framework did not. It opens a cek database through an engine OpenDB
callback and never capped it, so its DocType store has been running with an
uncapped pool. That is the defect, and it is the one a 33-way copied prologue
exists to produce: the rule holds until someone writes the 34th store.

sqlpool.Open is the rule moved INSIDE the opener, so there is nothing left to
forget — the same argument as cloud.App being the only way to obtain an app.
36 call sites converted, framework included; the stores that open differently
(git takes a *sql.DB from the OrgStore cache, others carry extra migrations)
still do, because they are different and the point is not uniformity.

Measured, not assumed: shapes compared by AST field set, not by name. Of the
other collision families the brief named, Result (9 decls) and Config (10) have
ZERO structurally identical pairs, and Client has exactly one family of 3
(`base string; http *http.Client; token string`). Those are Go's package
qualifier working, and they were left alone.

Regression set EMPTY by name: all 15 failures in the touched packages
(TestDocTypeAndDocumentRoundTrip, TestForkCreatesProjectFromTemplate, the Redeem
family, …) fail identically on forge/main.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 20:08:31 -07:00

71 lines
3.3 KiB
Go

// Package sqlpool states, once, how cloud pools connections to a SQLite file.
//
// It is one sentence — one connection — and it is a package so that the sentence
// has somewhere to be true. It used to be written at every store that opened a
// database, together with a block of PRAGMAs restating the driver's own defaults;
// fifty-odd copies of a fact is fifty-odd chances for one of them to drift.
//
// The PRAGMAs are gone because hanzoai/sqlite already applies them, on every
// connection, on every backend — see sqlpool_test.go, which asserts exactly that
// and fails if it ever stops being true. What is NOT already applied everywhere
// is the pool cap, so that is what remains here.
package sqlpool
import (
"database/sql"
"fmt"
"github.com/hanzoai/cek"
"github.com/hanzoai/namespace"
)
// Single caps db at one connection.
//
// These databases are single-writer by construction, and two facts depend on the
// cap rather than merely benefiting from it:
//
// - A read-modify-write spanning two statements (tracker's per-project issue
// number, agents' MAX(seq)+1 event allocation) is atomic ONLY because no
// second connection can interleave. Widen the pool and those become races
// that a UNIQUE index turns into errors instead of corruption — on a good day.
// - On the pure-Go codec the file is an envelope decrypted to one plaintext
// copy; a checkpoint quiesces writers by taking that single connection.
//
// hanzoai/sqlite already caps the pool on the envelope path (envelope.go, at the
// sql.OpenDB) but NOT on the live-libsqlcipher path, which is the one the shipped
// image builds. Until that asymmetry is fixed upstream the cap has to be stated
// by the caller, and this is where cloud states it.
func Single(db *sql.DB) { db.SetMaxOpenConns(1) }
// Open opens the named SYSTEM-namespace database under dir, with the cap already
// applied. It is the one call an app makes to get its store's handle.
//
// It exists because the two lines it replaces were a PAIR that nothing paired.
// Thirty-three stores opened themselves with byte-identical code —
//
// db, err := cek.Open(namespace.System(), "<app>", dir)
// if err != nil { return nil, fmt.Errorf("open <app> store: %w", err) }
// sqlpool.Single(db)
//
// — and the correctness argument above (a two-statement read-modify-write is
// atomic ONLY because no second connection can interleave) rests entirely on the
// third line, which is a separate call the caller has to remember. Thirty-three
// remembered. apps/framework did not: it opens a cek database through an engine
// callback and never capped it, so its DocType store ran uncapped in production.
//
// A rule that lives in thirty-three copies of a prologue holds until someone
// writes a thirty-fourth store; a rule INSIDE the opener is a property of every
// handle that exists. Same argument as cloud.App being the only way to get an app.
//
// The name is the FILE key in the system namespace — the app's own name — never a
// path: these are the deployment's databases, not a tenant's. A per-org store
// comes through OrgDB, which names its owner.
func Open(name, dir string) (*sql.DB, error) {
db, err := cek.Open(namespace.System(), name, dir)
if err != nil {
return nil, fmt.Errorf("open %s store: %w", name, err)
}
Single(db)
return db, nil
}