diff --git a/backend/controller/dal/dal.go b/backend/controller/dal/dal.go index 9c9df1924d..9372f030ac 100644 --- a/backend/controller/dal/dal.go +++ b/backend/controller/dal/dal.go @@ -85,7 +85,11 @@ func DeploymentArtefactFromProto(in *ftlv1.DeploymentArtefact) (DeploymentArtefa func runnerFromDB(row sql.GetRunnerRow) Runner { var deployment optional.Option[model.DeploymentName] if name, ok := row.DeploymentName.Get(); ok { - deployment = optional.Some(model.DeploymentName(name)) + parsed, err := model.ParseDeploymentName(name) + if err != nil { + return Runner{} + } + deployment = optional.Some(parsed) } attrs := model.Labels{} if err := json.Unmarshal(row.Labels, &attrs); err != nil { @@ -303,7 +307,11 @@ func (d *DAL) GetStatus( domainRunners, err := slices.MapErr(runners, func(in sql.GetActiveRunnersRow) (Runner, error) { var deployment optional.Option[model.DeploymentName] if name, ok := in.DeploymentName.Get(); ok { - deployment = optional.Some(model.DeploymentName(name)) + parsed, err := model.ParseDeploymentName(name) + if err != nil { + return Runner{}, fmt.Errorf("invalid deployment name %q: %w", name, err) + } + deployment = optional.Some(parsed) } attrs := model.Labels{} if err := json.Unmarshal(in.Labels, &attrs); err != nil { @@ -408,15 +416,16 @@ func (d *DAL) CreateDeployment(ctx context.Context, language string, moduleSchem // Start the transaction tx, err := d.db.Begin(ctx) if err != nil { - return "", fmt.Errorf("%s: %w", "could not start transaction", err) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "could not start transaction", err) } defer tx.CommitOrRollback(ctx, &err) existingDeployment, err := d.checkForExistingDeployments(ctx, tx, moduleSchema, artefacts) + var zero model.DeploymentName if err != nil { - return "", err - } else if existingDeployment != "" { + return model.DeploymentName{}, err + } else if existingDeployment != zero { logger.Tracef("Returning existing deployment %s", existingDeployment) return existingDeployment, nil } @@ -427,30 +436,31 @@ func (d *DAL) CreateDeployment(ctx context.Context, language string, moduleSchem schemaBytes, err := proto.Marshal(moduleSchema.ToProto()) if err != nil { - return "", fmt.Errorf("%s: %w", "failed to marshal schema", err) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "failed to marshal schema", err) } // TODO(aat): "schema" containing language? _, err = tx.UpsertModule(ctx, language, moduleSchema.Name) if err != nil { - return "", fmt.Errorf("%s: %w", "failed to upsert module", translatePGError(err)) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "failed to upsert module", translatePGError(err)) } deploymentName := model.NewDeploymentName(moduleSchema.Name) + // Create the deployment - err = tx.CreateDeployment(ctx, deploymentName, moduleSchema.Name, schemaBytes) + err = tx.CreateDeployment(ctx, moduleSchema.Name, schemaBytes, deploymentName) if err != nil { - return "", fmt.Errorf("%s: %w", "failed to create deployment", translatePGError(err)) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "failed to create deployment", translatePGError(err)) } uploadedDigests := slices.Map(artefacts, func(in DeploymentArtefact) []byte { return in.Digest[:] }) artefactDigests, err := tx.GetArtefactDigests(ctx, uploadedDigests) if err != nil { - return "", fmt.Errorf("%s: %w", "failed to get artefact digests", err) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "failed to get artefact digests", err) } if len(artefactDigests) != len(artefacts) { missingDigests := strings.Join(slices.Map(artefacts, func(in DeploymentArtefact) string { return in.Digest.String() }), ", ") - return "", fmt.Errorf("missing %d artefacts: %s", len(artefacts)-len(artefactDigests), missingDigests) + return model.DeploymentName{}, fmt.Errorf("missing %d artefacts: %s", len(artefacts)-len(artefactDigests), missingDigests) } // Associate the artefacts with the deployment @@ -463,7 +473,7 @@ func (d *DAL) CreateDeployment(ctx context.Context, language string, moduleSchem Path: artefact.Path, }) if err != nil { - return "", fmt.Errorf("%s: %w", "failed to associate artefact with deployment", translatePGError(err)) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "failed to associate artefact with deployment", translatePGError(err)) } } @@ -476,7 +486,7 @@ func (d *DAL) CreateDeployment(ctx context.Context, language string, moduleSchem Verb: ingressRoute.Verb, }) if err != nil { - return "", fmt.Errorf("%s: %w", "failed to create ingress route", translatePGError(err)) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "failed to create ingress route", translatePGError(err)) } } @@ -496,10 +506,6 @@ func (d *DAL) GetDeployment(ctx context.Context, name model.DeploymentName) (*mo // ErrConflict will be returned if a runner with the same endpoint and a // different key already exists. func (d *DAL) UpsertRunner(ctx context.Context, runner Runner) error { - var pgDeploymentName optional.Option[string] - if dkey, ok := runner.Deployment.Get(); ok { - pgDeploymentName = optional.Some(dkey.String()) - } attrBytes, err := json.Marshal(runner.Labels) if err != nil { return fmt.Errorf("%s: %w", "failed to JSON encode runner labels", err) @@ -508,7 +514,7 @@ func (d *DAL) UpsertRunner(ctx context.Context, runner Runner) error { Key: runner.Key, Endpoint: runner.Endpoint, State: sql.RunnerState(runner.State), - DeploymentName: pgDeploymentName, + DeploymentName: runner.Deployment, Labels: attrBytes, }) if err != nil { @@ -609,7 +615,7 @@ func (p *postgresClaim) Rollback(ctx context.Context) error { func (p *postgresClaim) Runner() Runner { return p.runner } // SetDeploymentReplicas activates the given deployment. -func (d *DAL) SetDeploymentReplicas(ctx context.Context, key model.DeploymentName, minReplicas int) error { +func (d *DAL) SetDeploymentReplicas(ctx context.Context, name model.DeploymentName, minReplicas int) error { // Start the transaction tx, err := d.db.Begin(ctx) if err != nil { @@ -618,18 +624,18 @@ func (d *DAL) SetDeploymentReplicas(ctx context.Context, key model.DeploymentNam defer tx.CommitOrRollback(ctx, &err) - deployment, err := d.db.GetDeployment(ctx, key) + deployment, err := d.db.GetDeployment(ctx, name) if err != nil { return translatePGError(err) } - err = d.db.SetDeploymentDesiredReplicas(ctx, key, int32(minReplicas)) + err = d.db.SetDeploymentDesiredReplicas(ctx, name, int32(minReplicas)) if err != nil { return translatePGError(err) } err = tx.InsertDeploymentUpdatedEvent(ctx, sql.InsertDeploymentUpdatedEventParams{ - DeploymentName: string(key), + DeploymentName: name, MinReplicas: int32(minReplicas), PrevMinReplicas: deployment.MinReplicas, }) @@ -678,7 +684,7 @@ func (d *DAL) ReplaceDeployment(ctx context.Context, newDeploymentName model.Dep } err = tx.InsertDeploymentCreatedEvent(ctx, sql.InsertDeploymentCreatedEventParams{ - DeploymentName: newDeploymentName.String(), + DeploymentName: newDeploymentName, Language: newDeployment.Language, ModuleName: newDeployment.ModuleName, MinReplicas: int32(minReplicas), @@ -955,7 +961,7 @@ func (d *DAL) InsertCallEvent(ctx context.Context, call *CallEvent) error { requestName = optional.Some(string(rn)) } return translatePGError(d.db.InsertCallEvent(ctx, sql.InsertCallEventParams{ - DeploymentName: call.DeploymentName.String(), + DeploymentName: call.DeploymentName, RequestName: requestName, TimeStamp: call.Time, SourceModule: sourceModule, @@ -984,7 +990,7 @@ func (d *DAL) GetActiveRunners(ctx context.Context) ([]Runner, error) { func (*DAL) checkForExistingDeployments(ctx context.Context, tx *sql.Tx, moduleSchema *schema.Module, artefacts []DeploymentArtefact) (model.DeploymentName, error) { schemaBytes, err := schema.ModuleToBytes(moduleSchema) if err != nil { - return "", fmt.Errorf("failed to marshal schema: %w", err) + return model.DeploymentName{}, fmt.Errorf("failed to marshal schema: %w", err) } existing, err := tx.GetDeploymentsWithArtefacts(ctx, sha256esToBytes(slices.Map(artefacts, func(in DeploymentArtefact) sha256.SHA256 { return in.Digest })), @@ -992,12 +998,12 @@ func (*DAL) checkForExistingDeployments(ctx context.Context, tx *sql.Tx, moduleS int64(len(artefacts)), ) if err != nil { - return "", fmt.Errorf("%s: %w", "couldn't check for existing deployment", err) + return model.DeploymentName{}, fmt.Errorf("%s: %w", "couldn't check for existing deployment", err) } if len(existing) > 0 { return existing[0].DeploymentName, nil } - return "", nil + return model.DeploymentName{}, nil } func sha256esToBytes(digests []sha256.SHA256) [][]byte { diff --git a/backend/controller/dal/events.go b/backend/controller/dal/events.go index 8c9b236e85..b8ec66dbe3 100644 --- a/backend/controller/dal/events.go +++ b/backend/controller/dal/events.go @@ -269,8 +269,10 @@ func (d *DAL) QueryEvents(ctx context.Context, limit int, filters ...EventFilter if err := rows.Scan(&id, &name); err != nil { return nil, err } + deploymentName, _ := model.ParseDeploymentName(name) + deploymentIDs = append(deploymentIDs, id) - deploymentNames[id] = model.DeploymentName(name) + deploymentNames[id] = deploymentName } q += fmt.Sprintf(` AND e.deployment_id = ANY($%d::BIGINT[])`, param(deploymentIDs)) diff --git a/backend/controller/sql/querier.go b/backend/controller/sql/querier.go index 7aa57367f0..a0e940e6f1 100644 --- a/backend/controller/sql/querier.go +++ b/backend/controller/sql/querier.go @@ -16,7 +16,7 @@ type Querier interface { AssociateArtefactWithDeployment(ctx context.Context, arg AssociateArtefactWithDeploymentParams) error // Create a new artefact and return the artefact ID. CreateArtefact(ctx context.Context, digest []byte, content []byte) (int64, error) - CreateDeployment(ctx context.Context, name model.DeploymentName, moduleName string, schema []byte) error + CreateDeployment(ctx context.Context, moduleName string, schema []byte, name model.DeploymentName) error CreateIngressRequest(ctx context.Context, origin Origin, name string, sourceAddr string) error CreateIngressRoute(ctx context.Context, arg CreateIngressRouteParams) error DeregisterRunner(ctx context.Context, key model.RunnerKey) (int64, error) diff --git a/backend/controller/sql/queries.sql b/backend/controller/sql/queries.sql index b19db0a8be..98adc503d2 100644 --- a/backend/controller/sql/queries.sql +++ b/backend/controller/sql/queries.sql @@ -15,8 +15,8 @@ FROM modules WHERE id = ANY (@ids::BIGINT[]); -- name: CreateDeployment :exec -INSERT INTO deployments (module_id, "schema", name) -VALUES ((SELECT id FROM modules WHERE name = @module_name::TEXT LIMIT 1), @schema::BYTEA, $1); +INSERT INTO deployments (module_id, "schema", "name") +VALUES ((SELECT id FROM modules WHERE name = @module_name::TEXT LIMIT 1), @schema::BYTEA, sqlc.arg('name')); -- name: GetArtefactDigests :many -- Return the digests that exist in the database. @@ -86,11 +86,11 @@ WITH deployment_rel AS ( -- there is no corresponding deployment, then the deployment ID is -1 -- and the parent statement will fail due to a foreign key constraint. SELECT CASE - WHEN sqlc.narg('deployment_name')::TEXT IS NULL + WHEN sqlc.narg('deployment_name')::deployment_name IS NULL THEN NULL ELSE COALESCE((SELECT id FROM deployments d - WHERE d.name = sqlc.narg('deployment_name') + WHERE d.name = sqlc.narg('deployment_name')::deployment_name LIMIT 1), -1) END AS id) INSERT INTO runners (key, endpoint, state, labels, deployment_id, last_seen) @@ -210,7 +210,7 @@ SET state = 'reserved', -- and the update will fail due to a FK constraint. deployment_id = COALESCE((SELECT id FROM deployments d - WHERE d.name = sqlc.arg('deployment_name') + WHERE d.name = sqlc.arg('deployment_name')::deployment_name LIMIT 1), -1) WHERE id = (SELECT id FROM runners r @@ -274,7 +274,7 @@ FROM rows; -- name: InsertLogEvent :exec INSERT INTO events (deployment_id, request_id, time_stamp, custom_key_1, type, payload) -VALUES ((SELECT id FROM deployments d WHERE d.name = sqlc.arg('deployment_name') LIMIT 1), +VALUES ((SELECT id FROM deployments d WHERE d.name = sqlc.arg('deployment_name')::deployment_name LIMIT 1), (CASE WHEN sqlc.narg('request_name')::TEXT IS NULL THEN NULL ELSE (SELECT id FROM requests ir WHERE ir.name = sqlc.narg('request_name')::TEXT LIMIT 1) @@ -293,7 +293,7 @@ VALUES ((SELECT id FROM deployments d WHERE d.name = sqlc.arg('deployment_name') INSERT INTO events (deployment_id, type, custom_key_1, custom_key_2, payload) VALUES ((SELECT id FROM deployments - WHERE deployments.name = sqlc.arg('deployment_name')::TEXT), + WHERE deployments.name = sqlc.arg('deployment_name')::deployment_name), 'deployment_created', sqlc.arg('language')::TEXT, sqlc.arg('module_name')::TEXT, @@ -306,7 +306,7 @@ VALUES ((SELECT id INSERT INTO events (deployment_id, type, custom_key_1, custom_key_2, payload) VALUES ((SELECT id FROM deployments - WHERE deployments.name = sqlc.arg('deployment_name')::TEXT), + WHERE deployments.name = sqlc.arg('deployment_name')::deployment_name), 'deployment_updated', sqlc.arg('language')::TEXT, sqlc.arg('module_name')::TEXT, @@ -318,7 +318,7 @@ VALUES ((SELECT id -- name: InsertCallEvent :exec INSERT INTO events (deployment_id, request_id, time_stamp, type, custom_key_1, custom_key_2, custom_key_3, custom_key_4, payload) -VALUES ((SELECT id FROM deployments WHERE deployments.name = sqlc.arg('deployment_name')::TEXT), +VALUES ((SELECT id FROM deployments WHERE deployments.name = sqlc.arg('deployment_name')::deployment_name), (CASE WHEN sqlc.narg('request_name')::TEXT IS NULL THEN NULL ELSE (SELECT id FROM requests ir WHERE ir.name = sqlc.narg('request_name')::TEXT) diff --git a/backend/controller/sql/queries.sql.go b/backend/controller/sql/queries.sql.go index 073a0c3ba8..faf0f037a8 100644 --- a/backend/controller/sql/queries.sql.go +++ b/backend/controller/sql/queries.sql.go @@ -52,12 +52,12 @@ func (q *Queries) CreateArtefact(ctx context.Context, digest []byte, content []b } const createDeployment = `-- name: CreateDeployment :exec -INSERT INTO deployments (module_id, "schema", name) -VALUES ((SELECT id FROM modules WHERE name = $2::TEXT LIMIT 1), $3::BYTEA, $1) +INSERT INTO deployments (module_id, "schema", "name") +VALUES ((SELECT id FROM modules WHERE name = $1::TEXT LIMIT 1), $2::BYTEA, $3) ` -func (q *Queries) CreateDeployment(ctx context.Context, name model.DeploymentName, moduleName string, schema []byte) error { - _, err := q.db.Exec(ctx, createDeployment, name, moduleName, schema) +func (q *Queries) CreateDeployment(ctx context.Context, moduleName string, schema []byte, name model.DeploymentName) error { + _, err := q.db.Exec(ctx, createDeployment, moduleName, schema, name) return err } @@ -983,7 +983,7 @@ func (q *Queries) GetRunnersForDeployment(ctx context.Context, name model.Deploy const insertCallEvent = `-- name: InsertCallEvent :exec INSERT INTO events (deployment_id, request_id, time_stamp, type, custom_key_1, custom_key_2, custom_key_3, custom_key_4, payload) -VALUES ((SELECT id FROM deployments WHERE deployments.name = $1::TEXT), +VALUES ((SELECT id FROM deployments WHERE deployments.name = $1::deployment_name), (CASE WHEN $2::TEXT IS NULL THEN NULL ELSE (SELECT id FROM requests ir WHERE ir.name = $2::TEXT) @@ -1004,7 +1004,7 @@ VALUES ((SELECT id FROM deployments WHERE deployments.name = $1::TEXT), ` type InsertCallEventParams struct { - DeploymentName string + DeploymentName model.DeploymentName RequestName optional.Option[string] TimeStamp time.Time SourceModule optional.Option[string] @@ -1040,7 +1040,7 @@ const insertDeploymentCreatedEvent = `-- name: InsertDeploymentCreatedEvent :exe INSERT INTO events (deployment_id, type, custom_key_1, custom_key_2, payload) VALUES ((SELECT id FROM deployments - WHERE deployments.name = $1::TEXT), + WHERE deployments.name = $1::deployment_name), 'deployment_created', $2::TEXT, $3::TEXT, @@ -1051,7 +1051,7 @@ VALUES ((SELECT id ` type InsertDeploymentCreatedEventParams struct { - DeploymentName string + DeploymentName model.DeploymentName Language string ModuleName string MinReplicas int32 @@ -1073,7 +1073,7 @@ const insertDeploymentUpdatedEvent = `-- name: InsertDeploymentUpdatedEvent :exe INSERT INTO events (deployment_id, type, custom_key_1, custom_key_2, payload) VALUES ((SELECT id FROM deployments - WHERE deployments.name = $1::TEXT), + WHERE deployments.name = $1::deployment_name), 'deployment_updated', $2::TEXT, $3::TEXT, @@ -1084,7 +1084,7 @@ VALUES ((SELECT id ` type InsertDeploymentUpdatedEventParams struct { - DeploymentName string + DeploymentName model.DeploymentName Language string ModuleName string PrevMinReplicas int32 @@ -1137,7 +1137,7 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error const insertLogEvent = `-- name: InsertLogEvent :exec INSERT INTO events (deployment_id, request_id, time_stamp, custom_key_1, type, payload) -VALUES ((SELECT id FROM deployments d WHERE d.name = $1 LIMIT 1), +VALUES ((SELECT id FROM deployments d WHERE d.name = $1::deployment_name LIMIT 1), (CASE WHEN $2::TEXT IS NULL THEN NULL ELSE (SELECT id FROM requests ir WHERE ir.name = $2::TEXT LIMIT 1) @@ -1242,7 +1242,7 @@ SET state = 'reserved', -- and the update will fail due to a FK constraint. deployment_id = COALESCE((SELECT id FROM deployments d - WHERE d.name = $2 + WHERE d.name = $2::deployment_name LIMIT 1), -1) WHERE id = (SELECT id FROM runners r @@ -1316,11 +1316,11 @@ func (q *Queries) UpsertModule(ctx context.Context, language string, name string const upsertRunner = `-- name: UpsertRunner :one WITH deployment_rel AS ( SELECT CASE - WHEN $5::TEXT IS NULL + WHEN $5::deployment_name IS NULL THEN NULL ELSE COALESCE((SELECT id FROM deployments d - WHERE d.name = $5 + WHERE d.name = $5::deployment_name LIMIT 1), -1) END AS id) INSERT INTO runners (key, endpoint, state, labels, deployment_id, last_seen) @@ -1343,7 +1343,7 @@ type UpsertRunnerParams struct { Endpoint string State RunnerState Labels []byte - DeploymentName optional.Option[string] + DeploymentName NullDeploymentName } // Upsert a runner and return the deployment ID that it is assigned to, if any. diff --git a/backend/controller/sql/schema/001_init.sql b/backend/controller/sql/schema/001_init.sql index 695c39cd4f..a0478ea463 100644 --- a/backend/controller/sql/schema/001_init.sql +++ b/backend/controller/sql/schema/001_init.sql @@ -39,6 +39,7 @@ CREATE DOMAIN module_schema_pb AS BYTEA; CREATE DOMAIN runner_key AS varchar; CREATE DOMAIN controller_key AS varchar; +CREATE DOMAIN deployment_name AS varchar; CREATE TABLE deployments ( @@ -46,7 +47,7 @@ CREATE TABLE deployments created_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'), module_id BIGINT NOT NULL REFERENCES modules (id) ON DELETE CASCADE, -- Unique name for this deployment in the form -. - "name" VARCHAR UNIQUE NOT NULL, + "name" deployment_name UNIQUE NOT NULL, "schema" module_schema_pb NOT NULL, -- Labels are used to match deployments to runners. "labels" JSONB NOT NULL DEFAULT '{}', diff --git a/backend/controller/sql/types.go b/backend/controller/sql/types.go index 168b91843b..11ef3812f2 100644 --- a/backend/controller/sql/types.go +++ b/backend/controller/sql/types.go @@ -13,6 +13,10 @@ import ( type NullTime = optional.Option[time.Time] type NullDuration = optional.Option[time.Duration] type NullRunnerKey = optional.Option[model.RunnerKey] +type NullDeploymentName = optional.Option[model.DeploymentName] var _ sql.Scanner = (*NullRunnerKey)(nil) var _ driver.Valuer = (*NullRunnerKey)(nil) + +var _ sql.Scanner = (*NullDeploymentName)(nil) +var _ driver.Valuer = (*NullDeploymentName)(nil) diff --git a/internal/model/deployment_name.go b/internal/model/deployment_name.go index e7323695aa..4e66e7bad5 100644 --- a/internal/model/deployment_name.go +++ b/internal/model/deployment_name.go @@ -5,16 +5,14 @@ import ( "database/sql" "database/sql/driver" "encoding" - "encoding/hex" "fmt" "strings" - - "github.com/alecthomas/types/optional" ) -type DeploymentName string - -type MaybeDeploymentName optional.Option[DeploymentName] +type DeploymentName struct { + module string + hash string +} var _ interface { sql.Scanner @@ -29,27 +27,33 @@ func NewDeploymentName(module string) DeploymentName { if err != nil { panic(err) } - return DeploymentName(fmt.Sprintf("%s-%010x", module, hash)) + return DeploymentName{module: module, hash: fmt.Sprintf("%010x", hash)} } func ParseDeploymentName(name string) (DeploymentName, error) { - var zero DeploymentName parts := strings.Split(name, "-") if len(parts) < 2 { - return zero, fmt.Errorf("should be at least -: invalid deployment name %q", name) + return DeploymentName{}, fmt.Errorf("invalid deployment name %q: does not follow - pattern", name) } - hash, err := hex.DecodeString(parts[len(parts)-1]) - if err != nil { - return zero, fmt.Errorf("invalid deployment name %q: %w", name, err) + + module := strings.Join(parts[0:len(parts)-1], "-") + if len(module) == 0 { + return DeploymentName{}, fmt.Errorf("invalid deployment name %q: module name should not be empty", name) } - if len(hash) != 5 { - return zero, fmt.Errorf("hash should be 5 bytes: invalid deployment name %q", name) + + hash := parts[len(parts)-1] + if len(hash) != 10 { + return DeploymentName{}, fmt.Errorf("invalid deployment name %q: hash should be 10 hex characters long", name) } - return DeploymentName(fmt.Sprintf("%s-%010x", strings.Join(parts[0:len(parts)-1], "-"), hash)), nil + + return DeploymentName{ + module: module, + hash: parts[len(parts)-1], + }, nil } func (d *DeploymentName) String() string { - return string(*d) + return fmt.Sprintf("%s-%s", d.module, d.hash) } func (d *DeploymentName) UnmarshalText(bytes []byte) error { @@ -62,7 +66,7 @@ func (d *DeploymentName) UnmarshalText(bytes []byte) error { } func (d *DeploymentName) MarshalText() ([]byte, error) { - return []byte(*d), nil + return []byte(d.String()), nil } func (d *DeploymentName) Scan(value any) error { @@ -77,6 +81,6 @@ func (d *DeploymentName) Scan(value any) error { return nil } -func (d *DeploymentName) Value() (driver.Value, error) { +func (d DeploymentName) Value() (driver.Value, error) { return d.String(), nil } diff --git a/internal/model/deployment_name_test.go b/internal/model/deployment_name_test.go new file mode 100644 index 0000000000..3c84aa9f26 --- /dev/null +++ b/internal/model/deployment_name_test.go @@ -0,0 +1,60 @@ +package model + +import ( + "strings" + "testing" + + "github.com/alecthomas/assert/v2" +) + +func TestDeploymentName(t *testing.T) { + for _, test := range []struct { + str string // when full string is known + strPrefix string // when only prefix is known + module string + hash string + decodeErr bool + }{ + {module: "time", strPrefix: "time-"}, + {str: "time-00112233", decodeErr: true}, + {str: "time-001122334455", decodeErr: true}, + {str: "time-0011223344", module: "time", hash: "0011223344"}, + {str: "-0011223344", decodeErr: true}, + {str: "module-with-hyphens-0011223344", module: "module-with-hyphens", hash: "0011223344"}, + {str: "-", decodeErr: true}, + } { + decoded, decodeErr := ParseDeploymentName(test.str) + + if test.decodeErr { + assert.Error(t, decodeErr, "expected error for deployment name %q", test.str) + } else { + created := NewDeploymentName(test.module) + + forceEncoded := DeploymentName{ + module: test.module, + hash: test.hash, + } + + if test.str != "" && test.module != "" { + assert.Equal(t, test.module, decoded.module, "expected module %q for %q", test.module, decoded.module) + } + + if test.str != "" && test.hash != "" { + assert.Equal(t, test.hash, decoded.hash, "expected hash %q for %q", test.hash, decoded.hash) + } + + if test.module != "" && test.strPrefix != "" { + assert.True(t, strings.HasPrefix(created.String(), test.strPrefix), "expected string prefix %q for %q", test.strPrefix, created.String()) + } + + if test.module != "" && test.hash != "" && test.str != "" { + assert.Equal(t, test.str, forceEncoded.String(), "expected string %q for %q", test.str, forceEncoded.String()) + } + } + } +} + +func TestZeroDeploymentName(t *testing.T) { + _, err := ParseDeploymentName("") + assert.Error(t, err, "expected error for empty deployment name") +} diff --git a/sqlc.yaml b/sqlc.yaml index 3211426d7b..6c90ab4b19 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -36,6 +36,12 @@ sql: nullable: true go_type: type: "NullRunnerKey" + - db_type: "deployment_name" + go_type: "github.com/TBD54566975/ftl/internal/model.DeploymentName" + - db_type: "deployment_name" + nullable: true + go_type: + type: "NullDeploymentName" - db_type: "controller_key" go_type: "github.com/TBD54566975/ftl/internal/model.ControllerKey" - db_type: "text"