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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,42 @@ pg0 start -c shared_buffers=512MB -c work_mem=128MB
pg0 start -c shared_buffers=1GB -c maintenance_work_mem=2GB
```

### Runtime directories

The following environment variables allow deployments to keep executables in
an approved location and writable instance data in a separate directory:

| Variable | Purpose | Default |
|----------|---------|---------|
| `PG0_HOME` | Instance metadata and default data directories | `~/.pg0` |
| `PG0_INSTALLATION_DIR` | Root for extracted PostgreSQL and extension files; contains a version subdirectory such as `18.1.0` | `$PG0_HOME/installation` |
| `PG0_EXECUTABLE` | Python SDK only: full path to the pg0 executable, taking precedence over the wheel's bundled binary | Automatic binary discovery |

All overrides must be non-empty absolute paths. An invalid override fails with
an error instead of falling back to the user profile. `--data-dir` continues to
override the data directory for an individual instance. Keep `PG0_HOME` set to
the same value for subsequent `info`, `stop`, `drop`, and other commands.

For example, in PowerShell:

```powershell
$env:PG0_HOME = Join-Path $env:LOCALAPPDATA 'pg0-state'
$env:PG0_INSTALLATION_DIR = 'C:\ProgramData\pg0\installation'
$env:PG0_EXECUTABLE = 'C:\Program Files\pg0\pg0.exe'
& $env:PG0_EXECUTABLE start --name example
```

Provision the executable at `PG0_EXECUTABLE` separately. The first start needs
write access to `PG0_INSTALLATION_DIR` to extract PostgreSQL and pgvector.
On Windows, a complete installation can subsequently be used with read and
execute access while `PG0_HOME` remains writable. Installing or upgrading
extensions still requires write access to the installation. These settings
select locations; the applicable execution policy must allow those locations.
No symbolic links are needed on Windows.

`PG0_EXECUTABLE` is a runtime setting. The existing `PG0_BINARY_PATH` setting is
used only when building a Python wheel.

## How It Works

PostgreSQL and pgvector are **bundled directly** into the pg0 binary - no downloads required, works completely offline! On first start, pg0 extracts PostgreSQL and pgvector to `~/.pg0/installation/` and initializes the database.
Expand Down
14 changes: 14 additions & 0 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ print(info.username) # postgres
print(info.database) # postgres
```

## Runtime paths

Set `PG0_EXECUTABLE` to the absolute path of an existing pg0 executable to use
it instead of the binary bundled in the wheel. An invalid override raises
`Pg0NotFoundError`; it never silently falls back to the bundled binary.

