Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ builds:
goarch: arm64
mod_timestamp: "{{ .CommitTimestamp }}"
hooks:
post: # sign the windows binaries
post: # sign the windows binaries in-place with smctl, which is required
# for the Microsoft Store. We don't produce detached signatures for
# windows binaries since that's not really a thing on windows and smctl doesn't support it.
- if: '{{ and (eq .Os "windows") (isEnvSet "SM_API_KEY") }}'
cmd: >-
smctl sign --keypair-alias "{{ .Env.SM_KEYPAIR_ALIAS }}" --input "{{ .Path }}" --config-file "{{ .Env.PKCS11_CONFIG }}"
Expand Down
122 changes: 122 additions & 0 deletions cmd/dbc/latest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2026 Columnar Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"charm.land/lipgloss/v2"
"github.com/Masterminds/semver/v3"
"github.com/cli/safeexec"
"github.com/columnar-tech/dbc"
"github.com/columnar-tech/dbc/internal"
)

func isUnderHomebrew(bin string) bool {
brewExe, err := safeexec.LookPath("brew")
if err != nil {
return false
}

brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
if err != nil {
return false
}

brewBinPrefix := filepath.Join(strings.TrimSpace(
string(brewPrefixBytes)), "bin") + string(filepath.Separator)
return strings.HasPrefix(bin, brewBinPrefix)
}

func isPkgMgrInstall() bool {
exe, err := os.Executable()
if err != nil {
return false
}

if isUnderHomebrew(exe) {
return true
}

exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return false
}

if isManaged(exe) {
return true
}

if strings.Contains(exe, "conda") || strings.Contains(exe, "venv") || strings.Contains(exe, "miniforge") {
// likely a conda or virtual environment install
return true
}

return false
}

func writeLastUpdateCheck(configDir string) {
// file doesn't exist, create it with current timestamp and skip update check
_ = os.MkdirAll(configDir, 0o700)
_ = os.WriteFile(filepath.Join(configDir, ".last-update-check"), []byte(time.Now().Format(time.DateOnly)), 0o600)
}

func notifyLatest() {
configDir, err := internal.GetUserConfigPath()
if err != nil {
return
}

// skip notifying if $dbc_config_home/.no-update exists
_, err = os.Stat(filepath.Join(configDir, ".no-update"))
if err == nil {
return // file exists, skip update check
}

lastUpdate, err := os.ReadFile(filepath.Join(configDir, ".last-update-check"))
if errors.Is(err, os.ErrNotExist) {
writeLastUpdateCheck(configDir)
} else if err == nil {
lastCheckTime, err := time.Parse(time.DateOnly, string(lastUpdate))
if err != nil {
// if the file is corrupted, reset it
_ = os.WriteFile(filepath.Join(configDir, ".last-update-check"), []byte(time.Now().Format(time.DateOnly)), 0o600)
} else if time.Since(lastCheckTime) > 24*time.Hour {
writeLastUpdateCheck(configDir)
} else {
return // last check was within 24 hours, skip update check
}
}

if isPkgMgrInstall() {
// skip notifying if installed via package manager,
// since they likely have their own update mechanism
return
}

latestVer, err := dbc.GetLatestDbcVersion()
if dbc.Version != "(devel)" && err == nil {
if semver.MustParse(dbc.Version).LessThan(latestVer) {
lipgloss.Fprintf(os.Stderr, descStyle.Render("Update available: A new version of dbc is available. You're running v%s and v%s is available. Please upgrade.\nChangelog: %s. Docs: %s"),
dbc.Version, latestVer, "https://github.com/columnar-tech/dbc/releases/tag/v"+latestVer.String(), "https://docs.columnar.tech/dbc/getting_started/installation/")
lipgloss.Fprintln(os.Stderr)
}
}
}
26 changes: 26 additions & 0 deletions cmd/dbc/latest_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2026 Columnar Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import "path/filepath"

