|
3 | 3 | import { Spinner, Alert } from "flowbite-svelte"; |
4 | 4 | import { Heading, P, A } from "flowbite-svelte"; |
5 | 5 | import { Select, Input, Label, Helper } from "flowbite-svelte"; |
| 6 | + import { onMount } from "svelte"; |
6 | 7 |
|
7 | 8 | import { createDbWorker } from "sql.js-httpvfs"; |
8 | 9 | import { PowerTable } from "@muonw/powertable"; |
|
30 | 31 | { value: "16384", name: "16384" }, |
31 | 32 | { value: "32768", name: "32768" }, |
32 | 33 | ]; |
33 | | - let sqlQuery = `SELECT * FROM tcga_table WHERE "TAR-UUID" = '0011a67b-1ba9-4a32-a6b8-7850759a38cf';`; |
| 34 | + let sqlQuery = `SELECT * FROM tcga_table LIMIT 20';`; |
34 | 35 | // let dbUrl = "https://nishad.github.io/sql.js-httpvfs-playground/db/imdb-titles-100000_1024_indexed.db"; |
35 | 36 | // let dbUrl = |
36 | 37 | // "https://cnag-biomedical-informatics.github.io/sql.js-httpvfs-playground/db/tcga.db"; |
37 | 38 | let dbUrl = |
38 | | - "https://raw.githubusercontent.com/CNAG-Biomedical-Informatics/cbi-datahub/refs/heads/main/sqlite/tcga.db" |
39 | | - |
| 39 | + "https://raw.githubusercontent.com/CNAG-Biomedical-Informatics/cbi-datahub/refs/heads/main/sqlite/tcga.db"; |
| 40 | +
|
40 | 41 | import Prism from "prismjs"; |
41 | 42 | import "prismjs/components/prism-sql"; |
42 | 43 |
|
|
49 | 50 | ); |
50 | 51 | const wasmUrl = new URL("sql.js-httpvfs/dist/sql-wasm.wasm", import.meta.url); |
51 | 52 |
|
| 53 | + let sqliteFiles = []; |
| 54 | + let activeFile = null; |
| 55 | +
|
52 | 56 | let ptOptions = { |
53 | 57 | footerText: false, |
54 | 58 | footerFilters: false, |
|
57 | 61 | parseAs: "unsafe-html", |
58 | 62 | }; |
59 | 63 |
|
60 | | - let ptInstructs = [ |
61 | | - { key: "RANK", title: "RANK" }, |
62 | | - { key: "REFERENCE(ID)", title: "REFERENCE(ID)" }, |
63 | | - { key: "TARGET(ID)", title: "TARGET(ID)" }, |
64 | | - // { key: "FORMAT", title: "FORMAT" }, |
65 | | - { key: "LENGTH", title: "LENGTH" }, |
66 | | - // { key: "WEIGHTED", title: "WEIGHTED" }, |
67 | | - { key: "HAMMING-DISTANCE", title: "HAMMING-DISTANCE" }, |
68 | | - { key: "DISTANCE-Z-SCORE", title: "DISTANCE-Z-SCORE" }, |
69 | | - { key: "DISTANCE-P-VALUE", title: "DISTANCE-P-VALUE" }, |
70 | | - { key: "DISTANCE-Z-SCORE(RAND)", title: "DISTANCE-Z-SCORE(RAND)" }, |
71 | | - { key: "JACCARD-INDEX", title: "JACCARD-INDEX" }, |
72 | | - { key: "JACCARD-Z-SCORE", title: "JACCARD-Z-SCORE" }, |
73 | | - { key: "JACCARD-P-VALUE", title: "JACCARD-P-VALUE" }, |
74 | | - { key: "REFERENCE-VARS", title: "REFERENCE-VARS" }, |
75 | | - { key: "TARGET-VARS", title: "TARGET-VARS" }, |
76 | | - { key: "INTERSECT", title: "INTERSECT" }, |
77 | | - { key: "INTERSECT-RATE(%)", title: "INTERSECT-RATE(%)" }, |
78 | | - { key: "COMPLETENESS(%)", title: "COMPLETENESS(%)" }, |
79 | | - { key: "REF-UUID", title: "REF-UUID" }, |
80 | | - { key: "TAR-UUID", title: "TAR-UUID" }, |
81 | | - { key: "REF-UUID-URL", title: "REF-UUID-URL", parseAs: "unsafe-html" }, |
82 | | - { key: "TAR-UUID-URL", title: "TAR-UUID-URL", parseAs: "unsafe-html" }, |
83 | | - ]; |
| 64 | + let ptInstructs = []; |
| 65 | +
|
| 66 | + function updateInstructs(data) { |
| 67 | + if (Array.isArray(data) && data.length > 0) { |
| 68 | + ptInstructs = Object.keys(data[0]).map((key) => ({ |
| 69 | + key, |
| 70 | + title: key, |
| 71 | + ...(key.includes("URL") ? { parseAs: "unsafe-html" } : {}), |
| 72 | + })); |
| 73 | + } |
| 74 | + return ptInstructs; |
| 75 | + } |
| 76 | +
|
| 77 | + async function getSqliteFiles() { |
| 78 | + const apiUrl = |
| 79 | + "https://api.github.com/repos/CNAG-Biomedical-Informatics/cbi-datahub/contents/sqlite"; |
| 80 | + try { |
| 81 | + const res = await fetch(apiUrl); |
| 82 | + if (!res.ok) return []; |
| 83 | + const data = await res.json(); |
| 84 | + return data |
| 85 | + .filter( |
| 86 | + (item) => |
| 87 | + item.type === "file" && |
| 88 | + (item.name.endsWith(".db") || item.name.endsWith(".sqlite")) |
| 89 | + ) |
| 90 | + .map((item) => ({ |
| 91 | + name: item.name, |
| 92 | + url: |
| 93 | + item.download_url || |
| 94 | + `https://raw.githubusercontent.com/CNAG-Biomedical-Informatics/cbi-datahub/main/sqlite/${item.name}`, |
| 95 | + })); |
| 96 | + } catch (e) { |
| 97 | + console.error(e); |
| 98 | + return []; |
| 99 | + } |
| 100 | + } |
| 101 | +
|
| 102 | + async function loadDb(file) { |
| 103 | + const dbUrl = file.url; |
| 104 | +
|
| 105 | + // Map of table ➔ custom WHERE clauses |
| 106 | + const exampleQueries = { |
| 107 | + tcga_table: `"TAR-UUID" = '0011a67b-1ba9-4a32-a6b8-7850759a38cf'`, |
| 108 | + omim_table: `"TAR-UUID" = '100100'`, |
| 109 | + }; |
| 110 | +
|
| 111 | + const tablesData = await queryDb( |
| 112 | + dbUrl, |
| 113 | + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" |
| 114 | + ); |
| 115 | + if (tablesData.result.length) { |
| 116 | + const table = tablesData.result[0].name; |
| 117 | + console.log("Selected table: ", table); |
| 118 | +
|
| 119 | + // Override if this table has a special filter |
| 120 | + if (Object.keys(exampleQueries).includes(table)) { |
| 121 | + console.log("Using example query for table:", table); |
| 122 | + sqlQuery = `SELECT * FROM "${table}" WHERE ${exampleQueries[table]};`; |
| 123 | + } |
84 | 124 |
|
85 | | - async function queryDb() { |
| 125 | + await runQuery(dbUrl, sqlQuery); |
| 126 | + } else { |
| 127 | + sqlQuery = ""; |
| 128 | + result = []; |
| 129 | + } |
| 130 | + } |
| 131 | +
|
| 132 | + onMount(async () => { |
| 133 | + sqliteFiles = await getSqliteFiles(); |
| 134 | + if (sqliteFiles.length) { |
| 135 | + await loadDb(sqliteFiles[0]); |
| 136 | + } |
| 137 | + }); |
| 138 | +
|
| 139 | + async function queryDb(url = dbUrl, query = sqlQuery) { |
86 | 140 | const worker = await createDbWorker( |
87 | 141 | [ |
88 | 142 | { |
89 | 143 | from: "inline", |
90 | 144 | config: { |
91 | 145 | serverMode: "full", |
92 | | - url: dbUrl, |
| 146 | + url, |
93 | 147 | requestChunkSize: Number(pageSize), |
94 | 148 | }, |
95 | 149 | }, |
96 | 150 | ], |
97 | 151 | workerUrl.toString(), |
98 | 152 | wasmUrl.toString() |
99 | 153 | ); |
100 | | - const result = await worker.db.query(sqlQuery); |
| 154 | + const result = await worker.db.query(query); |
101 | 155 | const bytesRead = await worker.worker.bytesRead; |
102 | 156 | const stats = await worker.worker.getStats(); |
103 | 157 | return { result, bytesRead, stats }; |
|
113 | 167 | let totalBytes; |
114 | 168 | let totalRequests; |
115 | 169 |
|
116 | | - async function runQuery() { |
| 170 | + async function runQuery(url = dbUrl, query = sqlQuery) { |
117 | 171 | result = null; |
118 | 172 | querying = true; |
119 | 173 | error = false; |
120 | | - let queryData = pTime(queryDb)(); |
| 174 | + let queryData = pTime(() => queryDb(url, query))(); |
121 | 175 | await queryData |
122 | 176 | .then((data) => { |
123 | 177 | result = data.result; |
| 178 | + updateInstructs(result); |
124 | 179 | timeTaken = queryData.time; |
125 | 180 | bytesRead = data.bytesRead; |
126 | 181 | totalRequests = data.stats.totalRequests; |
|
137 | 192 | console.log("Query Error: ", queryError.message); |
138 | 193 | console.log(queryError); |
139 | 194 | querying = false; |
| 195 | + updateInstructs([]); |
140 | 196 | jsonFile = null; |
141 | 197 | }); |
142 | 198 | } |
|
149 | 205 | >sql.js-httpvfs Playground</Heading |
150 | 206 | > |
151 | 207 | <P class="my-4 text-gray-500"> |
152 | | - <code>sql.js-httpvfs</code> is a fork of and wrapper around sql.js to provide |
153 | | - a read-only HTTP-Range-request based virtual file system for SQLite. It allows |
154 | | - hosting an SQLite database on a static file hoster and querying that database |
155 | | - from the browser without fully downloading it.</P |
156 | | - > |
157 | | - <P class="mb-4" |
158 | | - >Provide the URL of any SQLite database file and edit the default SQL |
159 | | - Query.</P |
160 | | - > |
161 | | - <A href="https://github.com/phiresky/sql.js-httpvfs" |
162 | | - >Read more |
| 208 | + This is a fork of nishad's sql.js-httpvfs playground |
| 209 | + </P> |
| 210 | + <A href="https://github.com/nishad/sql.js-httpvfs-playground" |
| 211 | + >Check it out here: |
163 | 212 | <svg |
164 | 213 | class="ml-1 w-6 h-6" |
165 | 214 | fill="currentColor" |
|
174 | 223 | </A> |
175 | 224 | </div> |
176 | 225 |
|
177 | | - <div class="p-6"> |
| 226 | + {#if sqliteFiles.length} |
| 227 | + <div class="border-b mb-4"> |
| 228 | + <nav class="flex space-x-4" aria-label="Tabs"> |
| 229 | + {#each sqliteFiles as f} |
| 230 | + <button |
| 231 | + class={`py-2 px-4 text-sm font-medium border-b-2 ${ |
| 232 | + activeFile === f.name |
| 233 | + ? "border-blue-600 text-blue-600" |
| 234 | + : "border-transparent text-gray-500" |
| 235 | + }`} |
| 236 | + on:click={() => loadDb(f)} |
| 237 | + > |
| 238 | + {f.name} |
| 239 | + </button> |
| 240 | + {/each} |
| 241 | + </nav> |
| 242 | + </div> |
| 243 | + {/if} |
| 244 | +
|
| 245 | + <!-- <div class="p-6"> |
178 | 246 | <Label class="space-y-2"> |
179 | 247 | <span>SQLite DB file URL</span> |
180 | 248 | <Input type="url" placeholder="" size="md" bind:value={dbUrl} /> |
|
183 | 251 | >Select page size |
184 | 252 | <Select class="mt-2" items={pageSizes} bind:value={pageSize} /> |
185 | 253 | </Label> |
186 | | - </div> |
| 254 | + </div> --> |
187 | 255 | <div class="p-6"> |
188 | 256 | <Label class="space-y-2"> |
189 | 257 | <span>Edit SQL Query</span> |
|
0 commit comments