The CLI also accepts `PG0_HOME` for writable instance state and
`PG0_INSTALLATION_DIR` for extracted PostgreSQL and pgvector files. Both must
be absolute paths and are inherited by SDK subprocesses. The installation root
contains a version subdirectory, for example `18.1.0/bin/postgres.exe`.
`--data-dir` / `Pg0(data_dir=...)` continues to override an instance's data path.
See [runtime directories](https://github.com/vectorize-io/pg0#runtime-directories)
for provisioning and permission requirements.

## Supported Platforms

Pre-built wheels are available for:
Expand Down
13 changes: 13 additions & 0 deletions sdk/python/pg0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ def _get_install_dir() -> Path:

def _find_pg0() -> str:
"""Find the pg0 binary or raise an error if not found."""
# An explicit executable takes precedence over the wheel's bundled binary.
# Do not silently fall back to a location blocked by an execution policy.
configured = os.environ.get("PG0_EXECUTABLE")
if configured is not None:
binary = Path(configured)
if not binary.is_absolute() or not binary.is_file():
raise Pg0NotFoundError(
"PG0_EXECUTABLE must be an absolute path to an existing pg0 executable"
)
if sys.platform != "win32" and not os.access(binary, os.X_OK):
raise Pg0NotFoundError("PG0_EXECUTABLE is not executable")
return str(binary)

# Check for bundled binary first (from platform-specific wheel)
bundled = _get_bundled_binary()
if bundled:
Expand Down
98 changes: 98 additions & 0 deletions sdk/python/tests/test_runtime_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Runtime executable selection and separate native/state directories."""

import json
import subprocess
import shutil
import sys
from pathlib import Path

import pytest
import pg0


def test_explicit_executable_precedes_bundled_binary(tmp_path, monkeypatch):
executable = tmp_path / "approved directory" / ("pg0.exe" if sys.platform == "win32" else "pg0")
executable.parent.mkdir()
executable.write_bytes(b"test executable")
executable.chmod(0o755)
monkeypatch.setenv("PG0_EXECUTABLE", str(executable))
monkeypatch.setattr(pg0, "_get_bundled_binary", lambda: pytest.fail("unexpected fallback"))
assert pg0._find_pg0() == str(executable)


@pytest.mark.parametrize("value", ["", "relative/pg0", "missing"])
def test_invalid_executable_does_not_fall_back(tmp_path, monkeypatch, value):
if value == "missing":
value = str(tmp_path / "missing")
monkeypatch.setenv("PG0_EXECUTABLE", value)
monkeypatch.setattr(pg0, "_get_bundled_binary", lambda: pytest.fail("unexpected fallback"))
with pytest.raises(pg0.Pg0NotFoundError, match="PG0_EXECUTABLE"):
pg0._find_pg0()


def test_directory_is_not_an_executable(tmp_path, monkeypatch):
monkeypatch.setenv("PG0_EXECUTABLE", str(tmp_path))
with pytest.raises(pg0.Pg0NotFoundError, match="PG0_EXECUTABLE"):
pg0._find_pg0()


@pytest.mark.skipif(sys.platform == "win32", reason="POSIX executable permission")
def test_non_executable_file_is_rejected(tmp_path, monkeypatch):
executable = tmp_path / "pg0"
executable.touch(mode=0o600)
monkeypatch.setenv("PG0_EXECUTABLE", str(executable))
with pytest.raises(pg0.Pg0NotFoundError, match="not executable"):
pg0._find_pg0()


def test_unset_override_keeps_bundled_discovery(tmp_path, monkeypatch):
monkeypatch.delenv("PG0_EXECUTABLE", raising=False)
bundled = tmp_path / "bundled-pg0"
monkeypatch.setattr(pg0, "_get_bundled_binary", lambda: bundled)
assert pg0._find_pg0() == str(bundled)


def test_native_installation_and_state_can_be_separated(tmp_path, monkeypatch):
"""Exercise the built CLI through SDK discovery, SQL, stop, and restart."""
if sys.platform == "win32":
# Elevated Windows sessions can make pytest's mode=0700 directory
# owned by Administrators. initdb removes that group from its token.
sid = subprocess.check_output(
["powershell.exe", "-NoProfile", "-Command",
"[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value"],
text=True,
).strip()
subprocess.run(
["icacls", str(tmp_path), "/grant:r", f"*{sid}:(OI)(CI)(F)"],
check=True, capture_output=True,
)
original = Path(pg0._find_pg0())
approved = tmp_path / "approved executables"
approved.mkdir()
executable = approved / original.name
shutil.copy2(original, executable)
state = tmp_path / "writable state"
installation = approved / "installation"
monkeypatch.setenv("PG0_EXECUTABLE", str(executable))
monkeypatch.setenv("PG0_HOME", str(state))
monkeypatch.setenv("PG0_INSTALLATION_DIR", str(installation))
instance = pg0.Pg0(name="runtime-paths")
try:
instance.start()
metadata = json.loads(
(state / "instances/runtime-paths/instance.json").read_text()
)
assert Path(metadata["installation_dir"]) == installation
assert Path(instance.info().data_dir) == state / "instances/runtime-paths/data"
instance.execute("CREATE EXTENSION IF NOT EXISTS vector")
result = instance.execute("SELECT '[1,2]'::vector <-> '[1,2]'::vector")
assert "0" in result
instance.execute("CREATE TABLE path_test (value integer)")
instance.execute("INSERT INTO path_test VALUES (42)")
instance.stop()
instance.start()
assert "42" in instance.execute("SELECT value FROM path_test")
assert any(installation.glob("*/bin/postgres*"))
assert not (state / "installation").exists()
finally:
instance.drop(force=True)
66 changes: 64 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,45 @@ struct InfoOutput {
}

fn get_base_dir() -> Result<PathBuf, CliError> {
if let Some(path) = configured_directory("PG0_HOME", std::env::var_os("PG0_HOME"))? {
return Ok(path);
}
dirs::home_dir()
.map(|h| h.join(".pg0"))
.ok_or(CliError::NoDataDir)
}

/// Explicit paths must be absolute so a later command can find the same files
/// even when invoked from a different working directory. Preserve OS strings.
fn configured_directory(
variable: &str,
value: Option<std::ffi::OsString>,
) -> Result<Option<PathBuf>, CliError> {
match value {
None => Ok(None),
Some(value) => {
let path = PathBuf::from(value);
if !path.is_absolute() {
return Err(CliError::Other(format!(
"{} must be a non-empty absolute directory path",
variable
)));
}
Ok(Some(path))
}
}
}

fn get_installation_dir() -> Result<PathBuf, CliError> {
match configured_directory(
"PG0_INSTALLATION_DIR",
std::env::var_os("PG0_INSTALLATION_DIR"),
)? {
Some(path) => Ok(path),
None => Ok(get_base_dir()?.join("installation")),
}
}

fn get_instances_dir() -> Result<PathBuf, CliError> {
Ok(get_base_dir()?.join("instances"))
}
Expand Down Expand Up @@ -814,7 +848,6 @@ fn start(
port
};

let base_dir = get_base_dir()?;
let instance_dir = get_instance_dir(&name)?;

// Use provided data_dir or default to instance-specific directory
Expand All @@ -823,7 +856,7 @@ fn start(
None => instance_dir.join("data"),
};

let installation_dir = base_dir.join("installation");
let installation_dir = get_installation_dir()?;

fs::create_dir_all(&data_dir)?;
fs::create_dir_all(&installation_dir)?;
Expand Down Expand Up @@ -1695,6 +1728,35 @@ fn init_logging(verbose: bool) {
.init();
}

#[cfg(test)]
mod directory_tests {
use super::*;

#[test]
fn unset_directory_keeps_the_default() {
assert!(configured_directory("PG0_HOME", None).unwrap().is_none());
}

#[test]
fn explicit_directory_preserves_spaces() {
let path = std::env::temp_dir().join("pg0 installation with spaces");
assert_eq!(
configured_directory("PG0_INSTALLATION_DIR", Some(path.clone().into_os_string()))
.unwrap(),
Some(path)
);
}

#[test]
fn invalid_directory_does_not_fall_back() {
for value in ["", "relative/path"] {
let error = configured_directory("PG0_HOME", Some(value.into())).unwrap_err();
assert!(error.to_string().contains("PG0_HOME"));
assert!(error.to_string().contains("absolute"));
}
}
}

#[cfg(all(test, unix))]
mod tests {
use super::*;
Expand Down