stratum-sqlite 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/stratum-sqlite.esm.js +240 -0
- package/dist/stratum-sqlite.umd.js +247 -0
- package/package.json +51 -0
- package/src/index.js +237 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YOUR_NAME
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# stratum-sqlite
|
|
2
|
+
|
|
3
|
+
Load and query a **read-only SQLite database on any static website** — plain HTML,
|
|
4
|
+
Quarto, Jekyll, Hugo, and more. No server. No backend.
|
|
5
|
+
|
|
6
|
+
The database file is fetched once from any public URL (GitHub Releases, Zenodo,
|
|
7
|
+
your own GitHub Pages, Cloudflare R2, …) and stored in the browser's **Cache API**,
|
|
8
|
+
so every page on your site after the first one loads it instantly from local cache.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Your browser Your static site server
|
|
16
|
+
│ │
|
|
17
|
+
│ first visit │
|
|
18
|
+
├─────── GET /libs/sqljs/sql-wasm.js ────►│
|
|
19
|
+
├─────── GET /libs/sqljs/sql-wasm.wasm ──►│
|
|
20
|
+
├─────── GET /data/mydb.sqlite ──────────►│
|
|
21
|
+
│ │
|
|
22
|
+
│ second visit (and all other pages) │
|
|
23
|
+
│ ◄── served from browser Cache API ─────┤ (no network request!)
|
|
24
|
+
│ │
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
sql.js compiles SQLite to WebAssembly and runs it entirely in the browser.
|
|
28
|
+
`stratum-sqlite` wraps sql.js with a simple `open()` / `query()` API and adds
|
|
29
|
+
transparent caching.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
### For the demo site (local preview)
|
|
36
|
+
|
|
37
|
+
The fastest way to get the demo running locally:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/stratum-toolkit/stratum-sqlite
|
|
41
|
+
cd stratum-sqlite
|
|
42
|
+
node build.mjs # build the library
|
|
43
|
+
bash setup.sh # download sql.js binaries into docs/libs/sqljs/
|
|
44
|
+
python3 -m http.server 8000 --directory docs
|
|
45
|
+
# open http://localhost:8000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Option A — Self-hosted (for your own project)
|
|
49
|
+
|
|
50
|
+
1. **Download sql.js and the library** using `setup.sh` (if you have the repo),
|
|
51
|
+
or manually:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
mkdir -p libs/sqljs
|
|
55
|
+
curl -sSfL https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js \
|
|
56
|
+
-o libs/sqljs/sql-wasm.js
|
|
57
|
+
curl -sSfL https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.wasm \
|
|
58
|
+
-o libs/sqljs/sql-wasm.wasm
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then download `stratum-sqlite.umd.js` from the
|
|
62
|
+
[Releases page](https://github.com/stratum-toolkit/stratum-sqlite/releases) and
|
|
63
|
+
place it alongside sql.js:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
libs/
|
|
67
|
+
└── sqljs/
|
|
68
|
+
├── sql-wasm.js
|
|
69
|
+
├── sql-wasm.wasm
|
|
70
|
+
└── stratum-sqlite.umd.js
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. **Use it in your HTML**:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<script src="libs/sqljs/sql-wasm.js"></script>
|
|
77
|
+
<script src="libs/sqljs/stratum-sqlite.umd.js"></script>
|
|
78
|
+
|
|
79
|
+
<script type="module">
|
|
80
|
+
const db = await StratumSQLite.open("data/mydb.sqlite", {
|
|
81
|
+
sqlJsPath: "libs/sqljs/",
|
|
82
|
+
cacheKey: "mydb@v1", // bump version when you publish a new DB
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const rows = db.query("SELECT * FROM countries WHERE region = ?", ["Europe"]);
|
|
86
|
+
console.log(rows);
|
|
87
|
+
</script>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Option B — npm + bundler
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install stratum-sqlite
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import StratumSQLite from 'stratum-sqlite';
|
|
98
|
+
|
|
99
|
+
const db = await StratumSQLite.open("/data/mydb.sqlite", {
|
|
100
|
+
sqlJsPath: "/libs/sqljs/",
|
|
101
|
+
cacheKey: "mydb@v1",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const rows = db.query("SELECT * FROM countries");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## API
|
|
110
|
+
|
|
111
|
+
### `StratumSQLite.open(url, options)` → `Promise<Database>`
|
|
112
|
+
|
|
113
|
+
Fetches the SQLite file at `url` and returns a `Database` instance.
|
|
114
|
+
|
|
115
|
+
| Option | Type | Default | Description |
|
|
116
|
+
|--------|------|---------|-------------|
|
|
117
|
+
| `sqlJsPath` | `string` | cdnjs URL | Path (relative or absolute) to the folder containing `sql-wasm.js` and `sql-wasm.wasm`. |
|
|
118
|
+
| `cacheKey` | `string` | `"stratum-sqlite:<url>@1"` | Browser Cache API bucket name. Bump the suffix (e.g. `@v2`) to force a re-download on your next publish. |
|
|
119
|
+
| `onProgress` | `function(loaded, total)` | — | Called during the first download to update a loading bar. |
|
|
120
|
+
|
|
121
|
+
### `db.query(sql, params?)` → `Array<Object>`
|
|
122
|
+
|
|
123
|
+
Runs a SQL statement and returns rows as plain objects.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
|
|
127
|
+
// → [{ name: "Norway", capital: "Oslo" }, …]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `db.tables()` → `string[]`
|
|
131
|
+
|
|
132
|
+
Returns the names of all user tables.
|
|
133
|
+
|
|
134
|
+
### `db.columns(tableName)` → `Array<Object>`
|
|
135
|
+
|
|
136
|
+
Returns column definitions from `PRAGMA table_info`.
|
|
137
|
+
|
|
138
|
+
### `db.count(tableName)` → `number`
|
|
139
|
+
|
|
140
|
+
Returns the total row count of a table.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Quarto / ObservableJS integration
|
|
145
|
+
|
|
146
|
+
Add `_sqljs-init.html` to your project root:
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<!-- _sqljs-init.html -->
|
|
150
|
+
<script>
|
|
151
|
+
(function () {
|
|
152
|
+
// Detect page depth by reading the relative path of any Quarto site_libs script.
|
|
153
|
+
var siteLibScript = document.querySelector('script[src*="site_libs/"]');
|
|
154
|
+
var root = siteLibScript
|
|
155
|
+
? siteLibScript.getAttribute('src').replace(/site_libs\/.*$/, '')
|
|
156
|
+
: '';
|
|
157
|
+
|
|
158
|
+
window._dbPath = root + 'data/mydb.sqlite';
|
|
159
|
+
window._sqljsBase = root + 'libs/sqljs/';
|
|
160
|
+
|
|
161
|
+
window._sqlJsReady = new Promise(function (resolve, reject) {
|
|
162
|
+
var s = document.createElement('script');
|
|
163
|
+
s.src = window._sqljsBase + 'sql-wasm.js';
|
|
164
|
+
s.onload = function () {
|
|
165
|
+
initSqlJs({ locateFile: function () { return window._sqljsBase + 'sql-wasm.wasm'; } })
|
|
166
|
+
.then(resolve).catch(reject);
|
|
167
|
+
};
|
|
168
|
+
s.onerror = function () { reject(new Error('sql.js load failed: ' + s.src)); };
|
|
169
|
+
document.head.appendChild(s);
|
|
170
|
+
});
|
|
171
|
+
}());
|
|
172
|
+
</script>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
In `_quarto.yml`:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
format:
|
|
179
|
+
html:
|
|
180
|
+
include-in-header: _sqljs-init.html
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
In any `.qmd` file:
|
|
184
|
+
|
|
185
|
+
```{ojs}
|
|
186
|
+
db = {
|
|
187
|
+
const SqlJs = await window._sqlJsReady;
|
|
188
|
+
const r = await fetch(window._dbPath);
|
|
189
|
+
const raw = new SqlJs.Database(new Uint8Array(await r.arrayBuffer()));
|
|
190
|
+
return {
|
|
191
|
+
query: sql => {
|
|
192
|
+
const res = raw.exec(sql);
|
|
193
|
+
if (!res.length) return [];
|
|
194
|
+
const { columns, values } = res[0];
|
|
195
|
+
return values.map(row =>
|
|
196
|
+
Object.fromEntries(columns.map((c, i) => [c, row[i]])));
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
rows = (await db).query("SELECT * FROM countries")
|
|
202
|
+
Inputs.table(rows)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Hosting your database
|
|
208
|
+
|
|
209
|
+
Your SQLite file can be hosted anywhere with public HTTPS and
|
|
210
|
+
`Access-Control-Allow-Origin: *`:
|
|
211
|
+
|
|
212
|
+
| Host | Free | Max size | CORS |
|
|
213
|
+
|------|------|----------|------|
|
|
214
|
+
| **GitHub Pages** (same origin) | ✓ | 1 GB repo limit | same-origin |
|
|
215
|
+
| **GitHub Releases** | ✓ | 2 GB per file | ✓ automatic |
|
|
216
|
+
| **Zenodo** | ✓ | 50 GB | ✓ |
|
|
217
|
+
| **Figshare** | ✓ | 20 GB | ✓ |
|
|
218
|
+
| **Cloudflare R2** | free tier | unlimited | configure bucket policy |
|
|
219
|
+
| **AWS S3** | free tier | unlimited | configure bucket policy |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Updating the database
|
|
224
|
+
|
|
225
|
+
1. Publish the new `.sqlite` file to your chosen host.
|
|
226
|
+
2. Bump the `cacheKey` option: `"mydb@v1"` → `"mydb@v2"`.
|
|
227
|
+
`stratum-sqlite` automatically evicts the old cached file on next page load.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Developing this library
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
git clone https://github.com/stratum-toolkit/stratum-sqlite
|
|
235
|
+
cd stratum-sqlite
|
|
236
|
+
|
|
237
|
+
# 1. Build dist bundles
|
|
238
|
+
node build.mjs
|
|
239
|
+
|
|
240
|
+
# 2. Download sql.js binaries and copy the built library (one command)
|
|
241
|
+
bash setup.sh
|
|
242
|
+
|
|
243
|
+
# 3. Create sample database if needed
|
|
244
|
+
python3 scripts/create_demo_db.py # Python
|
|
245
|
+
Rscript scripts/create_demo_db.R # R
|
|
246
|
+
|
|
247
|
+
# 4. Serve demo site
|
|
248
|
+
python3 -m http.server 8000 --directory docs
|
|
249
|
+
# open http://localhost:8000
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`setup.sh` downloads `sql-wasm.js` and `sql-wasm.wasm` from cdnjs into
|
|
253
|
+
`docs/libs/sqljs/` and copies the built library there. These binary files
|
|
254
|
+
are in `.gitignore` — the CI pipeline downloads them fresh on every deploy.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/* stratum-sqlite v0.1.2 | MIT license | https://github.com/stratum-toolkit/stratum-sqlite */
|
|
2
|
+
/**
|
|
3
|
+
* stratum-sqlite
|
|
4
|
+
* Load and query a read-only SQLite database on any static website.
|
|
5
|
+
*
|
|
6
|
+
* Works with plain HTML, Quarto / ObservableJS, Jekyll, Hugo, and any other
|
|
7
|
+
* static site generator. The database is fetched once and cached in the
|
|
8
|
+
* browser's Cache API so repeat visits (and other pages on the same site)
|
|
9
|
+
* skip the network entirely.
|
|
10
|
+
*
|
|
11
|
+
* @license MIT
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ─── Default sql.js location ────────────────────────────────────────────────
|
|
15
|
+
// Users can override this with the sqlJsPath option when calling open().
|
|
16
|
+
// The default points to cdnjs, but for offline / restricted environments you
|
|
17
|
+
// should download sql-wasm.js and sql-wasm.wasm and serve them yourself.
|
|
18
|
+
const DEFAULT_SQLJS_CDN =
|
|
19
|
+
'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/';
|
|
20
|
+
|
|
21
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Inject a <script> tag and return a promise that resolves when it loads.
|
|
25
|
+
* Skips injection if a tag with the same src already exists.
|
|
26
|
+
*/
|
|
27
|
+
function loadScript(src) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
30
|
+
resolve();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const s = document.createElement('script');
|
|
34
|
+
s.src = src;
|
|
35
|
+
s.crossOrigin = 'anonymous';
|
|
36
|
+
s.onload = resolve;
|
|
37
|
+
s.onerror = () => reject(new Error(`stratum-sqlite: failed to load script: ${src}`));
|
|
38
|
+
document.head.appendChild(s);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fetch a URL as a Uint8Array, using the browser Cache API when available.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} url - URL of the resource to fetch
|
|
46
|
+
* @param {string} cacheKey - Cache storage name (bump to force re-download)
|
|
47
|
+
* @param {function} onProgress - Optional callback(loaded, total) for progress
|
|
48
|
+
*/
|
|
49
|
+
async function fetchWithCache(url, cacheKey, onProgress) {
|
|
50
|
+
// Cache API requires a secure context (HTTPS or localhost).
|
|
51
|
+
// Fall back to a plain fetch when unavailable (e.g. file://).
|
|
52
|
+
if (!('caches' in window)) {
|
|
53
|
+
return plainFetch(url, onProgress);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cache = await caches.open(cacheKey);
|
|
57
|
+
|
|
58
|
+
// Evict caches from older versions of the same key prefix.
|
|
59
|
+
// Convention: keys are "stratum-sqlite:<name>@<version>"
|
|
60
|
+
const prefix = cacheKey.replace(/@[^@]*$/, '@');
|
|
61
|
+
const allKeys = await caches.keys();
|
|
62
|
+
for (const key of allKeys) {
|
|
63
|
+
if (key.startsWith(prefix) && key !== cacheKey) {
|
|
64
|
+
await caches.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let cached = await cache.match(url);
|
|
69
|
+
if (!cached) {
|
|
70
|
+
const response = await fetch(url);
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
|
|
73
|
+
}
|
|
74
|
+
await cache.put(url, response.clone());
|
|
75
|
+
cached = await cache.match(url);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return streamToUint8Array(cached, onProgress);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function plainFetch(url, onProgress) {
|
|
82
|
+
const response = await fetch(url);
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
|
|
85
|
+
}
|
|
86
|
+
return streamToUint8Array(response, onProgress);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read a Response body as Uint8Array, reporting progress if the callback and
|
|
91
|
+
* Content-Length header are both available.
|
|
92
|
+
*/
|
|
93
|
+
async function streamToUint8Array(response, onProgress) {
|
|
94
|
+
const total = Number(response.headers.get('content-length')) || 0;
|
|
95
|
+
|
|
96
|
+
if (!onProgress || !total || !response.body) {
|
|
97
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const reader = response.body.getReader();
|
|
101
|
+
const chunks = [];
|
|
102
|
+
let loaded = 0;
|
|
103
|
+
|
|
104
|
+
while (true) {
|
|
105
|
+
const { done, value } = await reader.read();
|
|
106
|
+
if (done) break;
|
|
107
|
+
chunks.push(value);
|
|
108
|
+
loaded += value.length;
|
|
109
|
+
onProgress(loaded, total);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = new Uint8Array(loaded);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const chunk of chunks) {
|
|
115
|
+
result.set(chunk, offset);
|
|
116
|
+
offset += chunk.length;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Database wrapper ────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
class Database {
|
|
124
|
+
/** @param {object} sqlJsDb - raw sql.js Database instance */
|
|
125
|
+
constructor(sqlJsDb) {
|
|
126
|
+
this._db = sqlJsDb;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Run a SQL query and return results as an array of plain objects.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} sql - SQL statement (SELECT …)
|
|
133
|
+
* @param {Array} params - Optional positional parameters (? placeholders)
|
|
134
|
+
* @returns {Array<Object>}
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
|
|
138
|
+
*/
|
|
139
|
+
query(sql, params) {
|
|
140
|
+
const results = this._db.exec(sql, params);
|
|
141
|
+
if (!results.length) return [];
|
|
142
|
+
const { columns, values } = results[0];
|
|
143
|
+
return values.map(row =>
|
|
144
|
+
Object.fromEntries(columns.map((col, i) => [col, row[i]]))
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Return the names of all user tables in the database.
|
|
150
|
+
* @returns {string[]}
|
|
151
|
+
*/
|
|
152
|
+
tables() {
|
|
153
|
+
return this.query(
|
|
154
|
+
"SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
155
|
+
).map(r => r.name);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Return column definitions for a table.
|
|
160
|
+
* @param {string} tableName
|
|
161
|
+
* @returns {Array<{cid, name, type, notnull, dflt_value, pk}>}
|
|
162
|
+
*/
|
|
163
|
+
columns(tableName) {
|
|
164
|
+
return this.query(`PRAGMA table_info("${tableName}")`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Count rows in a table.
|
|
169
|
+
* @param {string} tableName
|
|
170
|
+
* @returns {number}
|
|
171
|
+
*/
|
|
172
|
+
count(tableName) {
|
|
173
|
+
return this.query(`SELECT COUNT(*) as n FROM "${tableName}"`)[0]?.n ?? 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @typedef {Object} OpenOptions
|
|
181
|
+
* @property {string} [sqlJsPath] - Base path/URL for sql-wasm.js and sql-wasm.wasm.
|
|
182
|
+
* Defaults to cdnjs. Set to a local path like
|
|
183
|
+
* "/libs/sqljs/" when serving sql.js yourself.
|
|
184
|
+
* @property {string} [cacheKey] - Browser Cache API key.
|
|
185
|
+
* Defaults to "stratum-sqlite:<url>@1".
|
|
186
|
+
* Bump the version suffix to force a re-download
|
|
187
|
+
* when you publish a new database release.
|
|
188
|
+
* @property {function} [onProgress] - Called with (bytesLoaded, bytesTotal) during
|
|
189
|
+
* the first download. Not called on cache hits.
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Fetch a SQLite database from `url` and return a Database instance ready to
|
|
194
|
+
* query. On first call the file is downloaded and stored in the browser's
|
|
195
|
+
* Cache API. Subsequent calls (including from other pages on the same origin)
|
|
196
|
+
* are served instantly from cache.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} url - URL of the .sqlite file (absolute or relative)
|
|
199
|
+
* @param {OpenOptions} options
|
|
200
|
+
* @returns {Promise<Database>}
|
|
201
|
+
*
|
|
202
|
+
* @example <caption>Plain HTML</caption>
|
|
203
|
+
* const db = await StratumSQLite.open("data/mydb.sqlite", {
|
|
204
|
+
* sqlJsPath: "libs/sqljs/",
|
|
205
|
+
* cacheKey: "mydb@v1.2",
|
|
206
|
+
* });
|
|
207
|
+
* const rows = db.query("SELECT * FROM countries");
|
|
208
|
+
*
|
|
209
|
+
* @example <caption>Quarto / ObservableJS cell</caption>
|
|
210
|
+
* db = StratumSQLite.open(window._dbPath, { sqlJsPath: window._sqljsBase })
|
|
211
|
+
* rows = (await db).query("SELECT * FROM countries")
|
|
212
|
+
*/
|
|
213
|
+
async function open(url, options = {}) {
|
|
214
|
+
const sqlJsBase = options.sqlJsPath || DEFAULT_SQLJS_CDN;
|
|
215
|
+
const cacheKey = options.cacheKey || `stratum-sqlite:${url}@1`;
|
|
216
|
+
const onProgress = options.onProgress || null;
|
|
217
|
+
|
|
218
|
+
// Ensure trailing slash on sqlJsBase
|
|
219
|
+
const base = sqlJsBase.endsWith('/') ? sqlJsBase : sqlJsBase + '/';
|
|
220
|
+
|
|
221
|
+
// Load the sql.js bootstrap script (sets global `initSqlJs`)
|
|
222
|
+
await loadScript(base + 'sql-wasm.js');
|
|
223
|
+
|
|
224
|
+
// Initialise the WASM module
|
|
225
|
+
const SQL = await initSqlJs({ // eslint-disable-line no-undef
|
|
226
|
+
locateFile: () => base + 'sql-wasm.wasm',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Fetch the database (cache hit after first load)
|
|
230
|
+
const bytes = await fetchWithCache(url, cacheKey, onProgress);
|
|
231
|
+
|
|
232
|
+
return new Database(new SQL.Database(bytes));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const StratumSQLite = { open, Database };
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
export { open, Database };
|
|
240
|
+
export default StratumSQLite;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/* stratum-sqlite v0.1.2 | MIT license | https://github.com/stratum-toolkit/stratum-sqlite */
|
|
2
|
+
(function (global, factory) {
|
|
3
|
+
typeof exports === 'object' && typeof module !== 'undefined'
|
|
4
|
+
? module.exports = factory()
|
|
5
|
+
: (global = typeof globalThis !== 'undefined' ? globalThis : global || self,
|
|
6
|
+
global.StratumSQLite = factory());
|
|
7
|
+
})(this, function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
/**
|
|
10
|
+
* stratum-sqlite
|
|
11
|
+
* Load and query a read-only SQLite database on any static website.
|
|
12
|
+
*
|
|
13
|
+
* Works with plain HTML, Quarto / ObservableJS, Jekyll, Hugo, and any other
|
|
14
|
+
* static site generator. The database is fetched once and cached in the
|
|
15
|
+
* browser's Cache API so repeat visits (and other pages on the same site)
|
|
16
|
+
* skip the network entirely.
|
|
17
|
+
*
|
|
18
|
+
* @license MIT
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ─── Default sql.js location ────────────────────────────────────────────────
|
|
22
|
+
// Users can override this with the sqlJsPath option when calling open().
|
|
23
|
+
// The default points to cdnjs, but for offline / restricted environments you
|
|
24
|
+
// should download sql-wasm.js and sql-wasm.wasm and serve them yourself.
|
|
25
|
+
const DEFAULT_SQLJS_CDN =
|
|
26
|
+
'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/';
|
|
27
|
+
|
|
28
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inject a <script> tag and return a promise that resolves when it loads.
|
|
32
|
+
* Skips injection if a tag with the same src already exists.
|
|
33
|
+
*/
|
|
34
|
+
function loadScript(src) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
37
|
+
resolve();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const s = document.createElement('script');
|
|
41
|
+
s.src = src;
|
|
42
|
+
s.crossOrigin = 'anonymous';
|
|
43
|
+
s.onload = resolve;
|
|
44
|
+
s.onerror = () => reject(new Error(`stratum-sqlite: failed to load script: ${src}`));
|
|
45
|
+
document.head.appendChild(s);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fetch a URL as a Uint8Array, using the browser Cache API when available.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} url - URL of the resource to fetch
|
|
53
|
+
* @param {string} cacheKey - Cache storage name (bump to force re-download)
|
|
54
|
+
* @param {function} onProgress - Optional callback(loaded, total) for progress
|
|
55
|
+
*/
|
|
56
|
+
async function fetchWithCache(url, cacheKey, onProgress) {
|
|
57
|
+
// Cache API requires a secure context (HTTPS or localhost).
|
|
58
|
+
// Fall back to a plain fetch when unavailable (e.g. file://).
|
|
59
|
+
if (!('caches' in window)) {
|
|
60
|
+
return plainFetch(url, onProgress);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cache = await caches.open(cacheKey);
|
|
64
|
+
|
|
65
|
+
// Evict caches from older versions of the same key prefix.
|
|
66
|
+
// Convention: keys are "stratum-sqlite:<name>@<version>"
|
|
67
|
+
const prefix = cacheKey.replace(/@[^@]*$/, '@');
|
|
68
|
+
const allKeys = await caches.keys();
|
|
69
|
+
for (const key of allKeys) {
|
|
70
|
+
if (key.startsWith(prefix) && key !== cacheKey) {
|
|
71
|
+
await caches.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let cached = await cache.match(url);
|
|
76
|
+
if (!cached) {
|
|
77
|
+
const response = await fetch(url);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
|
|
80
|
+
}
|
|
81
|
+
await cache.put(url, response.clone());
|
|
82
|
+
cached = await cache.match(url);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return streamToUint8Array(cached, onProgress);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function plainFetch(url, onProgress) {
|
|
89
|
+
const response = await fetch(url);
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
|
|
92
|
+
}
|
|
93
|
+
return streamToUint8Array(response, onProgress);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read a Response body as Uint8Array, reporting progress if the callback and
|
|
98
|
+
* Content-Length header are both available.
|
|
99
|
+
*/
|
|
100
|
+
async function streamToUint8Array(response, onProgress) {
|
|
101
|
+
const total = Number(response.headers.get('content-length')) || 0;
|
|
102
|
+
|
|
103
|
+
if (!onProgress || !total || !response.body) {
|
|
104
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const reader = response.body.getReader();
|
|
108
|
+
const chunks = [];
|
|
109
|
+
let loaded = 0;
|
|
110
|
+
|
|
111
|
+
while (true) {
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (done) break;
|
|
114
|
+
chunks.push(value);
|
|
115
|
+
loaded += value.length;
|
|
116
|
+
onProgress(loaded, total);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = new Uint8Array(loaded);
|
|
120
|
+
let offset = 0;
|
|
121
|
+
for (const chunk of chunks) {
|
|
122
|
+
result.set(chunk, offset);
|
|
123
|
+
offset += chunk.length;
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Database wrapper ────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
class Database {
|
|
131
|
+
/** @param {object} sqlJsDb - raw sql.js Database instance */
|
|
132
|
+
constructor(sqlJsDb) {
|
|
133
|
+
this._db = sqlJsDb;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run a SQL query and return results as an array of plain objects.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} sql - SQL statement (SELECT …)
|
|
140
|
+
* @param {Array} params - Optional positional parameters (? placeholders)
|
|
141
|
+
* @returns {Array<Object>}
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
|
|
145
|
+
*/
|
|
146
|
+
query(sql, params) {
|
|
147
|
+
const results = this._db.exec(sql, params);
|
|
148
|
+
if (!results.length) return [];
|
|
149
|
+
const { columns, values } = results[0];
|
|
150
|
+
return values.map(row =>
|
|
151
|
+
Object.fromEntries(columns.map((col, i) => [col, row[i]]))
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Return the names of all user tables in the database.
|
|
157
|
+
* @returns {string[]}
|
|
158
|
+
*/
|
|
159
|
+
tables() {
|
|
160
|
+
return this.query(
|
|
161
|
+
"SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
162
|
+
).map(r => r.name);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Return column definitions for a table.
|
|
167
|
+
* @param {string} tableName
|
|
168
|
+
* @returns {Array<{cid, name, type, notnull, dflt_value, pk}>}
|
|
169
|
+
*/
|
|
170
|
+
columns(tableName) {
|
|
171
|
+
return this.query(`PRAGMA table_info("${tableName}")`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Count rows in a table.
|
|
176
|
+
* @param {string} tableName
|
|
177
|
+
* @returns {number}
|
|
178
|
+
*/
|
|
179
|
+
count(tableName) {
|
|
180
|
+
return this.query(`SELECT COUNT(*) as n FROM "${tableName}"`)[0]?.n ?? 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @typedef {Object} OpenOptions
|
|
188
|
+
* @property {string} [sqlJsPath] - Base path/URL for sql-wasm.js and sql-wasm.wasm.
|
|
189
|
+
* Defaults to cdnjs. Set to a local path like
|
|
190
|
+
* "/libs/sqljs/" when serving sql.js yourself.
|
|
191
|
+
* @property {string} [cacheKey] - Browser Cache API key.
|
|
192
|
+
* Defaults to "stratum-sqlite:<url>@1".
|
|
193
|
+
* Bump the version suffix to force a re-download
|
|
194
|
+
* when you publish a new database release.
|
|
195
|
+
* @property {function} [onProgress] - Called with (bytesLoaded, bytesTotal) during
|
|
196
|
+
* the first download. Not called on cache hits.
|
|
197
|
+
*/
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Fetch a SQLite database from `url` and return a Database instance ready to
|
|
201
|
+
* query. On first call the file is downloaded and stored in the browser's
|
|
202
|
+
* Cache API. Subsequent calls (including from other pages on the same origin)
|
|
203
|
+
* are served instantly from cache.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} url - URL of the .sqlite file (absolute or relative)
|
|
206
|
+
* @param {OpenOptions} options
|
|
207
|
+
* @returns {Promise<Database>}
|
|
208
|
+
*
|
|
209
|
+
* @example <caption>Plain HTML</caption>
|
|
210
|
+
* const db = await StratumSQLite.open("data/mydb.sqlite", {
|
|
211
|
+
* sqlJsPath: "libs/sqljs/",
|
|
212
|
+
* cacheKey: "mydb@v1.2",
|
|
213
|
+
* });
|
|
214
|
+
* const rows = db.query("SELECT * FROM countries");
|
|
215
|
+
*
|
|
216
|
+
* @example <caption>Quarto / ObservableJS cell</caption>
|
|
217
|
+
* db = StratumSQLite.open(window._dbPath, { sqlJsPath: window._sqljsBase })
|
|
218
|
+
* rows = (await db).query("SELECT * FROM countries")
|
|
219
|
+
*/
|
|
220
|
+
async function open(url, options = {}) {
|
|
221
|
+
const sqlJsBase = options.sqlJsPath || DEFAULT_SQLJS_CDN;
|
|
222
|
+
const cacheKey = options.cacheKey || `stratum-sqlite:${url}@1`;
|
|
223
|
+
const onProgress = options.onProgress || null;
|
|
224
|
+
|
|
225
|
+
// Ensure trailing slash on sqlJsBase
|
|
226
|
+
const base = sqlJsBase.endsWith('/') ? sqlJsBase : sqlJsBase + '/';
|
|
227
|
+
|
|
228
|
+
// Load the sql.js bootstrap script (sets global `initSqlJs`)
|
|
229
|
+
await loadScript(base + 'sql-wasm.js');
|
|
230
|
+
|
|
231
|
+
// Initialise the WASM module
|
|
232
|
+
const SQL = await initSqlJs({ // eslint-disable-line no-undef
|
|
233
|
+
locateFile: () => base + 'sql-wasm.wasm',
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Fetch the database (cache hit after first load)
|
|
237
|
+
const bytes = await fetchWithCache(url, cacheKey, onProgress);
|
|
238
|
+
|
|
239
|
+
return new Database(new SQL.Database(bytes));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const StratumSQLite = { open, Database };
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
return StratumSQLite;
|
|
247
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stratum-sqlite",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Load and query a read-only SQLite database on any static website (plain HTML, Quarto, Jekyll, Hugo, …). Zero server required.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sqlite",
|
|
7
|
+
"static-site",
|
|
8
|
+
"quarto",
|
|
9
|
+
"observablejs",
|
|
10
|
+
"indexeddb",
|
|
11
|
+
"sql"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Your Name <you@example.com>",
|
|
15
|
+
"homepage": "https://github.com/stratum-toolkit/stratum-sqlite#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/stratum-toolkit/stratum-sqlite.git"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "dist/stratum-sqlite.umd.js",
|
|
22
|
+
"module": "dist/stratum-sqlite.esm.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./dist/stratum-sqlite.esm.js",
|
|
26
|
+
"require": "./dist/stratum-sqlite.umd.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node build.mjs",
|
|
37
|
+
"dev": "node build.mjs --watch",
|
|
38
|
+
"prepare": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"esbuild": "^0.28.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"sql.js": ">=1.10.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"sql.js": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stratum-sqlite
|
|
3
|
+
* Load and query a read-only SQLite database on any static website.
|
|
4
|
+
*
|
|
5
|
+
* Works with plain HTML, Quarto / ObservableJS, Jekyll, Hugo, and any other
|
|
6
|
+
* static site generator. The database is fetched once and cached in the
|
|
7
|
+
* browser's Cache API so repeat visits (and other pages on the same site)
|
|
8
|
+
* skip the network entirely.
|
|
9
|
+
*
|
|
10
|
+
* @license MIT
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ─── Default sql.js location ────────────────────────────────────────────────
|
|
14
|
+
// Users can override this with the sqlJsPath option when calling open().
|
|
15
|
+
// The default points to cdnjs, but for offline / restricted environments you
|
|
16
|
+
// should download sql-wasm.js and sql-wasm.wasm and serve them yourself.
|
|
17
|
+
const DEFAULT_SQLJS_CDN =
|
|
18
|
+
'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/';
|
|
19
|
+
|
|
20
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Inject a <script> tag and return a promise that resolves when it loads.
|
|
24
|
+
* Skips injection if a tag with the same src already exists.
|
|
25
|
+
*/
|
|
26
|
+
function loadScript(src) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
29
|
+
resolve();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const s = document.createElement('script');
|
|
33
|
+
s.src = src;
|
|
34
|
+
s.crossOrigin = 'anonymous';
|
|
35
|
+
s.onload = resolve;
|
|
36
|
+
s.onerror = () => reject(new Error(`stratum-sqlite: failed to load script: ${src}`));
|
|
37
|
+
document.head.appendChild(s);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetch a URL as a Uint8Array, using the browser Cache API when available.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} url - URL of the resource to fetch
|
|
45
|
+
* @param {string} cacheKey - Cache storage name (bump to force re-download)
|
|
46
|
+
* @param {function} onProgress - Optional callback(loaded, total) for progress
|
|
47
|
+
*/
|
|
48
|
+
async function fetchWithCache(url, cacheKey, onProgress) {
|
|
49
|
+
// Cache API requires a secure context (HTTPS or localhost).
|
|
50
|
+
// Fall back to a plain fetch when unavailable (e.g. file://).
|
|
51
|
+
if (!('caches' in window)) {
|
|
52
|
+
return plainFetch(url, onProgress);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cache = await caches.open(cacheKey);
|
|
56
|
+
|
|
57
|
+
// Evict caches from older versions of the same key prefix.
|
|
58
|
+
// Convention: keys are "stratum-sqlite:<name>@<version>"
|
|
59
|
+
const prefix = cacheKey.replace(/@[^@]*$/, '@');
|
|
60
|
+
const allKeys = await caches.keys();
|
|
61
|
+
for (const key of allKeys) {
|
|
62
|
+
if (key.startsWith(prefix) && key !== cacheKey) {
|
|
63
|
+
await caches.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let cached = await cache.match(url);
|
|
68
|
+
if (!cached) {
|
|
69
|
+
const response = await fetch(url);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
|
|
72
|
+
}
|
|
73
|
+
await cache.put(url, response.clone());
|
|
74
|
+
cached = await cache.match(url);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return streamToUint8Array(cached, onProgress);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function plainFetch(url, onProgress) {
|
|
81
|
+
const response = await fetch(url);
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
|
|
84
|
+
}
|
|
85
|
+
return streamToUint8Array(response, onProgress);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read a Response body as Uint8Array, reporting progress if the callback and
|
|
90
|
+
* Content-Length header are both available.
|
|
91
|
+
*/
|
|
92
|
+
async function streamToUint8Array(response, onProgress) {
|
|
93
|
+
const total = Number(response.headers.get('content-length')) || 0;
|
|
94
|
+
|
|
95
|
+
if (!onProgress || !total || !response.body) {
|
|
96
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const reader = response.body.getReader();
|
|
100
|
+
const chunks = [];
|
|
101
|
+
let loaded = 0;
|
|
102
|
+
|
|
103
|
+
while (true) {
|
|
104
|
+
const { done, value } = await reader.read();
|
|
105
|
+
if (done) break;
|
|
106
|
+
chunks.push(value);
|
|
107
|
+
loaded += value.length;
|
|
108
|
+
onProgress(loaded, total);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = new Uint8Array(loaded);
|
|
112
|
+
let offset = 0;
|
|
113
|
+
for (const chunk of chunks) {
|
|
114
|
+
result.set(chunk, offset);
|
|
115
|
+
offset += chunk.length;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Database wrapper ────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
class Database {
|
|
123
|
+
/** @param {object} sqlJsDb - raw sql.js Database instance */
|
|
124
|
+
constructor(sqlJsDb) {
|
|
125
|
+
this._db = sqlJsDb;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run a SQL query and return results as an array of plain objects.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} sql - SQL statement (SELECT …)
|
|
132
|
+
* @param {Array} params - Optional positional parameters (? placeholders)
|
|
133
|
+
* @returns {Array<Object>}
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
|
|
137
|
+
*/
|
|
138
|
+
query(sql, params) {
|
|
139
|
+
const results = this._db.exec(sql, params);
|
|
140
|
+
if (!results.length) return [];
|
|
141
|
+
const { columns, values } = results[0];
|
|
142
|
+
return values.map(row =>
|
|
143
|
+
Object.fromEntries(columns.map((col, i) => [col, row[i]]))
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Return the names of all user tables in the database.
|
|
149
|
+
* @returns {string[]}
|
|
150
|
+
*/
|
|
151
|
+
tables() {
|
|
152
|
+
return this.query(
|
|
153
|
+
"SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
154
|
+
).map(r => r.name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Return column definitions for a table.
|
|
159
|
+
* @param {string} tableName
|
|
160
|
+
* @returns {Array<{cid, name, type, notnull, dflt_value, pk}>}
|
|
161
|
+
*/
|
|
162
|
+
columns(tableName) {
|
|
163
|
+
return this.query(`PRAGMA table_info("${tableName}")`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Count rows in a table.
|
|
168
|
+
* @param {string} tableName
|
|
169
|
+
* @returns {number}
|
|
170
|
+
*/
|
|
171
|
+
count(tableName) {
|
|
172
|
+
return this.query(`SELECT COUNT(*) as n FROM "${tableName}"`)[0]?.n ?? 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @typedef {Object} OpenOptions
|
|
180
|
+
* @property {string} [sqlJsPath] - Base path/URL for sql-wasm.js and sql-wasm.wasm.
|
|
181
|
+
* Defaults to cdnjs. Set to a local path like
|
|
182
|
+
* "/libs/sqljs/" when serving sql.js yourself.
|
|
183
|
+
* @property {string} [cacheKey] - Browser Cache API key.
|
|
184
|
+
* Defaults to "stratum-sqlite:<url>@1".
|
|
185
|
+
* Bump the version suffix to force a re-download
|
|
186
|
+
* when you publish a new database release.
|
|
187
|
+
* @property {function} [onProgress] - Called with (bytesLoaded, bytesTotal) during
|
|
188
|
+
* the first download. Not called on cache hits.
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Fetch a SQLite database from `url` and return a Database instance ready to
|
|
193
|
+
* query. On first call the file is downloaded and stored in the browser's
|
|
194
|
+
* Cache API. Subsequent calls (including from other pages on the same origin)
|
|
195
|
+
* are served instantly from cache.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} url - URL of the .sqlite file (absolute or relative)
|
|
198
|
+
* @param {OpenOptions} options
|
|
199
|
+
* @returns {Promise<Database>}
|
|
200
|
+
*
|
|
201
|
+
* @example <caption>Plain HTML</caption>
|
|
202
|
+
* const db = await StratumSQLite.open("data/mydb.sqlite", {
|
|
203
|
+
* sqlJsPath: "libs/sqljs/",
|
|
204
|
+
* cacheKey: "mydb@v1.2",
|
|
205
|
+
* });
|
|
206
|
+
* const rows = db.query("SELECT * FROM countries");
|
|
207
|
+
*
|
|
208
|
+
* @example <caption>Quarto / ObservableJS cell</caption>
|
|
209
|
+
* db = StratumSQLite.open(window._dbPath, { sqlJsPath: window._sqljsBase })
|
|
210
|
+
* rows = (await db).query("SELECT * FROM countries")
|
|
211
|
+
*/
|
|
212
|
+
async function open(url, options = {}) {
|
|
213
|
+
const sqlJsBase = options.sqlJsPath || DEFAULT_SQLJS_CDN;
|
|
214
|
+
const cacheKey = options.cacheKey || `stratum-sqlite:${url}@1`;
|
|
215
|
+
const onProgress = options.onProgress || null;
|
|
216
|
+
|
|
217
|
+
// Ensure trailing slash on sqlJsBase
|
|
218
|
+
const base = sqlJsBase.endsWith('/') ? sqlJsBase : sqlJsBase + '/';
|
|
219
|
+
|
|
220
|
+
// Load the sql.js bootstrap script (sets global `initSqlJs`)
|
|
221
|
+
await loadScript(base + 'sql-wasm.js');
|
|
222
|
+
|
|
223
|
+
// Initialise the WASM module
|
|
224
|
+
const SQL = await initSqlJs({ // eslint-disable-line no-undef
|
|
225
|
+
locateFile: () => base + 'sql-wasm.wasm',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Fetch the database (cache hit after first load)
|
|
229
|
+
const bytes = await fetchWithCache(url, cacheKey, onProgress);
|
|
230
|
+
|
|
231
|
+
return new Database(new SQL.Database(bytes));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const StratumSQLite = { open, Database };
|
|
235
|
+
|
|
236
|
+
export { open, Database };
|
|
237
|
+
export default StratumSQLite;
|