Skip to content

Commit ee19a8d

Browse files
committed
init
Signed-off-by: George Lemon <georgelemon@protonmail.com>
1 parent 8aaea0d commit ee19a8d

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

greskewel.nimble

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Package
2+
3+
version = "0.1.0"
4+
author = "George Lemon"
5+
description = "Postgres in a box"
6+
license = "MIT"
7+
srcDir = "src"
8+
9+
# Dependencies
10+
11+
requires "nim >= 2.0.2"
12+
requires "db_connector"
13+
requires "threading"

src/greskewel.nim

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Postgres in a Box. For Nim
2+
#
3+
# This package provides an easy way to run an embedded Postgres server
4+
# for testing and development purposes. It allows you to start, stop,
5+
# and manage a Postgres server instance without needing to install it
6+
#
7+
# Mainly inspired by https://github.com/fergusstrange/embedded-postgres
8+
#
9+
# (c) 2026 George Lemon | MIT License
10+
# Made by Humans from OpenPeeps
11+
# https://github.com/openpeeps/postgresbox
12+
13+
import std/[tables, times, os, osproc, strutils, net]
14+
15+
import pkg/semver
16+
import pkg/threading/channels
17+
18+
type
19+
PostgresVersion* = enum
20+
v18 = "18.0.0"
21+
v17 = "17.5.0"
22+
v16 = "16.9.0"
23+
v15 = "15.13.0"
24+
v14 = "14.18.0"
25+
v13 = "13.21.0"
26+
27+
# https://github.com/fergusstrange/embedded-postgres/blob/master/config.go#L11
28+
29+
PostgresBoxConfig* = ref object
30+
version: PostgresVersion
31+
# The version of Postgres to use. You can specify a specific
32+
# version like "16.4.0" or use a predefined version
33+
# from the PostgresVersion enum.
34+
port*: Port = Port(5432)
35+
# Default port for Postgres is 5432, but you
36+
# can specify a different one if needed.
37+
database: string
38+
# Name of the default database to create when Postgres starts.
39+
username: string
40+
# Username for the default database. Default is "postgres".
41+
password: string
42+
# Password for the default database user. Default is "postgres".
43+
basePath*: string
44+
# Base path for all Postgres-related files. This is the root
45+
# directory under which all other paths (cache, runtime, data, binaries)
46+
# will be organized.
47+
cachePath*: string = "cache"
48+
# Path to cache downloaded Postgres binaries.
49+
# Ensure this directory is writable.
50+
runtimePath*: string = "runtime"
51+
# Path to store runtime files like logs and sockets.
52+
# Ensure this directory is writable.
53+
dataPath*: string = "data"
54+
# Path to store Postgres data. Ensure this directory
55+
# is writable and has enough space.
56+
binariesPath*: string = "bin"
57+
# Path to the Postgres binaries. This should point
58+
# to the directory containing the Postgres executables.
59+
locale: string
60+
# Locale settings for the Postgres cluster. Default is "en_US.UTF-8".
61+
startParameters: Table[string, string]
62+
# Additional parameters to pass when starting Postgres,
63+
# such as shared_buffers, max_connections, etc.
64+
binaryRepositoryURL: string
65+
# URL to download Postgres binaries. Default is the Maven Central repository.
66+
startTimeout: Duration
67+
# Timeout for starting the Postgres server. Default is 30 seconds.
68+
logger: string # todo
69+
70+
EmbeddedPostgres* = object
71+
config: PostgresBoxConfig
72+
started: bool
73+
74+
GreskewelConfigError* = object of CatchableError
75+
76+
var greskewChan = newChan[string]()
77+
78+
proc getDefaultConfig*(version: PostgresVersion): PostgresBoxConfig =
79+
## Returns a default configuration for the specified Postgres version.
80+
result = PostgresBoxConfig(
81+
version: PostgresVersion.v16,
82+
database: "postgres",
83+
username: "postgres",
84+
password: "postgres",
85+
locale: "en_US.UTF-8",
86+
startParameters: initTable[string, string](),
87+
dataPath: "data",
88+
binaryRepositoryURL: "https://repo1.maven.org/maven2/",
89+
startTimeout: initDuration(seconds = 30)
90+
)
91+
92+
proc initGreskewel*(config: PostgresBoxConfig = nil): EmbeddedPostgres =
93+
result = EmbeddedPostgres(
94+
config:
95+
if config != nil: config
96+
else: getDefaultConfig(PostgresVersion.v16),
97+
)
98+
if result.config.binaryRepositoryURL.len == 0:
99+
result.config.binaryRepositoryURL = "https://repo1.maven.org/maven2/"
100+
101+
#
102+
# Remote Controls
103+
#
104+
const
105+
binariesEndpoint = "io/zonky/test/postgres/embedded-postgres-binaries-$1-$2/$3/"
106+
jarBinaryEndpoint = "embedded-postgres-binaries-$1-$2-$3.jar"
107+
getCurrentOS* =
108+
when defined macosx: "darwin"
109+
else: system.hostOS
110+
binInitAppPath = "$1/$2/initdb" % [getCurrentOS, "bin"]
111+
# Note: The actual paths to the Postgres binaries may vary based on the version and platform.
112+
binPgCtlAppPath = "$1/$2/pg_ctl" % [getCurrentOS, "bin"]
113+
# Note: The actual paths to the Postgres binaries may vary based on the version and platform.
114+
115+
proc downloadBinaries*(ep: var EmbeddedPostgres) =
116+
## Download the Postgres binaries for the specified version and store them in the configured path.
117+
let jarFile = jarBinaryEndpoint % [getCurrentOS, hostCPU, $ep.config.version]
118+
let jarUrl = ep.config.binaryRepositoryURL & (binariesEndpoint % [getCurrentOS, hostCPU, $ep.config.version]) & jarFile
119+
120+
# Download the jar file containing the Postgres binaries if it doesn't already exist
121+
if not fileExists(ep.config.basePath / ep.config.binariesPath / jarFile):
122+
echo "Downloading Postgres binaries from:\n", jarUrl
123+
echo execCmdEx("curl -L -o " & ep.config.basePath / ep.config.binariesPath / jarFile & " " & jarUrl)
124+
125+
# unzip the jar file to extract the binaries
126+
createDir(ep.config.basePath / ep.config.binariesPath / $ep.config.version)
127+
let unzipPath = ep.config.basePath / ep.config.binariesPath / $ep.config.version / getCurrentOS
128+
if fileExists(ep.config.basePath / ep.config.binariesPath / jarFile) and not dirExists(unzipPath):
129+
echo "Unzipping Postgres binaries to:\n", unzipPath
130+
echo execCmdEx("unzip " & ep.config.basePath / ep.config.binariesPath / jarFile & " -d " & unzipPath)
131+
132+
# extract the tar file to get the actual binaries
133+
let tarPath = unzipPath / "postgres-" & getCurrentOS & "-x86_64.txz"
134+
let isExtracted = dirExists(unzipPath / "bin") and dirExists(unzipPath / "lib")
135+
if fileExists(tarPath) and not isExtracted:
136+
echo "Extracting Postgres binaries from:\n", tarPath
137+
let status = execCmdEx("tar -xvf " & tarPath & " -C \"" & unzipPath & "\"")
138+
if status.exitCode != 0:
139+
raise newException(GreskewelConfigError, "Failed to extract Postgres binaries: " & status.output)
140+
141+
#
142+
# Postgres Controls
143+
#
144+
proc encodeOptions(port: Port, parameters: Table[string, string]): string =
145+
var options = @["-p " & $port]
146+
for k, v in parameters:
147+
options.add("-c " & k & "=\"" & v & "\"")
148+
result = options.join(" ")
149+
150+
proc init*(ep: var EmbeddedPostgres) =
151+
## Initialize the embedded Postgres server with the specified configuration.
152+
echo "Initializing embedded Postgres version ", $ep.config.version
153+
if ep.config.basePath.len == 0 or ep.config.basePath.isAbsolute == false:
154+
raise newException(GreskewelConfigError,
155+
"Base path must be specified and absolute in the configuration.")
156+
if ep.config.dataPath.len == 0:
157+
raise newException(GreskewelConfigError,
158+
"Data path must be specified in the configuration.")
159+
if ep.config.binariesPath.len == 0:
160+
raise newException(GreskewelConfigError,
161+
"Binaries path must be specified in the configuration.")
162+
163+
discard existsOrCreateDir(ep.config.basePath)
164+
discard existsOrCreateDir(ep.config.basePath / ep.config.runtimePath)
165+
discard existsOrCreateDir(ep.config.basePath / ep.config.binariesPath)
166+
167+
let dataPath = ep.config.basePath / ep.config.dataPath
168+
if not dataPath.dirExists:
169+
# Initialize the Postgres data directory using the `initdb` binary
170+
let initDbApp = ep.config.basePath / ep.config.binariesPath / $ep.config.version / binInitAppPath
171+
if not initDbApp.fileExists():
172+
raise newException(GreskewelConfigError,
173+
"`initdb` binary not found at expected path: " & initDbApp)
174+
let res = execCmdEx(initDbApp & " -D " & dataPath & " --username postgres")
175+
if res.exitCode != 0:
176+
raise newException(GreskewelConfigError,
177+
"Failed to initialize Postgres data directory: " & res.output)
178+
179+
type
180+
PostgresThreadInfo = tuple
181+
channel: ptr Chan[string]
182+
dataPath: string
183+
binPath: string
184+
port: Port
185+
186+
proc postgresThread(pg: PostgresThreadInfo) {.thread.} =
187+
# this thread will run a loop to listen for commands to start/stop the Postgres server
188+
var pgProc: Process
189+
var isRunning = false
190+
while true:
191+
var command: string
192+
if pg[0][].tryRecv(command):
193+
let pidPath = pg[1] / "postmaster.pid"
194+
if command == "pg.start":
195+
if not pidPath.fileExists():
196+
let encodedOpts = encodeOptions(pg[3], initTable[string, string]())
197+
let pgProc = startProcess(pg.binPath / binPgCtlAppPath,
198+
args = ["start", "-w", "-D", pg[1], "-o", encodedOpts])
199+
elif command == "pg.stop" and pidPath.fileExists():
200+
let res = execCmdEx(pg.binPath / binPgCtlAppPath & " stop -w -D " & pg[1])
201+
# echo res
202+
sleep(100) # small delay to prevent busy waiting
203+
204+
var worker: Thread[PostgresThreadInfo]
205+
proc start*(ep: var EmbeddedPostgres) =
206+
## Start the embedded Postgres server
207+
let currentBinPath = ep.config.basePath / ep.config.binariesPath / $ep.config.version
208+
let dataPath = ep.config.basePath / ep.config.dataPath
209+
createThread(worker, postgresThread, (addr(greskewChan), dataPath, currentBinPath, ep.config.port))
210+
sleep(1000) # wait a bit for the server to start
211+
greskewChan.send("pg.start")
212+
sleep(2000) # wait a bit for the server to start
213+
ep.started = true # todo: this should be set based on the actual status of the server, not just after sending the start command
214+
215+
proc stop*(ep: var EmbeddedPostgres) =
216+
## Stop the embedded Postgres server
217+
greskewChan.send("pg.stop")
218+
219+
proc restart*(ep: var EmbeddedPostgres) =
220+
## Restart the embedded Postgres server
221+
stop(ep)
222+
start(ep)
223+
224+
proc status*(ep: EmbeddedPostgres): string =
225+
## Get the status of the embedded Postgres server
226+
let cmd = @["pg_ctl", "status", "-D", ep.config.dataPath]
227+
result = staticExec(cmd.join(" "))
228+
229+
proc isRunning*(ep: EmbeddedPostgres): bool =
230+
## Check if the embedded Postgres server is running
231+
result = ep.started
232+
233+
proc getConnectionString*(ep: EmbeddedPostgres): string =
234+
## Get the connection string for the embedded Postgres server
235+
result = "postgresql://" & ep.config.username & ":" & ep.config.password &
236+
"@localhost:" & $ep.config.port & "/" & ep.config.database
237+
238+
proc getVersion*(ep: EmbeddedPostgres): PostgresVersion =
239+
## Get the version of the embedded Postgres server
240+
result = ep.config.version
241+
242+
proc getConfig*(ep: EmbeddedPostgres): PostgresBoxConfig =
243+
## Get the configuration of the embedded Postgres server
244+
result = ep.config

0 commit comments

Comments
 (0)