SLOPSHOPPER

sqlite-th

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

newpanecommandprocess
★ 3v1.0.0MITupdated 2026-10-24tksoft11/claude-code-mods-th/mods/mod-47-sqlite
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · sqlite-th
│ ┃ sqlite-th ✕ › fix the failing auth test and add an audit log call │ ┃ 🗄️ ยังไม่ได้เปิดฐานข้อมูล │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /sqlite │ ⎿ sqlite-th: 🗄️ SQLite TH (Mod 47) │ ⎿ sqlite-th: ======================================== │ ⎿ sqlite-th: /sqlite open <ไฟล์.db> เปิดฐานข้อมูล (ไม่มีไ │ ⎿ sqlite-th: /sqlite tables รายชื่อตาราง + จำนวนแ │ ⎿ sqlite-th: /sqlite schema <ตาราง> โครงสร้างตาราง │ ⎿ sqlite-th: /sqlite <SQL> รัน SQL เช่น /sqlite │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · sqlite-th
🗄️ ยังไม่ได้เปิดฐานข้อมูล
README

Mod 47: SQLite TH

เปิดไฟล์ฐานข้อมูล 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. สคริปต์ Python ที่รับค่าทาง env ตอบเป็น JSON
  2. เลือก python ที่ใช้ได้ หรือใช้โปรแกรม sqlite3 แทน
  3. วาดตารางเป็นข้อความ
  4. กันคำสั่งอันตราย
  5. คำสั่ง /sqlite

หมายเหตุ

ต้องมี Python 3 (มี sqlite3 ในตัว) หรือโปรแกรม sqlite3 บนเครื่อง SQL และ path ส่งผ่าน env/argv ไม่ต่อเป็นคำสั่ง shell รันได้ทีละคำสั่ง แสดงผลสูงสุด 200 แถว

Source 1 files
hooks/register.tsx 301 lines
1/**
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