Providers read the seat's plans and retire only on the operator's yes, removing the credential
ADR 0570 clauses 2 and 5: gitea, keycloak, postgres and route-adapter read mesh-provisioner.plans and .provisions instead of the controller's assignments. postgres hands the database to a keeper and drops the login; keycloak replaces the client's secret and keeps the client, so the data stays (issue 1954, V4).
This commit is contained in:
@@ -120,8 +120,8 @@ func TestProvisionerCreatesTheDatabaseThenItsExtensions(t *testing.T) {
|
||||
if err := a.Retire(ctx, "mesh_ace_letta", nil, "test", time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
noDrop(t, f.statements())
|
||||
if !has(f.statements(), `NOLOGIN`) {
|
||||
// On the operator's yes the login goes and the database stays (ADR 0570 clause 5).
|
||||
if !has(f.statements(), `^DROP ROLE "mesh_ace_letta"$`) || has(f.statements(), `DROP DATABASE`) {
|
||||
t.Fatal(f.statements())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,14 @@ func TestLive(t *testing.T) {
|
||||
if err != nil || len(r.Rows) != 1 || cell(r.Rows[0], 0) != "1" {
|
||||
t.Fatal("retiring lost the data:", r, err)
|
||||
}
|
||||
// The login is removed, its database kept by its keeper (ADR 0570 clause 5).
|
||||
if has, _ := c.exists(ctx, "SELECT 1 FROM pg_roles WHERE rolname = "+Literal(p.As)); has {
|
||||
t.Fatal("the retired login is still there")
|
||||
}
|
||||
if has, _ := c.exists(ctx, "SELECT 1 FROM pg_database d JOIN pg_roles r ON r.oid = d.datdba WHERE d.datname = "+
|
||||
Literal(p.As)+" AND r.rolname = "+Literal(p.As+"~retired")); !has {
|
||||
t.Fatal("the database is not its keeper's")
|
||||
}
|
||||
inv, err := a.Inventory(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -35,10 +35,14 @@ func (a provisioner) Create(ctx context.Context, p harness.Provision) error {
|
||||
if err := a.pg.CreateDatabaseAndRole(ctx, database, p.As, p.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
// A login retired and asked for again takes back what its keeper kept (retire_pg.go).
|
||||
if err := a.pg.Reclaim(ctx, database, p.As); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.pg.EnsureExtensions(ctx, database, extensions); err != nil {
|
||||
return err
|
||||
}
|
||||
// The active mark: a retired consumer asked for again loses its mark to delete here, its LOGIN
|
||||
// The active mark: a login locked before ADR 0570 and asked for again loses its mark to delete here, its LOGIN
|
||||
// already set again by the role statement above (novox/hq ADR 0230).
|
||||
if err := a.pg.MarkActive(ctx, p.As, p.Consumer); err != nil {
|
||||
return err
|
||||
@@ -47,11 +51,10 @@ func (a provisioner) Create(ctx context.Context, p harness.Provision) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retire locks the login, never drops (novox/hq issue 241, ADR 0230): the database is kept under its
|
||||
// own name, the role marked retired with when and why (retire_pg.go). On 2026-10-04 a misread
|
||||
// contributions file withdrew every consumer at once, and dropping made that a loss of seven databases.
|
||||
// Deleting is `cleanup delete`, a person's act; taking a database out of service by hand is
|
||||
// postgres_retire_database, which renames rather than drops.
|
||||
// Retire removes the login on the operator's yes (novox/hq ADR 0570 clause 5, ADR 0114 §2): what it owns is handed
|
||||
// to its keeper, its sessions end and the login is dropped; the database and its data are kept (retire_pg.go).
|
||||
// Deleting is `cleanup delete`, a person's act; taking a database out of service by hand is postgres_retire_database,
|
||||
// which renames rather than drops.
|
||||
func (a provisioner) Retire(ctx context.Context, as string, _ map[string]any, why string, at time.Time) error {
|
||||
if err := a.pg.RetireRole(ctx, as, why, at); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
package main
|
||||
|
||||
// What retired means in postgres (novox/hq ADR 0230).
|
||||
// What retired means in postgres (novox/hq ADR 0570 clause 5, ADR 0114 §2 and §9; ADR 0230 for the delete).
|
||||
//
|
||||
// **Retired is the login locked, the database kept, and the role marked.** `ALTER ROLE … NOLOGIN` —
|
||||
// the consumer's login can no longer connect, as itself, to anything — and its open sessions are
|
||||
// ended. The database, its owner, its grants and every byte in it stay exactly as they were: CONNECT is
|
||||
// not revoked and the database still allows connections, because the backup contribution dumps every
|
||||
// database and a retired one is still worth backing up, and because re-enabling must give it back as it
|
||||
// was. The mark is the role's comment, a JSON object the mesh owns:
|
||||
// **Retired is the login removed, the database kept under a keeper role, and the keeper marked.** Retiring happens
|
||||
// only on the operator's yes to a retiring provision (the SDK's moved path, Gone). What the login owns, its database
|
||||
// first, is handed to its keeper, a role of its own that cannot log in (`<login>~retired`, KeeperOf), its open
|
||||
// sessions are ended, and the login is dropped: nothing can connect as it any more, and no credential of it is left.
|
||||
// The database, its grants to others and every byte in it stay: the backup contribution dumps every database and a
|
||||
// retired one is still worth backing up. The mark is the keeper's comment, a JSON object the mesh owns:
|
||||
//
|
||||
// {"mesh":"consumer","node":"ace"} active
|
||||
// {"mesh":"consumer","node":"ace","retired":"2026-10-06T…Z","why":"…"} retired
|
||||
// {"mesh":"consumer","node":"ace"} an active login
|
||||
// {"mesh":"consumer","node":"ace","retired":"2026-10-06T…Z","why":"…","login":"…"} a keeper
|
||||
//
|
||||
// Asked for again, the ordinary create sets LOGIN and the password again and writes the active mark.
|
||||
// Deleted — only through `cleanup delete` — the database is dropped, then the role, unless it still owns
|
||||
// another database (a set-aside copy), which is said and kept.
|
||||
// Asked for again, the ordinary create makes the login again and its keeper hands everything back (Reclaim) and
|
||||
// goes. Deleted — only through `cleanup delete` — the database is dropped, then the keeper.
|
||||
//
|
||||
// **A login retired before this record** was only locked (`NOLOGIN`, its own role marked retired); it is listed and
|
||||
// deleted as before, and create sets its LOGIN again.
|
||||
//
|
||||
// **What the mesh made is recognised by its mark, or by its shape before the mark existed**: a role
|
||||
// valid until 'infinity' (only the mesh's role statement says that) that owns a database of its own
|
||||
@@ -41,6 +43,17 @@ type roleMark struct {
|
||||
Node string `json:"node,omitempty"`
|
||||
Retired string `json:"retired,omitempty"`
|
||||
Why string `json:"why,omitempty"`
|
||||
// Login is, on a keeper, the login it keeps for.
|
||||
Login string `json:"login,omitempty"`
|
||||
}
|
||||
|
||||
// KeeperOf is the role a retired login's database and objects are handed to: the login's name with `~retired`.
|
||||
func KeeperOf(login string) (string, error) {
|
||||
k := login + "~retired"
|
||||
if len(k) > 63 {
|
||||
return "", fmt.Errorf("%s is too long a login for a keeper role (%s is over postgres's 63 bytes)", login, k)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func readMark(comment string) (roleMark, bool) {
|
||||
@@ -64,8 +77,9 @@ func (c *Client) MarkActive(ctx context.Context, role, node string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RetireRole locks the login, ends its sessions and marks it retired with when and why. Its database
|
||||
// is not touched. A role that does not exist has nothing to retire.
|
||||
// RetireRole removes a login on the operator's yes (ADR 0570 clause 5): what it owns is handed to its keeper, marked
|
||||
// with when and why, its sessions end and the login is dropped. The database and its data are kept. A login that does
|
||||
// not exist has nothing to retire.
|
||||
func (c *Client) RetireRole(ctx context.Context, role, why string, at time.Time) error {
|
||||
r, err := c.Query(ctx, "", "SELECT coalesce(shobj_description(oid, 'pg_authid'), '') FROM pg_roles WHERE rolname = "+
|
||||
Literal(role))
|
||||
@@ -76,10 +90,66 @@ func (c *Client) RetireRole(ctx context.Context, role, why string, at time.Time)
|
||||
return nil
|
||||
}
|
||||
prev, _ := readMark(cell(r.Rows[0], 0))
|
||||
keeper, err := KeeperOf(role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
has, err := c.exists(ctx, "SELECT 1 FROM pg_roles WHERE rolname = "+Literal(keeper))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
if _, err := c.Query(ctx, "", fmt.Sprintf("CREATE ROLE %s NOLOGIN", Ident(keeper))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := c.Query(ctx, "", MarkStatement(keeper, roleMark{Node: prev.Node, Retired: at.UTC().Format(time.RFC3339),
|
||||
Why: why, Login: role})); err != nil {
|
||||
return err
|
||||
}
|
||||
// No new session, and none left open.
|
||||
if err := c.LockRole(ctx, role); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.Query(ctx, "", MarkStatement(role, roleMark{Node: prev.Node, Retired: at.UTC().Format(time.RFC3339), Why: why}))
|
||||
// In its own database, where its objects are; the database itself is a shared object, handed with them.
|
||||
in := ""
|
||||
if own, err := c.exists(ctx, "SELECT 1 FROM pg_database WHERE datname = "+Literal(role)); err != nil {
|
||||
return err
|
||||
} else if own {
|
||||
in = role
|
||||
}
|
||||
for _, sql := range []string{
|
||||
fmt.Sprintf("REASSIGN OWNED BY %s TO %s", Ident(role), Ident(keeper)),
|
||||
fmt.Sprintf("DROP OWNED BY %s", Ident(role)),
|
||||
} {
|
||||
if _, err := c.Query(ctx, in, sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = c.Query(ctx, "", fmt.Sprintf("DROP ROLE %s", Ident(role)))
|
||||
return err
|
||||
}
|
||||
|
||||
// Reclaim gives a login made again what its keeper kept (ADR 0570 clause 5: a provision needed again): the keeper's
|
||||
// database and objects are handed back, and the keeper goes. A login with no keeper has nothing to reclaim.
|
||||
func (c *Client) Reclaim(ctx context.Context, database, role string) error {
|
||||
keeper, err := KeeperOf(role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
has, err := c.exists(ctx, "SELECT 1 FROM pg_roles WHERE rolname = "+Literal(keeper))
|
||||
if err != nil || !has {
|
||||
return err
|
||||
}
|
||||
for _, sql := range []string{
|
||||
fmt.Sprintf("REASSIGN OWNED BY %s TO %s", Ident(keeper), Ident(role)),
|
||||
fmt.Sprintf("DROP OWNED BY %s", Ident(keeper)),
|
||||
} {
|
||||
if _, err := c.Query(ctx, database, sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = c.Query(ctx, "", fmt.Sprintf("DROP ROLE %s", Ident(keeper)))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -89,7 +159,8 @@ var setAside = regexp.MustCompile(`_deleted_(\d{8})$`)
|
||||
// of its own database.
|
||||
const InventoryStatement = `SELECT r.rolname, r.rolcanlogin, coalesce(shobj_description(r.oid, 'pg_authid'), '') AS mark,
|
||||
coalesce(r.rolvaliduntil = 'infinity', false) AS mesh_shaped,
|
||||
(SELECT pg_database_size(d.datname) FROM pg_database d WHERE d.datname = r.rolname AND d.datdba = r.oid) AS size
|
||||
(SELECT pg_database_size(d.datname) FROM pg_database d WHERE d.datdba = r.oid
|
||||
AND (d.datname = r.rolname OR d.datname || '~retired' = r.rolname)) AS size
|
||||
FROM pg_roles r
|
||||
WHERE r.rolname !~ '^pg_' AND NOT r.rolsuper AND r.rolname <> ` + "'" + Reader + "'" + `
|
||||
ORDER BY r.rolname`
|
||||
@@ -118,6 +189,9 @@ func (c *Client) Inventory(ctx context.Context) (harness.Inventory, error) {
|
||||
continue
|
||||
}
|
||||
at, _ := time.Parse(time.RFC3339, m.Retired)
|
||||
if m.Login != "" {
|
||||
name = m.Login // a keeper: listed under the login it keeps for
|
||||
}
|
||||
inv.Retired = append(inv.Retired, harness.Retired{Consumer: name, Node: m.Node, RetiredAt: at, Why: m.Why,
|
||||
SizeBytes: sizeOf(size), Kind: harness.KindConsumer})
|
||||
}
|
||||
@@ -160,7 +234,19 @@ func (c *Client) DeleteRetired(ctx context.Context, r harness.Retired) (int64, e
|
||||
return freed, err
|
||||
}
|
||||
if len(row.Rows) == 0 {
|
||||
return freed, nil
|
||||
// Retired by removing the login: its keeper goes with the database it kept.
|
||||
keeper, err := KeeperOf(r.Consumer)
|
||||
if err != nil {
|
||||
return freed, err
|
||||
}
|
||||
if has, err := c.exists(ctx, "SELECT 1 FROM pg_roles WHERE rolname = "+Literal(keeper)); err != nil || !has {
|
||||
return freed, err
|
||||
}
|
||||
if _, err := c.Query(ctx, "", fmt.Sprintf("DROP OWNED BY %s", Ident(keeper))); err != nil {
|
||||
return freed, err
|
||||
}
|
||||
_, err = c.Query(ctx, "", fmt.Sprintf("DROP ROLE %s", Ident(keeper)))
|
||||
return freed, err
|
||||
}
|
||||
owns, err := c.Query(ctx, "", "SELECT datname FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = "+
|
||||
Literal(r.Consumer)+")")
|
||||
|
||||
@@ -10,31 +10,74 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRetiringLocksMarksAndDropsNothing(t *testing.T) {
|
||||
// ADR 0570 clause 5, on the operator's yes: the login's database and objects go to its keeper, marked; its sessions
|
||||
// end; the login is dropped; the database is not.
|
||||
func TestRetiringHandsTheDatabaseToAKeeperAndRemovesTheLogin(t *testing.T) {
|
||||
f, c := newFake(admin)
|
||||
f.answer(`shobj_description\(oid`, []string{"c"}, []string{`{"mesh":"consumer","node":"ace"}`})
|
||||
f.answer(`SELECT 1 FROM pg_roles`, []string{"?column?"}, []string{"1"})
|
||||
f.answer(`pg_roles WHERE rolname = 'mesh_ace_letta'$`, []string{"?column?"}, []string{"1"})
|
||||
f.answer(`pg_database WHERE datname = 'mesh_ace_letta'$`, []string{"?column?"}, []string{"1"})
|
||||
at := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
|
||||
if err := c.RetireRole(ctx, "mesh_ace_letta", "the mesh stopped asking for it", at); err != nil {
|
||||
if err := c.RetireRole(ctx, "mesh_ace_letta", "the operator approved its retirement", at); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sql := f.statements()
|
||||
if !has(sql, `ALTER ROLE "mesh_ace_letta" NOLOGIN`) || !has(sql, `pg_terminate_backend.*usename = 'mesh_ace_letta'`) ||
|
||||
!has(sql, `COMMENT ON ROLE "mesh_ace_letta" IS '\{"mesh":"consumer","node":"ace","retired":"2026-10-06T12:00:00Z","why":"the mesh stopped asking for it"\}'`) {
|
||||
t.Fatal(strings.Join(sql, "\n"))
|
||||
var order []string
|
||||
for _, call := range f.calls {
|
||||
switch {
|
||||
case strings.HasPrefix(call.SQL, "SELECT"):
|
||||
case strings.HasPrefix(call.SQL, "REASSIGN"), strings.HasPrefix(call.SQL, "DROP OWNED"):
|
||||
order = append(order, call.Database+": "+call.SQL)
|
||||
default:
|
||||
order = append(order, call.SQL)
|
||||
}
|
||||
}
|
||||
noDrop(t, sql)
|
||||
if has(sql, `REVOKE|ALLOW_CONNECTIONS|RENAME`) {
|
||||
t.Fatal("retiring touched the database:", strings.Join(sql, "\n"))
|
||||
if !has(f.statements(), `pg_terminate_backend.*usename = 'mesh_ace_letta'`) {
|
||||
t.Fatal(strings.Join(f.statements(), "\n"))
|
||||
}
|
||||
want := []string{
|
||||
`CREATE ROLE "mesh_ace_letta~retired" NOLOGIN`,
|
||||
`COMMENT ON ROLE "mesh_ace_letta~retired" IS '{"mesh":"consumer","node":"ace","retired":"2026-10-06T12:00:00Z","why":"the operator approved its retirement","login":"mesh_ace_letta"}'`,
|
||||
`ALTER ROLE "mesh_ace_letta" NOLOGIN`,
|
||||
`mesh_ace_letta: REASSIGN OWNED BY "mesh_ace_letta" TO "mesh_ace_letta~retired"`,
|
||||
`mesh_ace_letta: DROP OWNED BY "mesh_ace_letta"`,
|
||||
`DROP ROLE "mesh_ace_letta"`,
|
||||
}
|
||||
if strings.Join(order, "\n") != strings.Join(want, "\n") {
|
||||
t.Fatalf("sent:\n%s\nwant:\n%s", strings.Join(order, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
if has(f.statements(), `DROP DATABASE|REVOKE|ALLOW_CONNECTIONS|RENAME`) {
|
||||
t.Fatal("retiring touched the database:", strings.Join(f.statements(), "\n"))
|
||||
}
|
||||
|
||||
// No such role: nothing to retire, nothing done.
|
||||
f, c = newFake(admin)
|
||||
if err := c.RetireRole(ctx, "gone", "x", at); err != nil || has(f.statements(), `ALTER|COMMENT`) {
|
||||
if err := c.RetireRole(ctx, "gone", "x", at); err != nil || has(f.statements(), `ALTER|COMMENT|CREATE|DROP`) {
|
||||
t.Fatal(f.statements(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// A login made again takes back what its keeper kept, and the keeper goes; with no keeper, nothing is sent.
|
||||
func TestALoginMadeAgainReclaimsWhatItsKeeperKept(t *testing.T) {
|
||||
f, c := newFake(admin)
|
||||
if err := c.Reclaim(ctx, "mesh_ace_letta", "mesh_ace_letta"); err != nil || has(f.statements(), `REASSIGN|DROP`) {
|
||||
t.Fatal(f.statements(), err)
|
||||
}
|
||||
f, c = newFake(admin)
|
||||
f.answer(`pg_roles WHERE rolname = 'mesh_ace_letta~retired'$`, []string{"?column?"}, []string{"1"})
|
||||
if err := c.Reclaim(ctx, "mesh_ace_letta", "mesh_ace_letta"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []string
|
||||
for _, call := range f.calls[1:] {
|
||||
got = append(got, call.Database+": "+call.SQL)
|
||||
}
|
||||
want := `mesh_ace_letta: REASSIGN OWNED BY "mesh_ace_letta~retired" TO "mesh_ace_letta"` + "\n" +
|
||||
`mesh_ace_letta: DROP OWNED BY "mesh_ace_letta~retired"` + "\n" + `postgres: DROP ROLE "mesh_ace_letta~retired"`
|
||||
if strings.Join(got, "\n") != want {
|
||||
t.Fatal(strings.Join(got, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheInventoryIsWhatTheMeshMadeByMarkOrShape(t *testing.T) {
|
||||
f, c := newFake(admin)
|
||||
f.answer(`FROM pg_roles r`, []string{"rolname", "rolcanlogin", "mark", "mesh_shaped", "size"},
|
||||
@@ -44,6 +87,7 @@ func TestTheInventoryIsWhatTheMeshMadeByMarkOrShape(t *testing.T) {
|
||||
[]string{"locked_before", "f", "", "t", "400"},
|
||||
[]string{"hal_registry", "t", "", "f", "500"},
|
||||
[]string{"jochens", "t", "", "t", ""},
|
||||
[]string{"mesh_ace_umami~retired", "f", `{"mesh":"consumer","node":"ace","retired":"2026-10-29T12:00:00Z","why":"approved","login":"mesh_ace_umami"}`, "f", "600"},
|
||||
)
|
||||
f.answer(`_deleted_`, []string{"datname", "size"}, []string{"mesh_novox_gitea_deleted_20261005", "7771827"})
|
||||
inv, err := c.Inventory(ctx)
|
||||
@@ -53,7 +97,7 @@ func TestTheInventoryIsWhatTheMeshMadeByMarkOrShape(t *testing.T) {
|
||||
if strings.Join(inv.Active, ",") != "active_marked,active_shaped" {
|
||||
t.Fatalf("active: %v", inv.Active)
|
||||
}
|
||||
if len(inv.Retired) != 3 {
|
||||
if len(inv.Retired) != 4 {
|
||||
t.Fatalf("%+v", inv.Retired)
|
||||
}
|
||||
r := inv.Retired[0]
|
||||
@@ -64,7 +108,10 @@ func TestTheInventoryIsWhatTheMeshMadeByMarkOrShape(t *testing.T) {
|
||||
if r := inv.Retired[1]; r.Consumer != "locked_before" || !r.RetiredAt.IsZero() || r.Kind != harness.KindConsumer {
|
||||
t.Fatalf("found locked without a mark: %+v", r)
|
||||
}
|
||||
if r := inv.Retired[2]; r.Kind != KindSetAside || r.RetiredAt.Format("20060102") != "20261005" || r.SizeBytes != 7771827 {
|
||||
if r := inv.Retired[2]; r.Consumer != "mesh_ace_umami" || r.SizeBytes != 600 || r.Why != "approved" {
|
||||
t.Fatalf("a keeper is listed under the login it keeps for: %+v", r)
|
||||
}
|
||||
if r := inv.Retired[3]; r.Kind != KindSetAside || r.RetiredAt.Format("20060102") != "20261005" || r.SizeBytes != 7771827 {
|
||||
t.Fatalf("set aside: %+v", r)
|
||||
}
|
||||
}
|
||||
@@ -97,6 +144,18 @@ func TestDeletingDropsTheDatabaseThenTheRole(t *testing.T) {
|
||||
if db < 0 || role < db {
|
||||
t.Fatal(sql)
|
||||
}
|
||||
// A login removed on retiring: the database, then its keeper.
|
||||
f, c = newFake(admin)
|
||||
f.answer(`SELECT pg_database_size`, []string{"size"}, []string{"9000"})
|
||||
f.answer(`pg_roles WHERE rolname = 'mesh_ace_umami~retired'$`, []string{"?column?"}, []string{"1"})
|
||||
if _, err := c.DeleteRetired(ctx, harness.Retired{Consumer: "mesh_ace_umami", Kind: harness.KindConsumer}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sql = strings.Join(f.statements(), "\n")
|
||||
db, keeper := strings.Index(sql, `DROP DATABASE "mesh_ace_umami"`), strings.Index(sql, `DROP ROLE "mesh_ace_umami~retired"`)
|
||||
if db < 0 || keeper < db {
|
||||
t.Fatal(sql)
|
||||
}
|
||||
// A role still owning a set-aside database is kept, and said.
|
||||
f, c = newFake(admin)
|
||||
f.answer(`SELECT rolcanlogin`, []string{"rolcanlogin"}, []string{"f"})
|
||||
|
||||
Reference in New Issue
Block a user