Skip to content

Commit aeb17c7

Browse files
sumimakitonekomeowww
authored andcommitted
feat: fs support with drizzle and duckdb-wasm (#30)
* feat: fs support with drizzle and duckdb-wasm * docs: fix lint errors * fix: apply suggestions * fix: lockfile
1 parent 9011d3e commit aeb17c7

File tree

18 files changed

+1425
-148
lines changed

18 files changed

+1425
-148
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ flowchart TD
8585
SVRT["@proj-airi/server-runtime"]
8686
MC_AGENT("Minecraft Agent")
8787
XSAI["xsai"]
88-
88+
8989
subgraph airi-vtuber
9090
DB0 --> DB1 --> DB2 --> CORE
9191
ICONS --> UI --> Stage --> CORE

cspell.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ words:
9797
- onnxruntime
9898
- openai
9999
- openrouter
100+
- OPFS
100101
- opusscript
101102
- pgvector
102103
- picklist

packages/drizzle-duckdb-wasm/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
"play:build": "vite build",
5656
"play:preview": "vite preview",
5757
"typecheck": "tsc --noEmit",
58-
"db:generate": "drizzle-kit generate"
58+
"db:generate": "drizzle-kit generate",
59+
"test": "vitest",
60+
"test:run": "vitest run"
5961
},
6062
"peerDependencies": {
6163
"web-worker": "^1.5.0"
@@ -67,7 +69,7 @@
6769
},
6870
"dependencies": {
6971
"@date-fns/tz": "^1.2.0",
70-
"@duckdb/duckdb-wasm": "^1.29.0",
72+
"@duckdb/duckdb-wasm": "1.29.1-dev68.0",
7173
"@proj-airi/duckdb-wasm": "workspace:^",
7274
"apache-arrow": "^19.0.1",
7375
"date-fns": "^4.1.0",
@@ -78,8 +80,10 @@
7880
"devDependencies": {
7981
"@unocss/reset": "^66.0.0",
8082
"@vitejs/plugin-vue": "^5.2.1",
83+
"@vitest/browser": "^3.0.6",
8184
"@vueuse/core": "^12.7.0",
8285
"drizzle-kit": "^0.30.4",
86+
"playwright": "^1.50.1",
8387
"superjson": "^2.2.2",
8488
"vite": "^6.1.1",
8589
"vue": "^3.5.13",

packages/drizzle-duckdb-wasm/playground/src/App.vue

Lines changed: 202 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,258 @@
11
<script setup lang="ts">
22
import type { DuckDBWasmDrizzleDatabase } from '../../src'
33
4-
import { useDebounceFn } from '@vueuse/core'
4+
import { DuckDBAccessMode } from '@duckdb/duckdb-wasm'
5+
import { DBStorageType } from '@proj-airi/duckdb-wasm'
56
import { serialize } from 'superjson'
6-
import { onMounted, onUnmounted, ref, watch } from 'vue'
7+
import { computed, onMounted, onUnmounted, ref } from 'vue'
78
89
import { drizzle } from '../../src'
10+
import { buildDSN } from '../../src/dsn'
911
import * as schema from '../db/schema'
1012
import { users } from '../db/schema'
1113
import migration1 from '../drizzle/0000_cute_kulan_gath.sql?raw'
1214
1315
const db = ref<DuckDBWasmDrizzleDatabase<typeof schema>>()
1416
const results = ref<Record<string, unknown>[]>()
1517
const schemaResults = ref<Record<string, unknown>[]>()
16-
const query = ref(`SELECT 1 + 1 AS result`)
18+
const isMigrated = ref(false)
1719
18-
onMounted(async () => {
19-
db.value = drizzle('duckdb-wasm://?bundles=import-url', { schema })
20+
const storage = ref<DBStorageType>()
21+
const path = ref('test.db')
22+
const logger = ref(true)
23+
const readOnly = ref(false)
24+
25+
const dsn = computed(() => {
26+
return buildDSN({
27+
scheme: 'duckdb-wasm:',
28+
bundles: 'import-url',
29+
logger: logger.value,
30+
...storage.value === DBStorageType.ORIGIN_PRIVATE_FS && {
31+
storage: {
32+
type: storage.value,
33+
path: path.value,
34+
accessMode: readOnly.value ? DuckDBAccessMode.READ_ONLY : DuckDBAccessMode.READ_WRITE,
35+
},
36+
},
37+
})
38+
})
39+
40+
const query = ref(`SELECT * FROM 'users'`)
41+
42+
async function connect() {
43+
isMigrated.value = false
44+
db.value = drizzle(dsn.value, { schema })
2045
await db.value?.execute('INSTALL vss;')
2146
await db.value?.execute('LOAD vss;')
47+
}
2248
49+
async function migrate() {
2350
await db.value?.execute(migration1)
24-
25-
results.value = await db.value?.execute(query.value)
26-
27-
await db.value.insert(users).values({
51+
await db.value?.insert(users).values({
2852
id: '9449af72-faad-4c97-8a45-69f9f1ca1b05',
2953
decimal: '1.23456',
3054
numeric: '1.23456',
3155
real: 1.23456,
3256
double: 1.23456,
3357
interval: '365 day',
3458
})
59+
isMigrated.value = true
60+
}
61+
62+
async function insert() {
63+
await db.value?.insert(users).values({
64+
id: crypto.randomUUID().replace(/-/g, ''),
65+
decimal: '1.23456',
66+
numeric: '1.23456',
67+
real: 1.23456,
68+
double: 1.23456,
69+
interval: '365 day',
70+
})
71+
}
72+
73+
async function reconnect() {
74+
const client = await db.value?.$client
75+
await client?.close()
76+
await connect()
77+
}
78+
79+
async function execute() {
80+
results.value = await db.value?.execute(query.value)
81+
}
82+
83+
async function executeORM() {
84+
schemaResults.value = await db.value?.select().from(users)
85+
}
3586
36-
const usersResults = await db.value.select().from(users)
87+
async function shallowListOPFS() {
88+
const opfsRoot = await navigator.storage.getDirectory()
89+
const files: string[] = []
90+
for await (const name of opfsRoot.keys()) {
91+
files.push(name)
92+
}
93+
// eslint-disable-next-line no-console
94+
console.log(['Files in OPFS:', ...files].join('\n'))
95+
}
96+
97+
async function wipeOPFS() {
98+
await db.value?.$client.then(client => client.close())
99+
const opfsRoot = await navigator.storage.getDirectory()
100+
const promises: Promise<void>[] = []
101+
for await (const name of opfsRoot.keys()) {
102+
promises.push(opfsRoot.removeEntry(name, { recursive: true }).then(() => {
103+
// eslint-disable-next-line no-console
104+
console.info(`File removed from OPFS: "${name}"`)
105+
}))
106+
}
107+
await Promise.all(promises)
108+
}
109+
110+
onMounted(async () => {
111+
await connect()
112+
await migrate()
113+
114+
results.value = await db.value?.execute(query.value)
115+
const usersResults = await db.value?.select().from(users)
37116
schemaResults.value = usersResults
38117
})
39118
40119
onUnmounted(() => {
41120
db.value?.$client.then(client => client.close())
42121
})
43-
44-
watch(query, useDebounceFn(async () => {
45-
results.value = await db.value?.execute(query.value)
46-
}, 1000))
47122
</script>
48123

49124
<template>
50-
<div flex flex-col gap-4 p-4>
125+
<div flex flex-col gap-2 p-4>
51126
<h1 text-2xl>
52127
<code>@duckdb/duckdb-wasm</code> + <code>drizzle-orm</code> Playground
53128
</h1>
54129
<div flex flex-col gap-2>
55130
<h2 text-xl>
56-
Executing
131+
Storage
57132
</h2>
58-
<div>
59-
<textarea v-model="query" h-full w-full rounded-lg bg="neutral-100 dark:neutral-800" p-4 font-mono />
133+
<div flex flex-row gap-2>
134+
<div flex flex-row gap-2>
135+
<input id="in-memory" v-model="storage" type="radio" :value="undefined">
136+
<label for="in-memory">In-Memory</label>
137+
</div>
138+
<div flex flex-row gap-2>
139+
<input id="opfs" v-model="storage" type="radio" :value="DBStorageType.ORIGIN_PRIVATE_FS">
140+
<label for="opfs">Origin Private FS</label>
141+
</div>
60142
</div>
61143
</div>
62-
<div flex flex-col gap-2>
144+
<div grid grid-cols-3 gap-2>
145+
<div flex flex-col gap-2>
146+
<h2 text-xl>
147+
Logger
148+
</h2>
149+
<div flex flex-row gap-2>
150+
<input id="logger" v-model="logger" type="checkbox">
151+
<label for="logger">Enable</label>
152+
</div>
153+
</div>
154+
<div flex flex-col gap-2>
155+
<h2 text-xl>
156+
Read-only
157+
</h2>
158+
<div flex flex-row gap-2>
159+
<input id="readOnly" v-model="readOnly" type="checkbox">
160+
<label for="readOnly">Read-only (DB file creation will fail)</label>
161+
</div>
162+
</div>
163+
</div>
164+
<div v-if="storage === DBStorageType.ORIGIN_PRIVATE_FS" flex flex-col gap-2>
63165
<h2 text-xl>
64-
Results
166+
Path
65167
</h2>
66-
<div whitespace-pre-wrap p-4 font-mono>
67-
{{ JSON.stringify(serialize(results).json, null, 2) }}
168+
<div flex flex-col gap-1>
169+
<input v-model="path" type="text" w-full rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
170+
<div text-sm>
171+
<ul list-disc-inside>
172+
<li>
173+
Leading slash is optional ("/path/to/database.db" is equivalent to "path/to/database.db")
174+
</li>
175+
<li>Empty path is INVALID</li>
176+
</ul>
177+
</div>
68178
</div>
69179
</div>
70180
<div flex flex-col gap-2>
71181
<h2 text-xl>
72-
Executing
182+
DSN (read-only)
73183
</h2>
74184
<div>
75-
<pre whitespace-pre-wrap rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
76-
await db.insert(users).values({ id: '9449af72-faad-4c97-8a45-69f9f1ca1b05' })
77-
await db.select().from(users)
78-
</pre>
185+
<input v-model="dsn" readonly type="text" w-full rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
79186
</div>
80187
</div>
81-
<div flex flex-col gap-2>
82-
<h2 text-xl>
83-
Schema Results
84-
</h2>
85-
<div whitespace-pre-wrap p-4 font-mono>
86-
{{ JSON.stringify(serialize(schemaResults).json, null, 2) }}
188+
<div flex flex-row justify-between gap-2>
189+
<div flex flex-row gap-2>
190+
<button rounded-lg bg="pink-100 dark:pink-700" px-4 py-2 @click="reconnect">
191+
Reconnect
192+
</button>
193+
<button rounded-lg bg="orange-100 dark:orange-700" px-4 py-2 :class="{ 'cursor-not-allowed': isMigrated }" :disabled="isMigrated" @click="migrate">
194+
{{ isMigrated ? 'Already migrated 🥳' : 'Migrate' }}
195+
</button>
196+
<button rounded-lg bg="purple-100 dark:purple-700" px-4 py-2 @click="insert">
197+
Insert
198+
</button>
199+
</div>
200+
<div flex flex-row gap-2>
201+
<button rounded-lg bg="green-100 dark:green-700" px-4 py-2 @click="shallowListOPFS">
202+
List OPFS (See console)
203+
</button>
204+
<button rounded-lg bg="red-100 dark:red-700" px-4 py-2 @click="wipeOPFS">
205+
Wipe OPFS
206+
</button>
207+
</div>
208+
</div>
209+
<div grid grid-cols-2 gap-2>
210+
<div flex flex-col gap-2>
211+
<h2 text-xl>
212+
Executing
213+
</h2>
214+
<div>
215+
<textarea v-model="query" h-full w-full rounded-lg bg="neutral-100 dark:neutral-800" p-4 font-mono />
216+
</div>
217+
<div flex flex-row gap-2>
218+
<button rounded-lg bg="blue-100 dark:blue-700" px-4 py-2 @click="execute">
219+
Execute
220+
</button>
221+
</div>
222+
<div flex flex-col gap-2>
223+
<h2 text-xl>
224+
Results
225+
</h2>
226+
<div whitespace-pre-wrap p-4 font-mono>
227+
{{ JSON.stringify(serialize(results).json, null, 2) }}
228+
</div>
229+
</div>
230+
</div>
231+
<div>
232+
<div flex flex-col gap-2>
233+
<h2 text-xl>
234+
Executing (ORM, read-only)
235+
</h2>
236+
<div>
237+
<pre whitespace-pre-wrap rounded-lg p-4 font-mono bg="neutral-100 dark:neutral-800">
238+
await db.insert(users).values({ id: '9449af72-faad-4c97-8a45-69f9f1ca1b05' })
239+
await db.select().from(users)
240+
</pre>
241+
</div>
242+
<div flex flex-row gap-2>
243+
<button rounded-lg bg="blue-100 dark:blue-700" px-4 py-2 @click="executeORM">
244+
Execute
245+
</button>
246+
</div>
247+
</div>
248+
<div flex flex-col gap-2>
249+
<h2 text-xl>
250+
Schema Results
251+
</h2>
252+
<div whitespace-pre-wrap p-4 font-mono>
253+
{{ JSON.stringify(serialize(schemaResults).json, null, 2) }}
254+
</div>
255+
</div>
87256
</div>
88257
</div>
89258
</div>

0 commit comments

Comments
 (0)