Belajar SQL.js Step-by-Step: SQLite Langsung di Browser

Belajar SQL.js Step-by-Step: SQLite Langsung di Browser

Pernah kepikiran gak, kalau kita bisa main database SQLite langsung di browser?
Nah, jawabannya ada di SQL.js.

SQL.js itu adalah SQLite yang ditranslate jadi WebAssembly, jadi bisa jalan langsung di browser. Serius, kita bisa bikin semacam SQL Client sendiri pakai HTML + JavaScript doang.

Di artikel ini, kita bakal bikin project sederhana:

✅ Upload file database SQLite (.db, .sqlite)
✅ Jalankan query langsung di browser
✅ Lihat hasil query dalam bentuk tabel
✅ Bikin sample data (biar gampang coba-coba)
✅ Download lagi database setelah diedit

1. Inisialisasi SQL.js

Pertama, kita perlu load library SQL.js dari CDN. Caranya gampang, cukup tambahin ini di <head> file HTML:

<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js"></script>

Lalu inisialisasi:

async function initSQLJS() {
    const sqlPromise = initSqlJs({
        locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`
    });
    const SQL = await sqlPromise;
    const db = new SQL.Database(); // bikin DB kosong di memori
    console.log("SQL.js siap dipakai di browser!");
}

atau jika pakai Node.js / Bundler

Kalau kamu pakai project dengan Node.js (misalnya pakai Webpack, Vite, atau Next.js), install dulu package-nya:

npm install sql.js

Lalu import di kode JavaScript:

const initSqlJs = require('sql.js');
const SQL = await initSqlJs({
  locateFile: file => `https://sql.js.org/dist/${file}`
});

const db = new SQL.Database();

Kenapa ada locateFile?

SQL.js itu JavaScript + WebAssembly (WASM).

  • File sql-wasm.js
    kode JS buat API (supaya kita bisa pakai db.run(), db.exec(), dll).
  • File sql-wasm.wasm
    mesin SQLite yang udah di-compile ke WebAssembly.

locateFile buat kasih tau “file .wasm ada di mana”.

Contoh

Pakai dari Website Resmi (sql.js.org)
Kalau ngikut dokumentasi bawaan, biasanya diarahkan ke

const sqlPromise = initSqlJs({
    locateFile: file => `https://sql.js.org/dist/${file}`
});

Atau pakai CDN

https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}

Pakai File Lokal
Kalau kamu download sql-wasm.wasm dan taruh di folder libs/, cukup kasih path lokal:

2. Jalankan query (executeQuery)

const results = db.exec(queryText);
displayResults(results);

Penjelasan praktis:

  • db.exec(sql) mengembalikan array hasil untuk statement yang menghasilkan rows (SELECT).
  • Untuk statement non-SELECT (CREATE, INSERT, UPDATE, DELETE) db.exec tetap mengeksekusi tapi tidak mengembalikan rows → results kosong. Kode kamu sudah menangani ini dengan menampilkan "Query executed, no data returned."

3. Upload file .db

const reader = new FileReader(); 
reader.onload = function(e) { 
  const uint8Array = new Uint8Array(e.target.result); 
  db = new SQL.Database(uint8Array); 
}; 
reader.readAsArrayBuffer(file);

Penjelasannya

  1. FileReader
    dipakai untuk membaca file yang dipilih user (misalnya file .db SQLite).
  2. reader.onload
    fungsi callback yang dijalankan setelah file selesai dibaca.
  3. new Uint8Array(e.target.result)
    hasil pembacaan file (ArrayBuffer) diubah menjadi Uint8Array supaya bisa diproses dalam bentuk biner.
  4. new SQL.Database(uint8Array)
    membuat instance database SQLite di browser menggunakan sql.js, dengan data dari file .db.
  5. reader.readAsArrayBuffer(file)
    memberitahu browser:
    "Baca file ini, tapi jangan diperlakukan sebagai teks atau gambar, melainkan kasih saya isi mentah binernya."
  6. Hasilnya nanti berupa ArrayBuffer yang bisa diakses di e.target.result ketika onload dipanggil.

Kenapa pakai ArrayBuffer di sini?

File .db (SQLite) bukan file teks, tapi file biner.
Kalau dibaca pakai readAsText(), hasilnya bakal rusak (karena dianggap string).
Dengan readAsArrayBuffer(), kita dapat isi asli file dalam bentuk byte-by-byte.

