├── CNAME ├── LICENSE ├── README.md ├── about.html ├── css ├── article.css ├── awsm.css ├── buttons.css ├── commandbar.css ├── core.css ├── editor.css ├── example.css ├── header.css ├── layout.css ├── result.css ├── status.css └── toolbar.css ├── demo.db ├── employees.db ├── functions.html ├── img ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── cover-examples.png ├── cover.png ├── favicon-64x64.png ├── favicon.ico ├── favicon.png ├── favicon.svg ├── mobile.jpg ├── sqlime-ai.jpg ├── sqlime-examples.png └── sqlime.jpg ├── index.html ├── js ├── cloud.js ├── cloud │ ├── github.js │ ├── http.js │ └── openai.js ├── components │ ├── action-button.js │ ├── command-bar.js │ ├── copy-link.js │ ├── db-name.js │ ├── sqlime-db.js │ ├── sqlime-editor.js │ ├── sqlime-result.js │ ├── sqlime-status.js │ └── toolbar.js ├── controllers │ ├── actions.js │ └── shortcuts.js ├── db-path.js ├── index.js ├── locator.js ├── printer.js ├── settings.js ├── sqlite │ ├── db.js │ ├── dumper.js │ ├── hasher.js │ ├── manager.js │ ├── sqlean.js │ └── sqlean.wasm ├── storage.js └── timeit.js ├── sales.db ├── settings.html ├── site.webmanifest └── test ├── suite.html ├── suite.js └── tester.js /CNAME: -------------------------------------------------------------------------------- 1 | sqlime.org -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Anton Zhiyanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sqlime 2 | 3 | **[Sqlime](http://sqlime.org/)** is an online SQLite playground for debugging and sharing SQL snippets. Kinda like JSFiddle, but for SQL instead of JavaScript. 4 | 5 | 6 | Sqlime 7 | 8 | 9 | Here are some notable features: 10 | 11 | ### 🔋 Full-blown database in the browser 12 | 13 | Sqlime is backed by the latest version of SQLite via the [sqlean.js](https://github.com/nalgeon/sqlean.js) project. It provides a full-featured SQL implementation, including indexes, triggers, views, transactions, CTEs, window functions and execution plans. 14 | 15 | It also includes essential SQLite extensions, from math statistics and regular expressions to hash functions and dynamic SQL. 16 | 17 | ### 🔌 Connect any data source 18 | 19 | Connect any local or remote SQLite database. Both files and URLs are supported. For example, try loading the [Employees database](http://sqlime.org/#https://raw.githubusercontent.com/nalgeon/sqliter/main/employees.en.db) from the GitHub repo. 20 | 21 | ### 🔗 Save and share with others 22 | 23 | Sqlime saves both the database and the queries to GitHub so that you can revisit them later or share them with a colleague. The database is stored as a plain text SQL dump, so it's easy to read the code or import data into PostgreSQL, MySQL, or other databases. 24 | 25 | For example, here is the [gist](https://gist.github.com/nalgeon/e012594111ce51f91590c4737e41a046) for the Employees database, and here is the [sharing link](https://sqlime.org/#gist:e012594111ce51f91590c4737e41a046) for it. 26 | 27 | ### 🤖 Ask AI 28 | 29 | Connect an OpenAI account to get help with your queries from the state-of-the-art ChatGPT assistant. AI can explain, teach, and troubleshoot your SQL. 30 | 31 | Ask AI 32 | 33 | ### 📱 Mobile friendly 34 | 35 | Most playgrounds are not meant for small screens. Sqlime was specifically designed and tested on mobile devices. No need to zoom or aim at tiny buttons — everything looks and works just fine. 36 | 37 | ### 🔒 Secure and private 38 | 39 | There is no server. Sqlime works completely in the browser. GitHub and OpenAI credentials are also stored locally. Queries are saved as private GitHub gists within your account. Your data is yours only. 40 | 41 | ### ⌨️ Dead simple 42 | 43 | Sqlime has zero third-party dependencies other than SQLite. Good old HTML, CSS, and vanilla JS — that's all. No frameworks, no heavy editors, no obsolete and vulnerable libraries. Just some modular open-source code, which is easy to grasp and extend. 44 | 45 | ## Last but not least 46 | 47 | **⭐️ Star the project** if you like it 48 | 49 | 🚀 [**Subscribe**](https://antonz.org/subscribe/) to stay on top of new features 50 | 51 | 🍋 [**Use Sqlime**](https://sqlime.org/) to debug and share SQL snippets 52 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sqlime / About 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

31 | 35 |

36 |

an online SQL playground

37 |
38 |
39 |
40 |

About

41 |

42 | Sqlime is an online SQLite playground 43 | for debugging and sharing SQL snippets. 44 |

45 |
46 | 47 |
48 |

Sqlime is like JSFiddle, but for SQL instead of JavaScript

49 |
50 |
51 | 52 |

Here are some notable features:

53 | 54 |
🔋 Full-blown database in the browser
55 |

56 | Sqlime is backed by the latest version of SQLite via the 57 | sqlean.js project. 58 | It provides a full-featured SQL implementation, including indexes, triggers, 59 | views, transactions, CTEs, window functions and execution plans. 60 |

61 |

62 | It also includes essential SQLite extensions, from math statistics and 63 | regular expressions to hash functions and dynamic SQL. 64 |

65 | 66 |
🔌 Connect any data source
67 |

68 | Connect any local or remote SQLite database. Both files and URLs 69 | are supported. For example, try loading the 70 | Employees database 71 | from the GitHub repo. 72 |

73 | 74 |
🔗 Save and share with others
75 |

76 | Sqlime saves both the database and the queries to GitHub 77 | so that you can revisit them later or share them with a colleague. 78 | The database is stored as a plain text SQL dump, so it's easy to 79 | read the code or import data into PostgreSQL, MySQL, 80 | or other databases. 81 |

82 |

83 | For example, here is the 84 | gist 85 | for the Employees database, and here is the 86 | sharing link for it. 87 |

88 | 89 |
🤖 Ask AI
90 |

91 | Connect an OpenAI account to get help with your queries 92 | from the state-of-the-art ChatGPT assistant. 93 |

94 |
95 | 96 |
97 |

AI can explain, teach, and troubleshoot your SQL

98 |
99 |
100 | 101 |
✨ Interactive examples
102 |

103 | With Codapi, 104 | you can turn static SQL code in your articles or blog posts 105 | into interactive examples. 106 |

107 |
108 | 109 |
110 | 111 |
📱 Mobile friendly
112 |

113 | Most playgrounds are not meant for small screens. Sqlime 114 | was specifically designed and tested on mobile devices. 115 |

116 |
117 | 118 |
119 |

120 | No need to zoom or aim at tiny buttons — everything 121 | looks and works just fine 122 |

123 |
124 |
125 | 126 |
🔒 Secure and private
127 |

128 | There is no server. Sqlime works completely in the browser. 129 | GitHub and OpenAI credentials are also stored locally. Queries are 130 | saved as private GitHub gists within your account. 131 | Your data is yours only. 132 |

133 | 134 |
⌨️ Dead simple
135 |

136 | Sqlime has zero third-party dependencies other than SQLite. 137 | Good old HTML, CSS, and vanilla JS — that's all. 138 | No frameworks, no heavy editors, no obsolete and vulnerable libraries. 139 | Just some modular 140 | open-source code, 141 | which is easy to grasp and extend. 142 |

143 | 144 |

Keyboard shortcuts

145 |

Here are some quick actions:

146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
winmacaction
⌃↵⌘↵run query
⌃O⌘Oopen file
⌃U⌘Uopen url
⌃S⌘Ssave and share
⌃/⌘/show tables
178 | 179 |

Last but not least

180 | 181 |

182 | 183 | ⭐️ Star the project 184 | on GitHub if you like it 185 |

186 |

187 | 🚀 Subscribe 188 | to stay on top of new features 189 |

190 |

191 | 🍋 Use Sqlime 192 | to debug and share SQL snippets 193 |

194 | 195 |

← back

196 |
197 |
198 | 201 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /css/article.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | h1 { 7 | margin-bottom: 1rem; 8 | } 9 | 10 | a { 11 | text-decoration: none; 12 | } 13 | 14 | pre { 15 | padding: 0.5rem 1rem; 16 | font-size: 1em; 17 | background: #f5f5f5; 18 | } 19 | 20 | pre > code { 21 | display: inline; 22 | border-left: none; 23 | border-right: none; 24 | } 25 | 26 | code { 27 | font-size: 0.85em; 28 | } 29 | 30 | figcaption { 31 | text-align: center; 32 | } 33 | 34 | table { 35 | background: none !important; 36 | border-top: 1px solid #f2f2f2; 37 | border-right: 1px solid #f2f2f2; 38 | border-left: 1px solid #f2f2f2; 39 | } 40 | table td, 41 | table th { 42 | background: none !important; 43 | } 44 | 45 | table td:first-child, 46 | table th:first-child { 47 | padding-left: 0.75em; 48 | } 49 | table td:last-child, 50 | table th:last-child { 51 | padding-right: 0.75em; 52 | } 53 | 54 | .logo { 55 | display: inline-block; 56 | } 57 | 58 | .logo img { 59 | display: inline-block; 60 | vertical-align: middle; 61 | width: 1.5rem; 62 | height: auto; 63 | } 64 | 65 | .bordered { 66 | border: 1px solid #aaa; 67 | padding: 1rem 2rem; 68 | } 69 | 70 | .columns-3 { 71 | columns: 2; 72 | } 73 | 74 | @media only screen and (min-width: 40rem) { 75 | .columns-3 { 76 | columns: 3; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /css/awsm.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /*! 3 | * awsm.css v3.0.7 (https://igoradamenko.github.io/awsm.css/) 4 | * Copyright 2015 Igor Adamenko (https://igoradamenko.com) 5 | * Licensed under MIT (https://github.com/igoradamenko/awsm.css/blob/master/LICENSE.md) 6 | */ 7 | html { 8 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", 9 | Roboto, Oxygen, Ubuntu, Cantarell, "PT Sans", "Open Sans", "Fira Sans", 10 | "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 11 | font-size: 100%; 12 | line-height: 1.4; 13 | background: white; 14 | color: black; 15 | -webkit-overflow-scrolling: touch; 16 | } 17 | 18 | body { 19 | margin: 1.2em; 20 | font-size: 1rem; 21 | } 22 | @media (min-width: 20rem) { 23 | body { 24 | font-size: calc(1rem + 0.00625 * (100vw - 20rem)); 25 | } 26 | } 27 | @media (min-width: 40rem) { 28 | body { 29 | font-size: 1.125rem; 30 | } 31 | } 32 | body header, 33 | body main, 34 | body footer, 35 | body article { 36 | position: relative; 37 | max-width: 40rem; 38 | margin: 0 auto; 39 | } 40 | body > header { 41 | margin-bottom: 3.5em; 42 | } 43 | body > header h1 { 44 | margin: 0; 45 | font-size: 1.5em; 46 | } 47 | body > header p { 48 | margin: 0; 49 | font-size: 0.85em; 50 | } 51 | body > footer { 52 | margin-top: 6em; 53 | padding-bottom: 1.5em; 54 | text-align: center; 55 | font-size: 0.8rem; 56 | color: #aaaaaa; 57 | } 58 | 59 | nav { 60 | margin: 1em 0; 61 | } 62 | nav ul { 63 | list-style: none; 64 | margin: 0; 65 | padding: 0; 66 | } 67 | nav li { 68 | display: inline-block; 69 | margin-right: 1em; 70 | margin-bottom: 0.25em; 71 | } 72 | nav li:last-child { 73 | margin-right: 0; 74 | } 75 | nav a:visited { 76 | color: #0064c1; 77 | } 78 | nav a:hover { 79 | color: #f00000; 80 | } 81 | 82 | ul, 83 | ol { 84 | margin-top: 0; 85 | padding-top: 0; 86 | padding-left: 2.5em; 87 | } 88 | ul li + li, 89 | ol li + li { 90 | margin-top: 0.25em; 91 | } 92 | ul li > details, 93 | ol li > details { 94 | margin: 0; 95 | } 96 | 97 | p { 98 | margin: 1em 0; 99 | } 100 | p:first-child { 101 | margin-top: 0; 102 | } 103 | p:last-child { 104 | margin-bottom: 0; 105 | } 106 | p + ul, 107 | p + ol { 108 | margin-top: -0.75em; 109 | } 110 | p img, 111 | p picture { 112 | float: right; 113 | margin-bottom: 0.5em; 114 | margin-left: 0.5em; 115 | } 116 | p picture img { 117 | float: none; 118 | margin: 0; 119 | } 120 | 121 | dd { 122 | margin-bottom: 1em; 123 | margin-left: 0; 124 | padding-left: 2.5em; 125 | } 126 | 127 | dt { 128 | font-weight: 700; 129 | } 130 | 131 | blockquote { 132 | margin: 0; 133 | padding-left: 2.5em; 134 | } 135 | 136 | aside { 137 | margin: 0.5em 0; 138 | font-style: italic; 139 | color: #aaaaaa; 140 | } 141 | @media (min-width: 65rem) { 142 | aside { 143 | position: absolute; 144 | right: -12.5rem; 145 | width: 9.375rem; 146 | max-width: 9.375rem; 147 | margin: 0; 148 | padding-left: 0.5em; 149 | font-size: 0.8em; 150 | border-left: 1px solid #f2f2f2; 151 | } 152 | } 153 | aside:first-child { 154 | margin-top: 0; 155 | } 156 | aside:last-child { 157 | margin-bottom: 0; 158 | } 159 | 160 | section + section { 161 | margin-top: 2em; 162 | } 163 | 164 | h1, 165 | h2, 166 | h3, 167 | h4, 168 | h5, 169 | h6 { 170 | margin: 1.25em 0 0; 171 | line-height: 1.2; 172 | } 173 | h1:hover > a[href^="#"][id]:empty, 174 | h1:focus > a[href^="#"][id]:empty, 175 | h2:hover > a[href^="#"][id]:empty, 176 | h2:focus > a[href^="#"][id]:empty, 177 | h3:hover > a[href^="#"][id]:empty, 178 | h3:focus > a[href^="#"][id]:empty, 179 | h4:hover > a[href^="#"][id]:empty, 180 | h4:focus > a[href^="#"][id]:empty, 181 | h5:hover > a[href^="#"][id]:empty, 182 | h5:focus > a[href^="#"][id]:empty, 183 | h6:hover > a[href^="#"][id]:empty, 184 | h6:focus > a[href^="#"][id]:empty { 185 | opacity: 1; 186 | } 187 | h1 + p, 188 | h1 + details, 189 | h2 + p, 190 | h2 + details, 191 | h3 + p, 192 | h3 + details, 193 | h4 + p, 194 | h4 + details, 195 | h5 + p, 196 | h5 + details, 197 | h6 + p, 198 | h6 + details { 199 | margin-top: 0.5em; 200 | } 201 | h1 > a[href^="#"][id]:empty, 202 | h2 > a[href^="#"][id]:empty, 203 | h3 > a[href^="#"][id]:empty, 204 | h4 > a[href^="#"][id]:empty, 205 | h5 > a[href^="#"][id]:empty, 206 | h6 > a[href^="#"][id]:empty { 207 | position: absolute; 208 | left: -0.65em; 209 | opacity: 0; 210 | text-decoration: none; 211 | font-weight: 400; 212 | line-height: 1; 213 | color: #aaaaaa; 214 | } 215 | @media (min-width: 40rem) { 216 | h1 > a[href^="#"][id]:empty, 217 | h2 > a[href^="#"][id]:empty, 218 | h3 > a[href^="#"][id]:empty, 219 | h4 > a[href^="#"][id]:empty, 220 | h5 > a[href^="#"][id]:empty, 221 | h6 > a[href^="#"][id]:empty { 222 | left: -0.8em; 223 | } 224 | } 225 | h1 > a[href^="#"][id]:empty:target, 226 | h1 > a[href^="#"][id]:empty:hover, 227 | h1 > a[href^="#"][id]:empty:focus, 228 | h2 > a[href^="#"][id]:empty:target, 229 | h2 > a[href^="#"][id]:empty:hover, 230 | h2 > a[href^="#"][id]:empty:focus, 231 | h3 > a[href^="#"][id]:empty:target, 232 | h3 > a[href^="#"][id]:empty:hover, 233 | h3 > a[href^="#"][id]:empty:focus, 234 | h4 > a[href^="#"][id]:empty:target, 235 | h4 > a[href^="#"][id]:empty:hover, 236 | h4 > a[href^="#"][id]:empty:focus, 237 | h5 > a[href^="#"][id]:empty:target, 238 | h5 > a[href^="#"][id]:empty:hover, 239 | h5 > a[href^="#"][id]:empty:focus, 240 | h6 > a[href^="#"][id]:empty:target, 241 | h6 > a[href^="#"][id]:empty:hover, 242 | h6 > a[href^="#"][id]:empty:focus { 243 | opacity: 1; 244 | box-shadow: none; 245 | color: black; 246 | } 247 | h1 > a[href^="#"][id]:empty:target:focus, 248 | h2 > a[href^="#"][id]:empty:target:focus, 249 | h3 > a[href^="#"][id]:empty:target:focus, 250 | h4 > a[href^="#"][id]:empty:target:focus, 251 | h5 > a[href^="#"][id]:empty:target:focus, 252 | h6 > a[href^="#"][id]:empty:target:focus { 253 | outline: none; 254 | } 255 | h1 > a[href^="#"][id]:empty::before, 256 | h2 > a[href^="#"][id]:empty::before, 257 | h3 > a[href^="#"][id]:empty::before, 258 | h4 > a[href^="#"][id]:empty::before, 259 | h5 > a[href^="#"][id]:empty::before, 260 | h6 > a[href^="#"][id]:empty::before { 261 | content: "§ "; 262 | } 263 | 264 | h1 { 265 | font-size: 2.5em; 266 | } 267 | 268 | h2 { 269 | font-size: 1.75em; 270 | } 271 | 272 | h3 { 273 | font-size: 1.25em; 274 | } 275 | 276 | h4 { 277 | font-size: 1.15em; 278 | } 279 | 280 | h5 { 281 | font-size: 1em; 282 | } 283 | 284 | h6 { 285 | margin-top: 1em; 286 | font-size: 1em; 287 | color: #aaaaaa; 288 | } 289 | 290 | article + article { 291 | margin-top: 4em; 292 | } 293 | article header p { 294 | font-size: 0.6em; 295 | color: #aaaaaa; 296 | } 297 | article header p + h1, 298 | article header p + h2 { 299 | margin-top: -0.25em; 300 | } 301 | article header h1 + p, 302 | article header h2 + p { 303 | margin-top: 0.25em; 304 | } 305 | article header h1 a, 306 | article header h2 a { 307 | color: black; 308 | } 309 | article header h1 a:visited, 310 | article header h2 a:visited { 311 | color: #aaaaaa; 312 | } 313 | article header h1 a:visited:hover, 314 | article header h2 a:visited:hover { 315 | color: #f00000; 316 | } 317 | article > footer { 318 | margin-top: 1.5em; 319 | font-size: 0.85em; 320 | } 321 | 322 | a { 323 | color: #0064c1; 324 | } 325 | a:visited { 326 | color: #8d39d0; 327 | } 328 | a:hover, 329 | a:active { 330 | outline-width: 0; 331 | } 332 | a:hover { 333 | color: #f00000; 334 | } 335 | a abbr { 336 | font-size: 1em; 337 | } 338 | 339 | abbr { 340 | margin-right: -0.075em; 341 | text-decoration: none; 342 | -webkit-hyphens: none; 343 | -ms-hyphens: none; 344 | hyphens: none; 345 | letter-spacing: 0.075em; 346 | font-size: 0.9em; 347 | } 348 | 349 | img, 350 | picture { 351 | display: block; 352 | max-width: 100%; 353 | margin: 0 auto; 354 | } 355 | 356 | audio, 357 | video { 358 | width: 100%; 359 | max-width: 100%; 360 | } 361 | 362 | figure { 363 | margin: 1em 0 0.5em; 364 | padding: 0; 365 | } 366 | figure + p { 367 | margin-top: 0.5em; 368 | } 369 | figure figcaption { 370 | opacity: 0.65; 371 | font-size: 0.85em; 372 | } 373 | 374 | table { 375 | display: inline-block; 376 | border-spacing: 0; 377 | border-collapse: collapse; 378 | overflow-x: auto; 379 | max-width: 100%; 380 | text-align: left; 381 | vertical-align: top; 382 | background: linear-gradient( 383 | rgba(0, 0, 0, 0.15) 0%, 384 | rgba(0, 0, 0, 0.15) 100% 385 | ) 386 | 0 0, 387 | linear-gradient(rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.15) 100%) 100% 0; 388 | background-attachment: scroll, scroll; 389 | background-size: 1px 100%, 1px 100%; 390 | background-repeat: no-repeat, no-repeat; 391 | } 392 | table caption { 393 | font-size: 0.9em; 394 | background: white; 395 | } 396 | table td, 397 | table th { 398 | padding: 0.35em 0.75em; 399 | vertical-align: top; 400 | font-size: 0.9em; 401 | border: 1px solid #f2f2f2; 402 | border-top: 0; 403 | border-left: 0; 404 | } 405 | table td:first-child, 406 | table th:first-child { 407 | padding-left: 0; 408 | background-image: linear-gradient( 409 | to right, 410 | white 50%, 411 | rgba(255, 255, 255, 0) 100% 412 | ); 413 | background-size: 2px 100%; 414 | background-repeat: no-repeat; 415 | } 416 | table td:last-child, 417 | table th:last-child { 418 | padding-right: 0; 419 | border-right: 0; 420 | background-image: linear-gradient( 421 | to left, 422 | white 50%, 423 | rgba(255, 255, 255, 0) 100% 424 | ); 425 | background-position: 100% 0; 426 | background-size: 2px 100%; 427 | background-repeat: no-repeat; 428 | } 429 | table td:only-child, 430 | table th:only-child { 431 | background-image: linear-gradient( 432 | to right, 433 | white 50%, 434 | rgba(255, 255, 255, 0) 100% 435 | ), 436 | linear-gradient(to left, white 50%, rgba(255, 255, 255, 0) 100%); 437 | background-position: 0 0, 100% 0; 438 | background-size: 2px 100%, 2px 100%; 439 | background-repeat: no-repeat, no-repeat; 440 | } 441 | table th { 442 | line-height: 1.2; 443 | } 444 | 445 | form { 446 | margin-right: auto; 447 | margin-left: auto; 448 | } 449 | form select, 450 | form label { 451 | display: block; 452 | } 453 | form label:not(:first-child) { 454 | margin-top: 1em; 455 | } 456 | form p label { 457 | display: inline; 458 | } 459 | form p label + label { 460 | margin-left: 1em; 461 | } 462 | form legend:first-child + label { 463 | margin-top: 0; 464 | } 465 | form select, 466 | form input[type], 467 | form textarea { 468 | margin-bottom: 1em; 469 | } 470 | form input[type="checkbox"], 471 | form input[type="radio"] { 472 | margin-bottom: 0; 473 | } 474 | 475 | fieldset { 476 | margin: 0; 477 | padding: 0.5em 1em; 478 | border: 1px solid #aaaaaa; 479 | } 480 | 481 | legend { 482 | color: #aaaaaa; 483 | } 484 | 485 | button { 486 | outline: none; 487 | box-sizing: border-box; 488 | height: 2em; 489 | margin: 0; 490 | padding: calc(0.25em - 1px) 0.5em; 491 | font-family: inherit; 492 | font-size: 1em; 493 | border: 1px solid #aaaaaa; 494 | border-radius: 2px; 495 | background: white; 496 | color: black; 497 | display: inline-block; 498 | width: auto; 499 | background: #f2f2f2; 500 | color: black; 501 | cursor: pointer; 502 | } 503 | button:focus { 504 | border: 1px solid black; 505 | } 506 | button:not([disabled]):hover { 507 | border: 1px solid black; 508 | } 509 | button:active { 510 | background-color: #aaaaaa; 511 | } 512 | button[disabled] { 513 | color: #aaaaaa; 514 | cursor: not-allowed; 515 | } 516 | 517 | select { 518 | outline: none; 519 | box-sizing: border-box; 520 | height: 2em; 521 | margin: 0; 522 | padding: calc(0.25em - 1px) 0.5em; 523 | font-family: inherit; 524 | font-size: 1em; 525 | border: 1px solid #aaaaaa; 526 | border-radius: 2px; 527 | background: white; 528 | color: black; 529 | display: inline-block; 530 | width: auto; 531 | background: #f2f2f2; 532 | color: black; 533 | cursor: pointer; 534 | padding-right: 1.2em; 535 | background-position: top 55% right 0.35em; 536 | background-size: 0.5em; 537 | background-repeat: no-repeat; 538 | -webkit-appearance: none; 539 | -moz-appearance: none; 540 | appearance: none; 541 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3Cpath fill='rgb(170, 170, 170)' fill-rule='nonzero' d='M1.5 2L3 0H0z'/%3E%3C/svg%3E"); 542 | } 543 | select:focus { 544 | border: 1px solid black; 545 | } 546 | select:not([disabled]):hover { 547 | border: 1px solid black; 548 | } 549 | select:active { 550 | background-color: #aaaaaa; 551 | } 552 | select[disabled] { 553 | color: #aaaaaa; 554 | cursor: not-allowed; 555 | } 556 | select:not([disabled]):focus, 557 | select:not([disabled]):hover { 558 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3Cpath fill='rgb(0, 0, 0)' fill-rule='nonzero' d='M1.5 2L3 0H0z'/%3E%3C/svg%3E"); 559 | } 560 | 561 | input[type="text"], 562 | input[type="password"], 563 | input[type^="date"], 564 | input[type="email"], 565 | input[type="number"], 566 | input[type="search"], 567 | input[type="tel"], 568 | input[type="time"], 569 | input[type="month"], 570 | input[type="week"], 571 | input[type="url"] { 572 | outline: none; 573 | box-sizing: border-box; 574 | height: 2em; 575 | margin: 0; 576 | padding: calc(0.25em - 1px) 0.5em; 577 | font-family: inherit; 578 | font-size: 1em; 579 | border: 1px solid #aaaaaa; 580 | border-radius: 2px; 581 | background: white; 582 | color: black; 583 | display: block; 584 | width: 100%; 585 | line-height: calc(2em - 1px * 2 - (0.25em - 1px) * 2); 586 | -webkit-appearance: none; 587 | -moz-appearance: none; 588 | appearance: none; 589 | } 590 | input[type="text"]:focus, 591 | input[type="password"]:focus, 592 | input[type^="date"]:focus, 593 | input[type="email"]:focus, 594 | input[type="number"]:focus, 595 | input[type="search"]:focus, 596 | input[type="tel"]:focus, 597 | input[type="time"]:focus, 598 | input[type="month"]:focus, 599 | input[type="week"]:focus, 600 | input[type="url"]:focus { 601 | border: 1px solid black; 602 | } 603 | input[type="text"]::-moz-placeholder, 604 | input[type="password"]::-moz-placeholder, 605 | input[type^="date"]::-moz-placeholder, 606 | input[type="email"]::-moz-placeholder, 607 | input[type="number"]::-moz-placeholder, 608 | input[type="search"]::-moz-placeholder, 609 | input[type="tel"]::-moz-placeholder, 610 | input[type="time"]::-moz-placeholder, 611 | input[type="month"]::-moz-placeholder, 612 | input[type="week"]::-moz-placeholder, 613 | input[type="url"]::-moz-placeholder { 614 | color: #aaaaaa; 615 | } 616 | input[type="text"]::-webkit-input-placeholder, 617 | input[type="password"]::-webkit-input-placeholder, 618 | input[type^="date"]::-webkit-input-placeholder, 619 | input[type="email"]::-webkit-input-placeholder, 620 | input[type="number"]::-webkit-input-placeholder, 621 | input[type="search"]::-webkit-input-placeholder, 622 | input[type="tel"]::-webkit-input-placeholder, 623 | input[type="time"]::-webkit-input-placeholder, 624 | input[type="month"]::-webkit-input-placeholder, 625 | input[type="week"]::-webkit-input-placeholder, 626 | input[type="url"]::-webkit-input-placeholder { 627 | color: #aaaaaa; 628 | } 629 | input[type="text"]:-ms-input-placeholder, 630 | input[type="password"]:-ms-input-placeholder, 631 | input[type^="date"]:-ms-input-placeholder, 632 | input[type="email"]:-ms-input-placeholder, 633 | input[type="number"]:-ms-input-placeholder, 634 | input[type="search"]:-ms-input-placeholder, 635 | input[type="tel"]:-ms-input-placeholder, 636 | input[type="time"]:-ms-input-placeholder, 637 | input[type="month"]:-ms-input-placeholder, 638 | input[type="week"]:-ms-input-placeholder, 639 | input[type="url"]:-ms-input-placeholder { 640 | color: #aaaaaa; 641 | } 642 | input[type="submit"], 643 | input[type="button"], 644 | input[type="reset"] { 645 | outline: none; 646 | box-sizing: border-box; 647 | height: 2em; 648 | margin: 0; 649 | padding: calc(0.25em - 1px) 0.5em; 650 | font-family: inherit; 651 | font-size: 1em; 652 | border: 1px solid #aaaaaa; 653 | border-radius: 2px; 654 | background: white; 655 | color: black; 656 | display: inline-block; 657 | width: auto; 658 | background: #f2f2f2; 659 | color: black; 660 | cursor: pointer; 661 | -webkit-appearance: none; 662 | -moz-appearance: none; 663 | appearance: none; 664 | } 665 | input[type="submit"]:focus, 666 | input[type="button"]:focus, 667 | input[type="reset"]:focus { 668 | border: 1px solid black; 669 | } 670 | input[type="submit"]:not([disabled]):hover, 671 | input[type="button"]:not([disabled]):hover, 672 | input[type="reset"]:not([disabled]):hover { 673 | border: 1px solid black; 674 | } 675 | input[type="submit"]:active, 676 | input[type="button"]:active, 677 | input[type="reset"]:active { 678 | background-color: #aaaaaa; 679 | } 680 | input[type="submit"][disabled], 681 | input[type="button"][disabled], 682 | input[type="reset"][disabled] { 683 | color: #aaaaaa; 684 | cursor: not-allowed; 685 | } 686 | input[type="color"] { 687 | outline: none; 688 | box-sizing: border-box; 689 | height: 2em; 690 | margin: 0; 691 | padding: calc(0.25em - 1px) 0.5em; 692 | font-family: inherit; 693 | font-size: 1em; 694 | border: 1px solid #aaaaaa; 695 | border-radius: 2px; 696 | background: white; 697 | color: black; 698 | display: block; 699 | width: 100%; 700 | line-height: calc(2em - 1px * 2 - (0.25em - 1px) * 2); 701 | -webkit-appearance: none; 702 | -moz-appearance: none; 703 | appearance: none; 704 | width: 6em; 705 | } 706 | input[type="color"]:focus { 707 | border: 1px solid black; 708 | } 709 | input[type="color"]::-moz-placeholder { 710 | color: #aaaaaa; 711 | } 712 | input[type="color"]::-webkit-input-placeholder { 713 | color: #aaaaaa; 714 | } 715 | input[type="color"]:-ms-input-placeholder { 716 | color: #aaaaaa; 717 | } 718 | input[type="color"]:hover { 719 | border: 1px solid black; 720 | } 721 | input[type="file"] { 722 | outline: none; 723 | box-sizing: border-box; 724 | height: 2em; 725 | margin: 0; 726 | padding: calc(0.25em - 1px) 0.5em; 727 | font-family: inherit; 728 | font-size: 1em; 729 | border: 1px solid #aaaaaa; 730 | border-radius: 2px; 731 | background: white; 732 | color: black; 733 | display: inline-block; 734 | width: auto; 735 | background: #f2f2f2; 736 | color: black; 737 | cursor: pointer; 738 | display: block; 739 | width: 100%; 740 | height: auto; 741 | padding: 0.75em 0.5em; 742 | font-size: 12px; 743 | line-height: 1; 744 | } 745 | input[type="file"]:focus { 746 | border: 1px solid black; 747 | } 748 | input[type="file"]:not([disabled]):hover { 749 | border: 1px solid black; 750 | } 751 | input[type="file"]:active { 752 | background-color: #aaaaaa; 753 | } 754 | input[type="file"][disabled] { 755 | color: #aaaaaa; 756 | cursor: not-allowed; 757 | } 758 | input[type="checkbox"], 759 | input[type="radio"] { 760 | margin: -0.2em 0.75em 0 0; 761 | vertical-align: middle; 762 | } 763 | 764 | textarea { 765 | outline: none; 766 | box-sizing: border-box; 767 | height: 2em; 768 | margin: 0; 769 | padding: calc(0.25em - 1px) 0.5em; 770 | font-family: inherit; 771 | font-size: 1em; 772 | border: 1px solid #aaaaaa; 773 | border-radius: 2px; 774 | background: white; 775 | color: black; 776 | display: block; 777 | width: 100%; 778 | line-height: calc(2em - 1px * 2 - (0.25em - 1px) * 2); 779 | -webkit-appearance: none; 780 | -moz-appearance: none; 781 | appearance: none; 782 | height: 4.5em; 783 | resize: vertical; 784 | padding-top: 0.5em; 785 | padding-bottom: 0.5em; 786 | } 787 | textarea:focus { 788 | border: 1px solid black; 789 | } 790 | textarea::-moz-placeholder { 791 | color: #aaaaaa; 792 | } 793 | textarea::-webkit-input-placeholder { 794 | color: #aaaaaa; 795 | } 796 | textarea:-ms-input-placeholder { 797 | color: #aaaaaa; 798 | } 799 | 800 | output { 801 | display: block; 802 | } 803 | 804 | code, 805 | kbd, 806 | var, 807 | samp { 808 | font-family: Consolas, "Lucida Console", Monaco, monospace; 809 | font-style: normal; 810 | } 811 | 812 | pre { 813 | overflow-x: auto; 814 | font-size: 0.8em; 815 | background: linear-gradient( 816 | rgba(0, 0, 0, 0.15) 0%, 817 | rgba(0, 0, 0, 0.15) 100% 818 | ) 819 | 0 0, 820 | linear-gradient(rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.15) 100%) 100% 0; 821 | background-attachment: scroll, scroll; 822 | background-size: 1px 100%, 1px 100%; 823 | background-repeat: no-repeat, no-repeat; 824 | } 825 | pre > code { 826 | display: inline-block; 827 | overflow-x: visible; 828 | box-sizing: border-box; 829 | min-width: 100%; 830 | border-right: 3px solid white; 831 | border-left: 1px solid white; 832 | } 833 | 834 | hr { 835 | height: 1px; 836 | margin: 2em 0; 837 | border: 0; 838 | background: #f2f2f2; 839 | } 840 | 841 | details { 842 | margin: 1em 0; 843 | } 844 | details[open] { 845 | padding-bottom: 0.5em; 846 | border-bottom: 1px solid #f2f2f2; 847 | } 848 | 849 | summary { 850 | display: inline-block; 851 | font-weight: 700; 852 | border-bottom: 1px dashed; 853 | cursor: pointer; 854 | } 855 | summary::-webkit-details-marker { 856 | display: none; 857 | } 858 | 859 | noscript { 860 | color: #d00000; 861 | } 862 | 863 | ::selection { 864 | background: rgba(0, 100, 193, 0.25); 865 | } 866 | -------------------------------------------------------------------------------- /css/buttons.css: -------------------------------------------------------------------------------- 1 | .button { 2 | border: 1px solid var(--sqlime-blue); 3 | border-radius: 0.25rem; 4 | background: transparent; 5 | padding: 0.5rem; 6 | color: var(--sqlime-blue); 7 | cursor: pointer; 8 | } 9 | .button:hover { 10 | background: var(--sqlime-blue); 11 | color: var(--sqlime-white); 12 | } 13 | .button:disabled { 14 | opacity: 0.5; 15 | cursor: default; 16 | } 17 | .button:disabled:hover { 18 | pointer-events: none; 19 | background: transparent; 20 | color: var(--sqlime-blue); 21 | } 22 | 23 | .button--small { 24 | border-radius: 0.125rem; 25 | padding: 0.125rem 0.5rem; 26 | font-size: 80%; 27 | } 28 | -------------------------------------------------------------------------------- /css/commandbar.css: -------------------------------------------------------------------------------- 1 | .sqlime-commandbar { 2 | display: flex; 3 | } 4 | 5 | .sqlime-commandbar > button { 6 | display: block; 7 | margin: 0 0.125rem; 8 | height: 2rem; 9 | padding: 0 0.5rem; 10 | border: 1px solid var(--sqlime-blue); 11 | border-radius: 0.125rem; 12 | background: transparent; 13 | color: var(--sqlime-blue); 14 | line-height: 1rem; 15 | font-size: 1rem; 16 | } 17 | 18 | .sqlime-commandbar > button:first-child { 19 | flex-grow: 4; 20 | background: var(--sqlime-blue); 21 | color: var(--sqlime-white); 22 | } 23 | 24 | .sqlime-commandbar > button:disabled { 25 | pointer-events: none; 26 | opacity: 0.5; 27 | cursor: default; 28 | } 29 | 30 | .sqlime-commandbar img, 31 | .sqlime-commandbar svg { 32 | display: inline-block; 33 | vertical-align: middle; 34 | height: 1rem; 35 | } 36 | 37 | @media only screen and (min-width: 40rem) { 38 | .sqlime-commandbar { 39 | position: absolute; 40 | bottom: 0.5rem; 41 | } 42 | 43 | .sqlime-commandbar > button { 44 | margin: 0; 45 | border: none; 46 | } 47 | .sqlime-commandbar > button:hover { 48 | background: var(--sqlime-blue) !important; 49 | color: var(--sqlime-white) !important; 50 | } 51 | 52 | .sqlime-commandbar > button:first-child { 53 | flex-grow: 0; 54 | background: transparent; 55 | color: var(--sqlime-blue); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /css/core.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --sqlime-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, 3 | Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 4 | "Segoe UI Symbol"; 5 | --sqlime-monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, 6 | Courier, monospace; 7 | --sqlime-blue: #008bf5; 8 | --sqlime-light-gray: #fafafa; 9 | --sqlime-dark-gray: #aaa; 10 | --sqlime-gray: #ccc; 11 | --sqlime-red: red; 12 | --sqlime-white: #fff; 13 | } 14 | *, 15 | *::before, 16 | *::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | .sqlime-disabled { 21 | pointer-events: none; 22 | filter: grayscale(); 23 | opacity: 0.625; 24 | } 25 | -------------------------------------------------------------------------------- /css/editor.css: -------------------------------------------------------------------------------- 1 | .sqlime-editor-section { 2 | position: relative; 3 | flex-grow: 1; 4 | padding: 0 0 0.5rem 0; 5 | } 6 | 7 | .sqlime-editor { 8 | position: relative; 9 | display: block; 10 | resize: vertical; 11 | width: 100%; 12 | min-height: 5rem; 13 | border-top: 1px solid var(--sqlime-gray); 14 | background: var(--sqlime-white); 15 | padding: 0.5rem 1rem; 16 | white-space: pre-wrap; 17 | font-family: var(--sqlime-monospace); 18 | } 19 | .sqlime-editor:focus { 20 | outline: none; 21 | } 22 | .sqlime-editor:empty::before { 23 | display: inline-block; 24 | content: "select * from ..."; 25 | color: var(--sqlime-dark-gray); 26 | } 27 | 28 | @media only screen and (min-width: 40rem) { 29 | .sqlime-editor { 30 | min-height: 8rem; 31 | border-left: 1px solid var(--sqlime-gray); 32 | border-right: 1px solid var(--sqlime-gray); 33 | border-bottom: 1px solid var(--sqlime-gray); 34 | padding: 0.5rem 0.5rem 2.125rem 0.5rem; 35 | } 36 | .sqlime-editor::after { 37 | position: absolute; 38 | right: 0; 39 | bottom: 0; 40 | display: inline-block; 41 | padding: 0.5rem; 42 | color: var(--sqlime-dark-gray); 43 | font-family: var(--sqlime-sans); 44 | content: "sqlite 3.45.0"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /css/example.css: -------------------------------------------------------------------------------- 1 | .sqlime-example { 2 | font-family: Consolas, "Lucida Console", Monaco, monospace; 3 | font-size: 0.85em; 4 | line-height: 1; 5 | } 6 | 7 | .sqlime-example div:nth-child(1) { 8 | margin: 1em 0; 9 | } 10 | 11 | .sqlime-example div:nth-child(2) { 12 | margin: 1em 0; 13 | padding: 1rem; 14 | background-color: #f5f5f5; 15 | } 16 | 17 | .sqlime-example table { 18 | border: 1px solid #000; 19 | line-height: 1; 20 | } 21 | 22 | .sqlime-example table th { 23 | vertical-align: top; 24 | padding: 0.5em; 25 | border-top: none; 26 | border-right: none; 27 | border-bottom: 1px solid #000; 28 | border-left: none; 29 | font-size: 1em; 30 | line-height: 1; 31 | } 32 | 33 | .sqlime-example table th:first-child { 34 | padding-left: 1em; 35 | } 36 | .sqlime-example table th:last-child { 37 | padding-right: 1em; 38 | } 39 | 40 | .sqlime-example table td { 41 | vertical-align: top; 42 | padding: 0.3em 0.5em; 43 | border: none; 44 | font-size: 1em; 45 | line-height: 1; 46 | } 47 | 48 | .sqlime-example table td:first-child { 49 | padding-left: 1em; 50 | } 51 | .sqlime-example table td:last-child { 52 | padding-right: 1em; 53 | } 54 | 55 | .sqlime-example table tr:first-child td { 56 | padding-top: 0.8em; 57 | } 58 | .sqlime-example table tr:last-child td { 59 | padding-bottom: 0.8em; 60 | } 61 | -------------------------------------------------------------------------------- /css/header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 1rem 1rem 0.75rem 1rem; 7 | } 8 | 9 | .header h1 { 10 | margin: 0; 11 | padding: 0; 12 | font-size: 1rem; 13 | } 14 | 15 | .header h1 > * { 16 | vertical-align: middle; 17 | } 18 | 19 | .header-logo { 20 | display: inline-block; 21 | width: 1.75rem; 22 | height: 1.75rem; 23 | padding: 0.125rem; 24 | text-decoration: none !important; 25 | } 26 | .header-logo img { 27 | height: auto; 28 | width: 1.5rem; 29 | } 30 | 31 | .db-name { 32 | display: inline-block; 33 | outline: none; 34 | border: 1px solid transparent; 35 | padding: 0.25rem; 36 | font-weight: bold; 37 | } 38 | .db-name.ready:hover { 39 | cursor: pointer; 40 | } 41 | .db-name.ready:focus { 42 | border-color: var(--sqlime-dark-gray); 43 | } 44 | .db-name.ready::after { 45 | content: " ✎"; 46 | } 47 | .db-name::after { 48 | content: "loading..."; 49 | } 50 | 51 | @media only screen and (min-width: 40rem) { 52 | .header { 53 | padding: 0 0 0.5rem 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /css/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background: var(--sqlime-light-gray); 5 | font-family: var(--sqlime-sans); 6 | } 7 | 8 | .grid { 9 | display: flex; 10 | flex-direction: column; 11 | max-width: 80rem; 12 | } 13 | 14 | .hidden { 15 | display: none !important; 16 | } 17 | 18 | @media only screen and (max-width: 40rem) { 19 | .hidden-mobile { 20 | display: none !important; 21 | } 22 | } 23 | 24 | @media only screen and (min-width: 40rem) { 25 | body { 26 | padding: 1rem 1.5rem; 27 | } 28 | .hidden-desktop { 29 | display: none !important; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /css/result.css: -------------------------------------------------------------------------------- 1 | .sqlime-result-section { 2 | flex-grow: 1; 3 | padding: 0 1rem 0.5rem 1rem; 4 | } 5 | 6 | .sqlime-result { 7 | display: block; 8 | overflow-x: auto; 9 | } 10 | 11 | .sqlime-result table { 12 | width: 100%; 13 | border: 1px solid var(--sqlime-gray); 14 | border-collapse: collapse; 15 | background: var(--sqlime-white); 16 | font-family: var(--sqlime-monospace); 17 | } 18 | 19 | .sqlime-result table th { 20 | border-bottom: 1px solid var(--sqlime-gray); 21 | padding: 0.25rem 0.5rem; 22 | text-align: left; 23 | } 24 | 25 | .sqlime-result table td { 26 | padding: 0.25rem 0.5rem; 27 | } 28 | 29 | .sqlime-result pre { 30 | font-family: var(--sqlime-monospace); 31 | white-space: pre-wrap; 32 | } 33 | 34 | .sqlime-link { 35 | display: inline; 36 | border: none; 37 | background: inherit; 38 | padding: 0; 39 | cursor: pointer; 40 | text-decoration: none; 41 | color: var(--sqlime-blue); 42 | font-size: 1em; 43 | } 44 | .sqlime-link:disabled { 45 | pointer-events: none; 46 | opacity: 0.5; 47 | cursor: default; 48 | } 49 | 50 | .sqlime-spinner { 51 | display: inline-block; 52 | width: 1rem; 53 | height: 1rem; 54 | vertical-align: middle; 55 | border: 3px solid rgb(0, 139, 245, 0.3); 56 | border-radius: 50%; 57 | border-top-color: var(--sqlime-blue); 58 | animation: spin 1s ease-in-out infinite; 59 | -webkit-animation: spin 1s ease-in-out infinite; 60 | } 61 | 62 | @keyframes spin { 63 | to { 64 | -webkit-transform: rotate(360deg); 65 | } 66 | } 67 | @-webkit-keyframes spin { 68 | to { 69 | -webkit-transform: rotate(360deg); 70 | } 71 | } 72 | 73 | @media only screen and (min-width: 40rem) { 74 | .sqlime-result-section { 75 | padding: 0 0 0.5rem 0; 76 | } 77 | 78 | .sqlime-result { 79 | overflow-x: visible; 80 | } 81 | 82 | .sqlime-result table { 83 | width: auto; 84 | } 85 | 86 | .sqlime-result pre { 87 | max-width: 50rem; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /css/status.css: -------------------------------------------------------------------------------- 1 | .sqlime-status { 2 | display: block; 3 | margin-bottom: 0.5rem; 4 | line-height: 1.3; 5 | } 6 | 7 | .sqlime-status--success::before { 8 | display: inline-block; 9 | width: 1rem; 10 | content: "✓"; 11 | } 12 | 13 | .sqlime-status--error::before { 14 | display: inline-block; 15 | width: 1rem; 16 | color: var(--sqlime-red); 17 | content: "✕"; 18 | } 19 | 20 | .sqlime-status p { 21 | margin-top: 0; 22 | margin-bottom: 0.8rem; 23 | } 24 | -------------------------------------------------------------------------------- /css/toolbar.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | margin-top: 0.5rem; 3 | } 4 | 5 | .toolbar > * { 6 | margin-right: 0.25rem; 7 | } 8 | .toolbar > *:last-child { 9 | margin-right: 0; 10 | } 11 | 12 | .toolbar .button--small { 13 | height: 1.5rem; 14 | line-height: 1.25rem; 15 | } 16 | 17 | .toolbar .button-help { 18 | font-weight: bold; 19 | } 20 | 21 | .toolbar svg { 22 | display: inline-block; 23 | vertical-align: middle; 24 | height: 1rem; 25 | } 26 | 27 | .toolbar a, 28 | .toolbar label { 29 | display: inline-block; 30 | height: 1.5rem; 31 | text-decoration: none; 32 | } 33 | 34 | .toolbar input[type="file"] { 35 | display: none; 36 | } 37 | 38 | @media only screen and (min-width: 40rem) { 39 | .toolbar { 40 | margin-top: 0; 41 | margin-left: 1rem; 42 | } 43 | 44 | .toolbar .button--small { 45 | border: none; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/demo.db -------------------------------------------------------------------------------- /employees.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/employees.db -------------------------------------------------------------------------------- /functions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sqlime / Functions 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

31 | 35 |

36 |

an online SQL playground

37 |
38 |
39 |
40 |

Available SQL Functions

41 |

42 | Sqlime offers both standard SQLite functions and many more from the 43 | Sqlean project: 44 |

45 |

46 | general-purpose
47 | date and time
48 | math
49 | aggregate
50 | window
51 | encode/decode
52 | dynamic SQL
53 | fuzzy matching
54 | IP addresses
55 | regular expressions
56 | statistics
57 | text
58 | UUIDs
59 |

60 | 61 |
General-purpose functions
62 |

63 | abs
64 | changes
65 | char
66 | coalesce
67 | format
68 | glob
69 | hex
70 | ifnull
71 | iif
72 | instr
73 | last_insert_rowid
74 | length
75 | like
76 | lower
77 | ltrim
78 | max
79 | min
80 | nullif
81 | quote
82 | random
83 | randomblob
84 | replace
85 | round
86 | rtrim
87 | sign
88 | soundex
89 | sqlite_version
90 | substring
91 | total_changes
92 | trim
93 | typeof
94 | unhex
95 | unicode
96 | upper
97 | zeroblob
98 |

99 | 100 |
Date and time functions
101 |

102 | date
103 | time
104 | datetime
105 | julianday
106 | unixepoch
107 | strftime
108 |

109 | 110 |
Math functions
111 |

112 | acos
113 | acosh
114 | asin
115 | asinh
116 | atan
117 | atan2
118 | atanh
119 | ceil
120 | cos
121 | cosh
122 | degrees
123 | exp
124 | floor
125 | ln
126 | log
127 | log10
128 | log2
129 | mod
130 | pi
131 | pow
132 | radians
133 | sin
134 | sinh
135 | sqrt
136 | tan
137 | tanh
138 | trunc
139 |

140 | 141 |
Aggregate functions
142 |

143 | avg
144 | count
145 | group_concat
146 | max
147 | min
148 | sum
149 | total
150 |

151 | 152 |
Window functions
153 |

154 | avg
155 | count
156 | cume_dist
157 | dense_rank
158 | first_value
159 | group_concat
160 | lag
161 | last_value
162 | lead
163 | max
164 | min
165 | nth_value
166 | ntile
167 | percent_rank
168 | rank
169 | row_number
170 | sum
171 | total
172 |

173 | 174 |
Encode/decode/digest functions
175 |

176 | decode
178 | encode
180 | crypto_blake3
181 | crypto_md5
182 | crypto_sha1
183 | crypto_sha256
184 | crypto_sha384
185 | crypto_sha512
186 |

187 | 188 |
Dynamic SQL
189 |

190 | define
191 | eval
193 | undefine 194 |

195 | 196 |
Fuzzy string matching
197 |

198 | fuzzy_caver
199 | fuzzy_damlev
200 | fuzzy_editdist
202 | fuzzy_hamming
203 | fuzzy_jarowin
204 | fuzzy_leven
205 | fuzzy_osadist
206 | fuzzy_phonetic
208 | fuzzy_rsoundex
210 | fuzzy_soundex
211 | fuzzy_translit
213 |

214 | 215 |
IP address manipulation
216 |

217 | ipcontains
218 | ipfamilyip
219 | iphost
220 | ipmasklen
221 | ipnetwork
222 |

223 | 224 |
Regular expressions
225 |

226 | regexp_capture
228 | regexp_like
229 | regexp_replace
231 | regexp_substr
232 | regexp
233 |

234 | 235 |
Mathematical Statistics
236 |

237 | stats_median
239 | stats_p25
241 | stats_p75
243 | stats_p90
245 | stats_p95
247 | stats_p99
249 | stats_perc
251 | stats_stddev_pop
253 | stats_stddev
255 | stats_var_pop
257 | stats_var
259 | stats_seq
260 |

261 | 262 |
Text functions
263 |

264 | text_bitsize
265 | text_concat
266 | text_contains
267 | text_count
268 | text_has_prefix
270 | text_has_suffix
272 | text_index
273 | text_join
274 | text_last_index
276 | text_left
277 | text_length
278 | text_like
279 | text_lower
280 | text_lpad
281 | text_ltrim
282 | text_repeat
283 | text_replace
284 | text_reverse
285 | text_right
286 | text_rpad
287 | text_rtrim
288 | text_size
289 | text_slice
290 | text_split
291 | text_substring
292 | text_title
293 | text_translate
294 | text_trim
295 | text_upper
296 |

297 | 298 |
Universally Unique IDentifiers
299 |

300 | uuid_blob
301 | uuid_str
302 | uuid4
303 |

304 | 305 |

← back

306 |
307 |
308 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /img/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/android-chrome-192x192.png -------------------------------------------------------------------------------- /img/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/android-chrome-512x512.png -------------------------------------------------------------------------------- /img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/apple-touch-icon.png -------------------------------------------------------------------------------- /img/cover-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/cover-examples.png -------------------------------------------------------------------------------- /img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/cover.png -------------------------------------------------------------------------------- /img/favicon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/favicon-64x64.png -------------------------------------------------------------------------------- /img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/favicon.ico -------------------------------------------------------------------------------- /img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/favicon.png -------------------------------------------------------------------------------- /img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | 16 | 17 | 20 | 23 | 24 | 27 | 30 | 33 | 36 | 39 | 40 | 43 | 46 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /img/mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/mobile.jpg -------------------------------------------------------------------------------- /img/sqlime-ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/sqlime-ai.jpg -------------------------------------------------------------------------------- /img/sqlime-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/sqlime-examples.png -------------------------------------------------------------------------------- /img/sqlime.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/img/sqlime.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sqlime - SQLite Playground 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 |
34 |

35 | 38 | SQLite Playground  // 39 | 40 |

41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /js/cloud.js: -------------------------------------------------------------------------------- 1 | // Cloud Storage API Facade. 2 | 3 | // Uses Github Gist API for users with credentials. 4 | 5 | import github from "./cloud/github.js"; 6 | 7 | const PROVIDERS = { 8 | [github.prefix]: github, 9 | }; 10 | 11 | // Cloud Storage API Facade. 12 | // Most methods delegate to the underlying provider, except for `get`. 13 | class Gister { 14 | constructor() { 15 | this._provider = null; 16 | } 17 | 18 | // name is the name of the provider. 19 | get name() { 20 | return this.provider.name; 21 | } 22 | 23 | // username is needed to decide whether 24 | // to call `create` or `update` on save. 25 | get username() { 26 | return this.provider.username; 27 | } 28 | 29 | // provider is the underlying cloud storage provider, 30 | // which does the actual work of saving and loading gists. 31 | get provider() { 32 | // lazy init the provider 33 | if (!this._provider) { 34 | this.reload(); 35 | } 36 | return this._provider; 37 | } 38 | 39 | // reload chooses GitHub as a provider. 40 | reload() { 41 | github.loadCredentials(); 42 | this._provider = github; 43 | } 44 | 45 | // loadCredentials loads credentials from the local storage. 46 | loadCredentials() { 47 | this.provider.loadCredentials(); 48 | } 49 | 50 | // hasCredentials returns `true` if the user has provided 51 | // API credentials, `false` otherwise. 52 | hasCredentials() { 53 | return this.provider.hasCredentials(); 54 | } 55 | 56 | // getUrl returns a gist url by its id. 57 | getUrl(id) { 58 | return this.provider.getUrl(id); 59 | } 60 | 61 | // get returns a gist by its id. 62 | // Uses the provider specified in the path value, 63 | // e.g. 'gist:12345'. 64 | get(pathValue) { 65 | const [prefix, id] = pathValue.split(":"); 66 | const provider = PROVIDERS[prefix]; 67 | return provider.get(id); 68 | } 69 | 70 | // create creates a new gist. 71 | create(name, schema, query) { 72 | return this.provider.create(name, schema, query); 73 | } 74 | 75 | // update updates an existing gist. 76 | update(id, name, schema, query) { 77 | return this.provider.update(id, name, schema, query); 78 | } 79 | } 80 | 81 | const gister = new Gister(); 82 | export default gister; 83 | -------------------------------------------------------------------------------- /js/cloud/github.js: -------------------------------------------------------------------------------- 1 | // Github Gist API client. 2 | 3 | import http from "./http.js"; 4 | 5 | const ID_PREFIX = "gist"; 6 | 7 | const HEADERS = { 8 | Accept: "application/json", 9 | "Content-Type": "application/json", 10 | }; 11 | 12 | class Github { 13 | constructor() { 14 | this.name = "GitHub"; 15 | this.prefix = ID_PREFIX; 16 | this.url = "https://api.github.com/gists"; 17 | this.headers = Object.assign({}, HEADERS); 18 | } 19 | 20 | // loadCredentials loads GitHub credentials 21 | // from the local storage. 22 | loadCredentials() { 23 | this.username = localStorage.getItem("github.username"); 24 | this.password = localStorage.getItem("github.token"); 25 | if (this.password) { 26 | this.headers.Authorization = `Token ${this.password}`; 27 | } 28 | } 29 | 30 | // hasCredentials returns `true` if the user has provided 31 | // GitHub username and password, `false` otherwise. 32 | hasCredentials() { 33 | return this.username && this.password; 34 | } 35 | 36 | // getUrl returns a gist url by its id. 37 | getUrl(id) { 38 | return `https://gist.github.com/${this.username}/${id}`; 39 | } 40 | 41 | // get returns a gist by its id. 42 | get(id) { 43 | const promise = fetch(`${this.url}/${id}`, { 44 | method: "get", 45 | headers: this.headers, 46 | }) 47 | .then((response) => http.toJson(response)) 48 | .then((response) => { 49 | if (!response.files || !("query.sql" in response.files)) { 50 | return null; 51 | } 52 | return buildGist(response); 53 | }); 54 | return promise; 55 | } 56 | 57 | // create creates a new gist. 58 | create(name, schema, query) { 59 | const data = buildData(name, schema, query); 60 | const promise = fetch(this.url, { 61 | method: "post", 62 | headers: this.headers, 63 | body: JSON.stringify(data), 64 | }) 65 | .then((response) => http.toJson(response)) 66 | .then((response) => buildGist(response)); 67 | return promise; 68 | } 69 | 70 | // update updates an existing gist. 71 | update(id, name, schema, query) { 72 | const data = buildData(name, schema, query); 73 | const promise = fetch(`${this.url}/${id}`, { 74 | method: "post", 75 | headers: this.headers, 76 | body: JSON.stringify(data), 77 | }) 78 | .then((response) => http.toJson(response)) 79 | .then((response) => buildGist(response)); 80 | return promise; 81 | } 82 | } 83 | 84 | // buildData creates an object for the GitHub request. 85 | function buildData(name, schema, query) { 86 | return { 87 | description: name, 88 | files: { 89 | "schema.sql": { 90 | content: schema || "--", 91 | }, 92 | "query.sql": { 93 | content: query || "--", 94 | }, 95 | }, 96 | }; 97 | } 98 | 99 | // buildGist creates a gist from the GitHub response. 100 | function buildGist(response) { 101 | const gist = { 102 | id: response.id, 103 | prefix: ID_PREFIX, 104 | name: response.description, 105 | owner: response.owner.login, 106 | schema: response.files["schema.sql"].content, 107 | query: response.files["query.sql"].content, 108 | }; 109 | if (gist.schema == "--") { 110 | gist.schema = ""; 111 | } 112 | if (gist.query == "--") { 113 | gist.query = ""; 114 | } 115 | return gist; 116 | } 117 | 118 | const github = new Github(); 119 | export default github; 120 | -------------------------------------------------------------------------------- /js/cloud/http.js: -------------------------------------------------------------------------------- 1 | // HTTP helper. 2 | 3 | // toJson converts the HTTP response to JSON, 4 | // throwing an error on anything different from 200 OK. 5 | function toJson(response) { 6 | if (!response.ok) { 7 | const msg = `got ${response.status} status code`; 8 | return Promise.reject(msg); 9 | } 10 | return response.json(); 11 | } 12 | 13 | const http = { toJson }; 14 | export default http; 15 | -------------------------------------------------------------------------------- /js/cloud/openai.js: -------------------------------------------------------------------------------- 1 | const URL = "https://api.openai.com/v1/chat/completions"; 2 | const MODEL = "gpt-4o-mini"; 3 | const PROMPT = "You are an SQLite AI assistant. Be brief and direct in your response."; 4 | 5 | const PARAMS = { 6 | temperature: 0.7, 7 | max_tokens: 1000, 8 | }; 9 | 10 | // OpenAI represents the OpenAI Chat Completion API 11 | class OpenAI { 12 | constructor(apiKey, prompt = "") { 13 | this.apiKey = apiKey; 14 | this.prompt = prompt || PROMPT; 15 | this.headers = { 16 | "Content-Type": "application/json", 17 | Authorization: `Bearer ${apiKey}`, 18 | }; 19 | } 20 | 21 | // ask queries the API for chat completion 22 | // and returns the resulting message 23 | async ask(question) { 24 | const params = this.prepareParams(question); 25 | const resp = await this.fetchResponse(params); 26 | if (resp.choices.length == 0) { 27 | throw new Error("received an empty answer"); 28 | } 29 | const answer = resp.choices[0].message.content.trim(); 30 | return answer; 31 | } 32 | 33 | // prepareParams returns the params for the API request 34 | prepareParams(question) { 35 | const messages = [ 36 | { role: "system", content: this.prompt }, 37 | { role: "user", content: question }, 38 | ]; 39 | const data = { 40 | model: MODEL, 41 | messages: messages, 42 | }; 43 | return Object.assign(data, PARAMS); 44 | } 45 | 46 | // fetchResponse queries the API for chat completion 47 | async fetchResponse(params) { 48 | const resp = await fetch(URL, { 49 | method: "post", 50 | headers: this.headers, 51 | body: JSON.stringify(params), 52 | }); 53 | return await resp.json(); 54 | } 55 | } 56 | 57 | export { OpenAI }; 58 | -------------------------------------------------------------------------------- /js/components/action-button.js: -------------------------------------------------------------------------------- 1 | function actionButton(name, text, arg = null) { 2 | const btn = document.createElement("button"); 3 | btn.className = "sqlime-link"; 4 | btn.dataset.action = name; 5 | btn.innerHTML = text; 6 | if (arg) { 7 | btn.dataset.arg = arg; 8 | } 9 | return btn.outerHTML; 10 | } 11 | 12 | export { actionButton }; 13 | -------------------------------------------------------------------------------- /js/components/command-bar.js: -------------------------------------------------------------------------------- 1 | // Command bar element 2 | class CommandBar extends HTMLElement { 3 | connectedCallback() { 4 | if (!this.rendered) { 5 | this.render(); 6 | this.rendered = true; 7 | } 8 | } 9 | 10 | render() { 11 | this.innerHTML = ` 12 | 18 | 25 | 32 | `; 39 | } 40 | } 41 | 42 | customElements.define("command-bar", CommandBar); 43 | -------------------------------------------------------------------------------- /js/components/copy-link.js: -------------------------------------------------------------------------------- 1 | // Copy-on-click element 2 | class CopyOnClick extends HTMLElement { 3 | connectedCallback() { 4 | if (!this.rendered) { 5 | this.render(); 6 | this.rendered = true; 7 | } 8 | } 9 | 10 | render() { 11 | this.addEventListener("click", () => { 12 | if (!navigator.clipboard) { 13 | this.legacyCopy(); 14 | this.markSuccess(); 15 | return; 16 | } 17 | navigator.clipboard.writeText(this.href).then(() => { 18 | this.markSuccess(); 19 | }); 20 | }); 21 | } 22 | 23 | legacyCopy() { 24 | const txt = document.createElement("textarea"); 25 | txt.value = this.href; 26 | 27 | // Avoid scrolling to bottom 28 | txt.style.top = "0"; 29 | txt.style.left = "0"; 30 | txt.style.position = "fixed"; 31 | 32 | document.body.appendChild(txt); 33 | txt.focus(); 34 | txt.select(); 35 | 36 | document.execCommand("copy"); 37 | document.body.removeChild(txt); 38 | } 39 | 40 | markSuccess() { 41 | const text = this.innerText; 42 | this.innerHTML = "copied!"; 43 | setTimeout(() => { 44 | this.innerHTML = text; 45 | }, 500); 46 | } 47 | 48 | get href() { 49 | return this.getAttribute("href"); 50 | } 51 | set value(newValue) { 52 | return this.setAttribute("href", newValue); 53 | } 54 | } 55 | 56 | customElements.define("copy-on-click", CopyOnClick); 57 | -------------------------------------------------------------------------------- /js/components/db-name.js: -------------------------------------------------------------------------------- 1 | // Database name component 2 | class DbName extends HTMLElement { 3 | connectedCallback() { 4 | if (!this.rendered) { 5 | this.listen(); 6 | this.rendered = true; 7 | } 8 | } 9 | 10 | listen() { 11 | // User changed database name 12 | this.addEventListener("keydown", (event) => { 13 | if (event.key != "Enter") { 14 | return; 15 | } 16 | event.preventDefault(); 17 | event.target.blur(); 18 | this.dispatchEvent(new Event("change")); 19 | }); 20 | } 21 | 22 | ready(name) { 23 | this.value = name; 24 | this.contentEditable = "true"; 25 | this.classList.add("ready"); 26 | } 27 | 28 | get value() { 29 | return this.innerText; 30 | } 31 | set value(newValue) { 32 | this.innerText = newValue; 33 | } 34 | } 35 | 36 | customElements.define("db-name", DbName); 37 | -------------------------------------------------------------------------------- /js/components/sqlime-db.js: -------------------------------------------------------------------------------- 1 | // SQLite database component. 2 | // Loads a database and makes it available for querying. 3 | 4 | import manager from "../sqlite/manager.js"; 5 | import { DatabasePath } from "../db-path.js"; 6 | 7 | // Do not support loading databases from the cloud. 8 | const gister = null; 9 | 10 | // sleep sleeps asynchronously for a specified number of ms. 11 | const sleep = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); 12 | 13 | class SqlimeDb extends HTMLElement { 14 | constructor() { 15 | super(); 16 | this.database = null; 17 | this.loaded = false; 18 | } 19 | 20 | connectedCallback() { 21 | if (!this.loaded) { 22 | this.path = new DatabasePath(this.getAttribute("path")); 23 | this.name = this.getAttribute("name") || path.extractName(); 24 | this.tryLoad(); 25 | } 26 | } 27 | 28 | // tryLoad tries to load the database. 29 | // If the global SQLite object is currently being initialized 30 | // by another SqlimeDb instance - sleeps for a while and tries again. 31 | async tryLoad() { 32 | let retries = 5; 33 | while (retries > 0) { 34 | this.loaded = await this.load(); 35 | if (this.loaded) { 36 | return; 37 | } 38 | await sleep(1000); 39 | retries--; 40 | } 41 | const err = "Timeout waiting for SQLite to load"; 42 | this.error(this.name, err); 43 | } 44 | 45 | // load loads the database from the specified path 46 | // and stores it in the global Sqlime window object 47 | // under the specified name. 48 | // Returns false if the global SQLite object is not yet loaded, 49 | // true otherwise. 50 | async load() { 51 | this.loading(this.name); 52 | try { 53 | const database = await manager.init(gister, this.name, this.path); 54 | if (!database) { 55 | const err = `Failed to load database from ${this.path}`; 56 | this.error(this.name, err); 57 | console.error(err); 58 | return true; 59 | } 60 | this.success(database); 61 | return true; 62 | } catch (exc) { 63 | if (exc.message === "loading") { 64 | return false; 65 | } 66 | this.error(this.name, exc); 67 | throw exc; 68 | } 69 | } 70 | 71 | // loading stores the database as loading. 72 | loading(name) { 73 | this.database = new LoadingDatabase(name); 74 | store(this.database); 75 | notify(this.database); 76 | } 77 | 78 | // loading stores the loaded database. 79 | success(database) { 80 | this.database = database; 81 | store(this.database); 82 | notify(this.database); 83 | } 84 | 85 | // loading stores the database as invalid. 86 | error(name, message) { 87 | this.database = new InvalidDatabase(name, message); 88 | store(this.database); 89 | notify(this.database); 90 | } 91 | } 92 | 93 | // LoadingDatabase represents a database that is still loading. 94 | class LoadingDatabase { 95 | constructor(name) { 96 | this.name = name; 97 | } 98 | 99 | execute(sql) { 100 | throw new Error("SQLite is still loading, try again in a second"); 101 | } 102 | } 103 | 104 | // InvalidDatabase represents a database that is failed to load. 105 | class InvalidDatabase { 106 | constructor(name, message) { 107 | this.name = name; 108 | this.message = message; 109 | } 110 | 111 | execute(sql) { 112 | throw new Error(this.message); 113 | } 114 | } 115 | 116 | // store saves the database in the global Sqlime window object. 117 | function store(database) { 118 | window.Sqlime = window.Sqlime || {}; 119 | window.Sqlime.db = window.Sqlime.db || {}; 120 | window.Sqlime.db[database.name] = database; 121 | } 122 | 123 | // notify emits an event with a database in its current state. 124 | function notify(database) { 125 | const event = new CustomEvent("sqlime-ready", { 126 | detail: { 127 | name: database.name, 128 | database: database, 129 | }, 130 | }); 131 | document.dispatchEvent(event); 132 | } 133 | 134 | if (!window.customElements.get("sqlime-db")) { 135 | window.SqlimeDb = SqlimeDb; 136 | customElements.define("sqlime-db", SqlimeDb); 137 | } 138 | -------------------------------------------------------------------------------- /js/components/sqlime-editor.js: -------------------------------------------------------------------------------- 1 | // SQL editor component 2 | const TAB_WIDTH = 2; 3 | 4 | class SqlimeEditor extends HTMLElement { 5 | connectedCallback() { 6 | if (!this.rendered) { 7 | this.render(); 8 | this.listen(); 9 | this.rendered = true; 10 | } 11 | } 12 | 13 | render() { 14 | this.contentEditable = "true"; 15 | this.spellcheck = false; 16 | } 17 | 18 | listen() { 19 | // shortcuts 20 | this.addEventListener("keydown", this.onKeydown.bind(this)); 21 | // always paste as plain text 22 | this.addEventListener("paste", this.onPaste.bind(this)); 23 | // first input event 24 | const onInput = (event) => { 25 | this.dispatchEvent(new Event("start")); 26 | this.removeEventListener("input", onInput); 27 | }; 28 | this.addEventListener("input", onInput); 29 | } 30 | 31 | // focus sets cursor at the end of the editor 32 | focus() { 33 | super.focus(); 34 | document.execCommand("selectAll", false, null); 35 | document.getSelection().collapseToEnd(); 36 | } 37 | 38 | // clear clears editor contents 39 | clear() { 40 | this.value = ""; 41 | } 42 | 43 | onKeydown(event) { 44 | if (handleIndent(this, event)) return; 45 | if (handleExecute(this, event)) return; 46 | } 47 | 48 | onPaste(event) { 49 | event.preventDefault(); 50 | // get text representation of clipboard 51 | const text = (event.originalEvent || event).clipboardData.getData( 52 | "text/plain" 53 | ); 54 | // insert text manually 55 | document.execCommand("insertHTML", false, text); 56 | } 57 | 58 | get value() { 59 | return this.innerText; 60 | } 61 | set value(newValue) { 62 | this.innerText = newValue; 63 | } 64 | 65 | get query() { 66 | const selectedQuery = window.getSelection().toString().trim(); 67 | return selectedQuery || this.innerText; 68 | } 69 | } 70 | 71 | // handleIndent indents text with Tab 72 | function handleIndent(elem, event) { 73 | if (event.key != "Tab") { 74 | return false; 75 | } 76 | event.preventDefault(); 77 | document.execCommand("insertHTML", false, " ".repeat(TAB_WIDTH)); 78 | return true; 79 | } 80 | 81 | // handleExecute truggers 'execute' event by Ctrl/Cmd+Enter 82 | function handleExecute(elem, event) { 83 | // Ctrl+Enter or Cmd+Enter 84 | if (!event.ctrlKey && !event.metaKey) { 85 | return false; 86 | } 87 | // 10 and 13 are Enter codes 88 | if (event.keyCode != 10 && event.keyCode != 13) { 89 | return false; 90 | } 91 | 92 | event.preventDefault(); 93 | elem.dispatchEvent(new CustomEvent("execute", { detail: elem.query })); 94 | return true; 95 | } 96 | 97 | if (!window.customElements.get("sqlime-editor")) { 98 | window.SqlimeEditor = SqlimeEditor; 99 | customElements.define("sqlime-editor", SqlimeEditor); 100 | } 101 | -------------------------------------------------------------------------------- /js/components/sqlime-result.js: -------------------------------------------------------------------------------- 1 | import printer from "../printer.js"; 2 | 3 | // SQL result component 4 | // shows the result of the SQL query as a table 5 | class SqlimeResult extends HTMLElement { 6 | // print prints SQL query result as a table 7 | print(result) { 8 | this.applyPrinter(result, printer.printResult); 9 | } 10 | 11 | // printTables prints table list as a table 12 | printTables(tables) { 13 | this.applyPrinter(tables, printer.printTables); 14 | } 15 | 16 | // printMarkdown prints markdown text 17 | printMarkdown(text) { 18 | this.applyPrinter(text, printer.printMarkdown); 19 | } 20 | 21 | // applyPrinter prints data structure 22 | // with specified printer function 23 | applyPrinter(data, printFunc) { 24 | if (!data) { 25 | this.clear(); 26 | return; 27 | } 28 | this.innerHTML = printFunc(data); 29 | } 30 | 31 | // clear hides the table 32 | clear() { 33 | this.innerHTML = ""; 34 | } 35 | } 36 | 37 | if (!window.customElements.get("sqlime-result")) { 38 | window.SqlimeResult = SqlimeResult; 39 | customElements.define("sqlime-result", SqlimeResult); 40 | } 41 | -------------------------------------------------------------------------------- /js/components/sqlime-status.js: -------------------------------------------------------------------------------- 1 | // SQL query status component 2 | // shows the state of the SQL query execution 3 | class SqlimeStatus extends HTMLElement { 4 | connectedCallback() { 5 | if (!this.rendered) { 6 | this.render(); 7 | this.rendered = true; 8 | } 9 | } 10 | 11 | render() { 12 | const el = document.createElement("div"); 13 | this.appendChild(el); 14 | this.el = el; 15 | } 16 | 17 | // loading shows the message with a spinner 18 | loading(message) { 19 | this.el.className = ""; 20 | this.value = ` ${message}...`; 21 | } 22 | 23 | // success shows the message and marks is as success 24 | success(message) { 25 | this.el.className = "sql-status--success"; 26 | this.value = message; 27 | } 28 | 29 | // info shows the message without styling it 30 | info(message) { 31 | this.el.className = ""; 32 | this.value = message; 33 | } 34 | 35 | // error shows the message and marks is as error 36 | error(message) { 37 | this.el.className = "sql-status--error"; 38 | this.value = message; 39 | } 40 | 41 | // fadeOut slightly fades out the element. 42 | fadeOut() { 43 | this.style.opacity = 0.4; 44 | } 45 | 46 | // fadeIn fades the element back in. 47 | fadeIn() { 48 | setTimeout(() => { 49 | this.style.opacity = ""; 50 | }, 100); 51 | } 52 | 53 | get value() { 54 | return this.el.innerText; 55 | } 56 | set value(newValue) { 57 | this.el.innerHTML = newValue; 58 | } 59 | } 60 | 61 | if (!window.customElements.get("sqlime-status")) { 62 | window.SqlimeStatus = SqlimeStatus; 63 | customElements.define("sqlime-status", SqlimeStatus); 64 | } 65 | -------------------------------------------------------------------------------- /js/components/toolbar.js: -------------------------------------------------------------------------------- 1 | // Toolbar element 2 | class Toolbar extends HTMLElement { 3 | connectedCallback() { 4 | if (!this.rendered) { 5 | this.render(); 6 | this.listen(); 7 | this.rendered = true; 8 | } 9 | } 10 | 11 | render() { 12 | this.innerHTML = ` 13 | 16 | 17 | 18 | 19 | 21 | 22 | settings 23 | 24 | 25 | 26 | 28 | 30 | 31 | `; 32 | this.btnOpenFile = this.querySelector(":nth-child(1)"); 33 | this.file = this.btnOpenFile.querySelector("input"); 34 | this.btnOpenUrl = this.querySelector(":nth-child(2)"); 35 | this.btnSettings = this.querySelector(":nth-child(3)"); 36 | this.btnAbout = this.querySelector(":nth-child(4)"); 37 | } 38 | 39 | listen() { 40 | this.file.addEventListener("change", (event) => { 41 | if (!event.target.files.length) { 42 | return; 43 | } 44 | const file = event.target.files[0]; 45 | this.dispatchEvent(new CustomEvent("open-file", { detail: file })); 46 | }); 47 | 48 | this.btnOpenUrl.addEventListener("click", (event) => { 49 | this.dispatchEvent(new Event("open-url")); 50 | }); 51 | } 52 | } 53 | 54 | customElements.define("tool-bar", Toolbar); 55 | -------------------------------------------------------------------------------- /js/controllers/actions.js: -------------------------------------------------------------------------------- 1 | // Action controller executes an action 2 | // when the corresponding button is clicked. 3 | class ActionController { 4 | constructor(handlers) { 5 | this.handlers = handlers; 6 | } 7 | 8 | // listen listens for clicks 9 | listen(...elems) { 10 | for (const el of elems) { 11 | el.addEventListener("click", this.onClick.bind(this)); 12 | } 13 | } 14 | 15 | // onClick executes an action 16 | // according to the button clicked 17 | onClick(event) { 18 | const btn = 19 | event.target.tagName == "BUTTON" 20 | ? event.target 21 | : event.target.parentElement; 22 | if (btn.tagName != "BUTTON") { 23 | return; 24 | } 25 | 26 | const handler = this.handlers[btn.dataset.action]; 27 | if (!handler) { 28 | return; 29 | } 30 | 31 | btn.setAttribute("disabled", ""); 32 | handler(btn.dataset.arg) 33 | .then(() => { 34 | btn.removeAttribute("disabled"); 35 | }) 36 | .catch(() => { 37 | btn.removeAttribute("disabled"); 38 | }); 39 | } 40 | } 41 | 42 | export { ActionController }; 43 | -------------------------------------------------------------------------------- /js/controllers/shortcuts.js: -------------------------------------------------------------------------------- 1 | // Shortcut controller listens for Ctrl / Cmd + key shortcuts 2 | // and executes an action when the shortcut is triggered. 3 | class ShortcutController { 4 | constructor(handlers) { 5 | this.handlers = handlers; 6 | } 7 | 8 | // listen listens for shortcuts 9 | listen(...elems) { 10 | for (const el of elems) { 11 | el.addEventListener("keydown", this.onKeydown.bind(this)); 12 | } 13 | } 14 | 15 | // onClick executes an action 16 | // according to the shortcut 17 | onKeydown(event) { 18 | if (!event.ctrlKey && !event.metaKey) { 19 | return; 20 | } 21 | const handler = this.handlers[event.key]; 22 | if (!handler) { 23 | return; 24 | } 25 | console.debug(`Triggered Ctrl/Cmd + ${event.key} shortcut`); 26 | event.preventDefault(); 27 | handler(); 28 | } 29 | } 30 | 31 | export { ShortcutController }; 32 | -------------------------------------------------------------------------------- /js/db-path.js: -------------------------------------------------------------------------------- 1 | // Path to an SQLite database. 2 | 3 | // Could be one of the following: 4 | // - local (../data.db), 5 | // - remote (https://domain.com/data.db) 6 | // - binary (binary database content) 7 | // - sql (sql script) 8 | // - id (gist:02994fe7f2de0611726d61dbf26f46e4) 9 | // - empty 10 | 11 | class DatabasePath { 12 | constructor(value, type = null) { 13 | this.value = value; 14 | this.type = type || this.inferType(value); 15 | } 16 | 17 | // inferType guesses the path type by its value. 18 | inferType(value) { 19 | if (!value) { 20 | return "empty"; 21 | } 22 | if (value instanceof ArrayBuffer) { 23 | return "binary"; 24 | } 25 | if (value.startsWith("http://") || value.startsWith("https://")) { 26 | return "remote"; 27 | } 28 | if (value.includes(":")) { 29 | return "id"; 30 | } 31 | return "local"; 32 | } 33 | 34 | // extractName extracts the database name from the path. 35 | extractName() { 36 | if (["binary", "sql", "id", "empty"].includes(this.type)) { 37 | return ""; 38 | } 39 | const parts = this.value.split("/"); 40 | return parts[parts.length - 1]; 41 | } 42 | 43 | // toHash returns the path as a window location hash string. 44 | toHash() { 45 | if ( 46 | this.type == "local" || 47 | this.type == "remote" || 48 | this.type == "id" 49 | ) { 50 | return `#${this.value}`; 51 | } else { 52 | return ""; 53 | } 54 | } 55 | 56 | // toString returns the path as a string. 57 | toString() { 58 | if (this.type == "local" || this.type == "remote") { 59 | return `URL ${this.value}`; 60 | } else if (this.type == "binary") { 61 | return "binary value"; 62 | } else if (this.type == "sql") { 63 | return "sql script"; 64 | } else if (this.type == "id") { 65 | return `ID ${this.value}`; 66 | } else { 67 | return "empty value"; 68 | } 69 | } 70 | } 71 | 72 | export { DatabasePath }; 73 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | // SQL Playground page 2 | 3 | import gister from "./cloud.js"; 4 | import locator from "./locator.js"; 5 | import manager from "./sqlite/manager.js"; 6 | import storage from "./storage.js"; 7 | import timeit from "./timeit.js"; 8 | 9 | import { actionButton } from "./components/action-button.js"; 10 | import { ActionController } from "./controllers/actions.js"; 11 | import { ShortcutController } from "./controllers/shortcuts.js"; 12 | import { DatabasePath } from "./db-path.js"; 13 | import { DEFAULT_NAME, MESSAGES, QUERIES } from "./sqlite/db.js"; 14 | import { OpenAI } from "./cloud/openai.js"; 15 | 16 | const ui = { 17 | buttons: { 18 | reset: document.querySelector("#reset"), 19 | execute: document.querySelector("#execute"), 20 | save: document.querySelector("#save"), 21 | showTables: document.querySelector("#show-tables"), 22 | }, 23 | name: document.querySelector("#db-name"), 24 | toolbar: document.querySelector("#toolbar"), 25 | commandbar: document.querySelector("#commandbar"), 26 | editor: document.querySelector("#editor"), 27 | status: document.querySelector("#status"), 28 | result: document.querySelector("#result"), 29 | }; 30 | 31 | const actions = { 32 | askAi: askAi, 33 | executeCurrent: executeCurrent, 34 | loadDemo: loadDemo, 35 | save: save, 36 | showTables: showTables, 37 | showTable: showTable, 38 | visit: visit, 39 | }; 40 | 41 | const shortcuts = { 42 | o: () => { 43 | ui.toolbar.btnOpenFile.click(); 44 | }, 45 | u: openUrl, 46 | s: save, 47 | "/": showTables, 48 | }; 49 | 50 | const DEMO_URL = "#demo.db"; 51 | 52 | let database; 53 | 54 | // for testing purposes 55 | window.app = { 56 | actions: actions, 57 | gister: gister, 58 | ui: ui, 59 | }; 60 | 61 | // startFromCurrentUrl loads existing database or creates a new one 62 | // using current window location as database path 63 | async function startFromCurrentUrl() { 64 | const path = locator.path(); 65 | const name = path.extractName(); 66 | const success = await start(name, path); 67 | if (!success) { 68 | return; 69 | } 70 | showStarted(); 71 | } 72 | 73 | // startFromUrl loads existing database 74 | // from specified url 75 | async function startFromUrl(url) { 76 | const path = new DatabasePath(url); 77 | const name = path.extractName(); 78 | const success = await start(name, path); 79 | if (!success) { 80 | return; 81 | } 82 | history.pushState(database.name, null, `#${database.path.value}`); 83 | showStarted(); 84 | } 85 | 86 | // startFromFile loads existing database 87 | // from binary or sql file 88 | async function startFromFile(file, contents, fileType) { 89 | const path = new DatabasePath(contents, fileType); 90 | const name = file.name; 91 | const success = await start(name, path); 92 | if (!success) { 93 | return; 94 | } 95 | history.pushState(database.name, null, "./"); 96 | showStarted(); 97 | } 98 | 99 | // start loads existing database or creates a new one 100 | // using specified database path 101 | async function start(name, path) { 102 | ui.result.clear(); 103 | ui.status.info(MESSAGES.loading); 104 | 105 | try { 106 | const loadedDatabase = await manager.init(gister, name, path); 107 | console.debug(loadedDatabase); 108 | database = loadedDatabase; 109 | if (!loadedDatabase) { 110 | ui.status.error(`Failed to load database from ${path}`); 111 | return false; 112 | } 113 | } catch (exc) { 114 | ui.status.error(`Failed to load database from ${path}: ${exc}`); 115 | return false; 116 | } 117 | 118 | database.query = database.query || storage.get(database.name); 119 | 120 | document.title = database.meaningfulName || document.title; 121 | ui.name.ready(database.name); 122 | ui.status.info(MESSAGES.invite); 123 | ui.editor.value = database.query; 124 | ui.editor.focus(); 125 | 126 | return true; 127 | } 128 | 129 | // executeCurrent runs the current SQL query 130 | function executeCurrent() { 131 | return execute(ui.editor.query); 132 | } 133 | 134 | // execute runs SQL query on the database 135 | // and shows results 136 | function execute(sql) { 137 | sql = sql.trim(); 138 | storage.set(database.name, sql); 139 | if (!sql) { 140 | ui.status.info(MESSAGES.invite); 141 | ui.result.clear(); 142 | return Promise.resolve(); 143 | } 144 | try { 145 | ui.status.fadeOut(); 146 | ui.status.info(MESSAGES.executing); 147 | timeit.start(); 148 | const result = database.execute(sql); 149 | const elapsed = timeit.finish(); 150 | showResult(result, elapsed); 151 | return Promise.resolve(); 152 | } catch (exc) { 153 | showError(exc); 154 | return Promise.reject(exc); 155 | } finally { 156 | ui.status.fadeIn(); 157 | } 158 | } 159 | 160 | // askAi queries the AI assistant using the contents of the editor 161 | // as a query and prints the answer. 162 | function askAi() { 163 | const key = localStorage.getItem("openai.apikey"); 164 | if (!key) { 165 | return visit("settings"); 166 | } 167 | const ai = new OpenAI(key); 168 | const question = ui.editor.query; 169 | ui.status.loading("Waiting for AI response (can take up to 30 seconds)"); 170 | timeit.start(); 171 | const promise = ai 172 | .ask(question) 173 | .then((answer) => { 174 | const elapsed = timeit.finish() / 1000; 175 | ui.status.success(`AI response, took ${elapsed} sec:`); 176 | ui.result.printMarkdown(answer); 177 | }) 178 | .catch((err) => { 179 | ui.status.error(err); 180 | ui.result.clear(); 181 | }); 182 | return promise; 183 | } 184 | 185 | // openUrl loads database from local or remote url 186 | function openUrl() { 187 | const url = prompt("Enter database file URL:", "https://path/to/database"); 188 | if (!url) { 189 | return; 190 | } 191 | startFromUrl(url); 192 | } 193 | 194 | // save persists database state and current query 195 | // to remote storage 196 | async function save() { 197 | const query = ui.editor.value.trim(); 198 | storage.set(database.name, query); 199 | gister.reload(); 200 | if (!gister.hasCredentials()) { 201 | return visit("settings"); 202 | } 203 | ui.status.info("Saving..."); 204 | ui.result.clear(); 205 | try { 206 | const savedDatabase = await manager.save(gister, database, query); 207 | if (!savedDatabase) { 208 | ui.status.error(`Failed to save database to ${gister.name}`); 209 | return Promise.reject(); 210 | } 211 | database = savedDatabase; 212 | } catch (exc) { 213 | showError(`Failed to save database to ${gister.name}: ${exc}`); 214 | return Promise.reject(exc); 215 | } 216 | changeName(database.name); 217 | showSaved(database); 218 | return Promise.resolve(); 219 | } 220 | 221 | // changeName changes database name 222 | function changeName(name) { 223 | if (name == ui.name.value && name == database.name) { 224 | return; 225 | } 226 | storage.remove(database.name); 227 | database.name = name; 228 | storage.set(name, database.query); 229 | ui.name.value = name; 230 | document.title = name; 231 | } 232 | 233 | // showStarted shows the result of successful database load 234 | function showStarted() { 235 | if (database.query) { 236 | execute(database.query); 237 | enableCommandBar(); 238 | } else if (database.tables.length) { 239 | showTableContent(database.tables[0]); 240 | enableCommandBar(); 241 | } else { 242 | showWelcome(); 243 | } 244 | } 245 | 246 | // showTables shows all database tables 247 | function showTables() { 248 | const tables = database.gatherTables(); 249 | if (!tables.length) { 250 | ui.status.info("Database is empty"); 251 | return Promise.reject(); 252 | } 253 | ui.status.info(`${tables.length} tables:`); 254 | ui.result.printTables(tables); 255 | return Promise.resolve(); 256 | } 257 | 258 | // showTable shows specific database table 259 | function showTable(table) { 260 | const result = database.getTableInfo(table); 261 | const all = actionButton("showTables", "tables"); 262 | ui.status.info(`${all} / ${table}:`); 263 | ui.result.print(result); 264 | return Promise.resolve(); 265 | } 266 | 267 | // showTableContent select data from specified table 268 | function showTableContent(table) { 269 | const query = QUERIES.tableContent.replace("{}", table); 270 | ui.editor.value = query; 271 | execute(query); 272 | } 273 | 274 | // loadDemo loads demo database 275 | function loadDemo() { 276 | window.location.assign(DEMO_URL); 277 | return Promise.resolve(); 278 | } 279 | 280 | // showWelcome show the welcome message 281 | function showWelcome() { 282 | const demo = actionButton("loadDemo", "demo database"); 283 | let message = `

${MESSAGES.invite}
or load the ${demo}.

`; 284 | message += `

Click the logo anytime to start from scratch.

`; 285 | if (!gister.hasCredentials()) { 286 | const settings = actionButton("visit", "settings", "settings"); 287 | message += `

Visit ${settings} to enable sharing.

`; 288 | } 289 | const functions = actionButton("visit", "Functions", "functions"); 290 | const about = actionButton("visit", "About", "about"); 291 | message += `

${functions} • ${about}

`; 292 | ui.status.info(message); 293 | } 294 | 295 | // showResult shows results and timing 296 | // of the SQL query execution 297 | function showResult(result, elapsed) { 298 | if (result && result.values.length) { 299 | ui.status.success(`${result.values.length} rows, took ${elapsed} ms`); 300 | ui.result.print(result); 301 | } else { 302 | ui.status.success(`0 rows, took ${elapsed} ms`); 303 | ui.result.print(""); 304 | } 305 | } 306 | 307 | // showError shows an error occured 308 | // during SQL query execution 309 | function showError(exc) { 310 | const err = exc.toString().split("\n")[0]; 311 | ui.result.clear(); 312 | ui.status.error(err); 313 | } 314 | 315 | // showSaved shows saved database information 316 | function showSaved(database) { 317 | history.pushState(database.id, null, database.path.toHash()); 318 | const shareUrl = ` 319 | copy share link`; 320 | 321 | const url = gister.getUrl(database.id); 322 | if (url) { 323 | const gistUrl = `gist`; 324 | let message = `

✓ Saved as ${gistUrl}

`; 325 | message += `

${shareUrl}

`; 326 | ui.status.info(message); 327 | } else { 328 | const settings = actionButton("visit", "settings", "settings"); 329 | let message = `

✓ Saved to the public cloud. Visit ${settings} for private sharing.

`; 330 | message += `

${shareUrl}

`; 331 | ui.status.info(message); 332 | } 333 | } 334 | 335 | // enableCommandBar enables all buttons 336 | // in the command bar 337 | function enableCommandBar() { 338 | ui.commandbar.classList.remove("sqlime-disabled"); 339 | } 340 | 341 | function visit(page) { 342 | window.location.assign(`${page}.html`); 343 | return Promise.resolve(); 344 | } 345 | 346 | // User changed database name 347 | ui.name.addEventListener("change", (event) => { 348 | changeName(event.target.value); 349 | }); 350 | 351 | // Toolbar 'open file' button click 352 | ui.toolbar.addEventListener("open-file", (event) => { 353 | const file = event.detail; 354 | const reader = new FileReader(); 355 | const fileType = file.name.endsWith(".sql") ? "sql" : "binary"; 356 | reader.onload = function () { 357 | event.target.value = ""; 358 | startFromFile(file, reader.result, fileType); 359 | }; 360 | if (fileType == "sql") { 361 | reader.readAsText(file); 362 | } else { 363 | reader.readAsArrayBuffer(file); 364 | } 365 | }); 366 | 367 | // Toolbar 'open url' button click 368 | ui.toolbar.addEventListener("open-url", () => { 369 | openUrl(); 370 | }); 371 | 372 | // Toolbar 'reset' button click 373 | ui.buttons.reset.addEventListener("click", () => { 374 | storage.remove(DEFAULT_NAME); 375 | }); 376 | 377 | // Navigate back to previous database 378 | window.addEventListener("popstate", () => { 379 | startFromCurrentUrl(); 380 | }); 381 | 382 | // SQL editor 'execute' event 383 | ui.editor.addEventListener("execute", (event) => { 384 | execute(event.detail); 385 | }); 386 | 387 | // SQL editor 'started typing' event 388 | ui.editor.addEventListener("start", (event) => { 389 | enableCommandBar(); 390 | }); 391 | 392 | // Handle user actions 393 | new ActionController(actions).listen(ui.commandbar, ui.status, ui.result); 394 | new ShortcutController(shortcuts).listen(document); 395 | 396 | gister.loadCredentials(); 397 | startFromCurrentUrl(); 398 | -------------------------------------------------------------------------------- /js/locator.js: -------------------------------------------------------------------------------- 1 | // Helper for working with window.location. 2 | 3 | import { DatabasePath } from "./db-path.js"; 4 | 5 | // path creates a database path from the window location. 6 | function path() { 7 | return new DatabasePath(window.location.hash.slice(1)); 8 | } 9 | 10 | const locator = { path }; 11 | export default locator; 12 | -------------------------------------------------------------------------------- /js/printer.js: -------------------------------------------------------------------------------- 1 | // Prints SQL query results as text. 2 | 3 | // printResult converts SQL query results to an HTML table. 4 | function printResult(result) { 5 | if (result === null) { 6 | return "(empty)"; 7 | } 8 | const [columns, values] = [result.columns, result.values]; 9 | let html = "" + join(columns, "th") + ""; 10 | const rows = values.map(function (v) { 11 | return join(v.map(sanitize), "td"); 12 | }); 13 | html += "" + join(rows, "tr") + ""; 14 | return `${html}
`; 15 | } 16 | 17 | // printTables prints the specified database tables. 18 | function printTables(tables) { 19 | let html = "table"; 20 | const rows = tables.map(function (table) { 21 | return ` 22 | 23 | 26 | 27 | `; 28 | }); 29 | html += "" + rows.join("\n") + ""; 30 | return `${html}
`; 31 | } 32 | 33 | // printMarkdown prints the specified markdown text. 34 | function printMarkdown(text) { 35 | return `
${text}
`; 36 | } 37 | 38 | // join joins the values, wrapping each one into a tag. 39 | function join(values, tagName) { 40 | if (values.length === 0) { 41 | return ""; 42 | } 43 | const open = "<" + tagName + ">"; 44 | const close = ""; 45 | return open + values.join(close + open) + close; 46 | } 47 | 48 | // sanitize strips HTML from the text. 49 | function sanitize(text) { 50 | const div = document.createElement("div"); 51 | div.innerText = text; 52 | return div.innerHTML; 53 | } 54 | 55 | const printer = { printMarkdown, printResult, printTables }; 56 | export default printer; 57 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | const ui = { 2 | settings: document.querySelector("#settings"), 3 | github: { 4 | username: document.querySelector("#github-username"), 5 | token: document.querySelector("#github-token"), 6 | }, 7 | openai: { 8 | apikey: document.querySelector("#openai-apikey"), 9 | }, 10 | }; 11 | 12 | ui.settings.addEventListener("submit", (event) => { 13 | event.preventDefault(); 14 | localStorage.setItem("github.username", ui.github.username.value); 15 | localStorage.setItem("github.token", ui.github.token.value); 16 | localStorage.setItem("openai.apikey", ui.openai.apikey.value); 17 | }); 18 | 19 | ui.github.username.addEventListener("change", (event) => { 20 | localStorage.setItem("github.username", event.target.value); 21 | }); 22 | 23 | ui.github.token.addEventListener("change", (event) => { 24 | localStorage.setItem("github.token", event.target.value); 25 | }); 26 | 27 | ui.openai.apikey.addEventListener("change", (event) => { 28 | localStorage.setItem("openai.apikey", event.target.value); 29 | }); 30 | 31 | ui.github.username.value = localStorage.getItem("github.username") || ""; 32 | ui.github.token.value = localStorage.getItem("github.token") || ""; 33 | ui.openai.apikey.value = localStorage.getItem("openai.apikey") || ""; 34 | -------------------------------------------------------------------------------- /js/sqlite/db.js: -------------------------------------------------------------------------------- 1 | // SQLite database wrapper and metadata. 2 | 3 | import hasher from "./hasher.js"; 4 | 5 | // default database name 6 | const DEFAULT_NAME = "new.db"; 7 | 8 | // system queries 9 | const QUERIES = { 10 | version: "select sqlite_version() as version", 11 | tables: `select name as "table" from sqlite_schema 12 | where type = 'table' 13 | and name not like 'sqlite_%' 14 | and name not like 'sqlean_%'`, 15 | tableContent: "select * from {} limit 10", 16 | tableInfo: `select 17 | iif(pk=1, '✓', '') as pk, name, type, iif("notnull"=0, '✓', '') as "null?" 18 | from pragma_table_info('{}')`, 19 | }; 20 | 21 | // database messages 22 | const MESSAGES = { 23 | empty: "The query returned nothing", 24 | executing: "Executing query...", 25 | invite: "Run SQL query to see the results", 26 | loading: "Loading database...", 27 | }; 28 | 29 | // SQLite database wrapper. 30 | // Wraps SQLite WASM API and calls it in the following methods: 31 | // - execute() 32 | // - each() 33 | // - gatherTables() 34 | // - calcHashcode() 35 | // The rest of the methods are WASM-agnostic. 36 | class SQLite { 37 | constructor(name, path, capi, db, query = "") { 38 | this.id = null; 39 | this.owner = null; 40 | this.name = name || DEFAULT_NAME; 41 | this.path = path; 42 | this.capi = capi; 43 | this.db = db; 44 | this.query = query; 45 | this.hashcode = 0; 46 | this.tables = []; 47 | } 48 | 49 | // execute runs one ore more sql queries 50 | // and returns the last result. 51 | execute(sql) { 52 | if (!sql) { 53 | // sqlite api fails when trying to execute an empty query 54 | return null; 55 | } 56 | this.query = sql; 57 | let rows = []; 58 | this.db.exec({ 59 | sql: sql, 60 | rowMode: "object", 61 | resultRows: rows, 62 | }); 63 | if (!rows.length) { 64 | return null; 65 | } 66 | const result = { 67 | columns: Object.getOwnPropertyNames(rows[0]), 68 | values: [], 69 | }; 70 | for (let row of rows) { 71 | result.values.push(Object.values(row)); 72 | } 73 | return result; 74 | } 75 | 76 | // each runs the query and invokes the callback 77 | // on each of the resulting rows. 78 | each(sql, callback) { 79 | this.db.exec({ 80 | sql: sql, 81 | rowMode: "object", 82 | callback: callback, 83 | }); 84 | } 85 | 86 | // gatherTables fills the `.tables` attribute 87 | // with an array of database tables and returns it. 88 | gatherTables() { 89 | let rows = []; 90 | this.db.exec({ 91 | sql: QUERIES.tables, 92 | rowMode: "array", 93 | resultRows: rows, 94 | }); 95 | if (!rows.length) { 96 | this.tables = []; 97 | return this.tables; 98 | } 99 | this.tables = rows.map((row) => row[0]); 100 | return this.tables; 101 | } 102 | 103 | // getTableInfo returns the table schema. 104 | getTableInfo(table) { 105 | const sql = QUERIES.tableInfo.replace("{}", table); 106 | return this.execute(sql); 107 | } 108 | 109 | // calcHashcode fills the `.hashcode` attribute 110 | // with the database hashcode and returns it. 111 | calcHashcode() { 112 | if (!this.tables.length) { 113 | // sqlite api fails when trying to export an empty database 114 | this.hashcode = 0; 115 | return 0; 116 | } 117 | const dbArr = this.capi.sqlite3_js_db_export(this.db.pointer); 118 | const dbHash = hasher.uint8Array(dbArr); 119 | const queryHash = hasher.string(this.query); 120 | const hash = dbHash & queryHash || dbHash || queryHash; 121 | this.hashcode = hash; 122 | return hash; 123 | } 124 | 125 | // meaningfulName returns the database name 126 | // if it differs from default one. 127 | get meaningfulName() { 128 | if (this.name == DEFAULT_NAME) { 129 | return ""; 130 | } 131 | return this.name; 132 | } 133 | 134 | // ensureName changes the default name to something more meaningful. 135 | ensureName() { 136 | if (this.meaningfulName) { 137 | return this.meaningfulName; 138 | } 139 | if (this.tables.length) { 140 | this.name = this.tables[0] + ".db"; 141 | return this.name; 142 | } 143 | if (this.id) { 144 | this.name = this.id.substr(0, 6) + ".db"; 145 | return this.name; 146 | } 147 | return this.name; 148 | } 149 | } 150 | 151 | export { DEFAULT_NAME, MESSAGES, QUERIES, SQLite }; 152 | -------------------------------------------------------------------------------- /js/sqlite/dumper.js: -------------------------------------------------------------------------------- 1 | // Dumps database schema and contents into plain text formats. 2 | 3 | const SCHEMA_SQL = ` 4 | select "name", "type", "sql" 5 | from "sqlite_schema" 6 | where "sql" not null 7 | and "type" == 'table' 8 | order by "name" 9 | `; 10 | 11 | const CREATE_TABLE_PREFIX = "CREATE TABLE "; 12 | 13 | // toSql dumps database schema and contents as SQL statements. 14 | // Adapted from https://github.com/simonw/sqlite-dump 15 | function toSql(database) { 16 | const schema = schemaToSql(database); 17 | if (!schema.length) { 18 | return ""; 19 | } 20 | const tables = tablesToSql(database); 21 | let script = []; 22 | script.push("BEGIN TRANSACTION;"); 23 | script.push("PRAGMA writable_schema=ON;"); 24 | script.push(...schema); 25 | script.push(...tables); 26 | script.push("PRAGMA writable_schema=OFF;"); 27 | script.push("COMMIT;"); 28 | return script.join("\n"); 29 | } 30 | 31 | // schemaToSql returns the database schema as SQL statements. 32 | function schemaToSql(database) { 33 | let script = []; 34 | database.each(SCHEMA_SQL, (item) => { 35 | const sql = schemaItemToSql(item); 36 | if (sql) { 37 | script.push(sql); 38 | } 39 | }); 40 | return script; 41 | } 42 | 43 | // schemaItemToSql returns an SQL schema statement 44 | // for the database object. 45 | function schemaItemToSql(item) { 46 | if (item.name == "sqlite_sequence") { 47 | return 'DELETE FROM "sqlite_sequence";'; 48 | } else if (item.name == "sqlite_stat1") { 49 | return 'ANALYZE "sqlite_schema";'; 50 | } else if (item.name.startsWith("sqlite_")) { 51 | return ""; 52 | } else if (item.sql.startsWith("CREATE VIRTUAL TABLE")) { 53 | const qtable = item.name.replace("'", "''"); 54 | return `INSERT INTO sqlite_schema(type,name,tbl_name,rootpage,sql) 55 | VALUES('table','${qtable}','${qtable}',0,'${item.sql}');`; 56 | } else if (item.sql.toUpperCase().startsWith(CREATE_TABLE_PREFIX)) { 57 | const qtable = item.sql.substr(CREATE_TABLE_PREFIX.length); 58 | return `CREATE TABLE IF NOT EXISTS ${qtable};`; 59 | } else { 60 | return `${item.sql};`; 61 | } 62 | } 63 | 64 | // tablesToSql returns database contents as SQL statements. 65 | function tablesToSql(database) { 66 | let script = []; 67 | database.each(SCHEMA_SQL, (item) => { 68 | const sql = tableContentsToSql(database, item); 69 | if (sql) { 70 | script.push(sql); 71 | } 72 | }); 73 | return script; 74 | } 75 | 76 | // tableContentsToSql returns table contents as SQL statements. 77 | function tableContentsToSql(database, item) { 78 | if ( 79 | item.name.startsWith("sqlite_") || 80 | item.sql.startsWith("CREATE VIRTUAL TABLE") 81 | ) { 82 | return ""; 83 | } 84 | item.nameIdent = item.name.replace('"', '""'); 85 | let res = database.execute(`PRAGMA table_info("${item.nameIdent}")`); 86 | const columnNames = res.values.map((row) => row[1]); 87 | const valuesArr = columnNames.map((name) => { 88 | const col = name.replace('"', '""'); 89 | return `'||quote("${col}")||'`; 90 | }); 91 | const values = valuesArr.join(","); 92 | const sql = `SELECT 'INSERT INTO "${item.nameIdent}" VALUES(${values})' as stmt FROM "${item.nameIdent}";`; 93 | const contents = []; 94 | database.each(sql, (row) => { 95 | contents.push(`${row.stmt};`); 96 | }); 97 | return contents.join("\n"); 98 | } 99 | 100 | const dumper = { toSql }; 101 | export default dumper; 102 | -------------------------------------------------------------------------------- /js/sqlite/hasher.js: -------------------------------------------------------------------------------- 1 | // Simple 32-bit integer hashcode implementation. 2 | 3 | // string calculates a hashcode for the String value. 4 | function string(str) { 5 | let hash = 0; 6 | for (let i = 0; i < str.length; i++) { 7 | const char = str.charCodeAt(i); 8 | hash = (hash << 5) - hash + char; 9 | hash = hash & hash; 10 | } 11 | return hash; 12 | } 13 | 14 | // uint8Array calculates a hashcode for the Uint8Array value. 15 | function uint8Array(arr) { 16 | let hash = 0; 17 | for (let i = 0; i < arr.length; i++) { 18 | hash = (hash << 5) - hash + arr[i]; 19 | hash = hash & hash; 20 | } 21 | return hash; 22 | } 23 | 24 | const hasher = { string, uint8Array }; 25 | export default hasher; 26 | -------------------------------------------------------------------------------- /js/sqlite/manager.js: -------------------------------------------------------------------------------- 1 | // SQLite database manager. 2 | 3 | import dumper from "./dumper.js"; 4 | import { SQLite } from "./db.js"; 5 | 6 | // global SQLite WASM API object. 7 | let sqlite3; 8 | 9 | // required by the SQLite WASM API. 10 | const CONFIG = { 11 | print: console.log, 12 | printErr: console.error, 13 | }; 14 | 15 | // init loads a database from the specified path 16 | // using the SQLite WASM API. 17 | async function init(gister, name, path) { 18 | if (sqlite3 === "loading") { 19 | return Promise.reject(Error("loading")); 20 | } 21 | if (sqlite3 === undefined) { 22 | sqlite3 = "loading"; 23 | sqlite3 = await sqlite3InitModule(CONFIG); 24 | const version = sqlite3.capi.sqlite3_libversion(); 25 | console.log(`Loaded SQLite ${version}`); 26 | } 27 | if (path.type == "local" || path.type == "remote") { 28 | if (path.value.endsWith(".sql")) { 29 | return await loadSql(name, path); 30 | } 31 | return await loadFile(name, path); 32 | } 33 | if (path.type == "binary") { 34 | return await loadArrayBuffer(name, path); 35 | } 36 | if (path.type == "sql") { 37 | return await loadSqlScript(name, path); 38 | } 39 | if (path.type == "id") { 40 | return await loadGist(gister, path); 41 | } 42 | // empty 43 | return await create(name, path); 44 | } 45 | 46 | // create creates an empty database. 47 | async function create(name, path) { 48 | console.debug("Creating new database..."); 49 | const db = new sqlite3.oo1.DB(); 50 | return new SQLite(name, path, sqlite3.capi, db); 51 | } 52 | 53 | // loadArrayBuffer loads a database from the binary database file content. 54 | async function loadArrayBuffer(name, path) { 55 | console.debug("Loading database from array buffer..."); 56 | const db = loadDbFromArrayBuffer(path.value); 57 | path.value = null; 58 | const database = new SQLite(name, path, sqlite3.capi, db); 59 | database.gatherTables(); 60 | return database; 61 | } 62 | 63 | // loadSqlScript loads a database from a plain text SQL script. 64 | async function loadSqlScript(name, path) { 65 | console.debug(`Loading SQL from script...`); 66 | const sql = path.value; 67 | if (!sql) { 68 | return null; 69 | } 70 | 71 | const db = new sqlite3.oo1.DB(); 72 | const database = new SQLite(name, path, sqlite3.capi, db); 73 | database.execute(sql); 74 | database.gatherTables(); 75 | database.query = ""; 76 | return database; 77 | } 78 | 79 | // loadFile loads a database from the specified local or remote binary file. 80 | async function loadFile(name, path) { 81 | console.debug(`Loading database from url ${path.value}...`); 82 | const promise = fetch(path.value) 83 | .then((response) => { 84 | if (!response.ok) { 85 | return null; 86 | } 87 | return response.arrayBuffer(); 88 | }) 89 | .catch((reason) => { 90 | return null; 91 | }); 92 | const buf = await promise; 93 | if (!buf) { 94 | return null; 95 | } 96 | 97 | const db = loadDbFromArrayBuffer(buf); 98 | const database = new SQLite(name, path, sqlite3.capi, db); 99 | database.gatherTables(); 100 | return database; 101 | } 102 | 103 | // loadSql loads a database from the specified local or remote SQL file. 104 | async function loadSql(name, path) { 105 | console.debug(`Loading SQL from url ${path.value}...`); 106 | const promise = fetch(path.value) 107 | .then((response) => { 108 | if (!response.ok) { 109 | return null; 110 | } 111 | return response.text(); 112 | }) 113 | .catch((reason) => { 114 | return null; 115 | }); 116 | const sql = await promise; 117 | if (!sql) { 118 | return null; 119 | } 120 | 121 | const db = new sqlite3.oo1.DB(); 122 | const database = new SQLite(name, path, sqlite3.capi, db); 123 | database.execute(sql); 124 | database.gatherTables(); 125 | database.query = ""; 126 | return database; 127 | } 128 | 129 | // loadGist loads a database from the cloud with the specified id. 130 | async function loadGist(gister, path) { 131 | if (!gister) { 132 | return Promise.reject("Saving to the cloud is not supported"); 133 | } 134 | console.debug(`Loading database from gist ${path.value}...`); 135 | const gist = await gister.get(path.value); 136 | if (!gist) { 137 | return null; 138 | } 139 | const db = new sqlite3.oo1.DB(); 140 | const database = new SQLite(gist.name, path, sqlite3.capi, db); 141 | database.id = gist.id; 142 | database.owner = gist.owner; 143 | database.execute(gist.schema); 144 | database.query = gist.query; 145 | database.calcHashcode(); 146 | database.ensureName(); 147 | return database; 148 | } 149 | 150 | // save saves the database to the cloud. 151 | async function save(gister, database, query) { 152 | if (!gister) { 153 | return Promise.reject("Saving to the cloud is not supported"); 154 | } 155 | console.debug(`Saving database to gist...`); 156 | const schema = dumper.toSql(database, query); 157 | database.query = query; 158 | if (!schema && !query) { 159 | return Promise.resolve(null); 160 | } 161 | const oldHashcode = database.hashcode; 162 | database.gatherTables(); 163 | if (!database.tables.length && !query) { 164 | return Promise.resolve(null); 165 | } 166 | database.calcHashcode(); 167 | database.ensureName(); 168 | let promise; 169 | if (!database.id || database.owner != gister.username) { 170 | promise = gister.create(database.name, schema, database.query); 171 | } else { 172 | // do not update gist if nothing has changed 173 | if (database.hashcode == oldHashcode) { 174 | return Promise.resolve(database); 175 | } 176 | promise = gister.update( 177 | database.id, 178 | database.name, 179 | schema, 180 | database.query 181 | ); 182 | } 183 | return promise.then((gist) => afterSave(database, gist)); 184 | } 185 | 186 | // afterSave updates database attributes after a successful save. 187 | function afterSave(database, gist) { 188 | if (!gist.id) { 189 | return null; 190 | } 191 | database.id = gist.id; 192 | database.owner = gist.owner; 193 | database.path.type = "id"; 194 | database.path.value = `${gist.prefix}:${database.id}`; 195 | database.ensureName(); 196 | return database; 197 | } 198 | 199 | // loadDbFromArrayBuffer loads an SQLite database from the array buffer. 200 | function loadDbFromArrayBuffer(buf) { 201 | const bytes = new Uint8Array(buf); 202 | const p = sqlite3.wasm.allocFromTypedArray(bytes); 203 | const db = new sqlite3.oo1.DB(); 204 | sqlite3.capi.sqlite3_deserialize( 205 | db.pointer, 206 | "main", 207 | p, 208 | bytes.length, 209 | bytes.length, 210 | sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE 211 | ); 212 | return db; 213 | } 214 | 215 | export default { init, save }; 216 | -------------------------------------------------------------------------------- /js/sqlite/sqlean.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/js/sqlite/sqlean.wasm -------------------------------------------------------------------------------- /js/storage.js: -------------------------------------------------------------------------------- 1 | // Stores various database information 2 | // in brower storage. 3 | 4 | const PREFIX = "sqlime"; 5 | 6 | // get loads SQL query from the local storage 7 | function get(key) { 8 | return localStorage.getItem(`${PREFIX}.query.${key}`); 9 | } 10 | 11 | // save saves SQL query to the local storage 12 | function set(key, sql) { 13 | if (!sql) { 14 | remove(key); 15 | } 16 | localStorage.setItem(`${PREFIX}.query.${key}`, sql); 17 | } 18 | 19 | // remove deletes SQL query from the local storage 20 | function remove(key) { 21 | localStorage.removeItem(`${PREFIX}.query.${key}`); 22 | } 23 | 24 | const storage = { get, set, remove }; 25 | export default storage; 26 | -------------------------------------------------------------------------------- /js/timeit.js: -------------------------------------------------------------------------------- 1 | // Measures operation execution time 2 | 3 | let started; 4 | 5 | // start starts measuring the execution time 6 | function start() { 7 | started = performance.now(); 8 | } 9 | 10 | // finish stops measuring the execution time 11 | // and returns elasped time in ms 12 | function finish() { 13 | var elapsed = performance.now() - started; 14 | return Math.round(elapsed); 15 | } 16 | 17 | const timeit = { start, finish }; 18 | export default timeit; 19 | -------------------------------------------------------------------------------- /sales.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalgeon/sqlime/ccbe67710b6049cc423209344239001e2aa7735d/sales.db -------------------------------------------------------------------------------- /settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sqlime / Settings 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

Settings

22 |
23 |
24 |
25 |

All settings are optional, you can use Sqlime without them.

26 |
27 |
28 | GitHub credentials (to enable sharing) 29 | 30 | 31 | 32 | 33 |

34 | This is not your GitHub password. 35 | Create an API token with a 'gist' scope in the 36 | GitHub settings 37 |

38 |
39 |
40 |
41 | OpenAI credentials (to enable 'Ask AI' feature) 42 | 43 | 44 |

45 | Create an API key in the 46 | OpenAI account 47 |

48 |
49 |
50 |

← back

51 |
52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/img/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/img/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /test/suite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tests 9 | 10 | 11 | 12 | 13 |

14 |     
15 |     
16 | 
17 | 
18 | 


--------------------------------------------------------------------------------
/test/suite.js:
--------------------------------------------------------------------------------
  1 | import { assert, log, mock, unmock, summary, wait } from "./tester.js";
  2 | 
  3 | const LONG_DELAY = 1000;
  4 | const MEDIUM_DELAY = 500;
  5 | const SMALL_DELAY = 100;
  6 | 
  7 | const EMPTY_SCHEMA = `BEGIN TRANSACTION;
  8 | PRAGMA writable_schema=ON;
  9 | CREATE TABLE IF NOT EXISTS sqlean_define(name text primary key, type text, body text);
 10 | PRAGMA writable_schema=OFF;
 11 | COMMIT;`;
 12 | 
 13 | async function testNewDatabase() {
 14 |     log("New database...");
 15 |     const app = await loadApp();
 16 |     const h1 = app.document.querySelector(".header h1");
 17 |     assert(
 18 |         "shows header",
 19 |         h1.innerText.trim() == "SQLite Playground  // new.db"
 20 |     );
 21 |     assert("editor is empty", app.ui.editor.value == "");
 22 |     assert(
 23 |         "command bar is disabled",
 24 |         app.ui.commandbar.classList.contains("sqlime-disabled")
 25 |     );
 26 |     assert("shows welcome text", app.ui.status.value.includes("demo database"));
 27 |     assert("result is empty", app.ui.result.innerText == "");
 28 | }
 29 | 
 30 | async function testExecuteQuery() {
 31 |     log("Execute query...");
 32 |     const app = await loadApp();
 33 |     const sql = "select 'hello' as message";
 34 |     // activate buttons
 35 |     app.ui.editor.dispatchEvent(new Event("input"));
 36 |     app.ui.editor.value = sql;
 37 |     app.ui.buttons.execute.click();
 38 |     await wait(MEDIUM_DELAY);
 39 |     assert("shows result", app.ui.result.innerText.includes("hello"));
 40 |     assert("shows query in editor", app.ui.editor.value == sql);
 41 |     assert(
 42 |         "caches query in local storage",
 43 |         localStorage.getItem("sqlime.query.new.db") == sql
 44 |     );
 45 | }
 46 | 
 47 | async function testExecuteSelection() {
 48 |     log("Execute selection...");
 49 |     const app = await loadApp();
 50 |     const sql = "select 54321, 17423";
 51 |     // activate buttons
 52 |     app.ui.editor.dispatchEvent(new Event("input"));
 53 |     app.ui.editor.value = sql;
 54 |     selectText(app, app.ui.editor, 0, 12);
 55 |     app.ui.buttons.execute.click();
 56 |     await wait(MEDIUM_DELAY);
 57 |     assert("executes selected part", app.ui.result.innerText.includes("54321"));
 58 |     assert("ignores other parts", !app.ui.result.innerText.includes("17423"));
 59 |     assert(
 60 |         "caches query in local storage",
 61 |         localStorage.getItem("sqlime.query.new.db") == sql.substring(0, 12)
 62 |     );
 63 | }
 64 | 
 65 | async function testLoadDemo() {
 66 |     log("Load demo...");
 67 |     const app = await loadApp();
 68 |     const sql = "select * from employees";
 69 |     const btn = app.ui.status.querySelector('[data-action="loadDemo"]');
 70 |     btn.click();
 71 |     await wait(MEDIUM_DELAY);
 72 |     assert("shows query in editor", app.ui.editor.value.startsWith(sql));
 73 |     assert("shows row count", app.ui.status.value.includes("10 rows"));
 74 |     assert("shows employees", app.ui.result.innerText.includes("Diane"));
 75 | }
 76 | 
 77 | async function testLoadUrl() {
 78 |     log("Load url...");
 79 |     const app = await loadApp();
 80 |     app.window.location.assign("../index.html#demo.db");
 81 |     await wait(MEDIUM_DELAY);
 82 |     assert("shows database name", app.ui.name.value == "demo.db");
 83 |     app.ui.buttons.showTables.click();
 84 |     await wait(MEDIUM_DELAY);
 85 |     assert("shows tables", app.ui.status.value == "2 tables:");
 86 | }
 87 | 
 88 | async function testLoadUrlInvalid() {
 89 |     log("Load invalid url...");
 90 |     const app = await loadApp();
 91 |     app.window.location.assign("../index.html#whatever");
 92 |     await wait(MEDIUM_DELAY);
 93 |     assert("shows error", app.ui.status.value.includes("Failed to load"));
 94 |     assert("editor is empty", app.ui.editor.value == "");
 95 |     assert("result is empty", app.ui.result.innerText == "");
 96 | }
 97 | 
 98 | async function testLoadGist() {
 99 |     log("Load gist...");
100 |     const app = await loadApp();
101 |     app.window.location.assign(
102 |         "../index.html#gist:e012594111ce51f91590c4737e41a046"
103 |     );
104 |     await wait(LONG_DELAY);
105 |     assert("shows database name", app.ui.name.value == "employees.en.db");
106 |     assert("shows query in editor", app.ui.editor.value.startsWith("select"));
107 |     assert("shows result", app.ui.result.innerText.includes("Diane"));
108 | }
109 | 
110 | async function testLoadGistInvalid() {
111 |     log("Load invalid gist...");
112 |     const app = await loadApp();
113 |     app.window.location.assign("../index.html#gist:42");
114 |     await wait(LONG_DELAY);
115 |     assert("shows error", app.ui.status.value.includes("Failed to load"));
116 |     assert("editor is empty", app.ui.editor.value == "");
117 |     assert("result is empty", app.ui.result.innerText == "");
118 | }
119 | 
120 | async function testShowTables() {
121 |     log("Show tables...");
122 |     const app = await loadApp();
123 |     app.window.location.assign("../index.html#demo.db");
124 |     await wait(MEDIUM_DELAY);
125 |     app.ui.buttons.showTables.click();
126 |     await wait(MEDIUM_DELAY);
127 |     assert("shows table count", app.ui.status.value == "2 tables:");
128 |     assert("shows table list", app.ui.result.innerText.includes("employees"));
129 |     const btn = app.ui.result.querySelector('[data-action="showTable"]');
130 |     btn.click();
131 |     await wait(MEDIUM_DELAY);
132 |     assert("shows table navbar", app.ui.status.value == "tables / employees:");
133 |     assert(
134 |         "shows table columns",
135 |         app.ui.result.innerText.includes("department")
136 |     );
137 | }
138 | 
139 | async function testSaveEmpty() {
140 |     log("Save empty snippet...");
141 |     const app = await loadApp();
142 | 
143 |     // activate buttons
144 |     app.ui.editor.dispatchEvent(new Event("input"));
145 |     app.ui.editor.value = "";
146 |     app.ui.buttons.save.click();
147 |     await wait(MEDIUM_DELAY);
148 |     assert(
149 |         "fails to save empty snippet",
150 |         app.ui.status.value.startsWith("Failed to save")
151 |     );
152 | }
153 | 
154 | async function testSave() {
155 |     log("Save snippet...");
156 |     const app = await loadApp();
157 | 
158 |     mock(app.gister, "create", (name, schema, query) => {
159 |         assert("before save: database name is not set", name == "new.db");
160 |         assert("before save: database schema is empty", schema == EMPTY_SCHEMA);
161 |         assert("before save: database query equals query text", query == sql);
162 |         const gist = buildGist(name, schema, query);
163 |         return Promise.resolve(gist);
164 |     });
165 | 
166 |     const sql = "select 'hello' as message";
167 |     // activate buttons
168 |     app.ui.editor.dispatchEvent(new Event("input"));
169 |     app.ui.editor.value = sql;
170 |     app.ui.buttons.save.click();
171 |     await wait(MEDIUM_DELAY);
172 |     assert(
173 |         "after save: database named after gist id",
174 |         app.ui.name.value == "424242.db"
175 |     );
176 |     assert(
177 |         "after save: shows successful status",
178 |         app.ui.status.value.includes("✓ Saved")
179 |     );
180 | 
181 |     unmock(app.gister, "create");
182 | }
183 | 
184 | async function testUpdate() {
185 |     log("Update snippet...");
186 |     const app = await loadApp();
187 | 
188 |     const sql1 = "select 'created' as message";
189 |     const sql2 = "select 'updated' as message";
190 | 
191 |     mock(app.gister, "create", (name, schema, query) => {
192 |         const gist = buildGist(name, schema, query);
193 |         return Promise.resolve(gist);
194 |     });
195 | 
196 |     mock(app.gister, "update", (id, name, schema, query) => {
197 |         assert("before save: database name is set", name == "424242.db");
198 |         assert("before save: database schema is empty", schema == "");
199 |         assert(
200 |             "before save: database query equals updated text",
201 |             query == sql2
202 |         );
203 |         const gist = buildGist(id, name, schema, query);
204 |         return Promise.resolve(gist);
205 |     });
206 | 
207 |     // activate buttons
208 |     app.ui.editor.dispatchEvent(new Event("input"));
209 | 
210 |     // create
211 |     app.ui.editor.value = sql1;
212 |     app.ui.buttons.save.click();
213 |     await wait(MEDIUM_DELAY);
214 | 
215 |     // update
216 |     app.ui.editor.value = sql2;
217 |     app.ui.buttons.save.click();
218 |     await wait(MEDIUM_DELAY);
219 | 
220 |     assert(
221 |         "after save: shows successful status",
222 |         app.ui.status.value.includes("✓ Saved")
223 |     );
224 | 
225 |     unmock(app.gister, "create");
226 | }
227 | 
228 | async function testChangeName() {
229 |     log("Change database name...");
230 |     const app = await loadApp();
231 |     const name = "my.db";
232 |     app.ui.name.value = name;
233 |     app.ui.name.dispatchEvent(new Event("change"));
234 |     await wait(SMALL_DELAY);
235 |     assert("shows updated name", app.ui.name.value == "my.db");
236 | }
237 | 
238 | async function runTests() {
239 |     log("Running tests...");
240 |     await testNewDatabase();
241 |     await testExecuteQuery();
242 |     await testExecuteSelection();
243 |     await testLoadDemo();
244 |     await testLoadUrl();
245 |     await testLoadUrlInvalid();
246 |     await testLoadGist();
247 |     await testLoadGistInvalid();
248 |     await testShowTables();
249 |     await testSaveEmpty();
250 |     await testSave();
251 |     await testUpdate();
252 |     await testChangeName();
253 |     summary();
254 | }
255 | 
256 | async function loadApp(timeout = LONG_DELAY) {
257 |     localStorage.removeItem("sqlime.query.new.db");
258 |     localStorage.removeItem("sqlime.query.demo.db");
259 |     const app = {};
260 |     app.frame = document.querySelector("#app");
261 |     app.frame.src = "../index.html";
262 |     await wait(timeout);
263 |     app.window = app.frame.contentWindow;
264 |     app.document = app.window.document;
265 |     app.actions = app.window.app.actions;
266 |     app.gister = app.window.app.gister;
267 |     app.ui = app.window.app.ui;
268 |     return app;
269 | }
270 | 
271 | function selectText(app, el, start, end) {
272 |     const range = app.document.createRange();
273 |     range.setStart(el.firstChild, start);
274 |     range.setEnd(el.firstChild, end);
275 |     const selection = app.window.getSelection();
276 |     selection.removeAllRanges();
277 |     selection.addRange(range);
278 | }
279 | 
280 | function buildGist(name, schema = "", query = "") {
281 |     return {
282 |         id: "424242131313",
283 |         name: name,
284 |         owner: "test",
285 |         schema: schema,
286 |         query: query,
287 |     };
288 | }
289 | 
290 | runTests();
291 | 


--------------------------------------------------------------------------------
/test/tester.js:
--------------------------------------------------------------------------------
 1 | const engine = {
 2 |     errorCount: 0,
 3 |     console: document.querySelector("#console"),
 4 |     mocked: {},
 5 | };
 6 | 
 7 | function assert(desc, condition) {
 8 |     if (condition) {
 9 |         log(`  ✔ ${desc}`);
10 |     } else {
11 |         engine.errorCount++;
12 |         log(`  ✘ ${desc}`);
13 |     }
14 | }
15 | 
16 | function log(message) {
17 |     const line = document.createTextNode(message + "\n");
18 |     engine.console.appendChild(line);
19 |     console.log(message);
20 | }
21 | 
22 | function summary() {
23 |     if (engine.errorCount) {
24 |         log(`✘ FAILED with ${engine.errorCount} errors`);
25 |     } else {
26 |         log("✔ All tests passed");
27 |     }
28 | }
29 | 
30 | function wait(ms) {
31 |     return new Promise((resolve, reject) => {
32 |         setTimeout(() => {
33 |             resolve(ms);
34 |         }, ms);
35 |     });
36 | }
37 | 
38 | function mock(obj, property, repacement) {
39 |     obj[`${property}.mocked`] = obj[property];
40 |     obj[property] = repacement;
41 | }
42 | 
43 | function unmock(obj, property) {
44 |     obj[property] = obj[`${property}.mocked`];
45 |     delete obj[`${property}.mocked`];
46 | }
47 | 
48 | export { assert, log, mock, unmock, summary, wait };
49 | 


--------------------------------------------------------------------------------