474 lines
15 KiB
PHP
474 lines
15 KiB
PHP
<div class="dbm-content-header">
|
|
<div class="dbm-breadcrumb">
|
|
<a href="<?php echo Config::get('URL'); ?>database/index">Databases</a>
|
|
<span class="separator">/</span>
|
|
<a href="<?php echo Config::get('URL'); ?>database/show/<?php echo urlencode($this->database_name); ?>"><?php echo htmlspecialchars($this->database_name); ?></a>
|
|
<span class="separator">/</span>
|
|
<span>SQL Console</span>
|
|
</div>
|
|
<div class="dbm-title">
|
|
<h1>SQL Console</h1>
|
|
<span class="badge"><?php echo htmlspecialchars($this->database_name); ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dbm-content-body">
|
|
<div class="sql-console">
|
|
<form method="post" action="<?php echo Config::get('URL'); ?>sql/execute" id="sql-form">
|
|
<input type="hidden" name="database_name" value="<?php echo htmlspecialchars($this->database_name); ?>">
|
|
|
|
<div class="sql-editor-container">
|
|
<div class="sql-editor-wrapper">
|
|
<pre class="sql-highlight" id="sql-highlight" aria-hidden="true"></pre>
|
|
<textarea name="sql_query" id="sql_query" class="sql-textarea" spellcheck="false" placeholder="SELECT * FROM users LIMIT 10;"><?php echo isset($_POST['sql_query']) ? htmlspecialchars($_POST['sql_query']) : ''; ?></textarea>
|
|
</div>
|
|
<div class="sql-line-numbers" id="line-numbers">1</div>
|
|
</div>
|
|
|
|
<div class="sql-toolbar">
|
|
<div class="sql-toolbar-left">
|
|
<button type="submit" class="dbm-btn dbm-btn-success">
|
|
<i data-lucide="play"></i>
|
|
Execute
|
|
</button>
|
|
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="formatSQL()">
|
|
<i data-lucide="align-left"></i>
|
|
Format
|
|
</button>
|
|
<button type="button" class="dbm-btn dbm-btn-secondary" onclick="clearSQL()">
|
|
<i data-lucide="trash-2"></i>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
<div class="sql-toolbar-right">
|
|
<span class="sql-hint">Ctrl+Enter to execute</span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="sql-result" class="sql-result">
|
|
<?php
|
|
$result = Session::get('sql_result');
|
|
if ($result) {
|
|
Session::set('sql_result', null);
|
|
|
|
if ($result['success']) {
|
|
echo '<div class="sql-result-success">';
|
|
echo '<div class="sql-result-header">';
|
|
echo '<span class="sql-result-status"><i data-lucide="check-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
|
|
echo '<span class="sql-result-time">' . $result['execution_time'] . ' ms</span>';
|
|
echo '</div>';
|
|
|
|
if (!empty($result['result'])) {
|
|
echo '<div class="sql-result-table-wrapper"><table class="dbm-table"><thead><tr>';
|
|
foreach (array_keys($result['result'][0]) as $col) {
|
|
echo '<th>' . htmlspecialchars($col) . '</th>';
|
|
}
|
|
echo '</tr></thead><tbody>';
|
|
foreach ($result['result'] as $row) {
|
|
echo '<tr>';
|
|
foreach ($row as $value) {
|
|
echo '<td>' . ($value === null ? '<span class="null-value">NULL</span>' : htmlspecialchars(substr($value, 0, 200))) . '</td>';
|
|
}
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody></table></div>';
|
|
}
|
|
echo '</div>';
|
|
} else {
|
|
echo '<div class="sql-result-error">';
|
|
echo '<div class="sql-result-header">';
|
|
echo '<span class="sql-result-status"><i data-lucide="x-circle"></i> ' . htmlspecialchars($result['message']) . '</span>';
|
|
echo '</div>';
|
|
if (!empty($result['error'])) {
|
|
echo '<pre class="sql-error-details">' . htmlspecialchars($result['error']) . '</pre>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
}
|
|
?>
|
|
</div>
|
|
|
|
<?php if (!empty($this->history)): ?>
|
|
<div class="sql-history">
|
|
<h3>Recent Queries</h3>
|
|
<div class="sql-history-list">
|
|
<?php foreach (array_slice($this->history, 0, 10) as $item): ?>
|
|
<div class="sql-history-item" onclick="loadQuery(this)" data-query="<?php echo htmlspecialchars($item['query_text']); ?>">
|
|
<code><?php echo htmlspecialchars(substr($item['query_text'], 0, 80)); ?><?php echo strlen($item['query_text']) > 80 ? '...' : ''; ?></code>
|
|
<span class="sql-history-time"><?php echo date('M j, H:i', strtotime($item['query_timestamp'])); ?></span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<a href="<?php echo Config::get('URL'); ?>sql/clearHistory" class="dbm-btn dbm-btn-sm dbm-btn-secondary" style="margin-top: 12px;">Clear History</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.sql-console {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.sql-editor-container {
|
|
display: flex;
|
|
border: 1px solid var(--dbm-border);
|
|
border-radius: var(--dbm-radius);
|
|
overflow: hidden;
|
|
background: #1e1e1e;
|
|
}
|
|
|
|
.sql-line-numbers {
|
|
padding: 12px 8px;
|
|
background: #252526;
|
|
color: #858585;
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
text-align: right;
|
|
user-select: none;
|
|
min-width: 40px;
|
|
border-right: 1px solid #333;
|
|
}
|
|
|
|
.sql-editor-wrapper {
|
|
flex: 1;
|
|
position: relative;
|
|
min-height: 180px;
|
|
}
|
|
|
|
.sql-highlight, .sql-textarea {
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
padding: 12px;
|
|
margin: 0;
|
|
border: none;
|
|
width: 100%;
|
|
min-height: 180px;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.sql-highlight {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
pointer-events: none;
|
|
background: transparent;
|
|
color: #d4d4d4;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sql-textarea {
|
|
position: relative;
|
|
background: transparent;
|
|
color: transparent;
|
|
caret-color: #fff;
|
|
resize: none;
|
|
outline: none;
|
|
z-index: 1;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.sql-textarea::placeholder {
|
|
color: #666;
|
|
}
|
|
|
|
/* Syntax highlighting colors - VS Code dark theme */
|
|
.sql-highlight .sql-keyword { color: #569cd6 !important; font-weight: bold !important; }
|
|
.sql-highlight .sql-function { color: #dcdcaa !important; }
|
|
.sql-highlight .sql-string { color: #ce9178 !important; }
|
|
.sql-highlight .sql-number { color: #b5cea8 !important; }
|
|
.sql-highlight .sql-operator { color: #d4d4d4 !important; }
|
|
.sql-highlight .sql-comment { color: #6a9955 !important; font-style: italic !important; }
|
|
.sql-highlight .sql-table { color: #4ec9b0 !important; }
|
|
.sql-highlight .sql-column { color: #9cdcfe !important; }
|
|
|
|
.sql-toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.sql-toolbar-left {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.sql-hint {
|
|
font-size: 12px;
|
|
color: var(--dbm-text-muted);
|
|
}
|
|
|
|
.sql-result {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.sql-result-success {
|
|
background: #f0f9f0;
|
|
border: 1px solid #c3e6c3;
|
|
border-radius: var(--dbm-radius);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sql-result-error {
|
|
background: #fdf0f0;
|
|
border: 1px solid #f5c6c6;
|
|
border-radius: var(--dbm-radius);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sql-result-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
background: rgba(0,0,0,0.03);
|
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.sql-result-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.sql-result-success .sql-result-status { color: #2e7d32; }
|
|
.sql-result-error .sql-result-status { color: #c62828; }
|
|
|
|
.sql-result-time {
|
|
font-size: 12px;
|
|
color: #666;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.sql-result-table-wrapper {
|
|
overflow-x: auto;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.sql-result-table-wrapper .dbm-table {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.sql-result-table-wrapper .dbm-table th {
|
|
position: sticky;
|
|
top: 0;
|
|
background: #f8f9fa;
|
|
z-index: 1;
|
|
}
|
|
|
|
.null-value {
|
|
color: #999;
|
|
font-style: italic;
|
|
}
|
|
|
|
.sql-error-details {
|
|
margin: 0;
|
|
padding: 12px 16px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
color: #c62828;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.sql-history {
|
|
background: var(--dbm-bg-secondary);
|
|
border-radius: var(--dbm-radius);
|
|
padding: 16px;
|
|
}
|
|
|
|
.sql-history h3 {
|
|
margin: 0 0 12px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--dbm-text);
|
|
}
|
|
|
|
.sql-history-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.sql-history-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
background: var(--dbm-bg);
|
|
border-radius: var(--dbm-radius);
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.sql-history-item:hover {
|
|
background: var(--dbm-bg-tertiary);
|
|
}
|
|
|
|
.sql-history-item code {
|
|
font-size: 12px;
|
|
color: var(--dbm-text-secondary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
flex: 1;
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.sql-history-time {
|
|
font-size: 11px;
|
|
color: var(--dbm-text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const textarea = document.getElementById('sql_query');
|
|
const highlight = document.getElementById('sql-highlight');
|
|
const lineNumbers = document.getElementById('line-numbers');
|
|
|
|
function updateHighlight() {
|
|
const code = textarea.value;
|
|
highlight.innerHTML = highlightSQL(code) + '\n';
|
|
updateLineNumbers();
|
|
}
|
|
|
|
function updateLineNumbers() {
|
|
const lines = textarea.value.split('\n').length;
|
|
let nums = '';
|
|
for (let i = 1; i <= lines; i++) {
|
|
nums += i + '\n';
|
|
}
|
|
lineNumbers.textContent = nums;
|
|
}
|
|
|
|
function highlightSQL(code) {
|
|
if (!code) return '';
|
|
|
|
// Escape HTML
|
|
code = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
// Use inline styles for guaranteed coloring
|
|
const styles = {
|
|
keyword: 'color:#569cd6;font-weight:bold',
|
|
function: 'color:#dcdcaa',
|
|
string: 'color:#ce9178',
|
|
number: 'color:#b5cea8',
|
|
comment: 'color:#6a9955;font-style:italic'
|
|
};
|
|
|
|
// Comments (must be first to avoid highlighting keywords inside comments)
|
|
code = code.replace(/(--[^\n]*)/g, '<span style="' + styles.comment + '">$1</span>');
|
|
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span style="' + styles.comment + '">$1</span>');
|
|
|
|
// Strings
|
|
code = code.replace(/('[^']*')/g, '<span style="' + styles.string + '">$1</span>');
|
|
code = code.replace(/("[^"]*")/g, '<span style="' + styles.string + '">$1</span>');
|
|
|
|
// Numbers (but not inside already-styled spans)
|
|
code = code.replace(/\b(\d+\.?\d*)\b(?![^<]*>)/g, '<span style="' + styles.number + '">$1</span>');
|
|
|
|
// Keywords
|
|
const keywords = [
|
|
'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'NULL',
|
|
'ORDER', 'BY', 'ASC', 'DESC', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET',
|
|
'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE',
|
|
'CREATE', 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'TRIGGER', 'PROCEDURE', 'FUNCTION',
|
|
'ALTER', 'DROP', 'TRUNCATE', 'ADD', 'MODIFY', 'CHANGE', 'RENAME',
|
|
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'OUTER', 'CROSS', 'ON', 'USING',
|
|
'UNION', 'ALL', 'DISTINCT', 'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
|
|
'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'UNIQUE', 'DEFAULT',
|
|
'AUTO_INCREMENT', 'ENGINE', 'CHARSET', 'COLLATE',
|
|
'IF', 'EXISTS', 'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE',
|
|
'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION'
|
|
];
|
|
|
|
const keywordRegex = new RegExp('\\b(' + keywords.join('|') + ')\\b(?![^<]*>)', 'gi');
|
|
code = code.replace(keywordRegex, '<span style="' + styles.keyword + '">$1</span>');
|
|
|
|
// Functions
|
|
const functions = [
|
|
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH', 'UPPER', 'LOWER',
|
|
'TRIM', 'LTRIM', 'RTRIM', 'REPLACE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST', 'CONVERT',
|
|
'DATE', 'NOW', 'CURDATE', 'CURTIME', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND',
|
|
'DATE_FORMAT', 'DATEDIFF', 'DATE_ADD', 'DATE_SUB', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD',
|
|
'RAND', 'UUID', 'MD5', 'SHA1', 'SHA2', 'GROUP_CONCAT', 'JSON_OBJECT', 'JSON_ARRAY'
|
|
];
|
|
|
|
const funcRegex = new RegExp('\\b(' + functions.join('|') + ')\\s*\\(', 'gi');
|
|
code = code.replace(funcRegex, '<span style="' + styles.function + '">$1</span>(');
|
|
|
|
return code;
|
|
}
|
|
|
|
textarea.addEventListener('input', updateHighlight);
|
|
textarea.addEventListener('scroll', function() {
|
|
highlight.scrollTop = textarea.scrollTop;
|
|
highlight.scrollLeft = textarea.scrollLeft;
|
|
});
|
|
|
|
// Ctrl+Enter to execute
|
|
textarea.addEventListener('keydown', function(e) {
|
|
if (e.ctrlKey && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
document.getElementById('sql-form').submit();
|
|
}
|
|
// Tab to insert spaces
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
const start = this.selectionStart;
|
|
const end = this.selectionEnd;
|
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
|
this.selectionStart = this.selectionEnd = start + 4;
|
|
updateHighlight();
|
|
}
|
|
});
|
|
|
|
updateHighlight();
|
|
lucide.createIcons();
|
|
});
|
|
|
|
function loadQuery(element) {
|
|
document.getElementById('sql_query').value = element.dataset.query;
|
|
document.getElementById('sql_query').dispatchEvent(new Event('input'));
|
|
document.getElementById('sql_query').focus();
|
|
}
|
|
|
|
function clearSQL() {
|
|
document.getElementById('sql_query').value = '';
|
|
document.getElementById('sql_query').dispatchEvent(new Event('input'));
|
|
}
|
|
|
|
function formatSQL() {
|
|
const textarea = document.getElementById('sql_query');
|
|
let sql = textarea.value;
|
|
|
|
// Basic formatting
|
|
const keywords = ['SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'ON', 'SET', 'VALUES', 'INSERT INTO', 'UPDATE', 'DELETE FROM'];
|
|
|
|
keywords.forEach(kw => {
|
|
const regex = new RegExp('\\b' + kw.replace(' ', '\\s+') + '\\b', 'gi');
|
|
sql = sql.replace(regex, '\n' + kw);
|
|
});
|
|
|
|
sql = sql.trim();
|
|
textarea.value = sql;
|
|
textarea.dispatchEvent(new Event('input'));
|
|
}
|
|
</script>
|