4. Download atau Export .db

 if (!db) {
    showStatus('Database belum dimuat!', 'error');
    return;
  }

  // Export database ke bentuk Uint8Array
  const data = db.export();

  // Buat Blob dari data biner
  const blob = new Blob([data], { type: "application/octet-stream" });

  // Buat link download
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "database.db"; // nama file hasil download
  a.click();

  // Hapus URL object untuk menghemat memory
  URL.revokeObjectURL(url);

Penjelasan

  1. db.export()
    fungsi bawaan sql.js untuk mengubah database ke Uint8Array (isi file biner .db).
  2. new Blob([...])
    bikin file virtual dari data biner, dengan MIME type application/octet-stream (umum untuk file biner).
  3. URL.createObjectURL(blob)
    bikin link sementara untuk file virtual tadi.
  4. a.download = "database.db"
    kasih nama file saat diunduh.
  5. a.click()
    otomatis trigger download.
  6. URL.revokeObjectURL(url)
    bersihkan URL setelah dipakai

contoh lengkapnya :

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SQL.js Client</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
        }
        
        .section {
            background: white;
            padding: 20px;
            margin: 15px 0;
            border-radius: 5px;
            border: 1px solid #ddd;
        }
        
        h1, h2 {
            color: #333;
            margin: 0 0 15px 0;
        }
        
        .controls {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            margin: 15px 0;
        }
        
        button {
            background: #007cba;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 3px;
            cursor: pointer;
        }
        
        button:hover {
            background: #005a87;
        }
        
        button.danger {
            background: #dc3545;
        }
        
        button.danger:hover {
            background: #c82333;
        }
        
        textarea {
            width: 100%;
            height: 200px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 3px;
            font-family: monospace;
            font-size: 14px;
            resize: vertical;
        }
        
        input[type="file"] {
            padding: 5px;
            border: 1px solid #ddd;
            border-radius: 3px;
        }
        
        .status {
            padding: 10px;
            margin: 10px 0;
            border-radius: 3px;
        }
        
        .status.success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        
        .status.error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        
        .status.info {
            background: #cce7ff;
            color: #004085;
            border: 1px solid #b8daff;
        }
        
        table {
            width: 100%;
            border-collapse: collapse;
            margin: 15px 0;
        }
        
        th, td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        
        th {
            background: #f8f9fa;
            font-weight: bold;
        }
        
        .sample-queries {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
            gap: 5px;
            margin: 15px 0;
        }
        
        .sample-btn {
            background: white;
            color: #333;
            border: 1px solid #ddd;
            padding: 5px 10px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
        }
        
        .sample-btn:hover {
            background: #f8f9fa;
        }
    </style>
</head>
<body>
    <h1>SQL.js Client</h1>
    
    <div id="status" class="status info">Loading SQL.js...</div>

    <div class="section">
        <h2>Database</h2>
        <input type="file" id="fileInput" accept=".db,.sqlite,.sqlite3">
        <div class="controls">
            <button onclick="downloadDB()">Download DB</button>
            <button onclick="createSampleData()">Create Sample</button>
            <button class="danger" onclick="clearDB()">Clear DB</button>
        </div>
    </div>

    <!-- Query Section -->
    <div class="section">
        <h2>SQL Query</h2>
        <textarea id="sqlQuery" placeholder="-- Enter SQL query here
