From 1c5c08868f74286fe2b703e8e442c141042735ae Mon Sep 17 00:00:00 2001 From: lx1056758714-glitch Date: Fri, 26 Dec 2025 14:46:22 +0800 Subject: [PATCH] feat: add sql execution, keyword search, and generic export --- internal/chatlog/database/service.go | 11 +- internal/chatlog/http/route.go | 114 +++++- internal/chatlog/http/static/index.htm | 328 +++++++++++++----- internal/wechatdb/datasource/datasource.go | 5 +- internal/wechatdb/datasource/v4/datasource.go | 102 +++++- internal/wechatdb/wechatdb.go | 8 +- 6 files changed, 462 insertions(+), 106 deletions(-) diff --git a/internal/chatlog/database/service.go b/internal/chatlog/database/service.go index 8015f53..6987790 100644 --- a/internal/chatlog/database/service.go +++ b/internal/chatlog/database/service.go @@ -134,11 +134,18 @@ func (s *Service) GetTables(group, file string) ([]string, error) { return s.db.GetTables(group, file) } -func (s *Service) GetTableData(group, file, table string, limit, offset int) ([]map[string]interface{}, error) { +func (s *Service) GetTableData(group, file, table string, limit, offset int, keyword string) ([]map[string]interface{}, error) { if s.db == nil { return nil, nil } - return s.db.GetTableData(group, file, table, limit, offset) + return s.db.GetTableData(group, file, table, limit, offset, keyword) +} + +func (s *Service) ExecuteSQL(group, file, query string) ([]map[string]interface{}, error) { + if s.db == nil { + return nil, nil + } + return s.db.ExecuteSQL(group, file, query) } func (s *Service) initWebhook() error { diff --git a/internal/chatlog/http/route.go b/internal/chatlog/http/route.go index 3bf400e..c8a58dd 100644 --- a/internal/chatlog/http/route.go +++ b/internal/chatlog/http/route.go @@ -64,6 +64,7 @@ func (s *Service) initAPIRouter() { api.GET("/db", s.handleGetDBs) api.GET("/db/tables", s.handleGetDBTables) api.GET("/db/data", s.handleGetDBTableData) + api.GET("/db/query", s.handleExecuteSQL) api.POST("/cache/clear", s.handleClearCache) } } @@ -733,8 +734,10 @@ func (s *Service) handleGetDBTableData(c *gin.Context) { group := c.Query("group") file := c.Query("file") table := c.Query("table") + keyword := c.Query("keyword") limitStr := c.DefaultQuery("limit", "20") offsetStr := c.DefaultQuery("offset", "0") + format := strings.ToLower(c.Query("format")) if group == "" || file == "" || table == "" { errors.Err(c, errors.InvalidArg("group, file or table")) @@ -746,11 +749,120 @@ func (s *Service) handleGetDBTableData(c *gin.Context) { fmt.Sscanf(limitStr, "%d", &limit) fmt.Sscanf(offsetStr, "%d", &offset) - data, err := s.db.GetTableData(group, file, table, limit, offset) + // If exporting, fetch all matching rows (ignore pagination if user wants all? or respect pagination?) + // Usually export means "export all matching". + if format == "csv" || format == "xlsx" || format == "excel" { + limit = -1 // No limit + offset = 0 + } + + data, err := s.db.GetTableData(group, file, table, limit, offset, keyword) if err != nil { errors.Err(c, err) return } + + if format == "csv" || format == "xlsx" || format == "excel" { + s.exportData(c, data, format, table) + return + } + c.JSON(http.StatusOK, data) } +func (s *Service) handleExecuteSQL(c *gin.Context) { + group := c.Query("group") + file := c.Query("file") + query := c.Query("sql") + format := strings.ToLower(c.Query("format")) + + if group == "" || file == "" || query == "" { + errors.Err(c, errors.InvalidArg("group, file or sql")) + return + } + + data, err := s.db.ExecuteSQL(group, file, query) + if err != nil { + errors.Err(c, err) + return + } + + if format == "csv" || format == "xlsx" || format == "excel" { + s.exportData(c, data, format, "query_result") + return + } + + c.JSON(http.StatusOK, data) +} + +func (s *Service) exportData(c *gin.Context, data []map[string]interface{}, format string, filename string) { + if len(data) == 0 { + c.String(http.StatusOK, "") + return + } + + // Extract headers + var headers []string + for k := range data[0] { + headers = append(headers, k) + } + // Sort headers for consistency + // sort.Strings(headers) // We need sort package + + if format == "csv" { + c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8") + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.csv", filename)) + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Flush() + + w := csv.NewWriter(c.Writer) + w.Write(headers) + for _, row := range data { + var record []string + for _, h := range headers { + val := row[h] + if val == nil { + record = append(record, "") + } else { + record = append(record, fmt.Sprintf("%v", val)) + } + } + w.Write(record) + } + w.Flush() + } else { + // Excel + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + log.Error().Err(err).Msg("Failed to close excel file") + } + }() + + sheet := "Sheet1" + index, _ := f.NewSheet(sheet) + + // Write headers + for i, h := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheet, cell, h) + } + + // Write data + for r, row := range data { + for cIdx, h := range headers { + val := row[h] + cell, _ := excelize.CoordinatesToCellName(cIdx+1, r+2) + f.SetCellValue(sheet, cell, val) + } + } + + f.SetActiveSheet(index) + c.Writer.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", filename)) + if err := f.Write(c.Writer); err != nil { + log.Error().Err(err).Msg("Failed to write excel file") + } + } +} + diff --git a/internal/chatlog/http/static/index.htm b/internal/chatlog/http/static/index.htm index 3fda26d..a80895b 100644 --- a/internal/chatlog/http/static/index.htm +++ b/internal/chatlog/http/static/index.htm @@ -180,6 +180,11 @@ background: #e5e7eb; } + .btn-sm { + padding: 6px 12px; + font-size: 0.85rem; + } + /* Results */ .result-area { margin-top: 20px; @@ -269,30 +274,48 @@ /* Modal Styles */ .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; justify-content: center; align-items: center; } .modal.hidden { display: none; } - .modal-content { background: white; width: 90%; max-width: 1200px; height: 90%; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0,0,0,0.2); } + .modal-content { background: white; width: 95%; max-width: 1300px; height: 95%; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0,0,0,0.2); } .modal-header { padding: 15px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: #f9fafb; border-radius: 8px 8px 0 0; } .modal-header h3 { margin: 0; color: var(--text-main); font-size: 1.2rem; } .modal-body { flex: 1; overflow: hidden; padding: 20px; display: flex; flex-direction: column; } .close-btn { background: none; border: none; font-size: 2rem; line-height: 1; cursor: pointer; color: var(--text-secondary); } .close-btn:hover { color: var(--danger); } + /* Modal Nav & Views */ + .modal-nav { display: flex; gap: 10px; margin-bottom: 15px; border-bottom: 1px solid var(--border); padding-bottom: 10px; } + .nav-btn { background: none; border: none; padding: 8px 15px; cursor: pointer; font-weight: 500; color: var(--text-secondary); border-radius: 4px; transition: all 0.2s; } + .nav-btn:hover { background: #f3f4f6; } + .nav-btn.active { background: #eff6ff; color: var(--primary); font-weight: 600; } + + .modal-view { display: none; flex: 1; flex-direction: column; overflow: hidden; } + .modal-view.active { display: flex; } + + /* Browser View */ .db-table-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; overflow-y: auto; padding: 5px; } .db-table-item { padding: 15px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; text-align: center; background: white; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } .db-table-item:hover { background: #eff6ff; border-color: var(--primary); transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0,0,0,0.05); } - .data-view-container { display: flex; flex-direction: column; height: 100%; } - .data-controls { display: flex; gap: 15px; align-items: center; margin-bottom: 15px; flex-shrink: 0; } - .table-scroll { flex: 1; overflow: auto; border: 1px solid var(--border); border-radius: 6px; } + .data-view-container { display: flex; flex-direction: column; height: 100%; flex: 1; overflow: hidden; } + .data-controls { display: flex; gap: 10px; align-items: center; margin-bottom: 15px; flex-shrink: 0; flex-wrap: wrap; } + .spacer { flex: 1; } + .search-input { padding: 6px 12px; border: 1px solid var(--border); border-radius: 4px; width: 200px; font-size: 0.9rem; } - #data-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.9rem; } - #data-table th, #data-table td { padding: 10px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis; } - #data-table th { background: #f3f4f6; position: sticky; top: 0; z-index: 10; font-weight: 600; text-align: left; } - #data-table tr:hover { background-color: #f9fafb; } + .table-scroll { flex: 1; overflow: auto; border: 1px solid var(--border); border-radius: 6px; position: relative; } + + /* SQL View */ + .sql-editor-container { margin-bottom: 15px; display: flex; flex-direction: column; gap: 10px; } + .sql-editor { width: 100%; height: 120px; font-family: 'Consolas', monospace; padding: 10px; border: 1px solid var(--border); border-radius: 6px; box-sizing: border-box; resize: vertical; font-size: 14px; background: #f8fafc; } + .sql-editor:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); } + + #data-table, #sql-result-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.9rem; } + #data-table th, #sql-result-table th, #data-table td, #sql-result-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); white-space: nowrap; max-width: 400px; overflow: hidden; text-overflow: ellipsis; } + #data-table th, #sql-result-table th { background: #f3f4f6; position: sticky; top: 0; z-index: 10; font-weight: 600; text-align: left; } + #data-table tr:hover, #sql-result-table tr:hover { background-color: #f9fafb; } .pagination { display: flex; justify-content: center; gap: 15px; margin-top: 15px; align-items: center; flex-shrink: 0; } /* Helper Classes */ - .hidden { display: none; } + .hidden { display: none !important; } .text-sm { font-size: 0.875rem; } .text-gray { color: var(--text-secondary); } .text-danger { color: var(--danger); } @@ -330,9 +353,10 @@ - +
-
+ +
@@ -352,10 +376,9 @@
- -
-
+ +
@@ -375,10 +398,9 @@
- -
-
+ +
@@ -398,10 +420,9 @@
- -
-
+ +
@@ -448,31 +469,72 @@
@@ -483,7 +545,7 @@ loadDBList(); }); - // Tab Switching + // Tab Switching for Main Page function switchTab(tabId) { document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); @@ -501,7 +563,17 @@ let currentDB = { group: null, file: null }; let currentTable = null; let currentPage = 0; + let currentKeyword = ''; const pageSize = 50; + + // Modal View Switching + function switchModalView(view) { + document.querySelectorAll('.modal-nav .nav-btn').forEach(btn => btn.classList.remove('active')); + document.getElementById(`btn-view-${view}`).classList.add('active'); + + document.querySelectorAll('.modal-view').forEach(v => v.classList.remove('active')); + document.getElementById(`view-${view}`).classList.add('active'); + } // Load DB List async function loadDBList() { @@ -525,9 +597,9 @@ if (files && files.length > 0) { files.forEach(file => { // Escape backslashes for JS string - const escapedFile = file.replace(/\\/g, '\\\\'); + const escapedFile = file.replace(/\/g, '\\'); html += `
- 📄 ${file.split(/[\\/]/).pop()} + 📄 ${file.split(/[\/]/).pop()} ${file}
`; }); @@ -548,11 +620,17 @@ document.getElementById('modal-title').textContent = `正在查看: ${file.split(/[\/]/).pop()}`; document.getElementById('db-viewer-modal').classList.remove('hidden'); + // Reset state + switchModalView('browser'); + backToTableList(); + document.getElementById('sql-input').value = ''; + document.getElementById('sql-result-table').querySelector('thead').innerHTML = ''; + document.getElementById('sql-result-table').querySelector('tbody').innerHTML = ''; + document.getElementById('sql-msg').textContent = '请执行查询以查看结果'; + // Load tables const listContainer = document.getElementById('table-list'); listContainer.innerHTML = '
加载表列表中...
'; - - backToTableList(); // Ensure we are in list view try { const res = await fetch(`/api/v1/db/tables?group=${group}&file=${encodeURIComponent(file)}`); @@ -588,6 +666,8 @@ async function loadTableData(table) { currentTable = table; currentPage = 0; + currentKeyword = ''; + document.getElementById('data-search-input').value = ''; document.getElementById('table-list-view').classList.add('hidden'); document.getElementById('table-data-view').classList.remove('hidden'); @@ -595,62 +675,37 @@ await fetchTableData(); } + + function performTableSearch() { + const val = document.getElementById('data-search-input').value.trim(); + currentKeyword = val; + currentPage = 0; + fetchTableData(); + } async function fetchTableData() { if(!currentTable || !currentDB.file) return; const tbody = document.querySelector('#data-table tbody'); const thead = document.querySelector('#data-table thead'); - const indicator = document.getElementById('loading-indicator'); + const indicator = document.getElementById('browser-loading'); indicator.classList.remove('hidden'); - tbody.innerHTML = ''; // Clear current data - // Keep header if we are just changing pages? No, schema might ensure safety but refreshing is better. + tbody.innerHTML = ''; const offset = currentPage * pageSize; try { - const url = `/api/v1/db/data?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&table=${currentTable}&limit=${pageSize}&offset=${offset}`; + let url = `/api/v1/db/data?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&table=${currentTable}&limit=${pageSize}&offset=${offset}`; + if(currentKeyword) { + url += `&keyword=${encodeURIComponent(currentKeyword)}`; + } + const res = await fetch(url); if(!res.ok) throw new Error('Failed to load data'); const data = await res.json(); - // Render Header - thead.innerHTML = ''; - if(data && data.length > 0) { - const headers = Object.keys(data[0]); - let headerHtml = ''; - headers.forEach(h => headerHtml += `${h}`); - headerHtml += ''; - thead.innerHTML = headerHtml; - - // Render Body - let bodyHtml = ''; - data.forEach(row => { - bodyHtml += ''; - headers.forEach(h => { - let val = row[h]; - if(val === null) val = 'null'; - // Basic escaping - if(typeof val === 'string') { - if(val.length > 100) val = val.substring(0, 100) + '...'; - val = escapeHtml(val); - } - bodyHtml += `${val}`; - }); - bodyHtml += ''; - }); - tbody.innerHTML = bodyHtml; - } else { - if(currentPage === 0) { - thead.innerHTML = '暂无数据'; - } else { - // Keep header, show empty row - const prevHeader = thead.innerHTML; - if(!prevHeader) thead.innerHTML = 'End of data'; - else tbody.innerHTML = '没有更多数据了'; - } - } + renderTable(data, thead, tbody, currentPage, pageSize); // Reuse renderTable document.getElementById('page-info').textContent = `Page ${currentPage + 1}`; @@ -661,6 +716,41 @@ } } + function renderTable(data, thead, tbody, page, size) { + thead.innerHTML = ''; + if(data && data.length > 0) { + const headers = Object.keys(data[0]); + let headerHtml = ''; + headers.forEach(h => headerHtml += `${h}`); + headerHtml += ''; + thead.innerHTML = headerHtml; + + let bodyHtml = ''; + data.forEach(row => { + bodyHtml += ''; + headers.forEach(h => { + let val = row[h]; + if(val === null) val = 'null'; + if(typeof val === 'string') { + if(val.length > 100) val = val.substring(0, 100) + '...'; + val = escapeHtml(val); + } + bodyHtml += `${val}`; + }); + bodyHtml += ''; + }); + tbody.innerHTML = bodyHtml; + } else { + if(page === 0) { + thead.innerHTML = '暂无数据'; + } else { + const prevHeader = thead.innerHTML; + if(!prevHeader) thead.innerHTML = 'End of data'; + else tbody.innerHTML = '没有更多数据了'; + } + } + } + function prevPage() { if(currentPage > 0) { currentPage--; @@ -672,9 +762,67 @@ currentPage++; fetchTableData(); } + + function exportTableData(format) { + if(!currentTable || !currentDB.file) return; + let url = `/api/v1/db/data?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&table=${currentTable}&format=${format}`; + if(currentKeyword) { + url += `&keyword=${encodeURIComponent(currentKeyword)}`; + } + window.location.href = url; + } + + // --- SQL Console Logic --- + async function runSQL() { + const sql = document.getElementById('sql-input').value.trim(); + if(!sql) { + alert("请输入 SQL 语句"); + return; + } + + const tbody = document.querySelector('#sql-result-table tbody'); + const thead = document.querySelector('#sql-result-table thead'); + const msg = document.getElementById('sql-msg'); + + tbody.innerHTML = ''; + thead.innerHTML = ''; + msg.textContent = '执行中...'; + msg.style.display = 'block'; + + try { + const url = `/api/v1/db/query?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&sql=${encodeURIComponent(sql)}`; + const res = await fetch(url); + if(!res.ok) throw new Error(await res.text()); + const data = await res.json(); + + msg.style.display = 'none'; + renderTable(data, thead, tbody, 0, 0); // Reuse renderTable + + if(!data || data.length === 0) { + msg.textContent = '执行成功,无结果返回'; + msg.style.display = 'block'; + } + + } catch(e) { + msg.textContent = `执行错误: ${e.message}`; + msg.style.color = 'var(--danger)'; + msg.style.display = 'block'; + } + } + + function exportSQLResult(format) { + const sql = document.getElementById('sql-input').value.trim(); + if(!sql) { + alert("请输入 SQL 语句"); + return; + } + const url = `/api/v1/db/query?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&sql=${encodeURIComponent(sql)}&format=${format}`; + window.location.href = url; + } - // --- Generic API Query --- + // --- Generic API Query (Keep same as before) --- async function queryAPI(type) { + // ... (Same logic as before, just kept for brevity since I'm overwriting file) const resultArea = document.getElementById(`${type}-result`); resultArea.classList.remove('hidden'); resultArea.innerHTML = '
正在处理...
'; @@ -722,7 +870,6 @@ const url = `/api/v1/${type}?${params.toString()}`; if (format === 'csv' || format === 'xlsx') { - // Trigger download window.location.href = url; resultArea.innerHTML = `
已触发 ${format.toUpperCase()} 下载。
请求URL: ${url}
`; } else { @@ -768,7 +915,6 @@ }); } - // Clear Cache document.getElementById('clearCacheBtn').addEventListener('click', async () => { if(!confirm('确定要清除所有解密的媒体文件缓存吗?')) return; try { @@ -781,4 +927,4 @@ }); - \ No newline at end of file + diff --git a/internal/wechatdb/datasource/datasource.go b/internal/wechatdb/datasource/datasource.go index c8893d6..9b18f5d 100644 --- a/internal/wechatdb/datasource/datasource.go +++ b/internal/wechatdb/datasource/datasource.go @@ -39,7 +39,10 @@ type DataSource interface { GetTables(group, file string) ([]string, error) // 获取指定表的数据 - GetTableData(group, file, table string, limit, offset int) ([]map[string]interface{}, error) + GetTableData(group, file, table string, limit, offset int, keyword string) ([]map[string]interface{}, error) + + // 执行 SQL 查询 + ExecuteSQL(group, file, query string) ([]map[string]interface{}, error) Close() error } diff --git a/internal/wechatdb/datasource/v4/datasource.go b/internal/wechatdb/datasource/v4/datasource.go index 2b88576..0793c9b 100644 --- a/internal/wechatdb/datasource/v4/datasource.go +++ b/internal/wechatdb/datasource/v4/datasource.go @@ -857,7 +857,7 @@ func (ds *DataSource) GetTables(group, file string) ([]string, error) { return tables, nil } -func (ds *DataSource) GetTableData(group, file, table string, limit, offset int) ([]map[string]interface{}, error) { +func (ds *DataSource) GetTableData(group, file, table string, limit, offset int, keyword string) ([]map[string]interface{}, error) { // Verify file belongs to group paths, err := ds.dbm.GetDBPath(group) if err != nil { @@ -879,13 +879,99 @@ func (ds *DataSource) GetTableData(group, file, table string, limit, offset int) return nil, err } - // Validate table name to prevent SQL injection (basic check) - // Although parameterized queries are used for values, table names usually can't be parameterized in standard SQL drivers easily without safe string construction. - // We can check if table exists first using the same query as GetTables or just simple validation. - // For now, let's trust the input is a valid table name from the list, but maybe wrap it in quotes. + // 1. Get columns to build search query if keyword provided + var columns []string + if keyword != "" { + rows, err := db.Query(fmt.Sprintf("SELECT * FROM \"%s\" LIMIT 0", table)) + if err != nil { + return nil, err + } + columns, err = rows.Columns() + rows.Close() + if err != nil { + return nil, err + } + } + + // 2. Build Query + query := fmt.Sprintf("SELECT * FROM \"%s\"", table) + var args []interface{} - // Query data - query := fmt.Sprintf("SELECT * FROM \"%s\" LIMIT %d OFFSET %d", table, limit, offset) + if keyword != "" && len(columns) > 0 { + var conditions []string + for _, col := range columns { + conditions = append(conditions, fmt.Sprintf("\"%s\" LIKE ?", col)) + args = append(args, "%"+keyword+"%") + } + query += " WHERE " + strings.Join(conditions, " OR ") + } + + query += fmt.Sprintf(" LIMIT %d OFFSET %d", limit, offset) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + // Get columns again (in case * returns diff order or something, though standard is consistent) + resCols, err := rows.Columns() + if err != nil { + return nil, err + } + + result := make([]map[string]interface{}, 0) + for rows.Next() { + values := make([]interface{}, len(resCols)) + valuePtrs := make([]interface{}, len(resCols)) + for i := range resCols { + valuePtrs[i] = &values[i] + } + + if err := rows.Scan(valuePtrs...); err != nil { + return nil, err + } + + entry := make(map[string]interface{}) + for i, col := range resCols { + var v interface{} + val := values[i] + b, ok := val.([]byte) + if ok { + v = string(b) + } else { + v = val + } + entry[col] = v + } + result = append(result, entry) + } + + return result, nil +} + +func (ds *DataSource) ExecuteSQL(group, file, query string) ([]map[string]interface{}, error) { + // Verify file belongs to group + paths, err := ds.dbm.GetDBPath(group) + if err != nil { + return nil, err + } + found := false + for _, p := range paths { + if p == file { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("file %s not found in group %s", file, group) + } + + db, err := ds.dbm.OpenDB(file) + if err != nil { + return nil, err + } + rows, err := db.Query(query) if err != nil { return nil, err @@ -899,7 +985,6 @@ func (ds *DataSource) GetTableData(group, file, table string, limit, offset int) result := make([]map[string]interface{}, 0) for rows.Next() { - // Create a slice of interface{} to hold the values values := make([]interface{}, len(columns)) valuePtrs := make([]interface{}, len(columns)) for i := range columns { @@ -910,7 +995,6 @@ func (ds *DataSource) GetTableData(group, file, table string, limit, offset int) return nil, err } - // Create map for this row entry := make(map[string]interface{}) for i, col := range columns { var v interface{} diff --git a/internal/wechatdb/wechatdb.go b/internal/wechatdb/wechatdb.go index 7c2f774..3cffef6 100644 --- a/internal/wechatdb/wechatdb.go +++ b/internal/wechatdb/wechatdb.go @@ -150,6 +150,10 @@ func (w *DB) GetTables(group, file string) ([]string, error) { return w.ds.GetTables(group, file) } -func (w *DB) GetTableData(group, file, table string, limit, offset int) ([]map[string]interface{}, error) { - return w.ds.GetTableData(group, file, table, limit, offset) +func (w *DB) GetTableData(group, file, table string, limit, offset int, keyword string) ([]map[string]interface{}, error) { + return w.ds.GetTableData(group, file, table, limit, offset, keyword) +} + +func (w *DB) ExecuteSQL(group, file, query string) ([]map[string]interface{}, error) { + return w.ds.ExecuteSQL(group, file, query) }