|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import type { DuckDBWasmDrizzleDatabase } from '../../src' |
3 | 3 |
|
4 | | -import { useDebounceFn } from '@vueuse/core' |
| 4 | +import { DuckDBAccessMode } from '@duckdb/duckdb-wasm' |
| 5 | +import { DBStorageType } from '@proj-airi/duckdb-wasm' |
5 | 6 | import { serialize } from 'superjson' |
6 | | -import { onMounted, onUnmounted, ref, watch } from 'vue' |
| 7 | +import { computed, onMounted, onUnmounted, ref } from 'vue' |
7 | 8 |
|
8 | 9 | import { drizzle } from '../../src' |
| 10 | +import { buildDSN } from '../../src/dsn' |
9 | 11 | import * as schema from '../db/schema' |
10 | 12 | import { users } from '../db/schema' |
11 | 13 | import migration1 from '../drizzle/0000_cute_kulan_gath.sql?raw' |
12 | 14 |
|
13 | 15 | const db = ref<DuckDBWasmDrizzleDatabase<typeof schema>>() |
14 | 16 | const results = ref<Record<string, unknown>[]>() |
15 | 17 | const schemaResults = ref<Record<string, unknown>[]>() |
16 | | -const query = ref(`SELECT 1 + 1 AS result`) |
| 18 | +const isMigrated = ref(false) |
17 | 19 |
|
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 }) |
20 | 45 | await db.value?.execute('INSTALL vss;') |
21 | 46 | await db.value?.execute('LOAD vss;') |
| 47 | +} |
22 | 48 |
|
| 49 | +async function migrate() { |
23 | 50 | 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({ |
28 | 52 | id: '9449af72-faad-4c97-8a45-69f9f1ca1b05', |
29 | 53 | decimal: '1.23456', |
30 | 54 | numeric: '1.23456', |
31 | 55 | real: 1.23456, |
32 | 56 | double: 1.23456, |
33 | 57 | interval: '365 day', |
34 | 58 | }) |
| 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 | +} |
35 | 86 |
|
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) |
37 | 116 | schemaResults.value = usersResults |
38 | 117 | }) |
39 | 118 |
|
40 | 119 | onUnmounted(() => { |
41 | 120 | db.value?.$client.then(client => client.close()) |
42 | 121 | }) |
43 | | -
|
44 | | -watch(query, useDebounceFn(async () => { |
45 | | - results.value = await db.value?.execute(query.value) |
46 | | -}, 1000)) |
47 | 122 | </script> |
48 | 123 |
|
49 | 124 | <template> |
50 | | - <div flex flex-col gap-4 p-4> |
| 125 | + <div flex flex-col gap-2 p-4> |
51 | 126 | <h1 text-2xl> |
52 | 127 | <code>@duckdb/duckdb-wasm</code> + <code>drizzle-orm</code> Playground |
53 | 128 | </h1> |
54 | 129 | <div flex flex-col gap-2> |
55 | 130 | <h2 text-xl> |
56 | | - Executing |
| 131 | + Storage |
57 | 132 | </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> |
60 | 142 | </div> |
61 | 143 | </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> |
63 | 165 | <h2 text-xl> |
64 | | - Results |
| 166 | + Path |
65 | 167 | </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> |
68 | 178 | </div> |
69 | 179 | </div> |
70 | 180 | <div flex flex-col gap-2> |
71 | 181 | <h2 text-xl> |
72 | | - Executing |
| 182 | + DSN (read-only) |
73 | 183 | </h2> |
74 | 184 | <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"> |
79 | 186 | </div> |
80 | 187 | </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> |
87 | 256 | </div> |
88 | 257 | </div> |
89 | 258 | </div> |
|
0 commit comments