SELECT * FROM sqlite_master WHERE type='table';"></textarea>
        
        <div class="controls">
            <button onclick="executeQuery()">Execute</button>
            <button onclick="clearQuery()">Clear</button>
        </div>

        <div class="sample-queries">
            <button class="sample-btn" onclick="insertSampleQuery('TABLES')">Show Tables</button>
            <button class="sample-btn" onclick="insertSampleQuery('CREATE')">Create Table</button>
            <button class="sample-btn" onclick="insertSampleQuery('INSERT')">Insert Data</button>
            <button class="sample-btn" onclick="insertSampleQuery('SELECT')">Select All</button>
            <button class="sample-btn" onclick="insertSampleQuery('UPDATE')">Update</button>
            <button class="sample-btn" onclick="insertSampleQuery('DELETE')">Delete</button>
        </div>
    </div>

    <!-- Results -->
    <div id="results" class="section" style="display: none;">
        <h2>Results</h2>
        <div id="resultsContent"></div>
    </div>

    <script>
        let db = null;
        let SQL = null;

        const sampleQueries = {
            'TABLES': "SELECT name FROM sqlite_master WHERE type='table';",
            'CREATE': `CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE
);`,
            'INSERT': "INSERT INTO users (name, email) VALUES ('aaa', '[email protected]');",
            'SELECT': "SELECT * FROM users;",
            'UPDATE': "UPDATE users SET name = 'aa1' WHERE id = 1;",
            'DELETE': "DELETE FROM users WHERE id = 1;"
        };

        // Initialize SQL.js
        async function initSQLJS() {
            try {
                const sqlPromise = initSqlJs({
                    locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`
                });
                SQL = await sqlPromise;
                db = new SQL.Database();
                showStatus('SQL.js loaded successfully!', 'success');
                document.getElementById('fileInput').addEventListener('change', handleFileSelect);
            } catch (error) {
                showStatus('Error loading SQL.js: ' + error.message, 'error');
            }
        }

        function handleFileSelect(event) {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(e) {
                try {
                    const uint8Array = new Uint8Array(e.target.result);
                    db = new SQL.Database(uint8Array);
                    showStatus('Database loaded: ' + file.name, 'success');
                } catch (error) {
                    showStatus('Error reading file: ' + error.message, 'error');
                }
            };
            reader.readAsArrayBuffer(file);
        }

        function executeQuery() {
            const queryText = document.getElementById('sqlQuery').value.trim();
            if (!queryText) {
                showStatus('Please enter a query!', 'error');
                return;
            }

            if (!db) {
                showStatus('Database not ready!', 'error');
                return;
            }

            try {
                const results = db.exec(queryText);
                displayResults(results);
                showStatus('Query executed successfully!', 'success');
            } catch (error) {
                showStatus('Query error: ' + error.message, 'error');
                document.getElementById('results').style.display = 'none';
            }
        }

        function displayResults(results) {
            const resultsDiv = document.getElementById('results');
            const resultsContent = document.getElementById('resultsContent');
            
            resultsDiv.style.display = 'block';
            
            if (!results || results.length === 0) {
                resultsContent.innerHTML = '<p>Query executed, no data returned.</p>';
                return;
            }

            let html = '';
            results.forEach(result => {
                if (result.values && result.values.length > 0) {
                    html += '<table>';
                    html += '<tr>';
                    result.columns.forEach(col => {
                        html += `<th>${col}</th>`;
                    });
                    html += '</tr>';
                    
                    result.values.forEach(row => {
                        html += '<tr>';
                        row.forEach(cell => {
                            html += `<td>${cell !== null ? cell : 'NULL'}</td>`;
                        });
                        html += '</tr>';
                    });
                    html += '</table>';
                    html += `<p>${result.values.length} rows</p>`;
                }
            });
            
            resultsContent.innerHTML = html || '<p>No data found.</p>';
        }

        function showStatus(message, type = 'info') {
            const statusDiv = document.getElementById('status');
            statusDiv.textContent = message;
            statusDiv.className = `status ${type}`;
        }

        function downloadDB() {
            if (!db) {
                showStatus('No database to download!', 'error');
                return;
            }

            try {
                const data = db.export();
                const blob = new Blob([data]);
                const url = URL.createObjectURL(blob);
                
                const a = document.createElement('a');
                a.href = url;
                a.download = 'database.db';
                a.click();
                URL.revokeObjectURL(url);
                
                showStatus('Database downloaded!', 'success');
            } catch (error) {
                showStatus('Download error: ' + error.message, 'error');
            }
        }

        function clearDB() {
            if (confirm('Clear all database data?')) {
                db = new SQL.Database();
                document.getElementById('results').style.display = 'none';
                showStatus('Database cleared!', 'success');
            }
        }

        function clearQuery() {
            document.getElementById('sqlQuery').value = '';
        }

        function insertSampleQuery(type) {
            document.getElementById('sqlQuery').value = sampleQueries[type];
        }

        function createSampleData() {
            if (!db) {
                showStatus('Database not ready!', 'error');
                return;
            }

            try {
                db.run(`CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY,
                    name TEXT NOT NULL,
                    email TEXT UNIQUE
                )`);

                db.run(`INSERT OR REPLACE INTO users (name, email) VALUES 
                    ('aaa', '[email protected]'),
                    ('bbb', '[email protected]'),
                    ('ccc', '[email protected]')`);

                showStatus('Sample data created!', 'success');
            } catch (error) {
                showStatus('Error creating sample data: ' + error.message, 'error');
            }
        }

        window.addEventListener('load', initSQLJS);
    </script>
</body>
</html>