ใช้ SQLite ใน Claude Code: เปิดฐานข้อมูล ดูตาราง schema รัน query แสดงเป็นตาราง ส่งออก CSV ผ่าน sqlite3 หรือ Python

เปิดไฟล์ฐานข้อมูล SQLite ของจริงใน Claude Code: ดูรายชื่อตารางพร้อมจำนวนแถว ดูโครงสร้าง รัน SQL แล้วแสดงผลเป็นตาราง (ภาษาไทยตรงคอลัมน์) ทั้งในแชตและในแผง ส่งออกเป็น CSV ที่เปิดใน Excel ภาษาไทยไม่เพี้ยน และกันพลาด: DROP หรือ DELETE/UPDATE ที่ไม่มี WHERE ต้องพิมพ์ confirm ต่อท้าย
| คำสั่ง | ทำอะไร | |
|---|---|---|
/sqlite open <ไฟล์.db> [new] | เปิด (หรือสร้างใหม่) | |
/sqlite tables | ตาราง + จำนวนแถว | |
/sqlite schema <ตาราง> | โครงสร้างตาราง | |
/sqlite <SQL> | รัน SQL | |
| `/sqlite export <ไฟล์.csv> <SQL\ | ตาราง>` | ส่งออก CSV |
/sqlite close | ปิด |
/plugin install sqlite-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ต้องมี Python 3 (มี sqlite3 ในตัว) หรือโปรแกรม sqlite3 บนเครื่อง SQL และ path ส่งผ่าน env/argv ไม่ต่อเป็นคำสั่ง shell รันได้ทีละคำสั่ง แสดงผลสูงสุด 200 แถว
hooks/register.tsx 301 lines1/**
2 * ============================================================================
3 * MOD 47: SQLite TH - เปิดฐานข้อมูล SQLite ดูตาราง รัน SQL ใน Claude Code (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: DB Browser for SQLite และ TablePlus
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. เปิดไฟล์ .db / .sqlite ของจริง ดูรายชื่อตาราง จำนวนแถว และโครงสร้าง
9 * 2. รัน SQL แล้วแสดงผลเป็นตารางในแชตและในแผง
10 * 3. ส่งออกผลลัพธ์เป็น CSV (เปิดใน Excel ภาษาไทยไม่เพี้ยน)
11 * 4. กันพลาด: DROP / DELETE หรือ UPDATE ที่ไม่มี WHERE ต้องพิมพ์ confirm ต่อท้าย
12 *
13 * ทำงานยังไง: เรียก Python (มี sqlite3 ในตัว) ถ้าไม่มีจะใช้โปรแกรม sqlite3
14 * ส่ง SQL และ path ผ่าน env/argv เสมอ ไม่ต่อเป็นคำสั่ง shell (กันคำสั่งแทรก)
15 */
16
17import type { EngineInterface, Register } from 'claude-code'
18
19interface Result {
20 columns: string[]
21 rows: unknown[][]
22 changes?: number
23 truncated?: boolean
24 error?: string
25}
26
27const PANE = 'sqlite-th'
28const LIMIT = 200
29
30let dbPath = ''
31let runner: string[] | undefined // คำสั่ง python ที่ใช้ได้บนเครื่องนี้
32let last: { sql: string; result: Result } | undefined
33
34// ==========================================================================
35// บทที่ 1: สคริปต์ Python ตัวเล็ก - รับค่าทาง env ตอบกลับเป็น JSON
36// ==========================================================================
37const PY = `
38import json, os, sqlite3
39db, sql, mode = os.environ["SQ_DB"], os.environ.get("SQ_SQL", ""), os.environ["SQ_MODE"]
40limit = int(os.environ.get("SQ_LIMIT", "200"))
41def cell(v):
42 return "<blob %d bytes>" % len(v) if isinstance(v, (bytes, bytearray)) else v
43out = {"columns": [], "rows": []}
44try:
45 if mode != "create" and not os.path.exists(db):
46 raise Exception("not found: " + db)
47 con = sqlite3.connect(db)
48 if mode == "create":
49 con.execute("select 1")
50 elif mode == "tables":
51 names = con.execute("select name, type from sqlite_master where type in ('table','view') and name not like 'sqlite_%' order by name").fetchall()
52 out["columns"] = ["table", "type", "rows"]
53 out["rows"] = [[n, t, con.execute('select count(*) from "%s"' % n.replace('"', '""')).fetchone()[0]] for n, t in names]
54 elif mode == "schema":
55 cols = con.execute('pragma table_info("%s")' % sql.replace('"', '""')).fetchall()
56 if not cols:
57 raise Exception("no such table: " + sql)
58 out["columns"] = ["column", "type", "not null", "default", "pk"]
59 out["rows"] = [[c[1], c[2], "yes" if c[3] else "", c[4], c[5] or ""] for c in cols]
60 else:
61 cur = con.execute(sql)
62 if cur.description:
63 out["columns"] = [d[0] for d in cur.description]
64 rows = cur.fetchmany(limit + 1)
65 out["truncated"] = len(rows) > limit
66 out["rows"] = [[cell(v) for v in r] for r in rows[:limit]]
67 else:
68 out["changes"] = con.total_changes
69 con.commit()
70except Exception as e:
71 out["error"] = str(e)
72print(json.dumps(out, default=str))
73`
74
75function parse(stdout: string): Result | undefined {
76 try {
77 const r = JSON.parse(stdout.trim().split('\n').at(-1) ?? '') as Result
78 return Array.isArray(r.columns) ? r : undefined
79 } catch {
80 return undefined
81 }
82}
83
84// ==========================================================================
85// บทที่ 2: รัน - ลอง python แต่ละชื่อ แล้วจำตัวที่ใช้ได้ ถ้าไม่มีเลยใช้ sqlite3 CLI
86// ==========================================================================
87async function exec($: EngineInterface, mode: string, sql: string, limit = LIMIT): Promise<Result> {
88 const env = { SQ_DB: dbPath, SQ_SQL: sql, SQ_MODE: mode, SQ_LIMIT: String(limit), PYTHONIOENCODING: 'utf-8' }
89 const candidates = runner ? [runner] : [['python', '-I', '-c'], ['py', '-3', '-I', '-c'], ['python3', '-I', '-c']]
90 for (const c of candidates) {
91 const run = await $.process.run([...c, PY], { env, timeoutMs: 60_000 }).catch(() => undefined)
92 const r = run && parse(run.stdout)
93 if (r) {
94 runner = c
95 return { ...r, truncated: r.truncated || run.isStdoutTruncated }
96 }
97 }
98 return execCli($, mode, sql, limit)
99}
100
101async function execCli($: EngineInterface, mode: string, sql: string, limit: number): Promise<Result> {
102 if (mode !== 'create' && !(await $.fs.exists(dbPath))) return { columns: [], rows: [], error: `not found: ${dbPath}` }
103 const q = (name: string) => `"${name.replace(/"/g, '""')}"`
104 const s = (text: string) => `'${text.replace(/'/g, "''")}'`
105 const query =
106 mode === 'create' ? 'select 1' : mode === 'tables' ? "select name as \"table\", type from sqlite_master where type in ('table','view') and name not like 'sqlite_%' order by name" : mode === 'schema' ? `select name as "column", type, case "notnull" when 1 then 'yes' else '' end as "not null", dflt_value as "default", case pk when 0 then '' else pk end as pk from pragma_table_info(${s(sql)})` : sql
107 const run = await $.process.run(['sqlite3', '-json', dbPath, query], { timeoutMs: 60_000 }).catch(() => undefined)
108 if (!run) return { columns: [], rows: [], error: 'ไม่พบ Python หรือโปรแกรม sqlite3 บนเครื่องนี้' }
109 if (run.exitCode !== 0) return { columns: [], rows: [], error: run.stderr.trim() || `sqlite3 exit ${run.exitCode}` }
110 const objects = run.stdout.trim() ? (JSON.parse(run.stdout) as Record<string, unknown>[]) : []
111 const columns = Object.keys(objects[0] ?? {})
112 const rows = objects.slice(0, limit).map(o => columns.map(c => o[c]))
113 // CLI ไม่บอกจำนวนแถวต่อตาราง: นับเพิ่มทีละตาราง
114 if (mode === 'tables') {
115 for (const row of rows) {
116 const n = await $.process.run(['sqlite3', dbPath, `select count(*) from ${q(String(row[0]))}`], { timeoutMs: 30_000 }).catch(() => undefined)
117 row.push(Number(n?.stdout.trim() ?? 0))
118 }
119 columns.push('rows')
120 }
121 return { columns, rows, truncated: objects.length > limit }
122}
123
124// ==========================================================================
125// บทที่ 3: วาดตารางเป็นข้อความ (สระบน/ล่างของไทยไม่กินที่)
126// ==========================================================================
127function width(text: string): number {
128 return [...text].filter(ch => !/[ัิ-ฺ็-๎]/.test(ch)).length
129}
130
131function show(v: unknown): string {
132 if (v === null || v === undefined) return 'NULL'
133 const s = typeof v === 'object' ? JSON.stringify(v) : String(v)
134 return s.length > 40 ? `${s.slice(0, 39)}…` : s.replace(/\s+/g, ' ')
135}
136
137function table(r: Result, maxRows = 30): string {
138 if (r.columns.length === 0) return ''
139 const cells = [r.columns, ...r.rows.slice(0, maxRows).map(row => row.map(show))]
140 const widths = r.columns.map((_, i) => Math.max(...cells.map(row => width(row[i] ?? ''))))
141 const line = (row: string[]) => row.map((c, i) => c + ' '.repeat((widths[i] ?? 0) - width(c))).join(' │ ')
142 const sep = widths.map(w => '─'.repeat(w)).join('─┼─')
143 const more = r.rows.length > maxRows || r.truncated ? `\n… แสดง ${Math.min(maxRows, r.rows.length)} จาก ${r.truncated ? `มากกว่า ${r.rows.length}` : r.rows.length} แถว` : ''
144 return [line(cells[0] ?? []), sep, ...cells.slice(1).map(line)].join('\n') + more
145}
146
147function csv(r: Result): string {
148 const esc = (v: unknown) => {
149 const s = v === null || v === undefined ? '' : String(v)
150 return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
151 }
152 return '' + [r.columns, ...r.rows].map(row => row.map(esc).join(',')).join('\r\n') + '\r\n'
153}
154
155// ==========================================================================
156// บทที่ 4: คำสั่งอันตราย - ต้องมีคำว่า confirm ต่อท้าย
157// ==========================================================================
158function danger(sql: string): string | undefined {
159 const s = sql.replace(/--.*$/gm, '').replace(/\s+/g, ' ').trim()
160 if (/^drop\b/i.test(s)) return 'DROP ลบทั้งตาราง'
161 if (/^(?:delete|update)\b/i.test(s) && !/\bwhere\b/i.test(s)) return `${s.split(' ')[0]?.toUpperCase()} ไม่มี WHERE = ทุกแถว`
162 if (/^alter\b.*\bdrop\b/i.test(s)) return 'ALTER ... DROP ลบคอลัมน์'
163 return undefined
164}
165
166async function resolvePath($: EngineInterface, p: string): Promise<string> {
167 const unquoted = p.replace(/^["']|["']$/g, '')
168 if (/^(?:[A-Za-z]:[\\/]|\/|\\\\)/.test(unquoted)) return unquoted
169 const root = (await $.session.cwd()).replace(/[\\/]$/, '')
170 return `${root}${root.includes('\\') ? '\\' : '/'}${unquoted}`
171}
172
173const HELP = `🗄️ SQLite TH (Mod 47)
174========================================
175/sqlite open <ไฟล์.db> เปิดฐานข้อมูล (ไม่มีไฟล์ใช้ open <ไฟล์> new)
176/sqlite tables รายชื่อตาราง + จำนวนแถว
177/sqlite schema <ตาราง> โครงสร้างตาราง
178/sqlite <SQL> รัน SQL เช่น /sqlite select * from users limit 10
179/sqlite export <ไฟล์.csv> <SQL|ตาราง> ส่งออก CSV
180/sqlite close ปิด
181DROP / DELETE หรือ UPDATE ไม่มี WHERE ต้องพิมพ์ confirm ต่อท้าย`
182
183export const register: Register = on => {
184 on('session.start', async ($, e, next) => {
185 dbPath = ((await $.store.get('db')) as string | undefined) ?? ''
186 await $.command.register({ name: 'sqlite', description: 'เปิดฐานข้อมูล SQLite ดูตาราง รัน SQL ส่งออก CSV (/sqlite help)', argumentHint: 'open <ไฟล์>|tables|schema <ตาราง>|<SQL>|export' })
187 return next(e)
188 })
189
190 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
191 const { Box, Text } = $.ui.resolve(e)
192 const lines = last ? table(last.result, 50).split('\n') : []
193 return (
194 <Box flexDirection="column">
195 <Text color="#4fc3f7">🗄️ {dbPath || 'ยังไม่ได้เปิดฐานข้อมูล'}</Text>
196 {last && <Text dimColor>{last.sql.slice(0, 100)}</Text>}
197 {lines.map((l, i) => (
198 <Text bold={i === 0}>{l}</Text>
199 ))}
200 </Box>
201 )
202 })
203
204 on('command.run', { command: 'sqlite' }, async ($, e) => {
205 const args = e.args.trim()
206 const [sub = '', ...rest] = args.split(/\s+/).filter(Boolean)
207 const need = () => (dbPath ? undefined : { text: '❌ ยังไม่ได้เปิดฐานข้อมูล: /sqlite open data.db' })
208 const shown = async (sql: string, r: Result) => {
209 last = { sql, result: r }
210 await $.ui.open({ id: PANE, title: '🗄️ SQLite' })
211 $.ui.invalidate('ui.render')
212 }
213
214 switch (sub.toLowerCase()) {
215 case '':
216 case 'help':
217 return { text: `${HELP}\n\nตอนนี้: ${dbPath || '(ยังไม่ได้เปิด)'}` }
218 case 'open': {
219 const isNew = rest.at(-1) === 'new'
220 const target = (isNew ? rest.slice(0, -1) : rest).join(' ')
221 if (!target) return { text: 'วิธีใช้: /sqlite open data.db' }
222 const path = await resolvePath($, target)
223 if (!isNew && !(await $.fs.exists(path))) return { text: `❌ ไม่พบไฟล์ ${path}\nสร้างใหม่: /sqlite open ${target} new` }
224 const prev = dbPath
225 dbPath = path
226 const r = await exec($, isNew ? 'create' : 'tables', '')
227 if (r.error) {
228 dbPath = prev
229 return { text: `❌ เปิดไม่ได้: ${r.error}` }
230 }
231 await $.store.set('db', dbPath)
232 if (isNew) return { text: `✨ สร้าง/เปิด ${path} แล้ว` }
233 await shown('tables', r)
234 return { text: `🗄️ เปิด ${path}\n\n${table(r) || '(ยังไม่มีตาราง)'}` }
235 }
236 case 'close':
237 dbPath = ''
238 last = undefined
239 await $.store.delete('db')
240 await $.ui.close({ id: PANE })
241 return { text: '👋 ปิดฐานข้อมูลแล้ว' }
242 case 'tables': {
243 const missing = need()
244 if (missing) return missing
245 const r = await exec($, 'tables', '')
246 if (r.error) return { text: `❌ ${r.error}` }
247 await shown('tables', r)
248 return { text: table(r) || '(ยังไม่มีตาราง)' }
249 }
250 case 'schema': {
251 const missing = need()
252 if (missing) return missing
253 if (!rest[0]) return { text: 'วิธีใช้: /sqlite schema users' }
254 const r = await exec($, 'schema', rest.join(' '))
255 if (r.error) return { text: `❌ ${r.error}` }
256 await shown(`schema ${rest.join(' ')}`, r)
257 return { text: `📐 ${rest.join(' ')}\n${table(r)}` }
258 }
259 case 'export': {
260 const missing = need()
261 if (missing) return missing
262 const [file, ...q] = rest
263 if (!file || q.length === 0) return { text: 'วิธีใช้: /sqlite export users.csv users หรือ /sqlite export top.csv select * from users where age > 30' }
264 const sql = q.length === 1 ? `select * from "${q[0]?.replace(/"/g, '""')}"` : q.join(' ')
265 if (!/^\s*(?:select|with|pragma)\b/i.test(sql)) return { text: '❌ export ใช้ได้กับ SELECT เท่านั้น' }
266 const r = await exec($, 'query', sql, 100_000)
267 if (r.error) return { text: `❌ ${r.error}` }
268 const out = await resolvePath($, file.endsWith('.csv') ? file : `${file}.csv`)
269 await $.fs.write(out, csv(r))
270 return { text: `💾 ส่งออก ${r.rows.length.toLocaleString('en-US')} แถวไปที่ ${out}${r.truncated ? '\n⚠️ ข้อมูลยาวเกินไป ได้ไม่ครบ ลองใส่ WHERE หรือ LIMIT' : ''}` }
271 }
272 default: {
273 // ที่เหลือคือ SQL ทั้งก้อน
274 const missing = need()
275 if (missing) return missing
276 const confirmed = /\s+confirm$/i.test(args)
277 const sql = args.replace(/\s+confirm$/i, '')
278 const risk = danger(sql)
279 if (risk && !confirmed) return { text: `⚠️ ${risk}\nถ้าแน่ใจ พิมพ์คำสั่งเดิมแล้วต่อท้ายด้วย confirm:\n/sqlite ${sql} confirm` }
280 const r = await exec($, 'query', sql)
281 if (r.error) return { text: `❌ ${r.error}` }
282 if (r.columns.length === 0) {
283 $.ui.invalidate('ui.render')
284 return { text: `✅ สำเร็จ${r.changes !== undefined ? ` (เปลี่ยนแปลง ${r.changes} แถว)` : ''}` }
285 }
286 await shown(sql, r)
287 return { text: `${table(r)}\n(${r.rows.length}${r.truncated ? '+' : ''} แถว)` }
288 }
289 }
290 })
291}
292
293/**
294 * ========================================================================
295 * การบ้าน / ต่อยอด
296 * ========================================================================
297 * 1. นำเข้า CSV เป็นตารางใหม่ (/sqlite import data.csv)
298 * 2. ให้ Claude เขียน SQL จากภาษาไทย ("ลูกค้าที่ซื้อเกิน 3 ครั้ง") ด้วย $.model.complete
299 * 3. เลื่อนดูผลลัพธ์ทีละหน้าในแผง (LIMIT/OFFSET)
300 */
301