spindb 0.31.0 → 0.31.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.
- package/README.md +7 -0
- package/cli/commands/query.ts +238 -0
- package/cli/index.ts +2 -0
- package/core/query-parser.ts +514 -0
- package/engines/base-engine.ts +15 -0
- package/engines/clickhouse/index.ts +55 -0
- package/engines/cockroachdb/index.ts +64 -0
- package/engines/couchdb/index.ts +70 -0
- package/engines/duckdb/index.ts +31 -0
- package/engines/ferretdb/index.ts +111 -1
- package/engines/mariadb/index.ts +55 -0
- package/engines/meilisearch/index.ts +69 -0
- package/engines/mongodb/index.ts +50 -1
- package/engines/mysql/index.ts +55 -0
- package/engines/postgresql/index.ts +56 -0
- package/engines/qdrant/index.ts +69 -0
- package/engines/questdb/index.ts +79 -0
- package/engines/redis/index.ts +46 -0
- package/engines/sqlite/index.ts +30 -0
- package/engines/surrealdb/index.ts +89 -0
- package/engines/valkey/index.ts +47 -0
- package/package.json +1 -1
- package/types/index.ts +16 -0
package/README.md
CHANGED
|
@@ -305,6 +305,13 @@ spindb run mydb -c "SELECT * FROM users" # Inline SQL
|
|
|
305
305
|
spindb run mydb seed.js # JavaScript (MongoDB)
|
|
306
306
|
spindb run mydb -c "SET foo bar" # Redis command
|
|
307
307
|
|
|
308
|
+
# Query with structured output
|
|
309
|
+
spindb query mydb "SELECT * FROM users LIMIT 5" # Tabular output
|
|
310
|
+
spindb query mydb "SELECT * FROM users" --json # JSON for scripting
|
|
311
|
+
spindb query mydb "users.find()" -d mydb # MongoDB/FerretDB
|
|
312
|
+
spindb query myredis "KEYS user:*" # Redis/Valkey
|
|
313
|
+
spindb query myqdrant "GET /collections" # REST API engines
|
|
314
|
+
|
|
308
315
|
# Get connection string
|
|
309
316
|
spindb url mydb # postgresql://postgres@localhost:5432/mydb
|
|
310
317
|
spindb url mydb --copy # Copy to clipboard
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { containerManager } from '../../core/container-manager'
|
|
5
|
+
import { processManager } from '../../core/process-manager'
|
|
6
|
+
import { getEngine } from '../../engines'
|
|
7
|
+
import { promptInstallDependencies } from '../ui/prompts'
|
|
8
|
+
import { uiError, uiWarning } from '../ui/theme'
|
|
9
|
+
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
10
|
+
import { Engine, isFileBasedEngine, type QueryResult } from '../../types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format a QueryResult as a table for terminal output
|
|
14
|
+
*/
|
|
15
|
+
function formatTable(result: QueryResult): string {
|
|
16
|
+
if (result.columns.length === 0 || result.rows.length === 0) {
|
|
17
|
+
return '(0 rows)'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Calculate column widths
|
|
21
|
+
const widths: number[] = result.columns.map((col) => col.length)
|
|
22
|
+
|
|
23
|
+
for (const row of result.rows) {
|
|
24
|
+
for (let i = 0; i < result.columns.length; i++) {
|
|
25
|
+
const col = result.columns[i]
|
|
26
|
+
const value = formatValue(row[col])
|
|
27
|
+
widths[i] = Math.max(widths[i], value.length)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Build header
|
|
32
|
+
const header = result.columns
|
|
33
|
+
.map((col, i) => col.padEnd(widths[i]))
|
|
34
|
+
.join(' | ')
|
|
35
|
+
|
|
36
|
+
// Build separator
|
|
37
|
+
const separator = widths.map((w) => '-'.repeat(w)).join('-+-')
|
|
38
|
+
|
|
39
|
+
// Build rows
|
|
40
|
+
const rows = result.rows.map((row) =>
|
|
41
|
+
result.columns
|
|
42
|
+
.map((col, i) => formatValue(row[col]).padEnd(widths[i]))
|
|
43
|
+
.join(' | '),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Combine
|
|
47
|
+
const lines = [header, separator, ...rows]
|
|
48
|
+
|
|
49
|
+
// Add row count
|
|
50
|
+
const countMsg =
|
|
51
|
+
result.rowCount === 1 ? '(1 row)' : `(${result.rowCount} rows)`
|
|
52
|
+
lines.push('')
|
|
53
|
+
lines.push(countMsg)
|
|
54
|
+
|
|
55
|
+
return lines.join('\n')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format a value for display
|
|
60
|
+
*/
|
|
61
|
+
function formatValue(value: unknown): string {
|
|
62
|
+
if (value === null || value === undefined) {
|
|
63
|
+
return 'NULL'
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'object') {
|
|
66
|
+
return JSON.stringify(value)
|
|
67
|
+
}
|
|
68
|
+
return String(value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const queryCommand = new Command('query')
|
|
72
|
+
.description('Execute a query and return results')
|
|
73
|
+
.argument('<name>', 'Container name')
|
|
74
|
+
.argument('<query>', 'Query to execute')
|
|
75
|
+
.option('-d, --database <name>', 'Target database (defaults to primary)')
|
|
76
|
+
.option('--json', 'Output results as JSON')
|
|
77
|
+
.action(
|
|
78
|
+
async (
|
|
79
|
+
name: string,
|
|
80
|
+
query: string,
|
|
81
|
+
options: { database?: string; json?: boolean },
|
|
82
|
+
) => {
|
|
83
|
+
try {
|
|
84
|
+
const containerName = name
|
|
85
|
+
|
|
86
|
+
const config = await containerManager.getConfig(containerName)
|
|
87
|
+
if (!config) {
|
|
88
|
+
if (options.json) {
|
|
89
|
+
console.log(
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
error: `Container "${containerName}" not found`,
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
} else {
|
|
95
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
96
|
+
}
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { engine: engineName } = config
|
|
101
|
+
|
|
102
|
+
// File-based databases: check file exists instead of running status
|
|
103
|
+
if (isFileBasedEngine(engineName)) {
|
|
104
|
+
if (!existsSync(config.database)) {
|
|
105
|
+
if (options.json) {
|
|
106
|
+
console.log(
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
error: `Database file not found: ${config.database}`,
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
} else {
|
|
112
|
+
console.error(
|
|
113
|
+
uiError(`Database file not found: ${config.database}`),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
process.exit(1)
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
// Server databases need to be running
|
|
120
|
+
const running = await processManager.isRunning(containerName, {
|
|
121
|
+
engine: engineName,
|
|
122
|
+
})
|
|
123
|
+
if (!running) {
|
|
124
|
+
if (options.json) {
|
|
125
|
+
console.log(
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
error: `Container "${containerName}" is not running`,
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
} else {
|
|
131
|
+
console.error(
|
|
132
|
+
uiError(
|
|
133
|
+
`Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const engine = getEngine(engineName)
|
|
142
|
+
|
|
143
|
+
let missingDeps = await getMissingDependencies(engineName)
|
|
144
|
+
if (missingDeps.length > 0) {
|
|
145
|
+
if (options.json) {
|
|
146
|
+
console.log(
|
|
147
|
+
JSON.stringify({
|
|
148
|
+
error: `Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(
|
|
155
|
+
uiWarning(
|
|
156
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const installed = await promptInstallDependencies(
|
|
161
|
+
missingDeps[0].binary,
|
|
162
|
+
engineName,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (!installed) {
|
|
166
|
+
process.exit(1)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
missingDeps = await getMissingDependencies(engineName)
|
|
170
|
+
if (missingDeps.length > 0) {
|
|
171
|
+
console.error(
|
|
172
|
+
uiError(
|
|
173
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
process.exit(1)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
180
|
+
console.log()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const database = options.database || config.database
|
|
184
|
+
|
|
185
|
+
// Execute the query
|
|
186
|
+
const result = await engine.executeQuery(config, query, { database })
|
|
187
|
+
|
|
188
|
+
// Output results
|
|
189
|
+
if (options.json) {
|
|
190
|
+
// JSON mode: output just the rows array
|
|
191
|
+
console.log(JSON.stringify(result.rows, null, 2))
|
|
192
|
+
} else {
|
|
193
|
+
// Table mode
|
|
194
|
+
console.log(formatTable(result))
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const e = error as Error
|
|
198
|
+
|
|
199
|
+
// Map of tool patterns to their engines
|
|
200
|
+
const toolPatternToEngine: Record<string, Engine> = {
|
|
201
|
+
'psql not found': Engine.PostgreSQL,
|
|
202
|
+
'mysql not found': Engine.MySQL,
|
|
203
|
+
'mysql client not found': Engine.MySQL,
|
|
204
|
+
'redis-cli not found': Engine.Redis,
|
|
205
|
+
'mongosh not found': Engine.MongoDB,
|
|
206
|
+
'sqlite3 not found': Engine.SQLite,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const matchingPattern = Object.keys(toolPatternToEngine).find((p) =>
|
|
210
|
+
e.message.toLowerCase().includes(p.toLowerCase()),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if (matchingPattern && !options.json) {
|
|
214
|
+
const missingTool = matchingPattern
|
|
215
|
+
.replace(' not found', '')
|
|
216
|
+
.replace(' client', '')
|
|
217
|
+
const toolEngine = toolPatternToEngine[matchingPattern]
|
|
218
|
+
const installed = await promptInstallDependencies(
|
|
219
|
+
missingTool,
|
|
220
|
+
toolEngine,
|
|
221
|
+
)
|
|
222
|
+
if (installed) {
|
|
223
|
+
console.log(
|
|
224
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
process.exit(1)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (options.json) {
|
|
231
|
+
console.log(JSON.stringify({ error: e.message }))
|
|
232
|
+
} else {
|
|
233
|
+
console.error(uiError(e.message))
|
|
234
|
+
}
|
|
235
|
+
process.exit(1)
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
)
|
package/cli/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { databasesCommand } from './commands/databases'
|
|
|
30
30
|
import { pullCommand } from './commands/pull'
|
|
31
31
|
import { whichCommand } from './commands/which'
|
|
32
32
|
import { exportCommand } from './commands/export'
|
|
33
|
+
import { queryCommand } from './commands/query'
|
|
33
34
|
import { updateManager } from '../core/update-manager'
|
|
34
35
|
import { configManager } from '../core/config-manager'
|
|
35
36
|
import { setCachedIconMode } from './constants'
|
|
@@ -142,6 +143,7 @@ export async function run(): Promise<void> {
|
|
|
142
143
|
program.addCommand(pullCommand)
|
|
143
144
|
program.addCommand(whichCommand)
|
|
144
145
|
program.addCommand(exportCommand)
|
|
146
|
+
program.addCommand(queryCommand)
|
|
145
147
|
|
|
146
148
|
if (process.argv.length <= 2) {
|
|
147
149
|
// Only show update notification in interactive menu mode (once at startup)
|