model-access A: provider→vendor + adapter dispatch + static-key (ADR 0050 foundation) #13
@@ -47,9 +47,9 @@ func licenceAdd(ctx context.Context, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(positionals) != 2 {
|
if len(positionals) != 2 {
|
||||||
return errors.New(`licence add <provider> <name> [--serves '{"model":"..."}']`)
|
return errors.New(`licence add <vendor> <name> [--serves '{"model":"..."}']`)
|
||||||
}
|
}
|
||||||
provider, name := positionals[0], positionals[1]
|
vendor, name := positionals[0], positionals[1]
|
||||||
|
|
||||||
values := map[string]any{}
|
values := map[string]any{}
|
||||||
if strings.TrimSpace(*serves) != "" {
|
if strings.TrimSpace(*serves) != "" {
|
||||||
@@ -63,11 +63,11 @@ func licenceAdd(ctx context.Context, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer held.Close()
|
defer held.Close()
|
||||||
if err := held.Add(ctx, name, provider, values); err != nil {
|
if err := held.Add(ctx, name, vendor, values); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("%s (%s) recorded. Nothing uses it yet, and it has no key:\n"+
|
fmt.Printf("%s (%s) recorded. Nothing uses it yet, and it has no key:\n"+
|
||||||
" licence use %s <node> <module>\n licence key %s\n", name, provider, name, name)
|
" licence use %s <node> <module>\n licence key %s\n", name, vendor, name, name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ func licenceList(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("%s (%s)\n", one.Name, one.Provider)
|
fmt.Printf("%s (%s)\n", one.Name, one.Vendor)
|
||||||
if len(holders) == 0 {
|
if len(holders) == 0 {
|
||||||
fmt.Printf(" nobody uses it\n")
|
fmt.Printf(" nobody uses it\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// Package adapters is the per-vendor lifecycle a licence needs, selected by the licence's vendor.
|
||||||
|
//
|
||||||
|
// novox/hq ADR 0050: model access is one consumer-facing, vendor-blind provision, and *a vendor is
|
||||||
|
// an adapter* keyed by `licence.vendor`. A consumer names `model-access` and never a vendor; which
|
||||||
|
// vendor answers, and the lifecycle that vendor's credential needs, lives here — exactly as
|
||||||
|
// `public-dns` is one neutral interface answered by registrar-scoped providers (ADR 0044).
|
||||||
|
//
|
||||||
|
// **Phase A ships only the `static-key` shape.** A static-key vendor implements almost nothing: the
|
||||||
|
// credential is an operator-supplied value, sealed to each holder by the generic anonymous box
|
||||||
|
// (ADR 0024) and delivered unchanged. The refreshable-grant machinery — central rotation, an
|
||||||
|
// identity guard, a usage reading, refresh-token-stripped delivery — is Phase B, and its verbs are
|
||||||
|
// named here as optional capabilities (Refresher, Identifier, UsageReader) so the seam exists
|
||||||
|
// before the code does. The abstraction earns its keep by making the common vendor small, not the
|
||||||
|
// rare one clever (ADR 0050).
|
||||||
|
package adapters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-control/internal/secrets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shape is how a vendor's credential behaves, and the switch the ADR 0050 carve-out turns on.
|
||||||
|
type Shape string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StaticKey is an operator-supplied value with nothing to rotate. The full "the mesh cannot
|
||||||
|
// read what it stores" guarantee holds: accept seals it and discards the plaintext.
|
||||||
|
StaticKey Shape = "static-key"
|
||||||
|
// RefreshableGrant is an OAuth-style grant a manager node refreshes centrally (ADR 0050,
|
||||||
|
// Phase B). It carries the one bounded relaxation of that guarantee. No adapter of this shape
|
||||||
|
// ships in Phase A.
|
||||||
|
RefreshableGrant Shape = "refreshable-grant"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Adapter is a vendor's lifecycle, selected by a licence's vendor.
|
||||||
|
//
|
||||||
|
// Only these verbs are required, and they are all a static-key vendor needs: what shape it is, how
|
||||||
|
// a supplied value is sealed, and what a consumer is delivered. The optional capabilities below are
|
||||||
|
// found by type assertion, so a static-key adapter simply does not implement them.
|
||||||
|
type Adapter interface {
|
||||||
|
// Vendor is the name a licence's `vendor` field carries to select this adapter.
|
||||||
|
Vendor() string
|
||||||
|
// Shape is static-key or refreshable-grant.
|
||||||
|
Shape() Shape
|
||||||
|
// Accept takes an operator-supplied credential and seals it to one holder — the `accept` verb
|
||||||
|
// ADR 0024 defines. For a static key this is the generic seal, unchanged.
|
||||||
|
Accept(value, consumerKey, providerKey string) (secrets.Sealed, error)
|
||||||
|
// Deliver is the credential value a consumer receives. For a sealed static key it is the sealed
|
||||||
|
// blob unchanged: the mesh never unseals it, only the holding node's private key can. A
|
||||||
|
// refreshable-grant adapter (Phase B) strips the refresh token here.
|
||||||
|
Deliver(sealed string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresher is implemented only by a refreshable-grant adapter (ADR 0050, Phase B): the
|
||||||
|
// lease / rotate / publish machinery a manager node runs. A static-key adapter does not implement
|
||||||
|
// it, and a caller finds its absence by a type assertion.
|
||||||
|
type Refresher interface {
|
||||||
|
Refresh(ctx context.Context, licence string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier is the mis-binding guard (ADR 0050, Phase B): a vendor whose credential carries an
|
||||||
|
// account identity worth checking implements it. A static-key adapter does not.
|
||||||
|
type Identifier interface {
|
||||||
|
Identity(credential string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageReader is a vendor's usage reading (ADR 0050 / ADR 0054, Phase B), mapped to the common
|
||||||
|
// normalised grain. A static-key adapter does not implement it.
|
||||||
|
type UsageReader interface {
|
||||||
|
Usage(ctx context.Context, licence string) ([]UsageRow, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageRow is the vendor-neutral usage grain ADR 0054 fixes: the metric is vendor-defined and no
|
||||||
|
// common unit is forced. Defined here as the Phase-B seam; nothing in Phase A produces one.
|
||||||
|
type UsageRow struct {
|
||||||
|
Licence string
|
||||||
|
Consumer string
|
||||||
|
Period string
|
||||||
|
Metric string
|
||||||
|
Value float64
|
||||||
|
// Raw is the vendor's own response, kept so a reading can be re-derived if the normalisation is
|
||||||
|
// later found wrong.
|
||||||
|
Raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// registry maps a vendor name to the shape its adapter has.
|
||||||
|
//
|
||||||
|
// The single place a vendor is taught to the mesh. For Phase A the only entry is anthropic as a
|
||||||
|
// static-key vendor, which is what the lab bed exercises.
|
||||||
|
//
|
||||||
|
// TODO(Phase B, ADR 0050): anthropic's real credential is a subscription OAuth grant, so its entry
|
||||||
|
// becomes RefreshableGrant and a refreshable-grant adapter is registered for it; the static-key
|
||||||
|
// path for the same vendor's plain API keys moves to a separate `anthropic-api-key` vendor, the
|
||||||
|
// early second static-key case ADR 0050 names. Static-key vendors are added here as one line each.
|
||||||
|
var registry = map[string]Shape{
|
||||||
|
"anthropic": StaticKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For returns the adapter for a vendor, refusing an unknown one clearly.
|
||||||
|
//
|
||||||
|
// The refusal names what this mesh does know, because a vendor typo and a vendor this build has no
|
||||||
|
// adapter for are the same symptom to whoever hits it, and the list is the difference between fixing
|
||||||
|
// the name and filing a bug.
|
||||||
|
func For(vendor string) (Adapter, error) {
|
||||||
|
shape, known := registry[vendor]
|
||||||
|
if !known {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"this mesh has no adapter for the vendor %q; it knows: %s",
|
||||||
|
vendor, strings.Join(vendors(), ", "))
|
||||||
|
}
|
||||||
|
switch shape {
|
||||||
|
case StaticKey:
|
||||||
|
return staticKey{vendor: vendor}, nil
|
||||||
|
default:
|
||||||
|
// A vendor registered with a shape this build does not implement yet — the refreshable-grant
|
||||||
|
// seam. Said plainly rather than answered with a static-key adapter that would silently
|
||||||
|
// mishandle it.
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"the vendor %q needs a %s adapter, which this build does not ship yet", vendor, shape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vendors is every vendor this mesh has an adapter for, sorted — what a refusal lists.
|
||||||
|
func vendors() []string {
|
||||||
|
out := make([]string, 0, len(registry))
|
||||||
|
for v := range registry {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// staticKey is the adapter for a vendor whose credential is a single operator-supplied value.
|
||||||
|
//
|
||||||
|
// It holds no vendor-specific logic, and that is the point: accept is the generic seal, deliver is
|
||||||
|
// the value, and refresh / identity / usage are not implemented at all. It is one type shared by
|
||||||
|
// every static-key vendor rather than one per vendor, because the sealing is vendor-independent.
|
||||||
|
type staticKey struct{ vendor string }
|
||||||
|
|
||||||
|
func (s staticKey) Vendor() string { return s.vendor }
|
||||||
|
func (s staticKey) Shape() Shape { return StaticKey }
|
||||||
|
|
||||||
|
// Accept is the generic anonymous-box seal (ADR 0024): a static key needs nothing vendor-specific.
|
||||||
|
func (s staticKey) Accept(value, consumerKey, providerKey string) (secrets.Sealed, error) {
|
||||||
|
return secrets.Accept(value, consumerKey, providerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver hands the consumer the credential value. For a sealed static key that is the sealed blob
|
||||||
|
// unchanged — there is no vendor step between the store and the node, and the mesh never sees the
|
||||||
|
// plaintext.
|
||||||
|
func (s staticKey) Deliver(sealed string) string { return sealed }
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/novox/mesh-control/internal/secrets"
|
"github.com/novox/mesh-control/internal/licences/adapters"
|
||||||
"github.com/novox/mesh-control/internal/store"
|
"github.com/novox/mesh-control/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,10 +63,14 @@ func (l *Licences) Ready(ctx context.Context, within time.Duration) error {
|
|||||||
|
|
||||||
// A Licence is one way to reach a model, under the name a person calls it.
|
// A Licence is one way to reach a model, under the name a person calls it.
|
||||||
type Licence struct {
|
type Licence struct {
|
||||||
Name string
|
Name string
|
||||||
Provider string
|
// Vendor is which company sells this licence, and selects the adapter that runs its lifecycle
|
||||||
Serves map[string]any
|
// (novox/hq ADR 0050). Named `vendor`, not `provider`: the inventory already uses "provider"
|
||||||
Added time.Time
|
// for *which node answers a brokered provision*, and one word must not carry two unrelated
|
||||||
|
// facts.
|
||||||
|
Vendor string
|
||||||
|
Serves map[string]any
|
||||||
|
Added time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Holder is one consumer using a licence, and whether it has been given the key.
|
// A Holder is one consumer using a licence, and whether it has been given the key.
|
||||||
@@ -79,9 +83,9 @@ type Holder struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add records a licence under the operator's own name for it.
|
// Add records a licence under the operator's own name for it.
|
||||||
func (l *Licences) Add(ctx context.Context, name, provider string, serves map[string]any) error {
|
func (l *Licences) Add(ctx context.Context, name, vendor string, serves map[string]any) error {
|
||||||
if strings.TrimSpace(name) == "" || strings.TrimSpace(provider) == "" {
|
if strings.TrimSpace(name) == "" || strings.TrimSpace(vendor) == "" {
|
||||||
return errors.New("a licence needs a name and a provider")
|
return errors.New("a licence needs a name and a vendor")
|
||||||
}
|
}
|
||||||
if serves == nil {
|
if serves == nil {
|
||||||
serves = map[string]any{}
|
serves = map[string]any{}
|
||||||
@@ -91,16 +95,16 @@ func (l *Licences) Add(ctx context.Context, name, provider string, serves map[st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = l.store.Pool().Exec(ctx,
|
_, err = l.store.Pool().Exec(ctx,
|
||||||
`insert into licence (name, provider, serves) values ($1, $2, $3)
|
`insert into licence (name, vendor, serves) values ($1, $2, $3)
|
||||||
on conflict (name) do update set provider = excluded.provider, serves = excluded.serves`,
|
on conflict (name) do update set vendor = excluded.vendor, serves = excluded.serves`,
|
||||||
name, provider, body)
|
name, vendor, body)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// All is every licence this mesh knows about.
|
// All is every licence this mesh knows about.
|
||||||
func (l *Licences) All(ctx context.Context) ([]Licence, error) {
|
func (l *Licences) All(ctx context.Context) ([]Licence, error) {
|
||||||
rows, err := l.store.Pool().Query(ctx,
|
rows, err := l.store.Pool().Query(ctx,
|
||||||
`select name, provider, serves, added_at from licence order by name`)
|
`select name, vendor, serves, added_at from licence order by name`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -110,7 +114,7 @@ func (l *Licences) All(ctx context.Context) ([]Licence, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var one Licence
|
var one Licence
|
||||||
var body []byte
|
var body []byte
|
||||||
if err := rows.Scan(&one.Name, &one.Provider, &body, &one.Added); err != nil {
|
if err := rows.Scan(&one.Name, &one.Vendor, &body, &one.Added); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &one.Serves); err != nil {
|
if err := json.Unmarshal(body, &one.Serves); err != nil {
|
||||||
@@ -194,6 +198,12 @@ func (l *Licences) Chosen(ctx context.Context, node, module string) (string, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// KeyFor is the sealed key for one holder, empty if none has been supplied since it was recorded.
|
// KeyFor is the sealed key for one holder, empty if none has been supplied since it was recorded.
|
||||||
|
//
|
||||||
|
// What is stored is what is delivered, routed through the vendor's adapter so a refreshable-grant
|
||||||
|
// vendor can strip its refresh token here in Phase B (novox/hq ADR 0050). For a static-key vendor
|
||||||
|
// that step is the identity — the sealed blob is what the holder receives — so this is unchanged
|
||||||
|
// for today's vendors. An unregistered vendor is not consulted: a static-key blob delivers as it is,
|
||||||
|
// and a licence whose key was accepted at all necessarily had a registered adapter.
|
||||||
func (l *Licences) KeyFor(ctx context.Context, licence, node, module string) (string, error) {
|
func (l *Licences) KeyFor(ctx context.Context, licence, node, module string) (string, error) {
|
||||||
var sealed *string
|
var sealed *string
|
||||||
err := l.store.Pool().QueryRow(ctx,
|
err := l.store.Pool().QueryRow(ctx,
|
||||||
@@ -205,9 +215,32 @@ func (l *Licences) KeyFor(ctx context.Context, licence, node, module string) (st
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if adapter, err := l.adapterFor(ctx, licence); err == nil {
|
||||||
|
return adapter.Deliver(*sealed), nil
|
||||||
|
}
|
||||||
return *sealed, nil
|
return *sealed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vendorOf reads a licence's vendor, the field that selects its adapter.
|
||||||
|
func (l *Licences) vendorOf(ctx context.Context, licence string) (string, error) {
|
||||||
|
var vendor string
|
||||||
|
err := l.store.Pool().QueryRow(ctx,
|
||||||
|
`select vendor from licence where name = $1`, licence).Scan(&vendor)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return "", fmt.Errorf("this mesh has no licence called %q", licence)
|
||||||
|
}
|
||||||
|
return vendor, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapterFor is the adapter a licence's vendor selects (novox/hq ADR 0050).
|
||||||
|
func (l *Licences) adapterFor(ctx context.Context, licence string) (adapters.Adapter, error) {
|
||||||
|
vendor, err := l.vendorOf(ctx, licence)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return adapters.For(vendor)
|
||||||
|
}
|
||||||
|
|
||||||
// SealingKeys is what Accept needs: each holder's node and the key to seal to it.
|
// SealingKeys is what Accept needs: each holder's node and the key to seal to it.
|
||||||
type SealingKeys func(node string) (string, error)
|
type SealingKeys func(node string) (string, error)
|
||||||
|
|
||||||
@@ -225,6 +258,14 @@ func (l *Licences) Accept(ctx context.Context, licence, value string, keys Seali
|
|||||||
if strings.TrimSpace(value) == "" {
|
if strings.TrimSpace(value) == "" {
|
||||||
return 0, errors.New("an empty key is not a key")
|
return 0, errors.New("an empty key is not a key")
|
||||||
}
|
}
|
||||||
|
// The vendor selects the adapter that seals it (novox/hq ADR 0050). A static-key vendor's accept
|
||||||
|
// is the generic seal; the dispatch is what lets a refreshable-grant vendor do otherwise in
|
||||||
|
// Phase B without this layer changing. An unknown vendor is refused here, before any key is
|
||||||
|
// touched.
|
||||||
|
adapter, err := l.adapterFor(ctx, licence)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
holders, err := l.HoldersOf(ctx, licence)
|
holders, err := l.HoldersOf(ctx, licence)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -248,7 +289,7 @@ func (l *Licences) Accept(ctx context.Context, licence, value string, keys Seali
|
|||||||
"%s has no sealing key, so nothing can be sealed to it — it joins again to get one",
|
"%s has no sealing key, so nothing can be sealed to it — it joins again to get one",
|
||||||
h.Node)
|
h.Node)
|
||||||
}
|
}
|
||||||
made, err := secrets.Accept(value, key, key)
|
made, err := adapter.Accept(value, key, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sealed, err
|
return sealed, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,11 +173,11 @@ func TestUsingALicenceThatDoesNotExistIsRefused(t *testing.T) {
|
|||||||
// apart without a schema knowing anything about sessions.
|
// apart without a schema knowing anything about sessions.
|
||||||
func TestTwoSessionsOnOneMachineHoldDifferentLicences(t *testing.T) {
|
func TestTwoSessionsOnOneMachineHoldDifferentLicences(t *testing.T) {
|
||||||
held, ctx := fresh(t)
|
held, ctx := fresh(t)
|
||||||
for _, l := range []struct{ name, provider string }{
|
for _, l := range []struct{ name, vendor string }{
|
||||||
{"personal", "anthropic"},
|
{"personal", "anthropic"},
|
||||||
{"company", "anthropic"},
|
{"company", "anthropic"},
|
||||||
} {
|
} {
|
||||||
if err := held.Add(ctx, l.name, l.provider, map[string]any{"model": "a-model"}); err != nil {
|
if err := held.Add(ctx, l.name, l.vendor, map[string]any{"model": "a-model"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
-- A licence is a named thing, and the name is the operator's.
|
-- A licence is a named thing, and the name is the operator's.
|
||||||
--
|
--
|
||||||
-- novox/hq ADR 0024. Not an anonymous credential hanging off a provider: *the personal account*,
|
-- novox/hq ADR 0024. Not an anonymous credential hanging off a vendor: *the personal account*,
|
||||||
-- *the organisation's account* are names a person uses, and the mesh has to use them too, because
|
-- *the organisation's account* are names a person uses, and the mesh has to use them too, because
|
||||||
-- the whole point is saying WHICH ONE a given consumer uses.
|
-- the whole point is saying WHICH ONE a given consumer uses.
|
||||||
--
|
--
|
||||||
-- **Many to many.** One provider has several licences; one licence serves several consumers. So it
|
-- **Many to many.** One vendor has several licences; one licence serves several consumers. So it
|
||||||
-- is deliberately not a claim — claims are for things only one holder may have, and two machines
|
-- is deliberately not a claim — claims are for things only one holder may have, and two machines
|
||||||
-- sharing an account is the ordinary case rather than a collision.
|
-- sharing an account is the ordinary case rather than a collision.
|
||||||
|
|
||||||
@@ -12,8 +12,10 @@ create table licence (
|
|||||||
-- The operator's name for it. The primary key, because that is what a person types and what a
|
-- The operator's name for it. The primary key, because that is what a person types and what a
|
||||||
-- consumer is pinned to.
|
-- consumer is pinned to.
|
||||||
name text primary key,
|
name text primary key,
|
||||||
-- Which service it is for: anthropic, openai, a model the mesh runs itself.
|
-- Which company sells it: anthropic, openai, a model the mesh runs itself. Selects the adapter
|
||||||
provider text not null,
|
-- that runs this licence's lifecycle (novox/hq ADR 0050). Named `vendor`, not `provider`: the
|
||||||
|
-- inventory already uses "provider" for which node answers a brokered provision.
|
||||||
|
vendor text not null,
|
||||||
-- What a consumer needs to know that is not secret -- a base URL, a model name. The key is
|
-- What a consumer needs to know that is not secret -- a base URL, a model name. The key is
|
||||||
-- never here.
|
-- never here.
|
||||||
serves jsonb not null default '{}'::jsonb,
|
serves jsonb not null default '{}'::jsonb,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- Vendor, not provider.
|
||||||
|
--
|
||||||
|
-- novox/hq ADR 0050 renames the licence's vendor field from `provider` to `vendor`. "provider" is
|
||||||
|
-- already the word the inventory uses for *which node answers a brokered provision*; reusing it for
|
||||||
|
-- *which company sells this licence* would collide two unrelated facts on one word. The consolidated
|
||||||
|
-- schema (0001) now creates the column as `vendor`; this migration carries an existing database the
|
||||||
|
-- same distance.
|
||||||
|
--
|
||||||
|
-- **Guarded so it is a no-op on a fresh database.** A database created after 0001 was updated
|
||||||
|
-- already has `vendor` and no `provider` column, and PostgreSQL has no `RENAME COLUMN IF EXISTS` --
|
||||||
|
-- so the rename is wrapped in an explicit existence check. On a database that predates the rename
|
||||||
|
-- the `provider` column is present and is renamed; on a fresh one nothing is done, and both end with
|
||||||
|
-- exactly the same schema.
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_name = 'licence' and column_name = 'provider'
|
||||||
|
) then
|
||||||
|
alter table licence rename column provider to vendor;
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
Reference in New Issue
Block a user