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 +13 -1
- package/cli/commands/menu/shell-handlers.ts +134 -23
- package/cli/commands/menu/sql-handlers.ts +46 -1
- package/config/engines.json +2 -2
- package/engines/base-engine.ts +8 -0
- package/engines/influxdb/index.ts +46 -1
- package/package.json +1 -1
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
|
|
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
|
|
241
|
-
defaultShellName = '
|
|
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:
|
|
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:
|
|
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:
|
|
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: '
|
|
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: '
|
|
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:
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
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}"...`))
|
package/config/engines.json
CHANGED
|
@@ -291,8 +291,8 @@
|
|
|
291
291
|
"defaultVersion": "3.8.0",
|
|
292
292
|
"defaultPort": 8086,
|
|
293
293
|
"runtime": "server",
|
|
294
|
-
"queryLanguage": "
|
|
295
|
-
"scriptFileLabel":
|
|
294
|
+
"queryLanguage": "rest",
|
|
295
|
+
"scriptFileLabel": "Run LP/SQL file",
|
|
296
296
|
"connectionScheme": "http",
|
|
297
297
|
"superuser": null,
|
|
298
298
|
"clientTools": ["influxdb3"],
|
package/engines/base-engine.ts
CHANGED
|
@@ -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.
|
|
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.",
|