|
| 1 | +package dbconnect |
| 2 | + |
| 3 | +import "fmt" |
| 4 | + |
| 5 | +// Command path components, defined once so a rename touches a single place |
| 6 | +// (spec §0 / invariant 8 / scenario 21). The cmd layer builds the Cobra |
| 7 | +// command from CommandGroup/CommandVerb; the --json "command" field uses |
| 8 | +// CommandName. No other string re-spells the command path. |
| 9 | +const ( |
| 10 | + CommandGroup = "dbconnect" |
| 11 | + CommandVerb = "sync" |
| 12 | + CommandName = CommandGroup + " " + CommandVerb |
| 13 | + |
| 14 | + // SchemaVersion is the version of the --json output contract (spec §6). |
| 15 | + // Bump it on any breaking change to the JSON shape. |
| 16 | + SchemaVersion = 1 |
| 17 | +) |
| 18 | + |
| 19 | +// Mode is the provisioning mode: a full environment (default) or the |
| 20 | +// constraints-only variant that omits the databricks-connect dependency. |
| 21 | +type Mode int |
| 22 | + |
| 23 | +const ( |
| 24 | + ModeDefault Mode = iota |
| 25 | + ModeConstraintsOnly |
| 26 | +) |
| 27 | + |
| 28 | +// String returns the JSON/text spelling of the mode ("default" | "constraints-only"). |
| 29 | +func (m Mode) String() string { |
| 30 | + if m == ModeConstraintsOnly { |
| 31 | + return "constraints-only" |
| 32 | + } |
| 33 | + return "default" |
| 34 | +} |
| 35 | + |
| 36 | +// PhaseName is a canonical execution phase (spec §3 / §6). The set is fixed and |
| 37 | +// ordered; the --json "phases" array reports every phase in this order. |
| 38 | +type PhaseName string |
| 39 | + |
| 40 | +const ( |
| 41 | + PhasePreflight PhaseName = "preflight" |
| 42 | + PhaseResolve PhaseName = "resolve" |
| 43 | + PhaseFetch PhaseName = "fetch" |
| 44 | + PhaseMerge PhaseName = "merge" |
| 45 | + PhaseProvision PhaseName = "provision" |
| 46 | + PhaseValidate PhaseName = "validate" |
| 47 | +) |
| 48 | + |
| 49 | +// Phase status values (spec §6.2). |
| 50 | +const ( |
| 51 | + StatusOK = "ok" |
| 52 | + StatusError = "error" |
| 53 | + StatusPending = "pending" |
| 54 | +) |
| 55 | + |
| 56 | +// ErrorCode is a stable failure-class identifier surfaced in --json error.code |
| 57 | +// (spec §7). Values are compared via the ErrorCode constants, never by |
| 58 | +// string-matching messages, and are defined once here. |
| 59 | +type ErrorCode string |
| 60 | + |
| 61 | +const ( |
| 62 | + ErrNoTarget ErrorCode = "E_NO_TARGET" |
| 63 | + ErrManagerUnsupported ErrorCode = "E_MANAGER_UNSUPPORTED" |
| 64 | + ErrUvMissing ErrorCode = "E_UV_MISSING" |
| 65 | + ErrNotWritable ErrorCode = "E_NOT_WRITABLE" |
| 66 | + ErrResolve ErrorCode = "E_RESOLVE" |
| 67 | + ErrEnvUnsupported ErrorCode = "E_ENV_UNSUPPORTED" |
| 68 | + ErrFetch ErrorCode = "E_FETCH" |
| 69 | + ErrWrite ErrorCode = "E_WRITE" |
| 70 | + ErrMerge ErrorCode = "E_MERGE" |
| 71 | + ErrPythonInstall ErrorCode = "E_PYTHON_INSTALL" |
| 72 | + ErrProvision ErrorCode = "E_PROVISION" |
| 73 | + ErrValidate ErrorCode = "E_VALIDATE" |
| 74 | +) |
| 75 | + |
| 76 | +// PipelineError is a failure carrying a stable code, the phase at which it |
| 77 | +// occurred, and whether disk was mutated before the failure. It marshals to the |
| 78 | +// --json error object (spec §6.2). Code and FailurePhase are the stable |
| 79 | +// contract; Err holds the wrapped cause for errors.Is/As and is not serialized. |
| 80 | +type PipelineError struct { |
| 81 | + Code ErrorCode `json:"code"` |
| 82 | + FailurePhase PhaseName `json:"failurePhase"` |
| 83 | + Msg string `json:"message"` |
| 84 | + DiskMutated bool `json:"diskMutated"` |
| 85 | + Err error `json:"-"` |
| 86 | +} |
| 87 | + |
| 88 | +func (e *PipelineError) Error() string { |
| 89 | + if e.Err != nil { |
| 90 | + return e.Msg + ": " + e.Err.Error() |
| 91 | + } |
| 92 | + return e.Msg |
| 93 | +} |
| 94 | + |
| 95 | +func (e *PipelineError) Unwrap() error { |
| 96 | + return e.Err |
| 97 | +} |
| 98 | + |
| 99 | +// NewError creates a PipelineError with a code and message. FailurePhase and |
| 100 | +// DiskMutated are filled in by the pipeline when it records the failure. The |
| 101 | +// message is formatted with fmt.Sprintf(format, args...); err may be nil. |
| 102 | +func NewError(code ErrorCode, err error, format string, args ...any) *PipelineError { |
| 103 | + return &PipelineError{ |
| 104 | + Code: code, |
| 105 | + Msg: fmt.Sprintf(format, args...), |
| 106 | + Err: err, |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +// TargetInfo is the resolved compute target (spec §6 "target"). Source records |
| 111 | +// which of the four precedence sources was used. SparkVersion is the raw cluster |
| 112 | +// runtime string the resolver read; it is folded into EnvKey (dbr/<SparkVersion>) |
| 113 | +// and is not part of the JSON contract, kept only as intermediate resolver state. |
| 114 | +type TargetInfo struct { |
| 115 | + Source string `json:"source"` |
| 116 | + ClusterID string `json:"clusterId,omitempty"` |
| 117 | + ServerlessVersion string `json:"serverlessVersion,omitempty"` |
| 118 | + EnvKey string `json:"envKey"` |
| 119 | + |
| 120 | + SparkVersion string `json:"-"` |
| 121 | +} |
| 122 | + |
| 123 | +// ResolvedInfo is the resolved environment definition (spec §6 "resolved"). |
| 124 | +// DBConnectVersion is omitted in constraints-only mode. |
| 125 | +type ResolvedInfo struct { |
| 126 | + PythonVersion string `json:"pythonVersion"` |
| 127 | + DBConnectVersion string `json:"dbconnectVersion,omitempty"` |
| 128 | + ArtifactSource string `json:"artifactSource"` |
| 129 | +} |
| 130 | + |
| 131 | +// Plan describes the changes a --check run would apply (spec §6.3). |
| 132 | +// ChangedRegions is retained for text output only and is not serialized. |
| 133 | +type Plan struct { |
| 134 | + WouldWrite string `json:"wouldWrite"` |
| 135 | + WouldBackup string `json:"wouldBackup,omitempty"` |
| 136 | + WouldInstallPython string `json:"wouldInstallPython,omitempty"` |
| 137 | + Diff string `json:"diff"` |
| 138 | + |
| 139 | + ChangedRegions []string `json:"-"` |
| 140 | +} |
| 141 | + |
| 142 | +// PhaseStatus is one entry in the --json "phases" array (spec §6). Detail is |
| 143 | +// used for human-readable text output only and is not serialized. |
| 144 | +type PhaseStatus struct { |
| 145 | + Phase PhaseName `json:"phase"` |
| 146 | + Status string `json:"status"` |
| 147 | + |
| 148 | + Detail string `json:"-"` |
| 149 | +} |
| 150 | + |
| 151 | +// Warning is a non-fatal advisory surfaced in --json "warnings" (spec §6). |
| 152 | +type Warning struct { |
| 153 | + Code string `json:"code"` |
| 154 | + Message string `json:"message"` |
| 155 | +} |
| 156 | + |
| 157 | +// Result is the full outcome of a sync run and the root of the --json object |
| 158 | +// (spec §6). Field order matches the spec's schema so JSON key order is stable. |
| 159 | +type Result struct { |
| 160 | + SchemaVersion int `json:"schemaVersion"` |
| 161 | + Command string `json:"command"` |
| 162 | + OK bool `json:"ok"` |
| 163 | + Mode string `json:"mode"` |
| 164 | + DryRun bool `json:"dryRun"` |
| 165 | + Target *TargetInfo `json:"target,omitempty"` |
| 166 | + Resolved *ResolvedInfo `json:"resolved,omitempty"` |
| 167 | + Greenfield bool `json:"greenfield"` |
| 168 | + Plan *Plan `json:"plan,omitempty"` |
| 169 | + VenvPath string `json:"venvPath,omitempty"` |
| 170 | + Phases []PhaseStatus `json:"phases"` |
| 171 | + Warnings []Warning `json:"warnings"` |
| 172 | + Error *PipelineError `json:"error"` |
| 173 | + BackupPath string `json:"backupPath,omitempty"` |
| 174 | + // DurationMs is part of the §6 contract but reserved for now: the pipeline |
| 175 | + // does not measure wall time (a real clock would make acceptance goldens |
| 176 | + // non-deterministic), so it is always emitted as 0 until timing is wired |
| 177 | + // through a clock the tests can control. |
| 178 | + DurationMs int64 `json:"durationMs"` |
| 179 | +} |
0 commit comments