spindb 0.34.0 → 0.34.3

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 CHANGED
@@ -105,6 +105,18 @@ spindb connect cache # Open redis-cli
105
105
  spindb connect cache --iredis # Enhanced shell
106
106
  ```
107
107
 
108
+ ### InfluxDB
109
+
110
+ ```bash
111
+ spindb create tsdata --engine influxdb --start
112
+ spindb run tsdata ./seed.lp # Seed with line protocol
113
+ spindb run tsdata -c "SHOW TABLES" # Run inline SQL
114
+ spindb run tsdata ./queries.sql # Run SQL file
115
+ spindb connect tsdata # Interactive SQL console
116
+ ```
117
+
118
+ > InfluxDB supports two file formats: `.lp` (line protocol) for writing data, `.sql` for queries.
119
+
108
120
  ### Enhanced Shells & Visual Tools
109
121
 
110
122
  ```bash
@@ -261,7 +273,7 @@ See [DEPLOY.md](DEPLOY.md) for comprehensive deployment documentation.
261
273
  - **Local only** - Databases bind to `127.0.0.1`. Remote connection support planned for v1.1.
262
274
  - **ClickHouse Windows** - Not supported (hostdb doesn't build for Windows).
263
275
  - **FerretDB Windows** - Not supported (postgresql-documentdb has startup issues on Windows).
264
- - **Qdrant, Meilisearch, CouchDB & InfluxDB** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
276
+ - **Qdrant, Meilisearch, CouchDB** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
265
277
 
266
278
  ---
267
279
 
@@ -42,6 +42,7 @@ import {
42
42
  import { getEngine } from '../../../engines'
43
43
  import { createSpinner } from '../../ui/spinner'
44
44
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
45
+ import { logDebug } from '../../../core/error-handler'
45
46
  import { pressEnterToContinue } from './shared'
46
47
  import { paths } from '../../../config/paths'
47
48
  import { getEngineConfig } from '../../../config/engines-registry'
@@ -237,8 +238,8 @@ export async function handleOpenShell(
237
238
  engineSpecificValue = null
238
239
  engineSpecificInstallValue = null
239
240
  } else if (config.engine === 'influxdb') {
240
- // InfluxDB uses REST API, no interactive shell
241
- defaultShellName = 'Web Dashboard'
241
+ // InfluxDB uses influxdb3 query subcommand (same binary as server)
242
+ defaultShellName = 'influxdb3 query'
242
243
  engineSpecificCli = null
243
244
  engineSpecificInstalled = false
244
245
  engineSpecificValue = null
@@ -334,7 +335,11 @@ export async function handleOpenShell(
334
335
  value: 'api-info',
335
336
  })
336
337
  } else if (config.engine === 'influxdb') {
337
- // InfluxDB: REST API only, no web dashboard or interactive shell
338
+ // InfluxDB: influxdb3 query CLI + API info
339
+ choices.push({
340
+ name: `▸ Use default shell (influxdb3 query)`,
341
+ value: 'default',
342
+ })
338
343
  choices.push({
339
344
  name: `ℹ Show API info`,
340
345
  value: 'api-info',
@@ -353,7 +358,7 @@ export async function handleOpenShell(
353
358
  } else {
354
359
  // Non-REST-API engines: show default shell option
355
360
  choices.push({
356
- name: `>_ Use default shell (${defaultShellName})`,
361
+ name: `▸ Use default shell (${defaultShellName})`,
357
362
  value: 'default',
358
363
  })
359
364
  }
@@ -362,7 +367,7 @@ export async function handleOpenShell(
362
367
  if (engineSpecificCli !== null) {
363
368
  if (engineSpecificInstalled) {
364
369
  choices.push({
365
- name: `⚡ Use ${engineSpecificCli} (enhanced features, recommended)`,
370
+ name: `★ Use ${engineSpecificCli} (enhanced features, recommended)`,
366
371
  value: engineSpecificValue!,
367
372
  })
368
373
  } else {
@@ -378,7 +383,7 @@ export async function handleOpenShell(
378
383
  if (engineConfig.queryLanguage === 'sql') {
379
384
  if (usqlInstalled) {
380
385
  choices.push({
381
- name: ' Use usql (universal SQL client)',
386
+ name: ' Use usql (universal SQL client)',
382
387
  value: 'usql',
383
388
  })
384
389
  } else {
@@ -394,7 +399,7 @@ export async function handleOpenShell(
394
399
  const dblabPath = await configManager.getBinaryPath('dblab')
395
400
  if (dblabPath) {
396
401
  choices.push({
397
- name: ' Use dblab (visual TUI)',
402
+ name: ' Use dblab (visual TUI)',
398
403
  value: 'dblab',
399
404
  })
400
405
  } else {
@@ -415,6 +420,15 @@ export async function handleOpenShell(
415
420
  })
416
421
  }
417
422
 
423
+ if (config.engine === 'questdb') {
424
+ const httpPort = config.port + 188
425
+ choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)))
426
+ choices.push({
427
+ name: `◎ Open Web Console (port ${httpPort})`,
428
+ value: 'browser',
429
+ })
430
+ }
431
+
418
432
  if (config.engine === 'duckdb') {
419
433
  choices.push(new inquirer.Separator(chalk.gray(`───── Web Panel ─────`)))
420
434
  choices.push({
@@ -487,6 +501,16 @@ export async function handleOpenShell(
487
501
  console.log()
488
502
  openInBrowser(playUrl)
489
503
  await pressEnterToContinue()
504
+ } else if (config.engine === 'questdb') {
505
+ // QuestDB Web Console on HTTP port (PG port + 188)
506
+ const httpPort = config.port + 188
507
+ const consoleUrl = `http://127.0.0.1:${httpPort}`
508
+ console.log()
509
+ console.log(uiInfo(`Opening QuestDB Web Console in browser...`))
510
+ console.log(chalk.gray(` ${consoleUrl}`))
511
+ console.log()
512
+ openInBrowser(consoleUrl)
513
+ await pressEnterToContinue()
490
514
  }
491
515
  return
492
516
  }
@@ -1482,22 +1506,109 @@ async function launchShell(
1482
1506
  await pressEnterToContinue()
1483
1507
  return
1484
1508
  } else if (config.engine === 'influxdb') {
1485
- // InfluxDB: REST API only, no web dashboard
1486
- // This branch shouldn't be reached since we removed the 'default' choice,
1487
- // but handle gracefully just in case
1488
- console.log()
1489
- console.log(chalk.cyan('InfluxDB REST API:'))
1490
- console.log(chalk.white(` HTTP: http://127.0.0.1:${config.port}`))
1491
- console.log()
1492
- console.log(chalk.gray('Example curl commands:'))
1493
- console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/health`))
1494
- console.log(
1495
- chalk.gray(
1496
- ` curl -H "Content-Type: application/json" http://127.0.0.1:${config.port}/api/v3/query_sql -d '{"db":"mydb","q":"SELECT 1"}'`,
1497
- ),
1498
- )
1499
- console.log()
1500
- await pressEnterToContinue()
1509
+ // InfluxDB: influxdb3 query is one-shot (no REPL), use interactive loop
1510
+ const engine = getEngine(config.engine)
1511
+ const influxdbPath = await engine
1512
+ .getInfluxDBPath(config.version)
1513
+ .catch(() => null)
1514
+ if (!influxdbPath) {
1515
+ console.log(
1516
+ uiWarning('influxdb3 not found. Run: spindb engines download influxdb'),
1517
+ )
1518
+ await pressEnterToContinue()
1519
+ return
1520
+ }
1521
+ // Query available databases from the REST API
1522
+ let db = database || config.name
1523
+ try {
1524
+ const resp = await fetch(
1525
+ `http://127.0.0.1:${config.port}/api/v3/configure/database?format=json`,
1526
+ )
1527
+ if (resp.ok) {
1528
+ const databases = (await resp.json()) as Array<Record<string, string>>
1529
+ const dbNames = databases
1530
+ .map((d) => d['iox::database'] || d.name)
1531
+ .filter((n) => n && n !== '_internal')
1532
+ if (dbNames.length === 0) {
1533
+ console.log(
1534
+ uiWarning(
1535
+ 'No databases exist yet. Write data first to create a database.',
1536
+ ),
1537
+ )
1538
+ console.log(
1539
+ chalk.gray(
1540
+ ` curl -X POST "http://127.0.0.1:${config.port}/api/v3/write_lp?db=${db}" -H "Content-Type: text/plain" -d 'measurement,tag=value field=1'`,
1541
+ ),
1542
+ )
1543
+ console.log()
1544
+ await pressEnterToContinue()
1545
+ return
1546
+ }
1547
+ if (!dbNames.includes(db)) {
1548
+ if (dbNames.length === 1) {
1549
+ db = dbNames[0]
1550
+ } else {
1551
+ const { chosenDb } = await escapeablePrompt<{ chosenDb: string }>([
1552
+ {
1553
+ type: 'list',
1554
+ name: 'chosenDb',
1555
+ message: 'Select database:',
1556
+ choices: dbNames,
1557
+ },
1558
+ ])
1559
+ db = chosenDb
1560
+ }
1561
+ }
1562
+ }
1563
+ } catch {
1564
+ // Server may not support this endpoint; proceed with default db
1565
+ }
1566
+ console.log(chalk.cyan(`InfluxDB SQL Console (${db})`))
1567
+ console.log(chalk.gray(` Type SQL queries, or "exit" to quit.\n`))
1568
+ let running = true
1569
+ while (running) {
1570
+ const { sql } = await escapeablePrompt<{ sql: string }>([
1571
+ {
1572
+ type: 'input',
1573
+ name: 'sql',
1574
+ message: chalk.blue('sql>'),
1575
+ },
1576
+ ])
1577
+ const trimmed = (sql || '').trim()
1578
+ if (
1579
+ trimmed.toLowerCase() === 'exit' ||
1580
+ trimmed.toLowerCase() === 'quit'
1581
+ ) {
1582
+ running = false
1583
+ break
1584
+ }
1585
+ if (!trimmed) {
1586
+ continue
1587
+ }
1588
+ const queryProcess = spawn(
1589
+ influxdbPath,
1590
+ [
1591
+ 'query',
1592
+ '--host',
1593
+ `http://127.0.0.1:${config.port}`,
1594
+ '--database',
1595
+ db,
1596
+ '--',
1597
+ trimmed,
1598
+ ],
1599
+ { stdio: 'inherit' },
1600
+ )
1601
+ await new Promise<void>((resolve) => {
1602
+ queryProcess.on('error', (err) => {
1603
+ console.error(uiError(`Query failed: ${err.message}`))
1604
+ resolve()
1605
+ })
1606
+ queryProcess.on('close', () => {
1607
+ logDebug('influxdb query process exited')
1608
+ resolve()
1609
+ })
1610
+ })
1611
+ }
1501
1612
  return
1502
1613
  } else if (config.engine === 'couchdb') {
1503
1614
  // CouchDB: Open Fauxton dashboard in browser (served at /_utils)
@@ -5,6 +5,7 @@ import { spawn } from 'child_process'
5
5
  import { containerManager } from '../../../core/container-manager'
6
6
  import { getMissingDependencies } from '../../../core/dependency-manager'
7
7
  import { getEngine } from '../../../engines'
8
+ import { Engine } from '../../../types'
8
9
  import { paths } from '../../../config/paths'
9
10
  import { promptInstallDependencies, escapeablePrompt } from '../../ui/prompts'
10
11
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
@@ -92,7 +93,51 @@ export async function handleRunSql(
92
93
  const filePath = stripQuotes(rawFilePath)
93
94
 
94
95
  // Use provided database or fall back to container's default
95
- const databaseName = database || config.database
96
+ let databaseName = database || config.database
97
+
98
+ // InfluxDB: discover real databases (they're created implicitly on write)
99
+ // Cannot use engine.listDatabases() here because it falls back to
100
+ // container.database when the list is empty, hiding the "no databases"
101
+ // state that we need to detect for the .lp warning.
102
+ if (config.engine === Engine.InfluxDB) {
103
+ try {
104
+ const resp = await fetch(
105
+ `http://127.0.0.1:${config.port}/api/v3/configure/database?format=json`,
106
+ )
107
+ if (resp.ok) {
108
+ const databases = (await resp.json()) as Array<Record<string, string>>
109
+ const dbNames = databases
110
+ .map((d) => d['iox::database'] || d.name)
111
+ .filter((n) => n && n !== '_internal')
112
+ if (dbNames.length === 0 && !filePath.endsWith('.lp')) {
113
+ console.log(
114
+ uiWarning(
115
+ 'No databases exist yet. Seed data with a .lp file first.',
116
+ ),
117
+ )
118
+ await pressEnterToContinue()
119
+ return
120
+ }
121
+ if (!dbNames.includes(databaseName)) {
122
+ if (dbNames.length === 1) {
123
+ databaseName = dbNames[0]
124
+ } else if (dbNames.length > 1) {
125
+ const { chosenDb } = await escapeablePrompt<{ chosenDb: string }>([
126
+ {
127
+ type: 'list',
128
+ name: 'chosenDb',
129
+ message: 'Select database:',
130
+ choices: dbNames,
131
+ },
132
+ ])
133
+ databaseName = chosenDb
134
+ }
135
+ }
136
+ }
137
+ } catch {
138
+ // Proceed with default
139
+ }
140
+ }
96
141
 
97
142
  console.log()
98
143
  console.log(uiInfo(`Running ${scriptType} file against "${databaseName}"...`))
@@ -291,8 +291,8 @@
291
291
  "defaultVersion": "3.8.0",
292
292
  "defaultPort": 8086,
293
293
  "runtime": "server",
294
- "queryLanguage": "sql",
295
- "scriptFileLabel": null,
294
+ "queryLanguage": "rest",
295
+ "scriptFileLabel": "Run LP/SQL file",
296
296
  "connectionScheme": "http",
297
297
  "superuser": null,
298
298
  "clientTools": ["influxdb3"],
@@ -161,6 +161,14 @@ export abstract class BaseEngine {
161
161
  throw new Error('typedb_console_bin not found')
162
162
  }
163
163
 
164
+ /**
165
+ * Get the path to the influxdb3 binary if available
166
+ * Default implementation throws; InfluxDB engine overrides this method.
167
+ */
168
+ async getInfluxDBPath(_version?: string): Promise<string> {
169
+ throw new Error('influxdb3 not found')
170
+ }
171
+
164
172
  /**
165
173
  * Get the path to the sqlite3 client if available
166
174
  * Default implementation returns null; SQLite engine overrides this method.
@@ -1044,8 +1044,41 @@ export class InfluxDBEngine extends BaseEngine {
1044
1044
  const database = options.database || container.database
1045
1045
 
1046
1046
  if (options.file) {
1047
- // Read file content and execute as SQL
1048
1047
  const content = await readFile(options.file, 'utf-8')
1048
+
1049
+ // Ensure the database exists (InfluxDB creates DBs implicitly on write,
1050
+ // but SQL queries fail if the DB doesn't exist yet)
1051
+ const createDbResp = await influxdbApiRequest(
1052
+ port,
1053
+ 'POST',
1054
+ '/api/v3/configure/database',
1055
+ { db: database },
1056
+ )
1057
+ if (createDbResp.status >= 400) {
1058
+ throw new Error(
1059
+ `Failed to create database "${database}": HTTP ${createDbResp.status} — ${JSON.stringify(createDbResp.data)}`,
1060
+ )
1061
+ }
1062
+
1063
+ // Line protocol files (.lp) → write via /api/v3/write_lp
1064
+ if (options.file.endsWith('.lp')) {
1065
+ const lines = content
1066
+ .split('\n')
1067
+ .filter((line) => line.trim().length > 0 && !line.startsWith('#'))
1068
+ .join('\n')
1069
+ const response = await influxdbApiRequest(
1070
+ port,
1071
+ 'POST',
1072
+ `/api/v3/write_lp?db=${encodeURIComponent(database)}`,
1073
+ lines,
1074
+ )
1075
+ if (response.status >= 400) {
1076
+ throw new Error(`Write error: ${JSON.stringify(response.data)}`)
1077
+ }
1078
+ return
1079
+ }
1080
+
1081
+ // SQL files → execute via /api/v3/query_sql
1049
1082
  const statements = content
1050
1083
  .split('\n')
1051
1084
  .filter((line) => !line.startsWith('--') && line.trim().length > 0)
@@ -1076,6 +1109,18 @@ export class InfluxDBEngine extends BaseEngine {
1076
1109
  }
1077
1110
 
1078
1111
  if (options.sql) {
1112
+ // Ensure database exists for inline SQL too
1113
+ const createDbResp2 = await influxdbApiRequest(
1114
+ port,
1115
+ 'POST',
1116
+ '/api/v3/configure/database',
1117
+ { db: database },
1118
+ )
1119
+ if (createDbResp2.status >= 400) {
1120
+ throw new Error(
1121
+ `Failed to create database "${database}": HTTP ${createDbResp2.status} — ${JSON.stringify(createDbResp2.data)}`,
1122
+ )
1123
+ }
1079
1124
  const response = await influxdbApiRequest(
1080
1125
  port,
1081
1126
  'POST',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.34.0",
3
+ "version": "0.34.3",
4
4
  "author": "Bob Bass <bob@bbass.co>",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",