spindb 0.9.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -10
- package/cli/commands/create.ts +72 -42
- package/cli/commands/engines.ts +61 -0
- package/cli/commands/logs.ts +3 -29
- package/cli/commands/menu/container-handlers.ts +32 -3
- package/cli/commands/menu/sql-handlers.ts +4 -26
- package/cli/helpers.ts +6 -6
- package/cli/index.ts +3 -3
- package/cli/utils/file-follower.ts +95 -0
- package/config/defaults.ts +3 -0
- package/config/os-dependencies.ts +79 -1
- package/core/binary-manager.ts +181 -66
- package/core/config-manager.ts +5 -65
- package/core/dependency-manager.ts +39 -1
- package/core/platform-service.ts +149 -11
- package/core/process-manager.ts +152 -33
- package/engines/base-engine.ts +27 -0
- package/engines/mysql/backup.ts +12 -5
- package/engines/mysql/index.ts +328 -110
- package/engines/mysql/restore.ts +22 -6
- package/engines/postgresql/backup.ts +7 -3
- package/engines/postgresql/binary-manager.ts +47 -31
- package/engines/postgresql/edb-binary-urls.ts +123 -0
- package/engines/postgresql/index.ts +109 -22
- package/engines/postgresql/version-maps.ts +63 -0
- package/engines/sqlite/index.ts +9 -19
- package/package.json +4 -2
package/engines/mysql/restore.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles detecting backup formats and restoring MySQL dumps.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn } from 'child_process'
|
|
7
|
+
import { spawn, type SpawnOptions } from 'child_process'
|
|
8
8
|
import { createReadStream } from 'fs'
|
|
9
9
|
import { open } from 'fs/promises'
|
|
10
10
|
import { createGunzip } from 'zlib'
|
|
@@ -202,12 +202,20 @@ export async function restoreBackup(
|
|
|
202
202
|
// Restore using mysql client
|
|
203
203
|
// CLI: mysql -h 127.0.0.1 -P {port} -u root {db} < {file}
|
|
204
204
|
// For compressed files: gunzip -c {file} | mysql ...
|
|
205
|
+
|
|
206
|
+
// Use plain spawn (no shell) so the executable path can be quoted when
|
|
207
|
+
// it contains spaces (e.g., 'C:\Program Files\...'). Using shell:true
|
|
208
|
+
// previously caused quoting issues on Windows.
|
|
209
|
+
const spawnOptions: SpawnOptions = {
|
|
210
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
211
|
+
}
|
|
212
|
+
|
|
205
213
|
return new Promise((resolve, reject) => {
|
|
206
214
|
const args = ['-h', '127.0.0.1', '-P', String(port), '-u', user, database]
|
|
207
215
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
logDebug('Restoring backup with mysql', { mysql, args, spawnOptions })
|
|
217
|
+
|
|
218
|
+
const proc = spawn(mysql, args, spawnOptions)
|
|
211
219
|
|
|
212
220
|
// Pipe backup file to stdin, decompressing if necessary
|
|
213
221
|
const fileStream = createReadStream(backupPath)
|
|
@@ -215,6 +223,10 @@ export async function restoreBackup(
|
|
|
215
223
|
if (format.format === 'compressed') {
|
|
216
224
|
// Decompress gzipped file before piping to mysql
|
|
217
225
|
const gunzip = createGunzip()
|
|
226
|
+
if (!proc.stdin) {
|
|
227
|
+
reject(new Error('MySQL process stdin is not available, cannot restore backup'))
|
|
228
|
+
return
|
|
229
|
+
}
|
|
218
230
|
fileStream.pipe(gunzip).pipe(proc.stdin)
|
|
219
231
|
|
|
220
232
|
// Handle gunzip errors
|
|
@@ -222,16 +234,20 @@ export async function restoreBackup(
|
|
|
222
234
|
reject(new Error(`Failed to decompress backup file: ${err.message}`))
|
|
223
235
|
})
|
|
224
236
|
} else {
|
|
237
|
+
if (!proc.stdin) {
|
|
238
|
+
reject(new Error('MySQL process stdin is not available, cannot restore backup'))
|
|
239
|
+
return
|
|
240
|
+
}
|
|
225
241
|
fileStream.pipe(proc.stdin)
|
|
226
242
|
}
|
|
227
243
|
|
|
228
244
|
let stdout = ''
|
|
229
245
|
let stderr = ''
|
|
230
246
|
|
|
231
|
-
proc.stdout
|
|
247
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
232
248
|
stdout += data.toString()
|
|
233
249
|
})
|
|
234
|
-
proc.stderr
|
|
250
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
235
251
|
stderr += data.toString()
|
|
236
252
|
})
|
|
237
253
|
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Creates database backups in SQL or custom (.dump) format using pg_dump.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn } from 'child_process'
|
|
7
|
+
import { spawn, type SpawnOptions } from 'child_process'
|
|
8
8
|
import { stat } from 'fs/promises'
|
|
9
9
|
import { configManager } from '../../core/config-manager'
|
|
10
|
+
import { getWindowsSpawnOptions } from '../../core/platform-service'
|
|
10
11
|
import { defaults } from '../../config/defaults'
|
|
11
12
|
import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
|
|
12
13
|
|
|
@@ -61,9 +62,12 @@ export async function createBackup(
|
|
|
61
62
|
outputPath,
|
|
62
63
|
]
|
|
63
64
|
|
|
64
|
-
const
|
|
65
|
+
const spawnOptions: SpawnOptions = {
|
|
65
66
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
-
|
|
67
|
+
...getWindowsSpawnOptions(),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const proc = spawn(pgDumpPath, args, spawnOptions)
|
|
67
71
|
|
|
68
72
|
let stderr = ''
|
|
69
73
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { getEngineDependencies } from '../../config/os-dependencies'
|
|
13
13
|
import { getPostgresHomebrewPackage } from '../../config/engine-defaults'
|
|
14
14
|
import { logDebug } from '../../core/error-handler'
|
|
15
|
+
import { isWindows, platformService } from '../../core/platform-service'
|
|
15
16
|
|
|
16
17
|
const execAsync = promisify(exec)
|
|
17
18
|
|
|
@@ -112,7 +113,7 @@ export async function getPostgresVersion(
|
|
|
112
113
|
*/
|
|
113
114
|
export async function findBinaryPath(binary: string): Promise<string | null> {
|
|
114
115
|
try {
|
|
115
|
-
const command =
|
|
116
|
+
const command = isWindows() ? 'where' : 'which'
|
|
116
117
|
const { stdout } = await execAsync(`${command} ${binary}`)
|
|
117
118
|
return stdout.trim().split('\n')[0] || null
|
|
118
119
|
} catch (error) {
|
|
@@ -133,12 +134,18 @@ export async function findBinaryPathFresh(
|
|
|
133
134
|
const path = await findBinaryPath(binary)
|
|
134
135
|
if (path) return path
|
|
135
136
|
|
|
136
|
-
//
|
|
137
|
+
// Windows doesn't need shell refresh - PATH is always current
|
|
138
|
+
if (isWindows()) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If not found on Unix, try to refresh PATH cache (especially after package updates)
|
|
137
143
|
try {
|
|
138
144
|
// Force shell to re-evaluate PATH
|
|
139
145
|
const shell = process.env.SHELL || '/bin/bash'
|
|
146
|
+
const shellConfig = shell.endsWith('zsh') ? '.zshrc' : '.bashrc'
|
|
140
147
|
const { stdout } = await execWithTimeout(
|
|
141
|
-
`${shell} -c 'source
|
|
148
|
+
`${shell} -c 'source ~/${shellConfig} && which ${binary}'`,
|
|
142
149
|
)
|
|
143
150
|
return stdout.trim().split('\n')[0] || null
|
|
144
151
|
} catch (error) {
|
|
@@ -171,7 +178,7 @@ async function execWithTimeout(
|
|
|
171
178
|
|
|
172
179
|
// Additional timeout safety
|
|
173
180
|
setTimeout(() => {
|
|
174
|
-
child.kill(
|
|
181
|
+
child.kill() // Cross-platform process termination
|
|
175
182
|
reject(new Error(`Command timed out after ${timeoutMs}ms: ${command}`))
|
|
176
183
|
}, timeoutMs)
|
|
177
184
|
})
|
|
@@ -179,36 +186,44 @@ async function execWithTimeout(
|
|
|
179
186
|
|
|
180
187
|
/**
|
|
181
188
|
* Parse dump file to get required PostgreSQL version
|
|
189
|
+
* Cross-platform: reads file header directly using Node.js
|
|
182
190
|
*/
|
|
183
191
|
export async function getDumpRequiredVersion(
|
|
184
192
|
dumpPath: string,
|
|
185
193
|
): Promise<string | null> {
|
|
186
194
|
try {
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
195
|
+
// Read first 32 bytes of the file to check for PostgreSQL custom dump format
|
|
196
|
+
// Custom dump magic: "PGDMP" followed by version info
|
|
197
|
+
const { open } = await import('fs/promises')
|
|
198
|
+
const fileHandle = await open(dumpPath, 'r')
|
|
199
|
+
const buffer = Buffer.alloc(32)
|
|
200
|
+
await fileHandle.read(buffer, 0, 32, 0)
|
|
201
|
+
await fileHandle.close()
|
|
202
|
+
|
|
203
|
+
// Check for PostgreSQL custom dump format magic bytes: "PGDMP"
|
|
204
|
+
const header = buffer.toString('ascii', 0, 5)
|
|
205
|
+
if (header === 'PGDMP') {
|
|
206
|
+
// Bytes 5-6 contain version info in custom format
|
|
207
|
+
// Byte 5: major version, Byte 6: minor version, Byte 7: revision
|
|
208
|
+
const dumpMajor = buffer[5]
|
|
209
|
+
const dumpMinor = buffer[6]
|
|
210
|
+
|
|
211
|
+
logDebug(`Detected pg_dump custom format version: ${dumpMajor}.${dumpMinor}`)
|
|
212
|
+
|
|
213
|
+
// pg_dump version typically maps to PostgreSQL version
|
|
214
|
+
// If it's a recent dump format, require a recent PostgreSQL
|
|
215
|
+
if (dumpMajor >= 1 && dumpMinor >= 14) {
|
|
216
|
+
return '15.0' // Modern dump format needs modern PostgreSQL
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
// For plain SQL dumps or older formats, check if file looks like SQL
|
|
221
|
+
const textHeader = buffer.toString('utf-8', 0, 20)
|
|
222
|
+
if (textHeader.includes('--') || textHeader.includes('pg_dump')) {
|
|
223
|
+
// Plain SQL dump - any PostgreSQL version should work
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
212
227
|
// Fallback: if we can't determine, assume it needs a recent version
|
|
213
228
|
return '15.0'
|
|
214
229
|
} catch (error) {
|
|
@@ -229,9 +244,8 @@ export function isVersionCompatible(
|
|
|
229
244
|
const current = parseFloat(currentVersion)
|
|
230
245
|
const required = parseFloat(requiredVersion)
|
|
231
246
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
return current >= required && Math.floor(current) === Math.floor(required)
|
|
247
|
+
// PostgreSQL is forward-compatible: newer pg_restore can restore older dumps
|
|
248
|
+
return current >= required
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
/**
|
|
@@ -264,8 +278,9 @@ export async function getBinaryInfo(
|
|
|
264
278
|
|
|
265
279
|
// Try to detect which package manager installed this binary
|
|
266
280
|
let packageManager: string | undefined
|
|
281
|
+
const { platform } = platformService.getPlatformInfo()
|
|
267
282
|
try {
|
|
268
|
-
if (
|
|
283
|
+
if (platform === 'darwin') {
|
|
269
284
|
// On macOS, check if it's from Homebrew
|
|
270
285
|
const { stdout } = await execAsync(
|
|
271
286
|
'brew list postgresql@* 2>/dev/null || brew list libpq 2>/dev/null || true',
|
|
@@ -273,7 +288,7 @@ export async function getBinaryInfo(
|
|
|
273
288
|
if (stdout.includes('postgresql') || stdout.includes('libpq')) {
|
|
274
289
|
packageManager = 'brew'
|
|
275
290
|
}
|
|
276
|
-
} else {
|
|
291
|
+
} else if (platform === 'linux') {
|
|
277
292
|
// On Linux, check common package managers
|
|
278
293
|
try {
|
|
279
294
|
await execAsync('dpkg -S $(which pg_restore) 2>/dev/null')
|
|
@@ -287,6 +302,7 @@ export async function getBinaryInfo(
|
|
|
287
302
|
}
|
|
288
303
|
}
|
|
289
304
|
}
|
|
305
|
+
// On Windows (win32), we don't detect package managers - user installs via choco/scoop/winget
|
|
290
306
|
} catch {
|
|
291
307
|
// Could not determine package manager
|
|
292
308
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EDB Binary URL Builder for Windows
|
|
3
|
+
*
|
|
4
|
+
* EnterpriseDB (EDB) provides official PostgreSQL binaries for Windows.
|
|
5
|
+
* Unlike zonky.io which uses predictable Maven URLs, EDB uses file IDs.
|
|
6
|
+
*
|
|
7
|
+
* Download page: https://www.enterprisedb.com/download-postgresql-binaries
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
POSTGRESQL_VERSION_MAP,
|
|
12
|
+
SUPPORTED_MAJOR_VERSIONS,
|
|
13
|
+
} from './version-maps'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mapping of PostgreSQL versions to EDB file IDs
|
|
17
|
+
* These IDs are from the EDB download page and need to be updated when new versions are released.
|
|
18
|
+
*
|
|
19
|
+
* Format: 'version' -> 'fileId'
|
|
20
|
+
*
|
|
21
|
+
* IMPORTANT: When updating POSTGRESQL_VERSION_MAP in version-maps.ts,
|
|
22
|
+
* also update this map with the corresponding EDB file IDs.
|
|
23
|
+
*/
|
|
24
|
+
export const EDB_FILE_IDS: Record<string, string> = {
|
|
25
|
+
// PostgreSQL 17.x
|
|
26
|
+
'17.7.0': '1259911',
|
|
27
|
+
'17': '1259911', // Alias for latest 17.x
|
|
28
|
+
|
|
29
|
+
// PostgreSQL 16.x
|
|
30
|
+
'16.11.0': '1259906',
|
|
31
|
+
'16': '1259906', // Alias for latest 16.x
|
|
32
|
+
|
|
33
|
+
// PostgreSQL 15.x
|
|
34
|
+
'15.15.0': '1259903',
|
|
35
|
+
'15': '1259903', // Alias for latest 15.x
|
|
36
|
+
|
|
37
|
+
// PostgreSQL 14.x
|
|
38
|
+
'14.20.0': '1259900',
|
|
39
|
+
'14': '1259900', // Alias for latest 14.x
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Re-export for backwards compatibility
|
|
43
|
+
export { SUPPORTED_MAJOR_VERSIONS as WINDOWS_SUPPORTED_VERSIONS }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the EDB download URL for a PostgreSQL version on Windows
|
|
47
|
+
*
|
|
48
|
+
* @param version - PostgreSQL version (e.g., '17', '17.7.0')
|
|
49
|
+
* @returns Download URL for the Windows binary ZIP
|
|
50
|
+
* @throws Error if version is not supported
|
|
51
|
+
*/
|
|
52
|
+
export function getEDBBinaryUrl(version: string): string {
|
|
53
|
+
// Try direct lookup first
|
|
54
|
+
let fileId = EDB_FILE_IDS[version]
|
|
55
|
+
|
|
56
|
+
// If not found, try to normalize version
|
|
57
|
+
if (!fileId) {
|
|
58
|
+
// Try major version
|
|
59
|
+
const major = version.split('.')[0]
|
|
60
|
+
const fullVersion = POSTGRESQL_VERSION_MAP[major]
|
|
61
|
+
if (fullVersion) {
|
|
62
|
+
fileId = EDB_FILE_IDS[fullVersion]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!fileId) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Unsupported PostgreSQL version for Windows: ${version}. ` +
|
|
69
|
+
`Supported versions: ${SUPPORTED_MAJOR_VERSIONS.join(', ')}`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `https://sbp.enterprisedb.com/getfile.jsp?fileid=${fileId}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the full version string for a major version on Windows
|
|
78
|
+
*
|
|
79
|
+
* @param majorVersion - Major version (e.g., '17')
|
|
80
|
+
* @returns Full version string (e.g., '17.7.0') or null if not supported
|
|
81
|
+
*/
|
|
82
|
+
export function getWindowsFullVersion(majorVersion: string): string | null {
|
|
83
|
+
return POSTGRESQL_VERSION_MAP[majorVersion] || null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a version is supported on Windows
|
|
88
|
+
*
|
|
89
|
+
* @param version - Version to check (major or full)
|
|
90
|
+
* @returns true if the version is supported AND has an EDB file ID
|
|
91
|
+
*/
|
|
92
|
+
export function isWindowsVersionSupported(version: string): boolean {
|
|
93
|
+
// Check if we have a file ID for this version directly
|
|
94
|
+
if (EDB_FILE_IDS[version]) {
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if it's a major version that maps to a full version with a file ID
|
|
99
|
+
const major = version.split('.')[0]
|
|
100
|
+
const fullVersion = POSTGRESQL_VERSION_MAP[major]
|
|
101
|
+
if (fullVersion && EDB_FILE_IDS[fullVersion]) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get available Windows versions (for display purposes)
|
|
110
|
+
*
|
|
111
|
+
* @returns Record of major versions to their full versions
|
|
112
|
+
*/
|
|
113
|
+
export function getAvailableWindowsVersions(): Record<string, string[]> {
|
|
114
|
+
const grouped: Record<string, string[]> = {}
|
|
115
|
+
for (const major of SUPPORTED_MAJOR_VERSIONS) {
|
|
116
|
+
const fullVersion = POSTGRESQL_VERSION_MAP[major]
|
|
117
|
+
// Only include if we have a file ID for it
|
|
118
|
+
if (fullVersion && EDB_FILE_IDS[fullVersion]) {
|
|
119
|
+
grouped[major] = [fullVersion]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return grouped
|
|
123
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { join } from 'path'
|
|
2
|
-
import { spawn, exec } from 'child_process'
|
|
2
|
+
import { spawn, exec, type SpawnOptions } from 'child_process'
|
|
3
3
|
import { promisify } from 'util'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
4
5
|
import { readFile, writeFile } from 'fs/promises'
|
|
5
6
|
import { BaseEngine } from '../base-engine'
|
|
6
7
|
import { binaryManager } from '../../core/binary-manager'
|
|
7
8
|
import { processManager } from '../../core/process-manager'
|
|
8
9
|
import { configManager } from '../../core/config-manager'
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
platformService,
|
|
12
|
+
isWindows,
|
|
13
|
+
getWindowsSpawnOptions,
|
|
14
|
+
} from '../../core/platform-service'
|
|
10
15
|
import { paths } from '../../config/paths'
|
|
11
16
|
import { defaults, getEngineDefaults } from '../../config/defaults'
|
|
12
17
|
import {
|
|
@@ -32,6 +37,34 @@ import type {
|
|
|
32
37
|
|
|
33
38
|
const execAsync = promisify(exec)
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Build a Windows-safe psql command string for either a file or inline SQL.
|
|
42
|
+
* This is exported for unit testing.
|
|
43
|
+
*/
|
|
44
|
+
export function buildWindowsPsqlCommand(
|
|
45
|
+
psqlPath: string,
|
|
46
|
+
port: number,
|
|
47
|
+
user: string,
|
|
48
|
+
db: string,
|
|
49
|
+
options: { file?: string; sql?: string },
|
|
50
|
+
): string {
|
|
51
|
+
if (!options.file && !options.sql) {
|
|
52
|
+
throw new Error('Either file or sql option must be provided')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let cmd = `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${user} -d ${db}`
|
|
56
|
+
|
|
57
|
+
if (options.file) {
|
|
58
|
+
cmd += ` -f "${options.file}"`
|
|
59
|
+
} else if (options.sql) {
|
|
60
|
+
// Escape double quotes in the SQL so the outer double quotes are preserved
|
|
61
|
+
const escaped = options.sql.replace(/"/g, '\\"')
|
|
62
|
+
cmd += ` -c "${escaped}"`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return cmd
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
export class PostgreSQLEngine extends BaseEngine {
|
|
36
69
|
name = 'postgresql'
|
|
37
70
|
displayName = 'PostgreSQL'
|
|
@@ -118,13 +151,27 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
118
151
|
|
|
119
152
|
/**
|
|
120
153
|
* Ensure PostgreSQL binaries are available
|
|
154
|
+
* Also registers client tools (psql, pg_dump, etc.) in config after download
|
|
121
155
|
*/
|
|
122
156
|
async ensureBinaries(
|
|
123
157
|
version: string,
|
|
124
158
|
onProgress?: ProgressCallback,
|
|
125
159
|
): Promise<string> {
|
|
126
160
|
const { platform: p, arch: a } = this.getPlatformInfo()
|
|
127
|
-
|
|
161
|
+
const binPath = await binaryManager.ensureInstalled(version, p, a, onProgress)
|
|
162
|
+
|
|
163
|
+
// Register client tools from downloaded binaries in config
|
|
164
|
+
// This ensures dependency checks find them without requiring system installation
|
|
165
|
+
const ext = platformService.getExecutableExtension()
|
|
166
|
+
const clientTools = ['psql', 'pg_dump', 'pg_restore', 'pg_basebackup'] as const
|
|
167
|
+
for (const tool of clientTools) {
|
|
168
|
+
const toolPath = join(binPath, 'bin', `${tool}${ext}`)
|
|
169
|
+
if (existsSync(toolPath)) {
|
|
170
|
+
await configManager.setBinaryPath(tool, toolPath, 'bundled')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return binPath
|
|
128
175
|
}
|
|
129
176
|
|
|
130
177
|
/**
|
|
@@ -144,7 +191,8 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
144
191
|
options: Record<string, unknown> = {},
|
|
145
192
|
): Promise<string> {
|
|
146
193
|
const binPath = this.getBinaryPath(version)
|
|
147
|
-
const
|
|
194
|
+
const ext = platformService.getExecutableExtension()
|
|
195
|
+
const initdbPath = join(binPath, 'bin', `initdb${ext}`)
|
|
148
196
|
const dataDir = paths.getContainerDataPath(containerName, {
|
|
149
197
|
engine: this.name,
|
|
150
198
|
})
|
|
@@ -231,7 +279,8 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
231
279
|
): Promise<{ port: number; connectionString: string }> {
|
|
232
280
|
const { name, version, port } = container
|
|
233
281
|
const binPath = this.getBinaryPath(version)
|
|
234
|
-
const
|
|
282
|
+
const ext = platformService.getExecutableExtension()
|
|
283
|
+
const pgCtlPath = join(binPath, 'bin', `pg_ctl${ext}`)
|
|
235
284
|
const dataDir = paths.getContainerDataPath(name, { engine: this.name })
|
|
236
285
|
const logFile = paths.getContainerLogPath(name, { engine: this.name })
|
|
237
286
|
|
|
@@ -254,7 +303,8 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
254
303
|
async stop(container: ContainerConfig): Promise<void> {
|
|
255
304
|
const { name, version } = container
|
|
256
305
|
const binPath = this.getBinaryPath(version)
|
|
257
|
-
const
|
|
306
|
+
const ext = platformService.getExecutableExtension()
|
|
307
|
+
const pgCtlPath = join(binPath, 'bin', `pg_ctl${ext}`)
|
|
258
308
|
const dataDir = paths.getContainerDataPath(name, { engine: this.name })
|
|
259
309
|
|
|
260
310
|
await processManager.stop(pgCtlPath, dataDir)
|
|
@@ -266,7 +316,8 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
266
316
|
async status(container: ContainerConfig): Promise<StatusResult> {
|
|
267
317
|
const { name, version } = container
|
|
268
318
|
const binPath = this.getBinaryPath(version)
|
|
269
|
-
const
|
|
319
|
+
const ext = platformService.getExecutableExtension()
|
|
320
|
+
const pgCtlPath = join(binPath, 'bin', `pg_ctl${ext}`)
|
|
270
321
|
const dataDir = paths.getContainerDataPath(name, { engine: this.name })
|
|
271
322
|
|
|
272
323
|
return processManager.status(pgCtlPath, dataDir)
|
|
@@ -370,6 +421,11 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
370
421
|
const db = database || 'postgres'
|
|
371
422
|
const psqlPath = await this.getPsqlPath()
|
|
372
423
|
|
|
424
|
+
const spawnOptions: SpawnOptions = {
|
|
425
|
+
stdio: 'inherit',
|
|
426
|
+
...getWindowsSpawnOptions(),
|
|
427
|
+
}
|
|
428
|
+
|
|
373
429
|
return new Promise((resolve, reject) => {
|
|
374
430
|
const proc = spawn(
|
|
375
431
|
psqlPath,
|
|
@@ -383,7 +439,7 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
383
439
|
'-d',
|
|
384
440
|
db,
|
|
385
441
|
],
|
|
386
|
-
|
|
442
|
+
spawnOptions,
|
|
387
443
|
)
|
|
388
444
|
|
|
389
445
|
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
@@ -405,10 +461,14 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
405
461
|
const { port } = container
|
|
406
462
|
const psqlPath = await this.getPsqlPath()
|
|
407
463
|
|
|
464
|
+
// On Windows, single quotes don't work in cmd.exe - use double quotes and escape inner quotes
|
|
465
|
+
const sql = `CREATE DATABASE "${database}"`
|
|
466
|
+
const cmd = isWindows()
|
|
467
|
+
? `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c "${sql.replace(/"/g, '\\"')}"`
|
|
468
|
+
: `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c '${sql}'`
|
|
469
|
+
|
|
408
470
|
try {
|
|
409
|
-
await execAsync(
|
|
410
|
-
`"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c 'CREATE DATABASE "${database}"'`,
|
|
411
|
-
)
|
|
471
|
+
await execAsync(cmd)
|
|
412
472
|
} catch (error) {
|
|
413
473
|
const err = error as Error
|
|
414
474
|
// Ignore "database already exists" error
|
|
@@ -429,10 +489,14 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
429
489
|
const { port } = container
|
|
430
490
|
const psqlPath = await this.getPsqlPath()
|
|
431
491
|
|
|
492
|
+
// On Windows, single quotes don't work in cmd.exe - use double quotes and escape inner quotes
|
|
493
|
+
const sql = `DROP DATABASE IF EXISTS "${database}"`
|
|
494
|
+
const cmd = isWindows()
|
|
495
|
+
? `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c "${sql.replace(/"/g, '\\"')}"`
|
|
496
|
+
: `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c '${sql}'`
|
|
497
|
+
|
|
432
498
|
try {
|
|
433
|
-
await execAsync(
|
|
434
|
-
`"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c 'DROP DATABASE IF EXISTS "${database}"'`,
|
|
435
|
-
)
|
|
499
|
+
await execAsync(cmd)
|
|
436
500
|
} catch (error) {
|
|
437
501
|
const err = error as Error
|
|
438
502
|
// Ignore "database does not exist" error
|
|
@@ -457,9 +521,12 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
457
521
|
try {
|
|
458
522
|
const psqlPath = await this.getPsqlPath()
|
|
459
523
|
// Query pg_database_size for the specific database
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
)
|
|
524
|
+
// On Windows, use escaped double quotes; on Unix, use single quotes
|
|
525
|
+
const sql = `SELECT pg_database_size('${db}')`
|
|
526
|
+
const cmd = isWindows()
|
|
527
|
+
? `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -t -A -c "${sql.replace(/'/g, "''")}"`
|
|
528
|
+
: `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -t -A -c "${sql}"`
|
|
529
|
+
const { stdout } = await execAsync(cmd)
|
|
463
530
|
const size = parseInt(stdout.trim(), 10)
|
|
464
531
|
return isNaN(size) ? null : size
|
|
465
532
|
} catch {
|
|
@@ -480,13 +547,15 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
480
547
|
): Promise<DumpResult> {
|
|
481
548
|
const pgDumpPath = await this.getPgDumpPath()
|
|
482
549
|
|
|
550
|
+
const spawnOptions: SpawnOptions = {
|
|
551
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
552
|
+
...getWindowsSpawnOptions(),
|
|
553
|
+
}
|
|
554
|
+
|
|
483
555
|
return new Promise((resolve, reject) => {
|
|
484
|
-
// Use custom format (-Fc) for best compatibility and compression
|
|
485
556
|
const args = [connectionString, '-Fc', '-f', outputPath]
|
|
486
557
|
|
|
487
|
-
const proc = spawn(pgDumpPath, args,
|
|
488
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
489
|
-
})
|
|
558
|
+
const proc = spawn(pgDumpPath, args, spawnOptions)
|
|
490
559
|
|
|
491
560
|
let stdout = ''
|
|
492
561
|
let stderr = ''
|
|
@@ -544,6 +613,20 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
544
613
|
const db = options.database || container.database || 'postgres'
|
|
545
614
|
const psqlPath = await this.getPsqlPath()
|
|
546
615
|
|
|
616
|
+
// On Windows, build a single command string and use exec to avoid
|
|
617
|
+
// passing an args array with shell:true (DEP0190 and quoting issues).
|
|
618
|
+
if (isWindows()) {
|
|
619
|
+
const cmd = buildWindowsPsqlCommand(psqlPath, port, defaults.superuser, db, options)
|
|
620
|
+
try {
|
|
621
|
+
await execAsync(cmd)
|
|
622
|
+
return
|
|
623
|
+
} catch (error) {
|
|
624
|
+
const err = error as Error
|
|
625
|
+
throw new Error(`psql failed: ${err.message}`)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Non-Windows: spawn directly with args (no shell)
|
|
547
630
|
const args = [
|
|
548
631
|
'-h',
|
|
549
632
|
'127.0.0.1',
|
|
@@ -563,8 +646,12 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
563
646
|
throw new Error('Either file or sql option must be provided')
|
|
564
647
|
}
|
|
565
648
|
|
|
649
|
+
const spawnOptions: SpawnOptions = {
|
|
650
|
+
stdio: 'inherit',
|
|
651
|
+
}
|
|
652
|
+
|
|
566
653
|
return new Promise((resolve, reject) => {
|
|
567
|
-
const proc = spawn(psqlPath, args,
|
|
654
|
+
const proc = spawn(psqlPath, args, spawnOptions)
|
|
568
655
|
|
|
569
656
|
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
570
657
|
reject(err)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Version Maps
|
|
3
|
+
*
|
|
4
|
+
* Shared version mappings used by both zonky.io (macOS/Linux) and EDB (Windows).
|
|
5
|
+
* Both sources use the same PostgreSQL releases.
|
|
6
|
+
*
|
|
7
|
+
* When updating versions:
|
|
8
|
+
* 1. Check zonky.io Maven Central for new versions:
|
|
9
|
+
* https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-darwin-arm64v8/
|
|
10
|
+
* 2. Check EDB download page for matching Windows versions:
|
|
11
|
+
* https://www.enterprisedb.com/download-postgresql-binaries
|
|
12
|
+
* 3. Update POSTGRESQL_VERSION_MAP with new full versions
|
|
13
|
+
* 4. Update EDB_FILE_IDS in edb-binary-urls.ts with new file IDs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map of major PostgreSQL versions to their latest stable patch versions.
|
|
18
|
+
* Used for both zonky.io (macOS/Linux) and EDB (Windows) binaries.
|
|
19
|
+
*/
|
|
20
|
+
export const POSTGRESQL_VERSION_MAP: Record<string, string> = {
|
|
21
|
+
'14': '14.20.0',
|
|
22
|
+
'15': '15.15.0',
|
|
23
|
+
'16': '16.11.0',
|
|
24
|
+
'17': '17.7.0',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Supported major PostgreSQL versions.
|
|
29
|
+
* Derived from POSTGRESQL_VERSION_MAP keys.
|
|
30
|
+
*/
|
|
31
|
+
export const SUPPORTED_MAJOR_VERSIONS = Object.keys(POSTGRESQL_VERSION_MAP)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the full version string for a major version.
|
|
35
|
+
*
|
|
36
|
+
* @param majorVersion - Major version (e.g., '17')
|
|
37
|
+
* @returns Full version string (e.g., '17.7.0') or null if not supported
|
|
38
|
+
*/
|
|
39
|
+
export function getFullVersion(majorVersion: string): string | null {
|
|
40
|
+
return POSTGRESQL_VERSION_MAP[majorVersion] || null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalize a version string to X.Y.Z format.
|
|
45
|
+
*
|
|
46
|
+
* @param version - Version string (e.g., '17', '17.7', '17.7.0')
|
|
47
|
+
* @returns Normalized version (e.g., '17.7.0')
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeVersion(version: string): string {
|
|
50
|
+
// If it's a major version only, use the map
|
|
51
|
+
const fullVersion = POSTGRESQL_VERSION_MAP[version]
|
|
52
|
+
if (fullVersion) {
|
|
53
|
+
return fullVersion
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Normalize to X.Y.Z format
|
|
57
|
+
const parts = version.split('.')
|
|
58
|
+
if (parts.length === 2) {
|
|
59
|
+
return `${version}.0`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return version
|
|
63
|
+
}
|