spindb 0.4.1 → 0.5.2

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.
@@ -62,7 +62,12 @@ export class BinaryManager {
62
62
  platform: string,
63
63
  arch: string,
64
64
  ): Promise<boolean> {
65
- const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
65
+ const binPath = paths.getBinaryPath({
66
+ engine: 'postgresql',
67
+ version,
68
+ platform,
69
+ arch,
70
+ })
66
71
  const postgresPath = join(binPath, 'bin', 'postgres')
67
72
  return existsSync(postgresPath)
68
73
  }
@@ -109,7 +114,12 @@ export class BinaryManager {
109
114
  onProgress?: ProgressCallback,
110
115
  ): Promise<string> {
111
116
  const url = this.getDownloadUrl(version, platform, arch)
112
- const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
117
+ const binPath = paths.getBinaryPath({
118
+ engine: 'postgresql',
119
+ version,
120
+ platform,
121
+ arch,
122
+ })
113
123
  const tempDir = join(paths.bin, `temp-${version}-${platform}-${arch}`)
114
124
  const jarFile = join(tempDir, 'postgres.jar')
115
125
 
@@ -190,7 +200,12 @@ export class BinaryManager {
190
200
  platform: string,
191
201
  arch: string,
192
202
  ): Promise<boolean> {
193
- const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
203
+ const binPath = paths.getBinaryPath({
204
+ engine: 'postgresql',
205
+ version,
206
+ platform,
207
+ arch,
208
+ })
194
209
  const postgresPath = join(binPath, 'bin', 'postgres')
195
210
 
196
211
  if (!existsSync(postgresPath)) {
@@ -241,7 +256,12 @@ export class BinaryManager {
241
256
  arch: string,
242
257
  binary: string,
243
258
  ): string {
244
- const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
259
+ const binPath = paths.getBinaryPath({
260
+ engine: 'postgresql',
261
+ version,
262
+ platform,
263
+ arch,
264
+ })
245
265
  return join(binPath, 'bin', binary)
246
266
  }
247
267
 
@@ -259,7 +279,12 @@ export class BinaryManager {
259
279
  stage: 'cached',
260
280
  message: 'Using cached PostgreSQL binaries',
261
281
  })
262
- return paths.getBinaryPath('postgresql', version, platform, arch)
282
+ return paths.getBinaryPath({
283
+ engine: 'postgresql',
284
+ version,
285
+ platform,
286
+ arch,
287
+ })
263
288
  }
264
289
 
265
290
  return this.download(version, platform, arch, onProgress)
@@ -3,10 +3,12 @@ import { mkdir, readdir, readFile, writeFile, rm, cp } from 'fs/promises'
3
3
  import { paths } from '../config/paths'
4
4
  import { processManager } from './process-manager'
5
5
  import { portManager } from './port-manager'
6
- import type { ContainerConfig } from '../types'
6
+ import { getEngineDefaults, getSupportedEngines } from '../config/defaults'
7
+ import { getEngine } from '../engines'
8
+ import type { ContainerConfig, EngineName } from '../types'
7
9
 
8
10
  export type CreateOptions = {
9
- engine: string
11
+ engine: EngineName
10
12
  version: string
11
13
  port: number
12
14
  database: string
@@ -30,14 +32,14 @@ export class ContainerManager {
30
32
  )
31
33
  }
32
34
 
33
- // Check if container already exists
34
- if (await this.exists(name)) {
35
- throw new Error(`Container "${name}" already exists`)
35
+ // Check if container already exists (for this engine)
36
+ if (await this.exists(name, { engine })) {
37
+ throw new Error(`Container "${name}" already exists for engine ${engine}`)
36
38
  }
37
39
 
38
- // Create container directory
39
- const containerPath = paths.getContainerPath(name)
40
- const dataPath = paths.getContainerDataPath(name)
40
+ // Create container directory (engine-scoped)
41
+ const containerPath = paths.getContainerPath(name, { engine })
42
+ const dataPath = paths.getContainerDataPath(name, { engine })
41
43
 
42
44
  await mkdir(containerPath, { recursive: true })
43
45
  await mkdir(dataPath, { recursive: true })
@@ -53,30 +55,54 @@ export class ContainerManager {
53
55
  status: 'created',
54
56
  }
55
57
 
56
- await this.saveConfig(name, config)
58
+ await this.saveConfig(name, { engine }, config)
57
59
 
58
60
  return config
59
61
  }
60
62
 
61
63
  /**
62
64
  * Get container configuration
65
+ * If engine is not provided, searches all engine directories
63
66
  */
64
- async getConfig(name: string): Promise<ContainerConfig | null> {
65
- const configPath = paths.getContainerConfigPath(name)
67
+ async getConfig(
68
+ name: string,
69
+ options?: { engine?: string },
70
+ ): Promise<ContainerConfig | null> {
71
+ const { engine } = options || {}
72
+
73
+ if (engine) {
74
+ // Look in specific engine directory
75
+ const configPath = paths.getContainerConfigPath(name, { engine })
76
+ if (!existsSync(configPath)) {
77
+ return null
78
+ }
79
+ const content = await readFile(configPath, 'utf8')
80
+ return JSON.parse(content) as ContainerConfig
81
+ }
66
82
 
67
- if (!existsSync(configPath)) {
68
- return null
83
+ // Search all engine directories
84
+ const engines = getSupportedEngines()
85
+ for (const eng of engines) {
86
+ const configPath = paths.getContainerConfigPath(name, { engine: eng })
87
+ if (existsSync(configPath)) {
88
+ const content = await readFile(configPath, 'utf8')
89
+ return JSON.parse(content) as ContainerConfig
90
+ }
69
91
  }
70
92
 
71
- const content = await readFile(configPath, 'utf8')
72
- return JSON.parse(content) as ContainerConfig
93
+ return null
73
94
  }
74
95
 
75
96
  /**
76
97
  * Save container configuration
77
98
  */
78
- async saveConfig(name: string, config: ContainerConfig): Promise<void> {
79
- const configPath = paths.getContainerConfigPath(name)
99
+ async saveConfig(
100
+ name: string,
101
+ options: { engine: string },
102
+ config: ContainerConfig,
103
+ ): Promise<void> {
104
+ const { engine } = options
105
+ const configPath = paths.getContainerConfigPath(name, { engine })
80
106
  await writeFile(configPath, JSON.stringify(config, null, 2))
81
107
  }
82
108
 
@@ -93,20 +119,36 @@ export class ContainerManager {
93
119
  }
94
120
 
95
121
  const updatedConfig = { ...config, ...updates }
96
- await this.saveConfig(name, updatedConfig)
122
+ await this.saveConfig(name, { engine: config.engine }, updatedConfig)
97
123
  return updatedConfig
98
124
  }
99
125
 
100
126
  /**
101
127
  * Check if a container exists
128
+ * If engine is not provided, checks all engine directories
102
129
  */
103
- async exists(name: string): Promise<boolean> {
104
- const configPath = paths.getContainerConfigPath(name)
105
- return existsSync(configPath)
130
+ async exists(name: string, options?: { engine?: string }): Promise<boolean> {
131
+ const { engine } = options || {}
132
+
133
+ if (engine) {
134
+ const configPath = paths.getContainerConfigPath(name, { engine })
135
+ return existsSync(configPath)
136
+ }
137
+
138
+ // Check all engine directories
139
+ const engines = getSupportedEngines()
140
+ for (const eng of engines) {
141
+ const configPath = paths.getContainerConfigPath(name, { engine: eng })
142
+ if (existsSync(configPath)) {
143
+ return true
144
+ }
145
+ }
146
+
147
+ return false
106
148
  }
107
149
 
108
150
  /**
109
- * List all containers
151
+ * List all containers across all engines
110
152
  */
111
153
  async list(): Promise<ContainerConfig[]> {
112
154
  const containersDir = paths.containers
@@ -115,19 +157,30 @@ export class ContainerManager {
115
157
  return []
116
158
  }
117
159
 
118
- const entries = await readdir(containersDir, { withFileTypes: true })
119
160
  const containers: ContainerConfig[] = []
161
+ const engines = getSupportedEngines()
162
+
163
+ for (const engine of engines) {
164
+ const engineDir = paths.getEngineContainersPath(engine)
165
+ if (!existsSync(engineDir)) {
166
+ continue
167
+ }
120
168
 
121
- for (const entry of entries) {
122
- if (entry.isDirectory()) {
123
- const config = await this.getConfig(entry.name)
124
- if (config) {
125
- // Check if actually running
126
- const running = await processManager.isRunning(entry.name)
127
- containers.push({
128
- ...config,
129
- status: running ? 'running' : 'stopped',
130
- })
169
+ const entries = await readdir(engineDir, { withFileTypes: true })
170
+
171
+ for (const entry of entries) {
172
+ if (entry.isDirectory()) {
173
+ const config = await this.getConfig(entry.name, { engine })
174
+ if (config) {
175
+ // Check if actually running
176
+ const running = await processManager.isRunning(entry.name, {
177
+ engine,
178
+ })
179
+ containers.push({
180
+ ...config,
181
+ status: running ? 'running' : 'stopped',
182
+ })
183
+ }
131
184
  }
132
185
  }
133
186
  }
@@ -141,19 +194,23 @@ export class ContainerManager {
141
194
  async delete(name: string, options: DeleteOptions = {}): Promise<void> {
142
195
  const { force = false } = options
143
196
 
144
- if (!(await this.exists(name))) {
197
+ // Get container config to find engine
198
+ const config = await this.getConfig(name)
199
+ if (!config) {
145
200
  throw new Error(`Container "${name}" not found`)
146
201
  }
147
202
 
203
+ const { engine } = config
204
+
148
205
  // Check if running
149
- const running = await processManager.isRunning(name)
206
+ const running = await processManager.isRunning(name, { engine })
150
207
  if (running && !force) {
151
208
  throw new Error(
152
209
  `Container "${name}" is running. Stop it first or use --force`,
153
210
  )
154
211
  }
155
212
 
156
- const containerPath = paths.getContainerPath(name)
213
+ const containerPath = paths.getContainerPath(name, { engine })
157
214
  await rm(containerPath, { recursive: true, force: true })
158
215
  }
159
216
 
@@ -171,18 +228,21 @@ export class ContainerManager {
171
228
  )
172
229
  }
173
230
 
174
- // Check source exists
175
- if (!(await this.exists(sourceName))) {
231
+ // Get source config
232
+ const sourceConfig = await this.getConfig(sourceName)
233
+ if (!sourceConfig) {
176
234
  throw new Error(`Source container "${sourceName}" not found`)
177
235
  }
178
236
 
179
- // Check target doesn't exist
180
- if (await this.exists(targetName)) {
237
+ const { engine } = sourceConfig
238
+
239
+ // Check target doesn't exist (for this engine)
240
+ if (await this.exists(targetName, { engine })) {
181
241
  throw new Error(`Target container "${targetName}" already exists`)
182
242
  }
183
243
 
184
244
  // Check source is not running
185
- const running = await processManager.isRunning(sourceName)
245
+ const running = await processManager.isRunning(sourceName, { engine })
186
246
  if (running) {
187
247
  throw new Error(
188
248
  `Source container "${sourceName}" is running. Stop it first`,
@@ -190,13 +250,13 @@ export class ContainerManager {
190
250
  }
191
251
 
192
252
  // Copy container directory
193
- const sourcePath = paths.getContainerPath(sourceName)
194
- const targetPath = paths.getContainerPath(targetName)
253
+ const sourcePath = paths.getContainerPath(sourceName, { engine })
254
+ const targetPath = paths.getContainerPath(targetName, { engine })
195
255
 
196
256
  await cp(sourcePath, targetPath, { recursive: true })
197
257
 
198
258
  // Update target config
199
- const config = await this.getConfig(targetName)
259
+ const config = await this.getConfig(targetName, { engine })
200
260
  if (!config) {
201
261
  throw new Error('Failed to read cloned container config')
202
262
  }
@@ -206,10 +266,13 @@ export class ContainerManager {
206
266
  config.clonedFrom = sourceName
207
267
 
208
268
  // Assign new port (excluding ports already used by other containers)
209
- const { port } = await portManager.findAvailablePortExcludingContainers()
269
+ const engineDefaults = getEngineDefaults(engine)
270
+ const { port } = await portManager.findAvailablePortExcludingContainers({
271
+ portRange: engineDefaults.portRange,
272
+ })
210
273
  config.port = port
211
274
 
212
- await this.saveConfig(targetName, config)
275
+ await this.saveConfig(targetName, { engine }, config)
213
276
 
214
277
  return config
215
278
  }
@@ -225,37 +288,40 @@ export class ContainerManager {
225
288
  )
226
289
  }
227
290
 
228
- // Check source exists
229
- if (!(await this.exists(oldName))) {
291
+ // Get source config
292
+ const sourceConfig = await this.getConfig(oldName)
293
+ if (!sourceConfig) {
230
294
  throw new Error(`Container "${oldName}" not found`)
231
295
  }
232
296
 
297
+ const { engine } = sourceConfig
298
+
233
299
  // Check target doesn't exist
234
- if (await this.exists(newName)) {
300
+ if (await this.exists(newName, { engine })) {
235
301
  throw new Error(`Container "${newName}" already exists`)
236
302
  }
237
303
 
238
304
  // Check container is not running
239
- const running = await processManager.isRunning(oldName)
305
+ const running = await processManager.isRunning(oldName, { engine })
240
306
  if (running) {
241
307
  throw new Error(`Container "${oldName}" is running. Stop it first`)
242
308
  }
243
309
 
244
310
  // Rename directory
245
- const oldPath = paths.getContainerPath(oldName)
246
- const newPath = paths.getContainerPath(newName)
311
+ const oldPath = paths.getContainerPath(oldName, { engine })
312
+ const newPath = paths.getContainerPath(newName, { engine })
247
313
 
248
314
  await cp(oldPath, newPath, { recursive: true })
249
315
  await rm(oldPath, { recursive: true, force: true })
250
316
 
251
317
  // Update config with new name
252
- const config = await this.getConfig(newName)
318
+ const config = await this.getConfig(newName, { engine })
253
319
  if (!config) {
254
320
  throw new Error('Failed to read renamed container config')
255
321
  }
256
322
 
257
323
  config.name = newName
258
- await this.saveConfig(newName, config)
324
+ await this.saveConfig(newName, { engine }, config)
259
325
 
260
326
  return config
261
327
  }
@@ -269,13 +335,11 @@ export class ContainerManager {
269
335
 
270
336
  /**
271
337
  * Get connection string for a container
338
+ * Delegates to the appropriate engine
272
339
  */
273
- getConnectionString(
274
- config: ContainerConfig,
275
- database: string = 'postgres',
276
- ): string {
277
- const { port } = config
278
- return `postgresql://postgres@localhost:${port}/${database}`
340
+ getConnectionString(config: ContainerConfig, database?: string): string {
341
+ const engine = getEngine(config.engine)
342
+ return engine.getConnectionString(config, database)
279
343
  }
280
344
  }
281
345
 
@@ -3,12 +3,20 @@ import { exec } from 'child_process'
3
3
  import { promisify } from 'util'
4
4
  import { existsSync } from 'fs'
5
5
  import { readdir, readFile } from 'fs/promises'
6
- import { defaults } from '../config/defaults'
6
+ import { defaults, getSupportedEngines } from '../config/defaults'
7
7
  import { paths } from '../config/paths'
8
8
  import type { ContainerConfig, PortResult } from '../types'
9
9
 
10
10
  const execAsync = promisify(exec)
11
11
 
12
+ /**
13
+ * Options for finding an available port
14
+ */
15
+ type FindPortOptions = {
16
+ preferredPort?: number
17
+ portRange?: { start: number; end: number }
18
+ }
19
+
12
20
  export class PortManager {
13
21
  /**
14
22
  * Check if a specific port is available
@@ -36,12 +44,13 @@ export class PortManager {
36
44
  }
37
45
 
38
46
  /**
39
- * Find the next available port starting from the default
47
+ * Find the next available port starting from the preferred port
40
48
  * Returns the port number and whether it's the default port
41
49
  */
42
- async findAvailablePort(
43
- preferredPort: number = defaults.port,
44
- ): Promise<PortResult> {
50
+ async findAvailablePort(options: FindPortOptions = {}): Promise<PortResult> {
51
+ const preferredPort = options.preferredPort ?? defaults.port
52
+ const portRange = options.portRange ?? defaults.portRange
53
+
45
54
  // First try the preferred port
46
55
  if (await this.isPortAvailable(preferredPort)) {
47
56
  return {
@@ -51,11 +60,7 @@ export class PortManager {
51
60
  }
52
61
 
53
62
  // Scan for available ports in the range
54
- for (
55
- let port = defaults.portRange.start;
56
- port <= defaults.portRange.end;
57
- port++
58
- ) {
63
+ for (let port = portRange.start; port <= portRange.end; port++) {
59
64
  if (port === preferredPort) continue // Already tried this one
60
65
 
61
66
  if (await this.isPortAvailable(port)) {
@@ -67,7 +72,7 @@ export class PortManager {
67
72
  }
68
73
 
69
74
  throw new Error(
70
- `No available ports found in range ${defaults.portRange.start}-${defaults.portRange.end}`,
75
+ `No available ports found in range ${portRange.start}-${portRange.end}`,
71
76
  )
72
77
  }
73
78
 
@@ -84,7 +89,7 @@ export class PortManager {
84
89
  }
85
90
 
86
91
  /**
87
- * Get all ports currently assigned to containers
92
+ * Get all ports currently assigned to containers across all engines
88
93
  */
89
94
  async getContainerPorts(): Promise<number[]> {
90
95
  const containersDir = paths.containers
@@ -94,18 +99,26 @@ export class PortManager {
94
99
  }
95
100
 
96
101
  const ports: number[] = []
97
- const entries = await readdir(containersDir, { withFileTypes: true })
98
-
99
- for (const entry of entries) {
100
- if (entry.isDirectory()) {
101
- const configPath = `${containersDir}/${entry.name}/container.json`
102
- if (existsSync(configPath)) {
103
- try {
104
- const content = await readFile(configPath, 'utf8')
105
- const config = JSON.parse(content) as ContainerConfig
106
- ports.push(config.port)
107
- } catch {
108
- // Skip invalid configs
102
+ const engines = getSupportedEngines()
103
+
104
+ for (const engine of engines) {
105
+ const engineDir = paths.getEngineContainersPath(engine)
106
+ if (!existsSync(engineDir)) continue
107
+
108
+ const entries = await readdir(engineDir, { withFileTypes: true })
109
+ for (const entry of entries) {
110
+ if (entry.isDirectory()) {
111
+ const configPath = paths.getContainerConfigPath(entry.name, {
112
+ engine,
113
+ })
114
+ if (existsSync(configPath)) {
115
+ try {
116
+ const content = await readFile(configPath, 'utf8')
117
+ const config = JSON.parse(content) as ContainerConfig
118
+ ports.push(config.port)
119
+ } catch {
120
+ // Skip invalid configs
121
+ }
109
122
  }
110
123
  }
111
124
  }
@@ -118,8 +131,10 @@ export class PortManager {
118
131
  * Find an available port that's not in use by any process AND not assigned to any container
119
132
  */
120
133
  async findAvailablePortExcludingContainers(
121
- preferredPort: number = defaults.port,
134
+ options: FindPortOptions = {},
122
135
  ): Promise<PortResult> {
136
+ const preferredPort = options.preferredPort ?? defaults.port
137
+ const portRange = options.portRange ?? defaults.portRange
123
138
  const containerPorts = await this.getContainerPorts()
124
139
 
125
140
  // First try the preferred port
@@ -134,11 +149,7 @@ export class PortManager {
134
149
  }
135
150
 
136
151
  // Scan for available ports in the range
137
- for (
138
- let port = defaults.portRange.start;
139
- port <= defaults.portRange.end;
140
- port++
141
- ) {
152
+ for (let port = portRange.start; port <= portRange.end; port++) {
142
153
  if (containerPorts.includes(port)) continue // Skip ports used by containers
143
154
  if (port === preferredPort) continue // Already tried this one
144
155
 
@@ -151,7 +162,7 @@ export class PortManager {
151
162
  }
152
163
 
153
164
  throw new Error(
154
- `No available ports found in range ${defaults.portRange.start}-${defaults.portRange.end}`,
165
+ `No available ports found in range ${portRange.start}-${portRange.end}`,
155
166
  )
156
167
  }
157
168
  }
@@ -202,10 +202,14 @@ export class ProcessManager {
202
202
  }
203
203
 
204
204
  /**
205
- * Check if PostgreSQL is running by looking for PID file
205
+ * Check if a database server is running by looking for PID file
206
206
  */
207
- async isRunning(containerName: string): Promise<boolean> {
208
- const pidFile = paths.getContainerPidPath(containerName)
207
+ async isRunning(
208
+ containerName: string,
209
+ options: { engine: string },
210
+ ): Promise<boolean> {
211
+ const { engine } = options
212
+ const pidFile = paths.getContainerPidPath(containerName, { engine })
209
213
  if (!existsSync(pidFile)) {
210
214
  return false
211
215
  }
@@ -223,10 +227,14 @@ export class ProcessManager {
223
227
  }
224
228
 
225
229
  /**
226
- * Get the PID of a running PostgreSQL server
230
+ * Get the PID of a running database server
227
231
  */
228
- async getPid(containerName: string): Promise<number | null> {
229
- const pidFile = paths.getContainerPidPath(containerName)
232
+ async getPid(
233
+ containerName: string,
234
+ options: { engine: string },
235
+ ): Promise<number | null> {
236
+ const { engine } = options
237
+ const pidFile = paths.getContainerPidPath(containerName, { engine })
230
238
  if (!existsSync(pidFile)) {
231
239
  return null
232
240
  }
package/engines/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { postgresqlEngine } from './postgresql'
2
+ import { mysqlEngine } from './mysql'
2
3
  import type { BaseEngine } from './base-engine'
3
4
  import type { EngineInfo } from '../types'
4
5
 
@@ -6,9 +7,13 @@ import type { EngineInfo } from '../types'
6
7
  * Registry of available database engines
7
8
  */
8
9
  export const engines: Record<string, BaseEngine> = {
10
+ // PostgreSQL and aliases
9
11
  postgresql: postgresqlEngine,
10
- postgres: postgresqlEngine, // Alias
11
- pg: postgresqlEngine, // Alias
12
+ postgres: postgresqlEngine,
13
+ pg: postgresqlEngine,
14
+ // MySQL and aliases
15
+ mysql: mysqlEngine,
16
+ mariadb: mysqlEngine, // MariaDB is MySQL-compatible
12
17
  }
13
18
 
14
19
  /**