feat: memory driver with @duckdb/duckdb-wasm

This commit is contained in:
Neko Ayaka
2025-02-08 23:12:46 +07:00
parent f5bfd16963
commit a48624ef1e
37 changed files with 3922 additions and 94 deletions
+5
View File
@@ -32,6 +32,10 @@ words:
- demi
- dotenvx
- dtype
- duckdb
- DuckDBWASM
- DuckDBWASMQuery
- DuckDBWASMQ
- elevenlabs
- formkit
- gcornut
@@ -82,6 +86,7 @@ words:
- pixi
- pixiv
- pretrained
- pthread
- rehype
- rushstack
- Shadcn
+1
View File
@@ -9,6 +9,7 @@ export default antfu(
'**/assets/js/**',
'**/assets/live2d/models/**',
'packages/stage-tamagotchi/out/**',
'**/drizzle/**',
],
},
)
+4 -2
View File
@@ -51,8 +51,10 @@
"vitest": "^3.0.4"
},
"workspaces": [
"packages/*",
"docs"
"packages/**",
"services/**",
"examples/**",
"docs/**"
],
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged && pnpm typecheck"
@@ -0,0 +1,7 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'postgresql',
schema: './playground/db/schema.ts',
out: './playground/drizzle',
})
@@ -0,0 +1,69 @@
{
"name": "@proj-airi/memory-driver-duckdb",
"type": "module",
"version": "1.0.0",
"private": true,
"description": "",
"author": {
"name": "Neko Ayaka",
"email": "neko@ayaka.moe",
"url": "https://github.com/nekomeowww"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "packages/memory-driver-duckdb"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./drizzle-orm": {
"types": "./dist/drizzle-orm/index.d.ts",
"import": "./dist/drizzle-orm/index.mjs",
"node": "./dist/drizzle-orm/index.cjs"
},
"./drizzle-orm/vite": {
"types": "./dist/drizzle-orm/vite.d.ts",
"import": "./dist/drizzle-orm/vite.mjs",
"node": "./dist/drizzle-orm/vite.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"README.md",
"dist",
"package.json"
],
"scripts": {
"playground:dev": "vite",
"dev": "pnpm run stub && vite",
"stub": "unbuild --stub",
"build": "unbuild",
"typecheck": "tsc --noEmit",
"db:generate": "drizzle-kit generate"
},
"dependencies": {
"@date-fns/tz": "^1.2.0",
"@duckdb/duckdb-wasm": "^1.29.0",
"apache-arrow": "^19.0.0",
"date-fns": "^4.1.0",
"defu": "^6.1.4",
"drizzle-orm": "^0.39.1",
"es-toolkit": "^1.32.0"
},
"devDependencies": {
"@unocss/reset": "^65.4.3",
"@vitejs/plugin-vue": "^5.2.1",
"@vueuse/core": "^12.5.0",
"drizzle-kit": "^0.30.4",
"vite": "^6.0.11",
"vue": "^3.5.13",
"vue-tsc": "^2.2.0"
}
}
@@ -0,0 +1,8 @@
import { sql } from 'drizzle-orm'
import { bigint, pgTable } from 'drizzle-orm/pg-core'
export const users = pgTable('users', () => {
return {
id: bigint({ mode: 'number' }).primaryKey().unique().default(sql`0`),
}
})
@@ -0,0 +1,4 @@
CREATE TABLE "users" (
"id" bigint PRIMARY KEY DEFAULT 0 NOT NULL,
CONSTRAINT "users_id_unique" UNIQUE("id")
);
@@ -0,0 +1,47 @@
{
"id": "243b9794-66eb-484e-8359-cc3354344cf6",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "bigint",
"primaryKey": true,
"notNull": true,
"default": "0"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_id_unique": {
"name": "users_id_unique",
"nullsNotDistinct": false,
"columns": [
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1739028279439,
"tag": "0000_blue_selene",
"breakpoints": true
}
]
}
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Project AIRI Memory Driver @duckdb/duckdb-wasm Playground</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script>
;(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
</head>
<body class="font-sans">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="24" height="24"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"></path>
<path fill="#fd7f9c" d="M18.296 3.045a1 1 0 0 1 .657.652l.03.119l1.341 7.154q.154.826.148 1.63l-.012.4H21a1 1 0 0 1 .117 1.993L21 15h-.894q-.164.531-.392 1.033l-.16.33l1.07.856a1 1 0 0 1-1.146 1.634l-.103-.072l-.936-.749A8.43 8.43 0 0 1 12 21a8.42 8.42 0 0 1-6.25-2.755l-.19-.213l-.935.749a1 1 0 0 1-1.343-1.477l.093-.085l1.07-.856a9 9 0 0 1-.435-1.012L3.894 15H3a1 1 0 0 1-.117-1.993L3 13h.54a8.5 8.5 0 0 1 .069-1.619l.067-.411l1.341-7.154a1 1 0 0 1 1.598-.604l.092.08l2.414 2.415a1 1 0 0 0 .576.284L9.828 6h4.344a1 1 0 0 0 .608-.206l.099-.087l2.414-2.414a1 1 0 0 1 1.003-.248m-.93 3.003L16.293 7.12a3 3 0 0 1-2.121.88H9.828a3 3 0 0 1-2.12-.879L6.632 6.048l-.992 5.29A6.5 6.5 0 0 0 5.545 13H7a1 1 0 1 1 0 2h-.492a.998.998 0 0 1 .71 1.696l-.095.086A6.44 6.44 0 0 0 12 19a6.43 6.43 0 0 0 4.696-2.02l.18-.2a1 1 0 0 1 .616-1.78H17a1 1 0 1 1 0-2h1.455a6.5 6.5 0 0 0-.096-1.662zm-3.472 9.005a1 1 0 0 1-.447 1.342l-.553.276a2 2 0 0 1-1.788 0l-.553-.276a1 1 0 0 1 .894-1.79l.553.277l.553-.276a1 1 0 0 1 1.341.447M9.5 10a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3m5 0a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3"></path>
</g>
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { DuckDBWasmDrizzleDatabase } from '../../src/drizzle-orm/browser'
import { onMounted, onUnmounted, ref } from 'vue'
import { drizzle } from '../../src/drizzle-orm/browser'
import * as schema from '../db/schema'
import { users } from '../db/schema'
import migration from '../migrations/0000_blue_selene.sql?raw'
const db = ref<DuckDBWasmDrizzleDatabase<typeof schema>>()
const results = ref<Record<string, unknown>[]>()
const query = ref(`SELECT 1 + 1 AS result`)
onMounted(async () => {
db.value = drizzle('duckdb-wasm://?bundles=worker-url', { schema })
await db.value?.execute(migration)
await db.value.insert(users).values({ id: 1 })
const res = await db.value.select().from(users)
results.value = res
})
onUnmounted(() => {
db.value?.$client.then(client => client.close())
})
</script>
<template>
<div flex flex-col gap-4 p-4>
<h1 text-2xl>
<code>@duckdb/duckdb-wasm</code> + <code>drizzle-orm</code> Playground
</h1>
<div flex flex-col gap-2>
<h2 text-xl>
Executing
</h2>
<div>
<textarea v-model="query" h-full w-full rounded-lg bg="gray-100 dark:gray-800" p-4 font-mono />
</div>
</div>
<div flex flex-col gap-2>
<h2 text-xl>
Results
</h2>
<div whitespace-pre-wrap p-4 font-mono>
{{ JSON.stringify(results, null, 2) }}
</div>
</div>
</div>
</template>
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
overscroll-behavior: none;
}
html {
background: #fff;
transition: all 0.3s ease-in-out;
}
html.dark {
background: #121212;
color-scheme: dark;
}
</style>
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import App from './App.vue'
import '@unocss/reset/tailwind.css'
import 'uno.css'
createApp(App)
.mount('#app')
@@ -0,0 +1,19 @@
/**
* A type predicate that is true if the given value is either undefined
* or null.
*/
export function isNullOrUndefined<T>(
value: T | null | undefined,
): value is null | undefined {
return <T>value === null || <T>value === undefined
}
/**
* A type predicate that is true if the given value is neither undefined
* nor null.
*/
export function notNullOrUndefined<T>(
value: T | null | undefined,
): value is T {
return <T>value !== null && <T>value !== undefined
}
@@ -0,0 +1,19 @@
import type { DuckDBBundles } from '@duckdb/duckdb-wasm'
export function getBundles(): DuckDBBundles {
return {
mvp: {
mainModule: './duckdb-mvp.wasm',
mainWorker: './duckdb-browser-mvp.worker.js',
},
eh: {
mainModule: './duckdb-eh.wasm',
mainWorker: './duckdb-browser-eh.worker.js',
},
coi: {
mainModule: './duckdb-coi.wasm',
mainWorker: './duckdb-browser-coi.worker.js',
pthreadWorker: './duckdb-browser-coi.pthread.worker.js',
},
}
}
@@ -0,0 +1,552 @@
import type { Field, StructRow } from 'apache-arrow'
import type { DataType } from './duckdb-types'
import { TZDate } from '@date-fns/tz'
import { Struct, TimeUnit, util } from 'apache-arrow'
import {
addDays,
addHours,
addMilliseconds,
addMinutes,
addMonths,
addQuarters,
addSeconds,
addWeeks,
addYears,
format as dateFormat,
formatDuration as dateFormatDuration,
fromUnixTime,
setDay,
} from 'date-fns'
import { trimEnd } from 'es-toolkit'
import { isNullOrUndefined, notNullOrUndefined } from './duckdb-common'
import {
isDatetimeType,
isDateType,
isDecimalType,
isDurationType,
isFloatType,
isIntervalType,
isListType,
isObjectType,
isPeriodType,
isTimeType,
} from './duckdb-types'
/**
* The frequency strings defined in pandas.
* See: https://pandas.pydata.org/docs/user_guide/timeseries.html#period-aliases
* Not supported: "N" (nanoseconds), "U" & "us" (microseconds), and "B" (business days).
* Reason is that these types are not supported by moment.js, but also they are not
* very commonly used in practice.
*/
type SupportedPandasOffsetType =
// yearly frequency:
| 'A' // deprecated alias
| 'Y'
// quarterly frequency:
| 'Q'
// monthly frequency:
| 'M'
// weekly frequency:
| 'W'
// calendar day frequency:
| 'D'
// hourly frequency:
| 'H' // deprecated alias
| 'h'
// minutely frequency
| 'T' // deprecated alias
| 'min'
// secondly frequency:
| 'S' // deprecated alias
| 's'
// milliseconds frequency:
| 'L' // deprecated alias
| 'ms'
type PandasPeriodFrequency =
| SupportedPandasOffsetType
| `${SupportedPandasOffsetType}-${string}`
const BASE_DATE = new Date(1970, 0, 1) // 1970-01-01
function formatMs(duration: number): string {
return dateFormat(addMilliseconds(BASE_DATE, duration), 'yyyy-MM-dd HH:mm:ss.SSS')
}
function formatSec(duration: number): string {
return dateFormat(addSeconds(BASE_DATE, duration), 'yyyy-MM-dd HH:mm:ss')
}
function formatMin(duration: number): string {
return dateFormat(addMinutes(BASE_DATE, duration), 'yyyy-MM-dd HH:mm')
}
function formatHours(duration: number): string {
return dateFormat(addHours(BASE_DATE, duration), 'yyyy-MM-dd HH:mm')
}
function formatDay(duration: number): string {
return dateFormat(addDays(BASE_DATE, duration), 'yyyy-MM-dd')
}
function formatMonth(duration: number): string {
return dateFormat(addMonths(BASE_DATE, duration), 'yyyy-MM')
}
function formatYear(duration: number): string {
return dateFormat(addYears(BASE_DATE, duration), 'yyyy')
}
function formatWeeks(duration: number, freqParam?: string): string {
if (!freqParam) {
throw new Error('Frequency "W" requires parameter')
}
const WEEKDAY_SHORT = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
const dayIndex = WEEKDAY_SHORT.indexOf(freqParam)
if (dayIndex < 0) {
throw new Error(
`Invalid value: ${freqParam}. Supported values: ${JSON.stringify(
WEEKDAY_SHORT,
)}`,
)
}
const weekDate = addWeeks(BASE_DATE, duration)
const startDate = dateFormat(setDay(weekDate, dayIndex - 6), 'yyyy-MM-dd')
const endDate = dateFormat(setDay(weekDate, dayIndex), 'yyyy-MM-dd')
return `${startDate}/${endDate}`
}
function formatQuarter(duration: number): string {
const date = addQuarters(BASE_DATE, duration)
const year = dateFormat(date, 'yyyy')
const quarter = Math.floor(date.getMonth() / 3) + 1
return `${year}Q${quarter}`
}
/**
* Formatters for the different pandas period frequencies.
*
* This is a mapping from the frequency strings to the function that formats the period.
*/
const PERIOD_TYPE_FORMATTERS: Record<
SupportedPandasOffsetType,
(duration: number, freqParam?: string) => string
> = {
L: formatMs,
ms: formatMs,
S: formatSec,
s: formatSec,
T: formatMin,
min: formatMin,
H: formatHours,
h: formatHours,
D: formatDay,
M: formatMonth,
W: formatWeeks,
Q: formatQuarter,
Y: formatYear,
A: formatYear,
}
/**
* Adjusts a time value to seconds based on the unit information in the field.
*
* The unit numbers are specified here:
* https://github.com/apache/arrow/blob/3ab246f374c17a216d86edcfff7ff416b3cff803/js/src/enum.ts#L95
*
* @param timestamp The timestamp to convert.
* @param unit The unit of the timestamp. 0 is seconds, 1 is milliseconds, 2 is microseconds, 3 is nanoseconds.
* @returns The timestamp in seconds.
*/
function convertTimestampToSeconds(
timestamp: number | bigint,
unit: TimeUnit,
): number {
let unitAdjustment
if (unit === TimeUnit.MILLISECOND) {
// Milliseconds
unitAdjustment = 1000
}
else if (unit === TimeUnit.MICROSECOND) {
// Microseconds
unitAdjustment = 1000 * 1000
}
else if (unit === TimeUnit.NANOSECOND) {
// Nanoseconds
unitAdjustment = 1000 * 1000 * 1000
}
else {
// Interpret it as seconds as a fallback
return Number(timestamp)
}
// Do the calculation based on bigints, if the value
// is a bigint and not safe for usage as number.
// This might lose some precision since it doesn't keep
// fractional parts.
if (
typeof timestamp === 'bigint'
&& !Number.isSafeInteger(Number(timestamp))
) {
return Number(timestamp / BigInt(unitAdjustment))
}
return Number(timestamp) / unitAdjustment
}
/**
* Converts a UTC time value (timestamp) to a date object.
*
* @param timestamp The timestamp to convert.
* @param field The field containing the unit information.
* @returns The date object in UTC timezone.
*/
export function convertTimeToDate(
timestamp: number | bigint,
field?: Field,
): Date {
// Time values from arrow are not converted to a shared unit and
// just return the raw arrow value. Therefore, we need to adjust
// the value to seconds based on the unit information in the field.
// https://github.com/apache/arrow/blob/9e08c57c0986531879aadf7942998d26a94a5d1b/js/src/visitor/get.ts#L193C7-L209
const timeInSeconds = convertTimestampToSeconds(
timestamp,
// The default is SECOND because that is the default unit for time values in pandas.
// Though we believe that actually always a unit is populated by arrow.
field?.type?.unit ?? TimeUnit.SECOND,
)
return fromUnixTime(timeInSeconds)
}
/**
* Formats a duration value based on the unit information in the field.
*
* @param duration The duration value to format.
* @param field The field containing the unit information.
* @returns The formatted duration value.
*/
export function formatDuration(duration: number | bigint, field?: Field): string {
// unit: 0 is seconds, 1 is milliseconds, 2 is microseconds, 3 is nanoseconds.
return dateFormatDuration({
seconds: convertTimestampToSeconds(
duration,
// The default is NANOSECOND because that is the default unit for duration in pandas.
// Though we believe that actually always a unit is populated by arrow.
field?.type?.unit ?? TimeUnit.NANOSECOND,
),
})
}
/**
* Formats a time value based on the unit information in the field.
*
* @param timestamp The time value to format.
* @param field The field containing the unit information.
* @returns The formatted time value.
*/
export function formatTime(timestamp: number | bigint, field?: Field): string {
const date = convertTimeToDate(timestamp, field)
return dateFormat(
date,
date.getMilliseconds() === 0 ? 'HH:mm:ss' : 'HH:mm:ss.SSS',
)
}
export function formatDate(date: number | Date): string {
// Date values from arrow are already converted to a date object
// or a timestamp in milliseconds even if the field unit belonging to the
// passed date might have indicated a different unit.
// Thats why we don't need the field information here (aka its not passed to the function)
// and we don't need to apply any unit conversion.
// https://github.com/apache/arrow/blob/9e08c57c0986531879aadf7942998d26a94a5d1b/js/src/visitor/get.ts#L167-L171
const formatPattern = 'yyyy-MM-dd'
if (
!(
date instanceof Date
|| (typeof date === 'number' && Number.isFinite(date))
)
) {
console.warn(`Unsupported date value: ${date}`)
return String(date)
}
return dateFormat(date, formatPattern)
}
/**
* Format datetime value from Arrow to string.
*/
export function formatDatetime(date: number | Date, field?: Field): string {
// Datetime values from arrow are already converted to a date object
// or a timestamp in milliseconds even if the field unit might indicate a
// different unit.
// https://github.com/apache/arrow/blob/9e08c57c0986531879aadf7942998d26a94a5d1b/js/src/visitor/get.ts#L174-L190
if (
!(
date instanceof Date
|| (typeof date === 'number' && Number.isFinite(date))
)
) {
console.warn(`Unsupported datetime value: ${date}`)
return String(date)
}
let datetime: TZDate
if (typeof date === 'number') {
datetime = new TZDate(date, 'UTC')
}
else {
datetime = new TZDate(date, 'UTC')
}
const timezone = field?.type?.timezone
if (timezone) {
datetime = datetime.withTimeZone(timezone)
return dateFormat(datetime, 'yyyy-MM-dd HH:mm:ss OOOO')
}
// Return the timestamp without timezone information
return dateFormat(datetime, 'yyyy-MM-dd HH:mm:ss')
}
/**
* Formats a decimal value with a given scale to a string.
*
* This code is partly based on: https://github.com/apache/arrow/issues/35745
*
* TODO: This is only a temporary workaround until ArrowJS can format decimals correctly.
* This is tracked here:
* https://github.com/apache/arrow/issues/37920
* https://github.com/apache/arrow/issues/28804
* https://github.com/apache/arrow/issues/35745
*/
export function formatDecimal(value: Uint32Array, field?: Field): string {
const scale = field?.type?.scale || 0
// Format Uint32Array to a numerical string and pad it with zeros
// So that it is exactly the length of the scale.
let numString = util.bigNumToString(new util.BN(value)).padStart(scale, '0')
// ArrowJS 13 correctly adds a minus sign for negative numbers.
// but it doesn't handle th fractional part yet. So we can just return
// the value if scale === 0, but we need to do some additional processing
// for the fractional part if scale > 0.
if (scale === 0) {
return numString
}
let sign = ''
if (numString.startsWith('-')) {
// Check if number is negative, and if so remember the sign and remove it.
// We will add it back later.
sign = '-'
numString = numString.slice(1)
}
// Extract the whole number part. If the number is < 1, it doesn't
// have a whole number part, so we'll use "0" instead.
// E.g for 123450 with scale 3, we'll get "123" as the whole part.
const wholePart = numString.slice(0, -scale) || '0'
// Extract the fractional part and remove trailing zeros.
// E.g. for 123450 with scale 3, we'll get "45" as the fractional part.
const decimalPart = trimEnd(numString.slice(-scale), '0') || ''
// Combine the parts and add the sign.
return `${sign}${wholePart}${decimalPart ? `.${decimalPart}` : ''}`
}
const formatter = new Intl.NumberFormat('en-US', { style: 'decimal', maximumFractionDigits: 4, minimumFractionDigits: 4, useGrouping: true })
/**
* Formats a float value to a string.
*
* @param num The float value to format.
* @returns The formatted float value.
*/
export function formatFloat(num: number): string {
if (!Number.isFinite(num)) {
return String(num)
}
return formatter.format(num)
}
/**
* Formats an interval value from arrow to string.
*/
export function formatInterval(x: StructRow, field?: Field): string {
// Serialization for pandas.Interval is provided by Arrow extensions
// https://github.com/pandas-dev/pandas/blob/235d9009b571c21b353ab215e1e675b1924ae55c/
// pandas/core/arrays/arrow/extension_types.py#L17
const extensionName = field && field.metadata.get('ARROW:extension:name')
if (extensionName && extensionName === 'pandas.interval') {
const extensionMetadata = JSON.parse(
field.metadata.get('ARROW:extension:metadata') as string,
)
const { closed } = extensionMetadata
const interval = (x as StructRow).toJSON() as {
left: number
right: number
}
const leftBracket = closed === 'both' || closed === 'left' ? '[' : '('
const rightBracket = closed === 'both' || closed === 'right' ? ']' : ')'
const leftInterval = format(interval.left, (field.type as Struct)?.children?.[0])
const rightInterval = format(interval.right, (field.type as Struct)?.children?.[1])
return `${leftBracket + leftInterval}, ${rightInterval + rightBracket}`
}
return String(x)
}
export function formatPeriodFromFreq(
duration: number | bigint,
freq: PandasPeriodFrequency,
): string {
const [freqName, freqParam] = freq.split('-', 2)
const momentConverter
= PERIOD_TYPE_FORMATTERS[freqName as SupportedPandasOffsetType]
if (!momentConverter) {
console.warn(`Unsupported period frequency: ${freq}`)
return String(duration)
}
const durationNumber = Number(duration)
if (!Number.isSafeInteger(durationNumber)) {
console.warn(
`Unsupported value: ${duration}. Supported values: [${Number.MIN_SAFE_INTEGER}-${Number.MAX_SAFE_INTEGER}]`,
)
return String(duration)
}
return momentConverter(durationNumber, freqParam)
}
export function formatPeriod(duration: number | bigint, field?: Field): string {
// Serialization for pandas.Period is provided by Arrow extensions
// https://github.com/pandas-dev/pandas/blob/70bb855cbbc75b52adcb127c84e0a35d2cd796a9/pandas/core/arrays/arrow/extension_types.py#L26
if (isNullOrUndefined(field)) {
console.warn('Field information is missing')
return String(duration)
}
const extensionName = field.metadata.get('ARROW:extension:name')
const extensionMetadata = field.metadata.get('ARROW:extension:metadata')
if (
isNullOrUndefined(extensionName)
|| isNullOrUndefined(extensionMetadata)
) {
console.warn('Arrow extension metadata is missing')
return String(duration)
}
if (extensionName !== 'pandas.period') {
console.warn(`Unsupported extension name for period type: ${extensionName}`)
return String(duration)
}
const parsedExtensionMetadata = JSON.parse(extensionMetadata as string)
const { freq } = parsedExtensionMetadata
return formatPeriodFromFreq(duration, freq)
}
/**
* Formats nested arrays and other objects to a JSON string.
*
* @param object The value to format.
* @param field The field metadata from arrow containing metadata about the column.
* @returns The formatted JSON string.
*/
export function formatObject(object: any, field?: Field): string {
if (field?.type instanceof Struct) {
// This type is used by python dictionary values
return JSON.stringify(object, (_key, value) => {
if (!notNullOrUndefined(value)) {
// Workaround: Arrow JS adds all properties from all cells
// as fields. When you convert to string, it will contain lots of fields with
// null values. To mitigate this, we filter out null values.
return undefined
}
if (typeof value === 'bigint') {
// JSON.stringify fails to serialize bigint values, therefore we have to
// handle them manually.
// TODO(lukasmasuch): Would it be better to serialize it to a string to
// not lose precision?
return Number(value)
}
return value
})
}
// TODO(lukasmasuch): Investigate if we can unify this with the logic above.
return JSON.stringify(object, (_key, value) =>
typeof value === 'bigint' ? Number(value) : value)
}
/**
* Takes the cell data and type metadata from arrow and nicely formats it into a human-readable string.
*
* This is mostly a best-effort logic and should not throw exceptions in case of unknown values
* or other issues. This makes it easier to use this method by consumers (table, dataframe) since
* they would have to somehow deal with the exception on a cell level to not crash the full table or app.
*
* @param x The cell value.
* @param field The field metadata from arrow containing metadata about the column.
* @returns The formatted cell value.
*/
export function format(x: DataType, field?: Field): string {
if (isNullOrUndefined(x)) {
return ''
}
const isDate = x instanceof Date || Number.isFinite(x)
if (isDate && isDateType(field)) {
return formatDate(x as Date | number)
}
if (typeof x === 'bigint' && isTimeType(field)) {
return formatTime(Number(x), field)
}
if (isDate && isDatetimeType(field)) {
return formatDatetime(x as Date | number, field)
}
if (isPeriodType(field)) {
return formatPeriod(x as bigint, field)
}
if (isIntervalType(field)) {
return formatInterval(x as StructRow, field)
}
if (isDurationType(field)) {
return formatDuration(x as number | bigint, field)
}
if (isDecimalType(field)) {
return formatDecimal(x as Uint32Array, field)
}
if (isFloatType(field) && Number.isFinite(x)) {
return formatFloat(x as number)
}
if (isObjectType(field) || isListType(field)) {
return formatObject(x, field)
}
return String(x)
}
@@ -0,0 +1,245 @@
import type { Dictionary, Field, Struct, StructRow, Vector } from 'apache-arrow'
import { DataType as ArrowDataType } from 'apache-arrow'
import { isNullOrUndefined } from './duckdb-common'
/** Data types used by ArrowJS. */
export type DataType =
| null
| boolean
| number
| string
| Date // datetime
| Int32Array // int
| Uint8Array // bytes
| Uint32Array // Decimal
| Vector // arrays
| StructRow // interval
| Dictionary // categorical
| Struct // dict
| bigint // period
/** The type of the cell. */
export enum DataFrameCellType {
// Index cells
INDEX = 'index',
// Data cells
DATA = 'data',
}
/**
* Converts an Arrow vector to a list of strings.
*
* @param vector The Arrow vector to convert.
* @returns The list of strings.
*/
export function convertVectorToList(vector: Vector<any>): string[] {
const values = []
for (let i = 0; i < vector.length; i++) {
values.push(vector.get(i))
}
return values
}
/** Returns the timezone of the arrow type metadata. */
export function getTimezone(field: Field): string | undefined {
return field.type?.timezone ?? field.metadata?.get('timezone')
}
/**
* True if the arrow type is an integer type.
* For example: int8, int16, int32, int64, uint8, uint16, uint32, uint64, range
*/
export function isIntegerType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
// Period types are integers with an extra extension name
(ArrowDataType.isInt(field.type) && !isPeriodType(field))
|| isUnsignedIntegerType(field)
)
}
/** True if the arrow type is an unsigned integer type. */
export function isUnsignedIntegerType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
(ArrowDataType.isInt(field.type)
&& field.type.isSigned === false)
)
}
/**
* True if the arrow type is a float type.
* For example: float16, float32, float64, float96, float128
*/
export function isFloatType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
(ArrowDataType.isFloat(field.type))
?? false
)
}
/** True if the arrow type is a decimal type. */
export function isDecimalType(field?: Field): boolean {
if (isNullOrUndefined(field)) {
return false
}
return (
ArrowDataType.isDecimal(field.type)
)
}
/** True if the arrow type is a numeric type. */
export function isNumericType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return isIntegerType(type) || isFloatType(type) || isDecimalType(type)
}
/** True if the arrow type is a boolean type. */
export function isBooleanType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isBool(type.type)
)
}
/** True if the arrow type is a duration type. */
export function isDurationType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isDuration(type.type)
)
}
/** True if the arrow type is a period type. */
export function isPeriodType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
(ArrowDataType.isInt(type.type)
&& type.metadata.get('ARROW:extension:name') === 'period')
)
}
/** True if the arrow type is a datetime type. */
export function isDatetimeType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isTimestamp(type.type)
)
}
/** True if the arrow type is a date type. */
export function isDateType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isDate(type.type)
)
}
/** True if the arrow type is a time type. */
export function isTimeType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isTime(type.type)
)
}
/** True if the arrow type is a categorical type. */
export function isCategoricalType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isDictionary(type.type)
)
}
/** True if the arrow type is a list type. */
export function isListType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isList(type.type)
|| ArrowDataType.isFixedSizeList(type.type)
)
}
/** True if the arrow type is an object type. */
export function isObjectType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isStruct(type.type)
|| ArrowDataType.isMap(type.type)
)
}
/** True if the arrow type is a bytes type. */
export function isBytesType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isBinary(type.type)
|| ArrowDataType.isLargeBinary(type.type)
)
}
/** True if the arrow type is a string type. */
export function isStringType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isUtf8(type.type)
|| ArrowDataType.isLargeUtf8(type.type)
)
}
/** True if the arrow type is an empty type. */
export function isEmptyType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
return (
ArrowDataType.isNull(type.type)
)
}
/** True if the arrow type is a interval type. */
export function isIntervalType(type?: Field): boolean {
if (isNullOrUndefined(type)) {
return false
}
// ArrowDataType.isInterval checks for a different (unsupported) type and not related
// to the pandas interval extension type.
return (
(ArrowDataType.isStruct(type.type)
&& type.metadata.get('ARROW:extension:name') === 'interval')
)
}
@@ -0,0 +1,30 @@
/* eslint-disable perfectionist/sort-imports */
import type { DuckDBBundles } from '@duckdb/duckdb-wasm'
import ehMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url'
import ehMainModule from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url'
import mvpMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url'
import mvpMainModule from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url'
import coiMainModule from '@duckdb/duckdb-wasm/dist/duckdb-coi.wasm?url'
import coiMainWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-coi.worker.js?url'
import coiPthreadWorker from '@duckdb/duckdb-wasm/dist/duckdb-browser-coi.pthread.worker.js?url'
export function getViteBundles(): DuckDBBundles {
return {
mvp: {
mainModule: mvpMainModule,
mainWorker: mvpMainWorker,
},
eh: {
mainModule: ehMainModule,
mainWorker: ehMainWorker,
},
coi: {
mainModule: coiMainModule,
mainWorker: coiMainWorker,
pthreadWorker: coiPthreadWorker,
},
}
}
@@ -0,0 +1,81 @@
import type { AsyncDuckDBConnection, DuckDBBundles, Logger } from '@duckdb/duckdb-wasm'
import { AsyncDuckDB, ConsoleLogger, selectBundle, VoidLogger } from '@duckdb/duckdb-wasm'
import { defu } from 'defu'
import { getBundles } from './duckdb-default-bundles'
export type ConnectOptions = ConnectRequiredOptions & ConnectOptionalOptions
export interface ConnectOptionalOptions {
bundles?: DuckDBBundles
logger?: boolean | Logger
}
export interface ConnectRequiredOptions {
}
export interface DuckDBWasmClient {
worker: Worker
db: AsyncDuckDB
conn: AsyncDuckDBConnection
close: () => Promise<void>
}
export async function connect(options: ConnectOptions): Promise<DuckDBWasmClient> {
const opts = defu(options, { bundles: getBundles(), logger: false })
const bundle = await selectBundle(opts.bundles)
const worker = new Worker(bundle.mainWorker!)
let logger: Logger
if (opts.logger === true) {
logger = new ConsoleLogger()
}
else if (opts.logger === false) {
logger = new VoidLogger()
}
else {
logger = opts.logger
}
const db = new AsyncDuckDB(logger, worker)
await db.instantiate(bundle.mainModule, bundle.pthreadWorker)
const conn = await db.connect()
return {
worker,
db,
conn,
close: async () => {
await conn.close()
await db.terminate()
await worker.terminate()
},
}
}
export async function beginTransaction(client: Promise<DuckDBWasmClient>, txFn: (client: Promise<DuckDBWasmClient>) => Promise<any>): Promise<any> {
await (await client).conn.send('BEGIN TRANSACTION')
try {
const result = await txFn(client)
await (await client).conn.send('COMMIT')
return result
}
catch (err) {
await (await client).conn.send('ROLLBACK')
throw err
}
}
export async function withSavepoint(client: Promise<DuckDBWasmClient>, spName: string, txFn: (client: Promise<DuckDBWasmClient>) => Promise<any>): Promise<any> {
await (await client).conn.send(`SAVEPOINT ${spName}`)
try {
const result = await txFn(client)
await (await client).conn.send(`RELEASE SAVEPOINT ${spName}`)
return result
}
catch (err) {
await (await client).conn.send(`ROLLBACK TO SAVEPOINT ${spName}`)
throw err
}
}
@@ -0,0 +1,6 @@
export * from './duckdb'
export * from './duckdb-common'
export * from './duckdb-default-bundles'
export * from './duckdb-format'
export * from './duckdb-types'
export * from './duckdb-vite-bundles'
@@ -0,0 +1,140 @@
import type { DuckDBBundles, Logger } from '@duckdb/duckdb-wasm'
import type { DrizzleConfig, RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm'
import type { DuckDBWasmClient } from './dialect'
import type { DuckDBWasmQueryResultHKT } from './session'
import { createTableRelationsHelpers, DefaultLogger, entityKind, extractTablesRelationalConfig, isConfig } from 'drizzle-orm'
import { PgDatabase, PgDialect } from 'drizzle-orm/pg-core'
import { connect } from './dialect'
import { DuckDBWasmSession } from './session'
export class DuckDBWasmDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
> extends PgDatabase<DuckDBWasmQueryResultHKT, TSchema> {
static override readonly [entityKind]: string = 'DuckDBWasmDatabase'
// ** @internal */
dialect: PgDialect
// ** @internal */
session: DuckDBWasmSession<Promise<DuckDBWasmClient>, TSchema, TablesRelationalConfig>
}
function construct<
TSchema extends Record<string, unknown> = Record<string, never>,
TClient extends Promise<DuckDBWasmClient> = Promise<DuckDBWasmClient>,
>(
client: Promise<DuckDBWasmClient>,
config: DrizzleConfig<TSchema> = {},
): DuckDBWasmDrizzleDatabase<TSchema, TClient> {
const dialect = new PgDialect({ casing: config.casing })
let logger
if (config.logger === true) {
logger = new DefaultLogger()
}
else if (config.logger !== false) {
logger = config.logger
}
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined
if (config.schema) {
const tablesConfig = extractTablesRelationalConfig(
config.schema,
createTableRelationsHelpers,
)
schema = {
fullSchema: config.schema,
schema: tablesConfig.tables,
tableNamesMap: tablesConfig.tableNamesMap,
}
}
const session = new DuckDBWasmSession(client, dialect, schema, { logger })
const db = new DuckDBWasmDatabase(dialect, session, schema as any) as DuckDBWasmDatabase<TSchema>;
(<any>db).$client = client
return db as any
}
export interface DuckDBWasmDrizzleDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
TClient extends Promise<DuckDBWasmClient> = Promise<DuckDBWasmClient>,
> extends DuckDBWasmDatabase<TSchema> {
$client: TClient
}
export function drizzle<
TSchema extends Record<string, unknown> = Record<string, never>,
TClient extends Promise<DuckDBWasmClient> = Promise<DuckDBWasmClient>,
>(
...params:
[ TClient | string ] |
[ TClient | string, DrizzleConfig<TSchema> ] |
[(DrizzleConfig<TSchema> & ({ connection: string | ({ url?: string, bundles?: DuckDBBundles, logger?: Logger }) } | { client: TClient })) ]
): DuckDBWasmDrizzleDatabase<TSchema, TClient> {
if (typeof params[0] === 'string') {
const parsedDSN = new URL(params[0] as string)
if (parsedDSN.searchParams.get('bundles') === 'worker-url') {
return construct(new Promise<DuckDBWasmClient>((resolve) => {
import('./dialect/duckdb-vite-bundles')
.then(res => res.getViteBundles())
.then(bundles => connect({ bundles }))
.then(resolve)
}), params[1]) as any
}
const instance = connect({})
return construct(instance, params[1]) as any
}
if (isConfig(params[0])) {
const {
connection,
client,
...drizzleConfig
} = params[0] as {
connection?: {
url?: string
bundles?: DuckDBBundles
}
client?: TClient
} & DrizzleConfig<TSchema>
if (client)
return construct(client, drizzleConfig) as any
if (typeof connection === 'object' && connection.url !== undefined) {
const { url } = connection
const parsedDSN = new URL(url)
if (parsedDSN.searchParams.get('bundles') === 'worker-url') {
return construct(new Promise<DuckDBWasmClient>((resolve) => {
import('./dialect/duckdb-vite-bundles')
.then(res => res.getViteBundles())
.then(bundles => connect({ bundles }))
.then(resolve)
}), drizzleConfig) as any
}
return construct(connect({ bundles: connection.bundles }), drizzleConfig) as any
}
return construct(connect({}), drizzleConfig) as any
}
return construct(params[0] as TClient, params[1] as DrizzleConfig<TSchema> | undefined) as any
}
// eslint-disable-next-line ts/no-namespace
export namespace drizzle {
export function mock<TSchema extends Record<string, unknown> = Record<string, never>>(
config?: DrizzleConfig<TSchema>,
): DuckDBWasmDatabase<TSchema> & {
$client: '$client is not available on drizzle.mock()'
} {
return construct({
options: {
parsers: {},
serializers: {},
},
} as any, config) as any
}
}
@@ -0,0 +1,7 @@
export type { ConnectOptionalOptions, ConnectOptions, ConnectRequiredOptions, DuckDBWasmClient } from './dialect'
export { connect, format, getBundles } from './dialect'
export * from './driver'
export * from './migrator'
export * from './session'
export type { AsyncDuckDBConnection, DuckDBBundles, Logger } from '@duckdb/duckdb-wasm'
export { AsyncDuckDB, ConsoleLogger, selectBundle, VoidLogger } from '@duckdb/duckdb-wasm'
@@ -0,0 +1,13 @@
import type { MigrationConfig } from 'drizzle-orm/migrator'
import type { PgSession } from 'drizzle-orm/pg-core'
import type { DuckDBWasmDatabase } from './driver'
import { readMigrationFiles } from 'drizzle-orm/migrator'
export async function migrate<TSchema extends Record<string, unknown>>(
db: DuckDBWasmDatabase<TSchema>,
config: MigrationConfig,
) {
const migrations = readMigrationFiles(config)
await db.dialect.migrate(migrations, db.session as unknown as PgSession, config)
}
@@ -0,0 +1,189 @@
import type { Schema, StructRow } from 'apache-arrow'
import type { Assume, Logger, Query, RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm'
import type { PgDialect, PgQueryResultHKT, PgTransactionConfig, PreparedQueryConfig, SelectedFieldsOrdered } from 'drizzle-orm/pg-core'
import type { DuckDBWasmClient } from './dialect'
import { entityKind, fillPlaceholders, NoopLogger } from 'drizzle-orm'
import { PgPreparedQuery, PgSession, PgTransaction } from 'drizzle-orm/pg-core'
import { beginTransaction, format, withSavepoint } from './dialect'
export type Row = Record<string, any>
export type RowList<T extends Row[]> = T
function toJSRepresentedRows<T extends { toArray: () => StructRow[], schema: Schema }>(results: T) {
const rows = (results.toArray() as StructRow[] || []).map(item => item.toJSON()) || []
const jsRepresentedRows = rows.map((row) => {
results.schema.fields.forEach((field) => {
return row[field.name] = format(row[field.name], field)
})
return row
})
return jsRepresentedRows
}
async function callQuery(client: Promise<DuckDBWasmClient>, query: string, params: unknown[]) {
const c = await client
if (!params || params.length === 0) {
const results = await c.conn.query(query)
return toJSRepresentedRows(results)
}
const stmt = await c.conn.prepare(query)
const results = await stmt.query(...params)
const rows = toJSRepresentedRows(results)
stmt.close()
return rows
}
export class DuckDBWASMPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
static override readonly [entityKind]: string = 'DuckDBWasmPreparedQuery'
constructor(
private client: Promise<DuckDBWasmClient>,
private queryString: string,
private params: unknown[],
private logger: Logger,
private fields: SelectedFieldsOrdered | undefined,
private customResultMapper?: (rows: unknown[][]) => T['execute'],
) {
super({ sql: queryString, params })
}
async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
const params = fillPlaceholders(this.params, placeholderValues)
this.logger.logQuery(this.queryString, params)
const { fields, queryString: query, client, customResultMapper } = this
if (!fields && !customResultMapper) {
return callQuery(client, query, params)
}
return callQuery(client, query, params)
}
async all(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['all']> {
const params = fillPlaceholders(this.params, placeholderValues)
this.logger.logQuery(this.queryString, params)
return callQuery(this.client, this.queryString, params)
}
}
export interface DuckDBWASMSessionOptions {
logger?: Logger
}
export class DuckDBWasmSession<
TSQL extends Promise<DuckDBWasmClient>,
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends PgSession<DuckDBWasmQueryResultHKT, TFullSchema, TSchema> {
static override readonly [entityKind]: string = 'DuckDBWasmSession'
logger: Logger
constructor(
public client: TSQL,
dialect: PgDialect,
private schema: RelationalSchemaConfig<TSchema> | undefined,
readonly options: DuckDBWASMSessionOptions = {},
) {
super(dialect)
this.logger = options.logger ?? new NoopLogger()
}
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
query: Query,
fields: SelectedFieldsOrdered | undefined,
name: string | undefined,
isResponseInArrayMode: boolean,
customResultMapper?: (rows: unknown[][]) => T['execute'],
): PgPreparedQuery<T> {
return new DuckDBWASMPreparedQuery(
this.client,
query.sql,
query.params,
this.logger,
fields,
customResultMapper,
)
}
async query(query: string, params: unknown[]): Promise<RowList<Row[]>> {
this.logger.logQuery(query, params)
return callQuery(this.client, query, params)
}
async queryObjects<T extends Row>(
query: string,
params: unknown[],
): Promise<RowList<T[]>> {
this.logger.logQuery(query, params)
return callQuery(this.client, query, params) as Promise<RowList<T[]>>
}
override transaction<T>(
transaction: (tx: DuckDBWasmTransaction<TFullSchema, TSchema>) => Promise<T>,
config?: PgTransactionConfig,
): Promise<T> {
return beginTransaction(this.client, async (client) => {
const session = new DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>(
client,
this.dialect,
this.schema,
this.options,
)
const tx = new DuckDBWasmTransaction(this.dialect, session, this.schema)
if (config) {
await tx.setTransaction(config)
}
return transaction(tx)
}) as Promise<T>
}
}
export class DuckDBWasmTransaction<
TFullSchema extends Record<string, unknown>,
TSchema extends TablesRelationalConfig,
> extends PgTransaction<DuckDBWasmQueryResultHKT, TFullSchema, TSchema> {
static override readonly [entityKind]: string = 'DuckDBWasmTransaction'
dialect: PgDialect
session: DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>
constructor(
dialect: PgDialect,
session: DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>,
schema: RelationalSchemaConfig<TSchema> | undefined,
nestedIndex = 0,
) {
super(dialect, session, schema, nestedIndex)
this.dialect = dialect
this.session = session
}
override async transaction<T>(
transaction: (tx: DuckDBWasmTransaction<TFullSchema, TSchema>) => Promise<T>,
): Promise<T> {
return withSavepoint(this.session.client, '', async (client) => {
const session = new DuckDBWasmSession<Promise<DuckDBWasmClient>, TFullSchema, TSchema>(
client,
this.dialect,
this.schema,
this.session.options,
)
const tx = new DuckDBWasmTransaction<TFullSchema, TSchema>(this.dialect, session, this.schema)
return transaction(tx)
}) as Promise<T>
}
}
export interface DuckDBWasmQueryResultHKT extends PgQueryResultHKT {
type: RowList<Assume<this['row'], Row>[]>
}
@@ -0,0 +1 @@
export { getViteBundles } from './dialect'
@@ -0,0 +1 @@
console.warn('Please import @proj-airi/memory-driver-duckdb/drizzle-orm instead')
@@ -2,10 +2,16 @@
"compilerOptions": {
"target": "ESNext",
"lib": [
"ESNext"
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"module": "ESNext",
"moduleResolution": "bundler",
"types": [
"vite/client"
],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
@@ -0,0 +1,7 @@
import { defineConfig, mergeConfigs } from 'unocss'
import UnoCSSConfig from '../../uno.config'
export default defineConfig(mergeConfigs([
UnoCSSConfig,
]))
@@ -0,0 +1,13 @@
import Vue from '@vitejs/plugin-vue'
import Unocss from 'unocss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
root: 'playground',
plugins: [
Vue(),
// https://github.com/antfu/unocss
// see uno.config.ts for config
Unocss(),
],
})
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@proj-airi/memory-alaya",
"type": "module",
"version": "1.0.0",
"private": true,
"description": "",
"author": {
"name": "Neko Ayaka",
"email": "neko@ayaka.moe",
"url": "https://github.com/nekomeowww"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "packages/memory-alaya"
},
"files": [
"README.md",
"dist",
"package.json"
],
"scripts": {
"dev": "pnpm run stub",
"stub": "unbuild --stub",
"build": "unbuild",
"typecheck": "tsc --noEmit"
}
}
View File
+3 -3
View File
@@ -29,6 +29,6 @@ const dark = useDark()
</template>
<route lang="yaml">
meta:
layout: default
</route>
meta:
layout: default
</route>
+2321 -59
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -2,6 +2,7 @@ packages:
- packages/**
- services/**
- examples/**
- docs/**
- '!**/dist/**'
catalog:
'@xsai/generate-speech': ^0.0.31