@sqldoc/db 0.0.6 → 0.0.8

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sqldoc/db",
4
- "version": "0.0.6",
4
+ "version": "0.0.8",
5
5
  "description": "Atlas WASI integration for sqldoc -- schema types, database adapters, WASI runner",
6
6
  "exports": {
7
7
  ".": {
@@ -26,11 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@electric-sql/pglite": "^0.4.0",
29
- "@testcontainers/postgresql": "^11.13.0",
30
29
  "mysql2": "^3.20.0",
31
30
  "pg": "^8.13.0",
32
- "picocolors": "1.1.1",
33
- "testcontainers": "^11.13.0"
31
+ "picocolors": "1.1.1"
34
32
  },
35
33
  "devDependencies": {
36
34
  "@types/pg": "^8.11.0",
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Docker container management for ephemeral dev databases.
3
+ * Uses Docker CLI directly — no testcontainers dependency.
4
+ *
5
+ * Containers are labeled `sqldoc-dev` and auto-removed after 60s via --stop-timeout.
6
+ * On startup, stale containers (>1 min old) are cleaned up.
7
+ */
8
+ import { execSync, spawnSync } from 'node:child_process'
9
+ import * as path from 'node:path'
10
+
11
+ const LABEL = 'sqldoc-dev'
12
+
13
+ /** Clean up any sqldoc-dev containers older than 1 minute */
14
+ export function cleanupStaleContainers(): void {
15
+ try {
16
+ const result = execSync(
17
+ `docker ps -aq --filter label=${LABEL} --filter status=running --format "{{.ID}} {{.CreatedAt}}"`,
18
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
19
+ )
20
+ for (const line of result.trim().split('\n').filter(Boolean)) {
21
+ const [id, ...dateParts] = line.split(' ')
22
+ const created = new Date(dateParts.join(' '))
23
+ if (Date.now() - created.getTime() > 60_000) {
24
+ spawnSync('docker', ['rm', '-f', id], { stdio: 'pipe' })
25
+ }
26
+ }
27
+ } catch {
28
+ // Docker not available or no containers — fine
29
+ }
30
+ }
31
+
32
+ export interface DockerContainer {
33
+ id: string
34
+ host: string
35
+ port: number
36
+ stop(): void
37
+ }
38
+
39
+ /**
40
+ * Start a Docker container with the given image and port mapping.
41
+ * Returns when the container is running and the port is mapped.
42
+ */
43
+ export function startContainer(opts: {
44
+ image: string
45
+ env: Record<string, string>
46
+ port: number
47
+ readyLog: string | RegExp
48
+ }): DockerContainer {
49
+ cleanupStaleContainers()
50
+
51
+ const envArgs = Object.entries(opts.env).flatMap(([k, v]) => ['-e', `${k}=${v}`])
52
+ const name = `sqldoc_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`
53
+
54
+ const result = spawnSync(
55
+ 'docker',
56
+ ['run', '-d', '--rm', '--name', name, '--label', LABEL, '-p', `0:${opts.port}`, ...envArgs, opts.image],
57
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
58
+ )
59
+
60
+ if (result.status !== 0) {
61
+ throw new Error(`Failed to start Docker container: ${result.stderr}`)
62
+ }
63
+
64
+ const id = result.stdout.trim().slice(0, 12)
65
+
66
+ // Get mapped port
67
+ const portOutput = execSync(`docker port ${id} ${opts.port}`, { encoding: 'utf-8' }).trim()
68
+ // Format: 0.0.0.0:55432 or [::]:55432
69
+ const portMatch = portOutput.match(/:(\d+)$/)
70
+ if (!portMatch) {
71
+ spawnSync('docker', ['rm', '-f', id], { stdio: 'pipe' })
72
+ throw new Error(`Failed to get mapped port for container ${id}: ${portOutput}`)
73
+ }
74
+ const port = parseInt(portMatch[1], 10)
75
+
76
+ // Wait for ready log message
77
+ waitForLog(id, opts.readyLog, 30_000)
78
+
79
+ return {
80
+ id,
81
+ host: '127.0.0.1',
82
+ port,
83
+ stop() {
84
+ spawnSync('docker', ['rm', '-f', id], { stdio: 'pipe' })
85
+ },
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Build a Docker image from a Dockerfile and start a container.
91
+ */
92
+ export function startContainerFromDockerfile(opts: {
93
+ dockerfilePath: string
94
+ env: Record<string, string>
95
+ port: number
96
+ readyLog: string | RegExp
97
+ }): DockerContainer {
98
+ const absPath = path.resolve(opts.dockerfilePath)
99
+ const dir = path.dirname(absPath)
100
+ const tag = `sqldoc-dev-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
101
+
102
+ const buildResult = spawnSync('docker', ['build', '-t', tag, '-f', absPath, dir], {
103
+ encoding: 'utf-8',
104
+ stdio: ['pipe', 'pipe', 'pipe'],
105
+ timeout: 60_000,
106
+ })
107
+
108
+ if (buildResult.status !== 0 && buildResult.status !== null) {
109
+ throw new Error(
110
+ `Failed to build Docker image (exit ${buildResult.status}): ${buildResult.stderr || buildResult.stdout}`,
111
+ )
112
+ }
113
+ if (buildResult.error) {
114
+ throw new Error(`Failed to build Docker image: ${buildResult.error.message}`)
115
+ }
116
+
117
+ const container = startContainer({ ...opts, image: tag })
118
+
119
+ // Override stop to also remove the built image
120
+ const originalStop = container.stop
121
+ container.stop = () => {
122
+ originalStop()
123
+ spawnSync('docker', ['rmi', tag], { stdio: 'pipe' })
124
+ }
125
+
126
+ return container
127
+ }
128
+
129
+ /** Poll docker logs until the ready message appears or timeout */
130
+ function waitForLog(containerId: string, pattern: string | RegExp, timeoutMs: number): void {
131
+ const start = Date.now()
132
+ while (Date.now() - start < timeoutMs) {
133
+ try {
134
+ const logs = execSync(`docker logs ${containerId} 2>&1`, { encoding: 'utf-8' })
135
+ if (typeof pattern === 'string' ? logs.includes(pattern) : pattern.test(logs)) {
136
+ return
137
+ }
138
+ } catch {
139
+ // Container might not be ready yet
140
+ }
141
+ spawnSync('sleep', ['0.5'])
142
+ }
143
+ spawnSync('docker', ['rm', '-f', containerId], { stdio: 'pipe' })
144
+ throw new Error(`Container ${containerId} did not become ready within ${timeoutMs}ms`)
145
+ }
@@ -1,31 +1,22 @@
1
- /**
2
- * MySQL Docker adapter using testcontainers GenericContainer.
3
- * Uses log-based wait strategy (same as Postgres adapter) for Bun compatibility —
4
- * MySqlContainer's built-in health check hangs in Bun's runtime.
5
- */
6
- import { GenericContainer, Wait } from 'testcontainers'
1
+ import { startContainer } from './docker.ts'
7
2
  import { createMysqlAdapter } from './mysql.ts'
8
3
  import type { DatabaseAdapter } from './types.ts'
9
4
 
10
5
  /**
11
- * Create a Docker-based MySQL DatabaseAdapter using testcontainers.
6
+ * Create a Docker-based MySQL DatabaseAdapter.
12
7
  * Supports: docker://mysql:8, docker://mysql:8.0, docker://mariadb:10, etc.
13
8
  */
14
9
  export async function createMysqlDockerAdapter(devUrl: string): Promise<DatabaseAdapter> {
15
10
  const imageName = devUrl.slice('docker://'.length)
16
11
 
17
- const container = await new GenericContainer(imageName)
18
- .withEnvironment({
19
- MYSQL_ROOT_PASSWORD: 'sqldoc',
20
- MYSQL_DATABASE: 'sqldoc_dev',
21
- })
22
- .withExposedPorts(3306)
23
- .withWaitStrategy(Wait.forLogMessage(/ready for connections.*port: 3306/, 2))
24
- .start()
12
+ const container = startContainer({
13
+ image: imageName,
14
+ env: { MYSQL_ROOT_PASSWORD: 'sqldoc', MYSQL_DATABASE: 'sqldoc_dev' },
15
+ port: 3306,
16
+ readyLog: /ready for connections.*port: 3306/,
17
+ })
25
18
 
26
- const host = container.getHost()
27
- const port = container.getMappedPort(3306)
28
- const connectionUri = `mysql://root:sqldoc@${host}:${port}/sqldoc_dev`
19
+ const connectionUri = `mysql://root:sqldoc@${container.host}:${container.port}/sqldoc_dev`
29
20
 
30
21
  // Retry connection — container may need a moment after port is mapped
31
22
  let mysqlAdapter: DatabaseAdapter | undefined
@@ -39,7 +30,7 @@ export async function createMysqlDockerAdapter(devUrl: string): Promise<Database
39
30
  }
40
31
 
41
32
  if (!mysqlAdapter) {
42
- await container.stop()
33
+ container.stop()
43
34
  throw new Error(`Failed to connect to Docker MySQL at ${connectionUri}`)
44
35
  }
45
36
 
@@ -48,7 +39,7 @@ export async function createMysqlDockerAdapter(devUrl: string): Promise<Database
48
39
  exec: mysqlAdapter.exec,
49
40
  async close() {
50
41
  await mysqlAdapter.close()
51
- await container.stop()
42
+ container.stop()
52
43
  },
53
44
  }
54
45
  }
@@ -1,68 +1,34 @@
1
- import * as path from 'node:path'
2
- import { PostgreSqlContainer } from '@testcontainers/postgresql'
3
- import { GenericContainer, Wait } from 'testcontainers'
1
+ import { startContainer, startContainerFromDockerfile } from './docker.ts'
4
2
  import { createPostgresAdapter } from './postgres.ts'
5
3
  import type { DatabaseAdapter } from './types.ts'
6
4
 
7
5
  /**
8
- * Create a Docker-based DatabaseAdapter using testcontainers.
9
- * Spins up an ephemeral Postgres container, connects via pg, and
10
- * automatically cleans up on close() (testcontainers + Ryuk handle
11
- * orphan cleanup even on process crash).
6
+ * Create a Docker-based DatabaseAdapter for Postgres.
7
+ * Spins up an ephemeral container, connects via pg, cleans up on close().
12
8
  *
13
9
  * Supports:
14
10
  * docker://postgres:16 — official or custom image
15
11
  * dockerfile://path/to/file — build from Dockerfile
16
12
  */
17
13
  export async function createPostgresDockerAdapter(devUrl: string): Promise<DatabaseAdapter> {
18
- if (devUrl.startsWith('dockerfile://')) {
19
- return createFromDockerfile(devUrl.slice('dockerfile://'.length))
20
- }
21
-
22
- // docker://image:tag
23
- const imageName = devUrl.slice('docker://'.length)
24
- const container = await new PostgreSqlContainer(imageName)
25
- .withDatabase('sqldoc_dev')
26
- .withUsername('sqldoc')
27
- .withPassword('sqldoc')
28
- .withExposedPorts(5432)
29
- .withWaitStrategy(Wait.forLogMessage('database system is ready to accept connections', 2))
30
- .start()
31
-
32
- const pgAdapter = await createPostgresAdapter(container.getConnectionUri())
33
-
34
- return {
35
- query: pgAdapter.query,
36
- exec: pgAdapter.exec,
37
- async close() {
38
- await pgAdapter.close()
39
- await container.stop()
40
- },
41
- }
42
- }
43
-
44
- async function createFromDockerfile(dockerfilePath: string): Promise<DatabaseAdapter> {
45
- const absPath = path.resolve(dockerfilePath)
46
- const dir = path.dirname(absPath)
47
- const file = path.basename(absPath)
48
-
49
- const image = await GenericContainer.fromDockerfile(dir, file).build()
50
-
51
- const container = await image
52
- .withEnvironment({
53
- POSTGRES_DB: 'sqldoc_dev',
54
- POSTGRES_USER: 'sqldoc',
55
- POSTGRES_PASSWORD: 'sqldoc',
56
- })
57
- .withExposedPorts(5432)
58
- .withWaitStrategy(Wait.forLogMessage('database system is ready to accept connections', 2))
59
- .withStartupTimeout(30_000)
60
- .start()
61
-
62
- // Wait for postgres to accept connections
63
- const host = container.getHost()
64
- const port = container.getMappedPort(5432)
65
- const connectionUri = `postgres://sqldoc:sqldoc@${host}:${port}/sqldoc_dev`
14
+ const isDockerfile = devUrl.startsWith('dockerfile://')
15
+ const readyLog = 'database system is ready to accept connections'
16
+
17
+ const container = isDockerfile
18
+ ? startContainerFromDockerfile({
19
+ dockerfilePath: devUrl.slice('dockerfile://'.length),
20
+ env: { POSTGRES_DB: 'sqldoc_dev', POSTGRES_USER: 'sqldoc', POSTGRES_PASSWORD: 'sqldoc' },
21
+ port: 5432,
22
+ readyLog,
23
+ })
24
+ : startContainer({
25
+ image: devUrl.slice('docker://'.length),
26
+ env: { POSTGRES_DB: 'sqldoc_dev', POSTGRES_USER: 'sqldoc', POSTGRES_PASSWORD: 'sqldoc' },
27
+ port: 5432,
28
+ readyLog,
29
+ })
30
+
31
+ const connectionUri = `postgres://sqldoc:sqldoc@${container.host}:${container.port}/sqldoc_dev`
66
32
 
67
33
  // Retry connection — container may need a moment after port is mapped
68
34
  let pgAdapter: DatabaseAdapter | undefined
@@ -76,7 +42,7 @@ async function createFromDockerfile(dockerfilePath: string): Promise<DatabaseAda
76
42
  }
77
43
 
78
44
  if (!pgAdapter) {
79
- await container.stop()
45
+ container.stop()
80
46
  throw new Error(`Failed to connect to Docker Postgres at ${connectionUri}`)
81
47
  }
82
48
 
@@ -85,7 +51,7 @@ async function createFromDockerfile(dockerfilePath: string): Promise<DatabaseAda
85
51
  exec: pgAdapter.exec,
86
52
  async close() {
87
53
  await pgAdapter.close()
88
- await container.stop()
54
+ container.stop()
89
55
  },
90
56
  }
91
57
  }
package/wasm/atlas.wasm CHANGED
Binary file