spindb 0.32.2 → 0.33.1

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.
@@ -0,0 +1,180 @@
1
+ # InfluxDB Engine Implementation
2
+
3
+ ## Overview
4
+
5
+ InfluxDB 3.x is a time-series database rewritten in Rust. It uses a REST API for all operations (no CLI client). InfluxDB 3.x supports SQL queries via its HTTP API, unlike earlier versions which used InfluxQL/Flux.
6
+
7
+ ## Platform Support
8
+
9
+ | Platform | Architecture | Status | Notes |
10
+ |----------|--------------|--------|-------|
11
+ | darwin | x64 | Supported | Uses hostdb binaries |
12
+ | darwin | arm64 | Supported | Uses hostdb binaries (Apple Silicon) |
13
+ | linux | x64 | Supported | Uses hostdb binaries |
14
+ | linux | arm64 | Supported | Uses hostdb binaries |
15
+ | win32 | x64 | Supported | Uses hostdb binaries |
16
+
17
+ ## Binary Packaging
18
+
19
+ ### Archive Format
20
+ - **Unix (macOS/Linux)**: `tar.gz`
21
+ - **Windows**: `zip`
22
+
23
+ ### Archive Structure
24
+ ```text
25
+ influxdb/
26
+ ├── influxdb3 # Server binary
27
+ ├── python/ # Bundled Python runtime
28
+ │ └── lib/
29
+ │ └── libpython3.13.dylib
30
+ ├── LICENSE-APACHE
31
+ └── LICENSE-MIT
32
+ ```
33
+
34
+ ### Binary + Python Runtime
35
+ InfluxDB 3.x ships as a single `influxdb3` binary that acts as the server, bundled with a Python runtime. The binary uses `@executable_path/python/lib/libpython3.13.dylib`, so the `python/` directory must be co-located with the binary. The custom `moveExtractedEntries` override ensures both end up in `bin/`. There is no separate CLI client — all interactions use the REST API.
36
+
37
+ ### Version Map Sync
38
+
39
+ ```typescript
40
+ export const INFLUXDB_VERSION_MAP: Record<string, string> = {
41
+ '3': '3.8.0',
42
+ }
43
+ ```
44
+
45
+ ## Implementation Details
46
+
47
+ ### Binary Manager
48
+
49
+ InfluxDB uses `BaseBinaryManager` since it's a server-based engine with single-digit major versions:
50
+
51
+ ```typescript
52
+ class InfluxDBBinaryManager extends BaseBinaryManager {
53
+ protected readonly config = {
54
+ engine: Engine.InfluxDB,
55
+ engineName: 'influxdb',
56
+ displayName: 'InfluxDB',
57
+ serverBinary: 'influxdb3',
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### REST API Engine
63
+
64
+ InfluxDB is a **REST API engine**:
65
+ - `spindb run` is **NOT applicable** (scriptFileLabel is `null`)
66
+ - `spindb connect` opens the health endpoint info in terminal
67
+ - All operations use HTTP REST API
68
+
69
+ ### Default Configuration
70
+
71
+ - **Default Port**: 8086
72
+ - **Health Endpoint**: `GET /health`
73
+ - **SQL Query Endpoint**: `POST /api/v3/query_sql`
74
+ - **Write Endpoint**: `POST /api/v3/write_lp`
75
+ - **No Authentication**: InfluxDB 3.x local dev has no auth by default
76
+ - **PID File**: `influxdb.pid` in container directory
77
+
78
+ ### Database Creation
79
+
80
+ InfluxDB 3.x creates databases **implicitly on first write**. There is no explicit `CREATE DATABASE` command. When you write data with a database name, it's auto-created.
81
+
82
+ ### Connection String Format
83
+
84
+ ```text
85
+ http://127.0.0.1:{port}
86
+ ```
87
+
88
+ ## Backup & Restore
89
+
90
+ ### Backup Formats
91
+
92
+ | Format | Extension | Method | Notes |
93
+ |--------|-----------|--------|-------|
94
+ | sql | `.sql` | REST API | SQL dump with CREATE TABLE + INSERT statements |
95
+
96
+ ### Backup Method
97
+
98
+ Uses InfluxDB's SQL query API to export data:
99
+ 1. `SHOW TABLES` — lists all tables/measurements
100
+ 2. `SELECT * FROM {table}` — exports all data per table
101
+ 3. Generates SQL INSERT statements for restore
102
+
103
+ ### Restore Method
104
+
105
+ Parses SQL dump file and executes statements via `POST /api/v3/query_sql`.
106
+
107
+ ## Integration Test Notes
108
+
109
+ ### REST API Testing
110
+
111
+ Integration tests use `fetch()` to interact with InfluxDB REST API.
112
+
113
+ ### Test Fixtures
114
+
115
+ Located in `tests/fixtures/influxdb/seeds/`:
116
+ - `README.md` documenting the API-based approach
117
+
118
+ ## Known Issues & Gotchas
119
+
120
+ ### 1. No CLI Client
121
+
122
+ InfluxDB 3.x has no bundled CLI client. All operations use the HTTP REST API. The `clientTools` array in engine-defaults is empty.
123
+
124
+ ### 2. Implicit Database Creation
125
+
126
+ Databases are created on first write, not via explicit commands. `createDatabase()` verifies server health but doesn't create anything.
127
+
128
+ ### 3. SQL Query Support
129
+
130
+ InfluxDB 3.x supports SQL queries (not InfluxQL or Flux from v1/v2). Query via:
131
+ ```bash
132
+ curl -X POST http://localhost:8086/api/v3/query_sql \
133
+ -H "Content-Type: application/json" \
134
+ -d '{"db":"mydb","q":"SELECT * FROM measurement","format":"json"}'
135
+ ```
136
+
137
+ ### 4. Write via Line Protocol
138
+
139
+ Data writes use InfluxDB line protocol format:
140
+ ```bash
141
+ curl -X POST "http://localhost:8086/api/v3/write_lp?db=mydb" \
142
+ -H "Content-Type: text/plain" \
143
+ -d 'measurement,tag=value field=123'
144
+ ```
145
+
146
+ ### 5. Windows PID Handling
147
+
148
+ On Windows, uses `platformService.findProcessByPort(port)` after startup to find the real PID, similar to QuestDB/TypeDB pattern.
149
+
150
+ ## REST API Quick Reference
151
+
152
+ ### Health
153
+ ```bash
154
+ GET /health
155
+ ```
156
+
157
+ ### Query (SQL)
158
+ ```bash
159
+ POST /api/v3/query_sql
160
+ Content-Type: application/json
161
+ {"db":"mydb","q":"SELECT 1","format":"json"}
162
+ ```
163
+
164
+ ### Write (Line Protocol)
165
+ ```bash
166
+ POST /api/v3/write_lp?db=mydb
167
+ Content-Type: text/plain
168
+ measurement,tag=value field=123
169
+ ```
170
+
171
+ ### Show Tables
172
+ ```bash
173
+ POST /api/v3/query_sql
174
+ {"db":"mydb","q":"SHOW TABLES","format":"json"}
175
+ ```
176
+
177
+ ### List Databases
178
+ ```bash
179
+ GET /api/v3/configure/database?format=json
180
+ ```
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared InfluxDB REST API client utilities
3
+ */
4
+
5
+ /**
6
+ * Make an HTTP request to InfluxDB REST API
7
+ *
8
+ * @param port - The HTTP port InfluxDB is listening on
9
+ * @param method - HTTP method (GET, POST, PUT, DELETE)
10
+ * @param path - API path (e.g., '/health', '/api/v3/query_sql')
11
+ * @param body - Optional body: object for JSON, string for text/plain (line protocol)
12
+ * @param timeoutMs - Request timeout in milliseconds (default: 30s)
13
+ */
14
+ export async function influxdbApiRequest(
15
+ port: number,
16
+ method: string,
17
+ path: string,
18
+ body?: Record<string, unknown> | string,
19
+ timeoutMs = 30000,
20
+ ): Promise<{ status: number; data: unknown }> {
21
+ const url = `http://127.0.0.1:${port}${path}`
22
+
23
+ const controller = new AbortController()
24
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
25
+
26
+ const options: RequestInit = {
27
+ method,
28
+ signal: controller.signal,
29
+ }
30
+
31
+ if (body !== undefined) {
32
+ if (typeof body === 'string') {
33
+ options.headers = { 'Content-Type': 'text/plain' }
34
+ options.body = body
35
+ } else {
36
+ options.headers = { 'Content-Type': 'application/json' }
37
+ options.body = JSON.stringify(body)
38
+ }
39
+ }
40
+
41
+ try {
42
+ const response = await fetch(url, options)
43
+
44
+ // Try to parse as JSON, fall back to text for endpoints like /health
45
+ let data: unknown
46
+ const contentType = response.headers.get('content-type') || ''
47
+ if (contentType.includes('application/json')) {
48
+ data = await response.json()
49
+ } else {
50
+ data = await response.text()
51
+ }
52
+
53
+ return { status: response.status, data }
54
+ } catch (error) {
55
+ if (error instanceof Error && error.name === 'AbortError') {
56
+ throw new Error(
57
+ `InfluxDB API request timed out after ${timeoutMs / 1000}s: ${method} ${path}`,
58
+ )
59
+ }
60
+ throw error
61
+ } finally {
62
+ clearTimeout(timeoutId)
63
+ }
64
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * InfluxDB backup module
3
+ * Supports SQL-based backup using InfluxDB's REST API to export data
4
+ */
5
+
6
+ import { mkdir, stat, writeFile } from 'fs/promises'
7
+ import { existsSync } from 'fs'
8
+ import { dirname } from 'path'
9
+ import { logDebug } from '../../core/error-handler'
10
+ import { influxdbApiRequest } from './api-client'
11
+ import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
12
+
13
+ /**
14
+ * Create an SQL backup using InfluxDB's REST API
15
+ * Queries all tables and exports data as SQL INSERT statements
16
+ */
17
+ export async function createBackup(
18
+ container: ContainerConfig,
19
+ outputPath: string,
20
+ options: BackupOptions,
21
+ ): Promise<BackupResult> {
22
+ const { port } = container
23
+ const database = options.database || container.database
24
+
25
+ // Ensure output directory exists
26
+ const outputDir = dirname(outputPath)
27
+ if (!existsSync(outputDir)) {
28
+ await mkdir(outputDir, { recursive: true })
29
+ }
30
+
31
+ logDebug(
32
+ `Creating InfluxDB SQL backup via REST API on port ${port} for database "${database}"`,
33
+ )
34
+
35
+ // Get list of tables in the database
36
+ const tablesResponse = await influxdbApiRequest(
37
+ port,
38
+ 'POST',
39
+ '/api/v3/query_sql',
40
+ {
41
+ db: database,
42
+ q: 'SHOW TABLES',
43
+ format: 'json',
44
+ },
45
+ )
46
+
47
+ if (tablesResponse.status !== 200) {
48
+ throw new Error(
49
+ `Failed to list tables: ${JSON.stringify(tablesResponse.data)}`,
50
+ )
51
+ }
52
+
53
+ const tablesData = tablesResponse.data as Array<Record<string, unknown>>
54
+ const tables: string[] = []
55
+
56
+ // Extract user table names: include rows with 'iox' schema or no schema field,
57
+ // skip system schemas (information_schema, system, etc.)
58
+ if (Array.isArray(tablesData)) {
59
+ for (const row of tablesData) {
60
+ const schema = row.table_schema as string | undefined
61
+ if (schema && schema !== 'iox') continue
62
+ const tableName =
63
+ (row.table_name as string) ||
64
+ (row.name as string) ||
65
+ (Object.values(row)[0] as string)
66
+ if (tableName) {
67
+ tables.push(tableName)
68
+ }
69
+ }
70
+ }
71
+
72
+ logDebug(`Found ${tables.length} tables: ${tables.join(', ')}`)
73
+
74
+ // Build SQL dump
75
+ let sqlContent = `-- InfluxDB SQL Backup\n`
76
+ sqlContent += `-- Database: ${database}\n`
77
+ sqlContent += `-- Created: ${new Date().toISOString()}\n\n`
78
+
79
+ for (const table of tables) {
80
+ logDebug(`Exporting table: ${table}`)
81
+
82
+ // Query column metadata to identify tag columns
83
+ // Tags use Dictionary(Int32, Utf8) type in InfluxDB 3.x
84
+ const tagColumns: string[] = []
85
+ try {
86
+ const colResponse = await influxdbApiRequest(
87
+ port,
88
+ 'POST',
89
+ '/api/v3/query_sql',
90
+ {
91
+ db: database,
92
+ q: `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '${table.replace(/'/g, "''")}'`,
93
+ format: 'json',
94
+ },
95
+ )
96
+ if (colResponse.status === 200 && Array.isArray(colResponse.data)) {
97
+ for (const col of colResponse.data as Array<Record<string, unknown>>) {
98
+ const dataType = String(col.data_type || '')
99
+ if (dataType.includes('Dictionary')) {
100
+ tagColumns.push(String(col.column_name))
101
+ }
102
+ }
103
+ }
104
+ } catch {
105
+ logDebug(`Warning: Could not query column metadata for ${table}`)
106
+ }
107
+
108
+ // Query all data from the table
109
+ const dataResponse = await influxdbApiRequest(
110
+ port,
111
+ 'POST',
112
+ '/api/v3/query_sql',
113
+ {
114
+ db: database,
115
+ q: `SELECT * FROM "${table.replace(/"/g, '""')}"`,
116
+ format: 'json',
117
+ },
118
+ )
119
+
120
+ if (dataResponse.status !== 200) {
121
+ logDebug(
122
+ `Warning: Failed to export table ${table}: ${JSON.stringify(dataResponse.data)}`,
123
+ )
124
+ continue
125
+ }
126
+
127
+ const rows = dataResponse.data as Array<Record<string, unknown>>
128
+
129
+ if (Array.isArray(rows) && rows.length > 0) {
130
+ sqlContent += `-- Table: ${table}\n`
131
+ if (tagColumns.length > 0) {
132
+ sqlContent += `-- Tags: ${tagColumns.join(', ')}\n`
133
+ }
134
+
135
+ for (const row of rows) {
136
+ const columns = Object.keys(row)
137
+ const values = columns.map((col) => {
138
+ const val = row[col]
139
+ if (val === null || val === undefined) return 'NULL'
140
+ if (typeof val === 'number') return String(val)
141
+ if (typeof val === 'boolean') return val ? 'true' : 'false'
142
+ return `'${String(val).replace(/'/g, "''")}'`
143
+ })
144
+ sqlContent += `INSERT INTO "${table.replace(/"/g, '""')}" (${columns.map((c) => `"${c.replace(/"/g, '""')}"`).join(', ')}) VALUES (${values.join(', ')});\n`
145
+ }
146
+ sqlContent += '\n'
147
+ }
148
+ }
149
+
150
+ // Write SQL content to file
151
+ await writeFile(outputPath, sqlContent, 'utf-8')
152
+
153
+ const stats = await stat(outputPath)
154
+
155
+ return {
156
+ path: outputPath,
157
+ format: 'sql',
158
+ size: stats.size,
159
+ }
160
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * InfluxDB Binary Manager
3
+ *
4
+ * Handles downloading, extracting, and managing InfluxDB binaries from hostdb.
5
+ * Extends BaseBinaryManager for shared download/extraction logic.
6
+ *
7
+ * InfluxDB 3.x archives extract to a flat `influxdb/` directory:
8
+ * influxdb/
9
+ * ├── influxdb3 (server binary)
10
+ * ├── python/ (bundled Python runtime)
11
+ * │ └── lib/
12
+ * │ └── libpython3.13.dylib
13
+ * ├── LICENSE-APACHE
14
+ * └── LICENSE-MIT
15
+ *
16
+ * The binary uses @executable_path/python/lib/libpython3.13.dylib, so python/
17
+ * must be in the same directory as the binary. We reorganize to:
18
+ * bin/
19
+ * ├── influxdb3
20
+ * └── python/ (co-located for @executable_path resolution)
21
+ */
22
+
23
+ import { mkdir, readdir } from 'fs/promises'
24
+ import { join } from 'path'
25
+ import {
26
+ BaseBinaryManager,
27
+ type BinaryManagerConfig,
28
+ } from '../../core/base-binary-manager'
29
+ import { moveEntry } from '../../core/fs-error-utils'
30
+ import { logDebug } from '../../core/error-handler'
31
+ import { getBinaryUrl } from './binary-urls'
32
+ import { normalizeVersion } from './version-maps'
33
+ import { Engine, type Platform, type Arch } from '../../types'
34
+
35
+ class InfluxDBBinaryManager extends BaseBinaryManager {
36
+ protected readonly config: BinaryManagerConfig = {
37
+ engine: Engine.InfluxDB,
38
+ engineName: 'influxdb',
39
+ displayName: 'InfluxDB',
40
+ serverBinary: 'influxdb3',
41
+ }
42
+
43
+ protected getBinaryUrlFromModule(
44
+ version: string,
45
+ platform: Platform,
46
+ arch: Arch,
47
+ ): string {
48
+ return getBinaryUrl(version, platform, arch)
49
+ }
50
+
51
+ protected normalizeVersionFromModule(version: string): string {
52
+ return normalizeVersion(version)
53
+ }
54
+
55
+ protected parseVersionFromOutput(stdout: string): string | null {
56
+ // Extract version from output like "influxdb3 3.8.0" or "InfluxDB 3 Edge v3.8.0"
57
+ const match = stdout.match(/v?(\d+\.\d+\.\d+)/)
58
+ return match?.[1] ?? null
59
+ }
60
+
61
+ /**
62
+ * Override moveExtractedEntries to co-locate python/ with the binary.
63
+ *
64
+ * The influxdb3 binary references @executable_path/python/lib/libpython3.13.dylib,
65
+ * so the python/ directory must be inside bin/ alongside the binary.
66
+ * The default flat-structure handler would put python/ at binPath/python/ instead
67
+ * of binPath/bin/python/, causing a dylib load failure.
68
+ */
69
+ protected override async moveExtractedEntries(
70
+ extractDir: string,
71
+ binPath: string,
72
+ ): Promise<void> {
73
+ const entries = await readdir(extractDir, { withFileTypes: true })
74
+
75
+ // Find the influxdb directory (e.g., "influxdb" or "influxdb-3.8.0")
76
+ const influxDir = entries.find(
77
+ (e) =>
78
+ e.isDirectory() &&
79
+ (e.name === 'influxdb' || e.name.startsWith('influxdb-')),
80
+ )
81
+
82
+ const sourceDir = influxDir ? join(extractDir, influxDir.name) : extractDir
83
+ const sourceEntries = influxDir
84
+ ? await readdir(sourceDir, { withFileTypes: true })
85
+ : entries
86
+
87
+ // Create bin/ directory
88
+ const destBinDir = join(binPath, 'bin')
89
+ await mkdir(destBinDir, { recursive: true })
90
+
91
+ for (const entry of sourceEntries) {
92
+ const sourcePath = join(sourceDir, entry.name)
93
+
94
+ if (entry.name === 'influxdb3' || entry.name === 'influxdb3.exe') {
95
+ // Server binary → bin/
96
+ await moveEntry(sourcePath, join(destBinDir, entry.name))
97
+ } else if (entry.name === 'python') {
98
+ // Python runtime → bin/python/ (must be co-located with binary for @executable_path)
99
+ await moveEntry(sourcePath, join(destBinDir, 'python'))
100
+ } else {
101
+ // Licenses, metadata, etc. → binPath root
102
+ await moveEntry(sourcePath, join(binPath, entry.name))
103
+ }
104
+ }
105
+
106
+ logDebug('InfluxDB binaries reorganized with python/ co-located in bin/')
107
+ }
108
+ }
109
+
110
+ export const influxdbBinaryManager = new InfluxDBBinaryManager()
@@ -0,0 +1,69 @@
1
+ import { normalizeVersion } from './version-maps'
2
+ import { buildHostdbUrl } from '../../core/hostdb-client'
3
+ import { Engine, Platform, type Arch } from '../../types'
4
+
5
+ /**
6
+ * Supported platform identifiers for hostdb downloads.
7
+ * hostdb uses standard Node.js platform naming - this set validates
8
+ * that a platform/arch combination is supported, not transforms it.
9
+ */
10
+ const SUPPORTED_PLATFORMS = new Set([
11
+ 'darwin-arm64',
12
+ 'darwin-x64',
13
+ 'linux-arm64',
14
+ 'linux-x64',
15
+ 'win32-x64',
16
+ ])
17
+
18
+ /**
19
+ * Get the hostdb platform identifier
20
+ *
21
+ * hostdb uses standard platform naming that matches Node.js identifiers directly.
22
+ * This function validates the platform/arch combination is supported.
23
+ *
24
+ * @param platform - Node.js platform (e.g., 'darwin', 'linux', 'win32')
25
+ * @param arch - Node.js architecture (e.g., 'arm64', 'x64')
26
+ * @returns hostdb platform identifier or null if unsupported
27
+ */
28
+ export function getHostdbPlatform(
29
+ platform: Platform,
30
+ arch: Arch,
31
+ ): string | null {
32
+ const key = `${platform}-${arch}`
33
+ return SUPPORTED_PLATFORMS.has(key) ? key : null
34
+ }
35
+
36
+ /**
37
+ * Build the download URL for InfluxDB binaries from hostdb
38
+ *
39
+ * Format: https://github.com/robertjbass/hostdb/releases/download/influxdb-{version}/influxdb-{version}-{platform}-{arch}.{ext}
40
+ *
41
+ * @param version - InfluxDB version (e.g., '3', '3.8.0')
42
+ * @param platform - Platform identifier (e.g., 'darwin', 'linux', 'win32')
43
+ * @param arch - Architecture identifier (e.g., 'arm64', 'x64')
44
+ * @returns Download URL for the binary
45
+ */
46
+ export function getBinaryUrl(
47
+ version: string,
48
+ platform: Platform,
49
+ arch: Arch,
50
+ ): string {
51
+ const platformKey = `${platform}-${arch}`
52
+ const hostdbPlatform = getHostdbPlatform(platform, arch)
53
+ if (!hostdbPlatform) {
54
+ const supported = Array.from(SUPPORTED_PLATFORMS).join(', ')
55
+ throw new Error(
56
+ `Unsupported platform: ${platformKey}. Supported platforms: ${supported}`,
57
+ )
58
+ }
59
+
60
+ // Normalize version (handles major version lookup and X.Y -> X.Y.Z conversion)
61
+ const fullVersion = normalizeVersion(version)
62
+ const ext = platform === Platform.Win32 ? 'zip' : 'tar.gz'
63
+
64
+ return buildHostdbUrl(Engine.InfluxDB, {
65
+ version: fullVersion,
66
+ hostdbPlatform,
67
+ extension: ext,
68
+ })
69
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * hostdb Releases Module for InfluxDB
3
+ *
4
+ * Fetches InfluxDB binary information from the hostdb repository at
5
+ * https://github.com/robertjbass/hostdb
6
+ */
7
+
8
+ import { createHostdbReleases } from '../../core/hostdb-releases-factory'
9
+ import { INFLUXDB_VERSION_MAP, SUPPORTED_MAJOR_VERSIONS } from './version-maps'
10
+ import { influxdbBinaryManager } from './binary-manager'
11
+ import { Engine } from '../../types'
12
+
13
+ const hostdbReleases = createHostdbReleases({
14
+ engine: Engine.InfluxDB,
15
+ displayName: 'InfluxDB',
16
+ versionMap: INFLUXDB_VERSION_MAP,
17
+ supportedMajorVersions: SUPPORTED_MAJOR_VERSIONS,
18
+ groupingStrategy: 'single-digit',
19
+ listInstalled: () => influxdbBinaryManager.listInstalled(),
20
+ })
21
+
22
+ export const fetchAvailableVersions = hostdbReleases.fetchAvailableVersions
23
+ export const getLatestVersion = hostdbReleases.getLatestVersion