func isManaged(exe string) bool {
if filepath.Dir(exe) == "/usr/local/bin" {
// system-wide pip install
return true
}

return false
}
45 changes: 45 additions & 0 deletions cmd/dbc/latest_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2026 Columnar Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"os/exec"
"path/filepath"
)

func isManaged(exe string) bool {
// check if we're a deb install
dpkgExe, err := exec.LookPath("dpkg")
if err == nil {
if err = exec.Command(dpkgExe, "-S", exe).Run(); err == nil {
return true
}
}

// check if we're an rpm install
rpmExe, err := exec.LookPath("rpm")
if err == nil {
if err = exec.Command(rpmExe, "-qf", exe).Run(); err == nil {
return true
}
}

if filepath.Dir(exe) == "/usr/local/bin" {
// pip installs here on linux
return true
}

return false
}
44 changes: 44 additions & 0 deletions cmd/dbc/latest_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2026 Columnar Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"path/filepath"
"strings"

"golang.org/x/sys/windows/registry"
)

func isManaged(exe string) bool {
const packedUpgradeCode = `F0742CB3EE450F7479C37A9886B49FE5`
const registryPath = `SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes\` + packedUpgradeCode

k, err := registry.OpenKey(registry.LOCAL_MACHINE, registryPath, registry.QUERY_VALUE)
if err == nil {
// If we can open the key, check if our executable is listed as an installed product under this upgrade code
// this means dbc is installed via MSI.
defer k.Close()

// so check if we're running from the location where dbc.msi installs to
return strings.Contains(exe, `AppData\Roaming\Columnar\dbc`)
}

if strings.HasSuffix(filepath.Dir(exe), "\\Scripts") {
// likely a pip install
return true
}

return false
}
4 changes: 4 additions & 0 deletions cmd/dbc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ func main() {
}
}

if !args.Quiet {
notifyLatest()
}

if m, err = prog.Run(); err != nil {
fmt.Fprintln(os.Stderr, "Error running program:", err)
os.Exit(1)
Expand Down
17 changes: 17 additions & 0 deletions docs/getting_started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ dbc can generate shell completions for a number of common shells.

You can use the `dbc completion` subcommand to print extended instructions for your shell, including how to enable your shell's completion mechanism. For example, to print setup instructions for Bash, run `dbc completion bash --help`.

## Updating

{{ since_version('v0.2.0') }}

If you installed dbc using the [Standalone Installer](#standalone-installer) or manually through [GitHub Releases](#github-releases), dbc will automatically notify you if your version is out of date and will provide instructions for updating.

For other installation methods, dbc won't automatically notify of updates and you should upgrade dbc in whatever way is standard for your installation method.

!!! note
To silence update notifications, create an empty file called `.no-update` in the dbc configuration directory for your operating system:

- Linux: `~/.config/columnar/dbc/.no-update`
- macOS: `~/Library/Application Support/Columnar/dbc/.no-update`
- Windows: `%AppData%/Columnar/dbc/.no-update`

If you use a custom `$XDG_CONFIG_HOME`, use `$XDG_CONFIG_HOME/columnar/dbc/.no-update`.

## Uninstallation

To remove dbc from your system, run the uninstall command corresponding to your installation method.
Expand Down
14 changes: 14 additions & 0 deletions drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,17 @@ func SignedByColumnar(lib, sig io.Reader) error {

return result.SignatureError()
}

func GetLatestDbcVersion() (*semver.Version, error) {
resp, err := makereq("https://dbc.columnar.tech")
Comment thread
ianmcook marked this conversation as resolved.
if err != nil {
return nil, fmt.Errorf("failed to fetch latest dbc version: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
return nil, fmt.Errorf("failed to fetch latest dbc version: %s", resp.Status)
}

return semver.NewVersion(resp.Header.Get("x-dbc-latest"))
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ require (
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/cli/safeexec v1.0.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEf
github.com/cli/oauth v1.2.1 h1:9+vketSVuBCbEIpx4XPHHDlTX2R9MbLnM79sfA2Ac+4=
github.com/cli/oauth v1.2.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
Expand Down
Loading