diff --git a/.backlog/tasks/task-046 - Add-git-release-creation-to-publish-workflow.md b/.backlog/tasks/task-046 - Add-git-release-creation-to-publish-workflow.md
new file mode 100644
index 0000000..aa75afe
--- /dev/null
+++ b/.backlog/tasks/task-046 - Add-git-release-creation-to-publish-workflow.md
@@ -0,0 +1,33 @@
+---
+id: TASK-046
+title: Add git release creation to publish workflow
+status: Done
+assignee: []
+created_date: '2026-06-05 22:46'
+updated_date: '2026-06-05 22:50'
+labels: []
+dependencies: []
+modified_files:
+ - .github/workflows/publish.yml
+ - .changeset/config.json
+priority: medium
+---
+
+## Description
+
+
+
+Make explicit that changesets/action creates GitHub Releases for both @woss/dali-orm and @woss/dali-memory after npm publish. Two changes:
+
+1. Add `createGithubReleases: true` to changesets/action config in publish.yml
+2. Fix `baseBranch` from `origin/main` to `main` in .changeset/config.json
+
+
+## Acceptance Criteria
+
+
+
+- [x] #1 publish.yml has `createGithubReleases: true` on changesets/action step
+- [x] #2 .changeset/config.json `baseBranch` is `"main"` not `"origin/main"`
+- [x] #3 Changes committed to a branch
+
diff --git a/.backlog/tasks/task-047 - Validate-generator-HNSW-SQL-against-embedded-SurrealDB.md b/.backlog/tasks/task-047 - Validate-generator-HNSW-SQL-against-embedded-SurrealDB.md
new file mode 100644
index 0000000..8bf3747
--- /dev/null
+++ b/.backlog/tasks/task-047 - Validate-generator-HNSW-SQL-against-embedded-SurrealDB.md
@@ -0,0 +1,74 @@
+---
+id: TASK-047
+title: Validate generator HNSW SQL against embedded SurrealDB
+status: Done
+assignee: []
+created_date: '2026-06-06 09:19'
+updated_date: '2026-06-06 09:41'
+labels:
+ - testing
+ - hnsw
+ - integration
+dependencies: []
+modified_files:
+ - packages/dali-orm/src/migration/core/__tests__/generator.integration.test.ts
+priority: high
+---
+
+## Description
+
+
+
+Generator tests (generator.test.ts) are pure string matching — they construct mock objects and assert SQL output. This misses syntax errors like `DISTANCE` vs `DIST` because no SQL is ever executed against a real SurrealDB engine. The only HNSW integration test (introspect.integration.test.ts) soft-skips HNSW because embedded `mode: 'memory'` doesn't support it.
+
+Add generator.integration.test.ts that:
+
+1. Connects to embedded SurrealDB (try file-backed mode, fall back to memory)
+2. Generates all HNSW index variations (COSINE, EUCLIDEAN, MANHATTAN, with/without vectorType)
+3. Executes the generated SQL against the live engine
+4. Asserts no error — syntax validation that catches generator bugs
+5. Cleans up test tables after each case
+
+If file-backed embedded mode also doesn't support HNSW, the test should fail explicitly (not silently skip like the current introspect test).
+
+See the HNSW test helpers at generator.test.ts:27-33 (index() helper) and the existing integration test pattern at introspect.integration.test.ts:1-49 (EmbeddedDriver setup).
+
+
+
+## Acceptance Criteria
+
+
+
+- [x] #1 generator.integration.test.ts exists alongside generator.test.ts
+- [x] #2 Uses EmbeddedDriver (file-backed mode preferred) for real SQL execution
+- [x] #3 Covers all HNSW distance types: COSINE, EUCLIDEAN, MANHATTAN
+- [x] #4 Covers HNSW with and without vectorType (float32, float64)
+- [x] #5 Test fails if generated SQL is rejected by SurrealDB engine
+- [x] #6 All existing 2419 tests still pass
+
+
+## Final Summary
+
+
+
+## Summary
+
+**Task**: Validate generator HNSW SQL against embedded SurrealDB
+
+**File**: `packages/dali-orm/src/migration/core/__tests__/generator.integration.test.ts` (182 lines)
+
+**Setup**: Uses `EmbeddedDriver` with `surrealkv` (file-backed) mode, falling back to `memory`. Each test creates a unique table, defines a vector field, generates HNSW index SQL via `SurrealQLGenerator.generateIndexDefinition()`, executes it against the live engine, asserts no error, then cleans up.
+
+**5 test cases covering all variations**:
+
+1. HNSW COSINE with float32 → `TYPE F32 DIST COSINE`
+2. HNSW with minimal params (dimension only) → no type/distance
+3. HNSW MANHATTAN + float64 → `TYPE F64 DIST MANHATTAN`
+4. HNSW EUCLIDEAN (no vectorType) → `DIST EUCLIDEAN`
+5. HNSW float deprecated alias + COSINE → `TYPE F64 DIST COSINE`
+
+**Bug caught**: All 5 tests originally failed because generator emitted `TYPE float32`/`TYPE float64`/`TYPE float` — SurrealDB expects `TYPE F32`/`TYPE F64`. Fixed by adding `VECTOR_TYPE_TO_SQL` mapping in `generator.ts`.
+
+**Result**: All 2424 tests pass (62 test files, 0 failures).
+
+
diff --git a/.changeset/README.md b/.changeset/README.md
deleted file mode 100644
index 654c6d4..0000000
--- a/.changeset/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Changesets
-
-Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
-with multi-package repos, or single-package repos to help you version and publish your code. You can
-find the full documentation for it [in our repository](https://github.com/changesets/changesets).
-
-We have a quick list of common questions to get you started engaging with this project in
-[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
diff --git a/.changeset/config.json b/.changeset/config.json
deleted file mode 100644
index 89f42e4..0000000
--- a/.changeset/config.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
- "changelog": [
- "@changesets/changelog-github",
- {
- "repo": "woss/dali"
- }
- ],
- "commit": true,
- "fixed": [],
- "linked": [],
- "access": "public",
- "baseBranch": "origin/main",
- "updateInternalDependencies": "patch",
- "ignore": []
-}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f4ce9f4..8f42aac 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,7 +38,7 @@ jobs:
- run: mkdir -p ~/.vite-plus/cache
- name: Cache vite-plus task cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.vite-plus/cache
key: ${{ runner.os }}-vp-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.run_id }}
@@ -49,12 +49,7 @@ jobs:
- run: pnpm vp run ci
- run: pnpm test:coverage
- run: pnpm lint
- - name: Fetch main branch
- run: git fetch origin main:main
- - name: Check for changeset
- if: github.event_name == 'pull_request'
- run: pnpm changeset status --since=main
- name: Report Coverage
if: always()
uses: davelosert/vitest-coverage-report-action@v2
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 77c8203..0ef597e 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -15,10 +15,11 @@ jobs:
permissions:
contents: write
id-token: write
- pull-requests: write
steps:
- uses: actions/checkout@v6
+ with:
+ fetch-depth: 0 # Need full history for tag lookup
- uses: jdx/mise-action@v4
- name: Get pnpm store directory
@@ -36,14 +37,39 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm build
- - name: Create Release Pull Request or Publish
- uses: changesets/action@v1
- with:
- publish: pnpm release
- version: pnpm version-packages:ci
- commit: 'chore: version packages'
- title: 'chore: version packages'
+ - name: Check version
+ id: version
+ run: |
+ VERSION=$(node -e "console.log(require('./packages/dali-orm/package.json').version)")
+ if git rev-parse "v$VERSION" >/dev/null 2>&1; then
+ echo "Tag v$VERSION already exists — nothing to publish"
+ echo "publish=false" >> $GITHUB_OUTPUT
+ else
+ echo "Detected new version v$VERSION"
+ echo "publish=true" >> $GITHUB_OUTPUT
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Publish @woss/dali-orm
+ if: steps.version.outputs.publish == 'true'
+ run: |
+ cd packages/dali-orm
+ pnpm publish --no-git-checks --provenance --access public
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Publish @woss/dali-memory
+ if: steps.version.outputs.publish == 'true'
+ run: |
+ cd packages/dali-memory
+ pnpm publish --no-git-checks --provenance --access public
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Create GitHub Release
+ if: steps.version.outputs.publish == 'true'
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- NPM_CONFIG_PROVENANCE: 'true'
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release create "v${{ steps.version.outputs.version }}" \
+ --generate-notes
diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json
index 8f6af93..1f2ef58 100644
--- a/.opencode/package-lock.json
+++ b/.opencode/package-lock.json
@@ -6,7 +6,7 @@
"": {
"name": "opencode-plugins",
"dependencies": {
- "@opencode-ai/plugin": "1.14.50"
+ "@opencode-ai/plugin": "1.15.13"
},
"devDependencies": {
"detect-terminal": "2.0.0",
@@ -17,9 +17,9 @@
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
- "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz",
+ "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==",
"cpu": [
"arm64"
],
@@ -30,9 +30,9 @@
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
- "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz",
+ "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==",
"cpu": [
"x64"
],
@@ -43,9 +43,9 @@
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
- "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz",
+ "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==",
"cpu": [
"arm"
],
@@ -56,9 +56,9 @@
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
- "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz",
+ "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==",
"cpu": [
"arm64"
],
@@ -69,9 +69,9 @@
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
- "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz",
+ "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==",
"cpu": [
"x64"
],
@@ -82,9 +82,9 @@
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
- "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz",
+ "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==",
"cpu": [
"x64"
],
@@ -95,19 +95,19 @@
]
},
"node_modules/@opencode-ai/plugin": {
- "version": "1.14.50",
- "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.50.tgz",
- "integrity": "sha512-2D4k6r8IFaAajEmezOZ3UKmRRGqEw2YzqotH5zLGT434yKtabduEsQgXa0fWlASut4FVT3JURhq5LqeBUV/k4g==",
+ "version": "1.15.13",
+ "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.13.tgz",
+ "integrity": "sha512-NFwZGhmxIPijtfz9swPJXDmhOpq4UWP8WjEE7GEMr7FwtJrK/hv6v36nFimed5+OKk+pQCrTJn/vhRW7Io72IA==",
"license": "MIT",
"dependencies": {
- "@opencode-ai/sdk": "1.14.50",
- "effect": "4.0.0-beta.65",
+ "@opencode-ai/sdk": "1.15.13",
+ "effect": "4.0.0-beta.66",
"zod": "4.1.8"
},
"peerDependencies": {
- "@opentui/core": ">=0.2.9",
- "@opentui/keymap": ">=0.2.9",
- "@opentui/solid": ">=0.2.9"
+ "@opentui/core": ">=0.2.16",
+ "@opentui/keymap": ">=0.2.16",
+ "@opentui/solid": ">=0.2.16"
},
"peerDependenciesMeta": {
"@opentui/core": {
@@ -129,9 +129,9 @@
}
},
"node_modules/@opencode-ai/sdk": {
- "version": "1.14.50",
- "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.50.tgz",
- "integrity": "sha512-IrTKTFviR4Tj+u0BI8h8XgXIvEpxwkkHqBj6E0aEc4DBErpS3qh2Lkp1xt0OdtCYiLE3wZ1bAIiHOiO66H/7TA==",
+ "version": "1.15.13",
+ "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.13.tgz",
+ "integrity": "sha512-4TwojIoQ8EG6/mVBuUVYZXiFcwNmiiytEnjnvyuvSJjGwFIlw2YIBFxtSVC3FbwwbwHT63teh1RHiQUUC4U5xw==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
@@ -173,9 +173,9 @@
"license": "MIT"
},
"node_modules/effect": {
- "version": "4.0.0-beta.65",
- "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.65.tgz",
- "integrity": "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw==",
+ "version": "4.0.0-beta.66",
+ "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.66.tgz",
+ "integrity": "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
@@ -286,18 +286,18 @@
"license": "Apache-2.0"
},
"node_modules/msgpackr": {
- "version": "1.11.12",
- "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz",
- "integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==",
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.13.tgz",
+ "integrity": "sha512-pWaxg0k1iiNdkAayUQ7Zlz/vYNfVefUttmHxqFcQjjtyqFa3w4x5rginOEzy/GvbWhBDD9K65/ZXyq8qz8utaQ==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
- "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz",
+ "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -308,12 +308,12 @@
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
- "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4",
+ "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4",
+ "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4",
+ "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4",
+ "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4",
+ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4"
}
},
"node_modules/multipasta": {
diff --git a/.opencode/package.json b/.opencode/package.json
index f2bf214..fa7360a 100644
--- a/.opencode/package.json
+++ b/.opencode/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"dependencies": {
- "@opencode-ai/plugin": "1.14.50"
+ "@opencode-ai/plugin": "1.15.13"
},
"devDependencies": {
"detect-terminal": "2.0.0",
diff --git a/README.md b/README.md
index df8aedf..5b45aa8 100644
--- a/README.md
+++ b/README.md
@@ -1,804 +1,103 @@
-# DaliORM
-
-A TypeScript ORM for SurrealDB with schema definitions, fluent query builders, and migrations. Built with 100% TypeScript for full type safety.
-
-## Table of Contents
-
-- [DaliORM](#daliorm)
- - [Table of Contents](#table-of-contents)
- - [Features](#features)
- - [Installation](#installation)
- - [Quick Start](#quick-start)
- - [Schema Definitions](#schema-definitions)
- - [Tables](#tables)
- - [Column Types](#column-types)
- - [Column Options](#column-options)
- - [Query Builders](#query-builders)
- - [SELECT](#select)
- - [INSERT](#insert)
- - [UPDATE](#update)
- - [DELETE](#delete)
- - [RELATE](#relate)
- - [Conditions](#conditions)
- - [Comparison Operators](#comparison-operators)
- - [String Operators](#string-operators)
- - [Null \& Array Checks](#null--array-checks)
- - [Combinators](#combinators)
- - [Typed Conditions](#typed-conditions)
- - [Database Functions](#database-functions)
- - [Driver Connection](#driver-connection)
- - [NodeDriver (Remote)](#nodedriver-remote)
- - [Embedded Modes](#embedded-modes)
- - [DaliORM Methods](#daliorm-methods)
- - [Configuration Files](#configuration-files)
- - [Migrations](#migrations)
- - [Shadow DB Pre-validation](#shadow-db-pre-validation)
- - [Demo Example](#demo-example)
- - [TypeScript Types](#typescript-types)
- - [Packages](#packages)
- - [License](#license)
-
-## Features
-
-- **TypeScript-First** - Full type inference for schema, queries, and results
-- **Schema Builder** - Define tables, columns, indexes, and relations programmatically
-- **Query Builders** - Fluent API for SELECT, INSERT, UPDATE, DELETE, and RELATE queries
-- **Migrations** - Generate and run database migrations with shadow DB pre-validation
-- **Multiple Drivers** - Support for remote (WebSocket) and embedded modes (memory, file, rocksdb)
-- **Config Files** - JSON, JSONC, and TypeScript configuration files with validation
-- **Database Functions** - Type-safe wrappers for all 28 SurrealDB function modules (array, math, string, crypto, geo, http, rand, vector, etc.)
-
-## Installation
+# Dali Packages
-```bash
-pnpm add @woss/dali-orm
-```
-
-## Quick Start
-
-```typescript
-import { DaliORM } from '@woss/dali-orm';
-import { defineTable, string, int, bool, select, insert, eq } from '@woss/dali-orm';
-
-// Define schema
-const userSchema = defineTable('user', {
- id: string('id'),
- name: string('name'),
- email: string('email').unique(),
- age: int('age').optional(),
- active: bool('active').default(true),
-});
-
-// Connect to SurrealDB
-const orm = await DaliORM.connect({
- driver: { url: 'ws://localhost:10101', namespace: 'test', database: 'test' },
-});
-
-// Insert a user
-await orm.execute(
- insert('user').values({ name: 'John', email: 'john@example.com', age: 30 }).returnAfter(),
-);
-
-// Query users
-const users = await orm.execute(
- select('user')
- .select('id', 'name', 'email')
- .where(eq('active', true))
- .orderBy('name', 'ASC')
- .limit(10),
-);
-
-await orm.disconnect();
-```
-
-## Schema Definitions
-
-### Tables
-
-```typescript
-import {
- defineTable,
- string,
- int,
- bool,
- index,
- datetime,
- defineRelationTable,
-} from '@woss/dali-orm';
-
-// Basic table
-const userSchema = defineTable('user', {
- id: string('id'),
- name: string('name'),
- email: string('email'),
- age: int('age'),
-});
-
-// Table with options
-const articleSchema = defineTable(
- 'article',
- {
- id: string('id'),
- created_at: datetime('created_at').defaultNow(),
- title: string('title'),
- content: string('content'),
- published_at: datetime('published_at').optional(),
- author: string('author'),
- },
- {
- schema: 'full', // 'full' or 'less'
- type: 'normal', // 'normal' or 'relation'
- permissions: {
- select: 'WHERE true',
- create: 'WHERE true',
- update: 'WHERE true',
- delete: 'WHERE true',
- },
- indexes: [
- index('email_idx').on('email').unique(),
- index('title_search').on('title').fulltext(),
- index('embedding_idx').on('embedding').hnsw(1536, { distance: 'cosine' }),
- ],
- },
-);
-
-// Relation table
-const wroteSchema = defineRelationTable(
- 'wrote',
- {
- id: string('id'),
- created_at: datetime('created_at').defaultNow(),
- },
- {
- in: 'user',
- out: 'article',
- enforced: true,
- },
-);
-```
-
-### Column Types
-
-| Function | SurrealQL Type |
-| ------------ | -------------- |
-| `string()` | `string` |
-| `int()` | `int` |
-| `float()` | `float` |
-| `bool()` | `bool` |
-| `datetime()` | `datetime` |
-| `duration()` | `duration` |
-| `decimal()` | `decimal` |
-| `array()` | `array` |
-| `object()` | `object` |
-| `record()` | `record` |
-| `geometry()` | `geometry` |
-
-### Column Options
-
-```typescript
-string()
- .optional() // Allow NULL values
- .default('value') // Set default value
- .assert('condition') // Add validation assertion
- .readonly() // Mark as read-only
- .flexible() // Allow flexible schema
- .unique(); // Create unique index
-```
-
-## Query Builders
-
-### SELECT
-
-```typescript
-import { select, eq, and, or, not, like, contains, inside, isNull } from '@woss/dali-orm';
-
-select('user')
- .select('id', 'name', 'email') // Select specific columns
- .selectAs('name', 'full_name') // Select with alias
- .selectOnly() // SELECT ONLY
- .where(eq('age', 18)) // WHERE clause
- .whereRaw('name LIKE "John%"') // Raw WHERE
- .orderBy('name', 'ASC') // ORDER BY (ASC or DESC)
- .limit(10) // LIMIT
- .start(20) // OFFSET/START
- .groupBy('status') // GROUP BY
- .having(eq('count', 5)) // HAVING
- .fetch('posts') // FETCH related records
- .fetchAs('posts', 'user_posts') // FETCH with alias
- .graph('out', 'friends') // Graph traversal
- .graphWith('out', 3, 'friends') // Graph with depth
- .parallel() // PARALLEL execution
- .split() // SPLIT each
- .timeout(5000) // TIMEOUT (seconds)
- .toSQL();
-```
-
-### INSERT
-
-```typescript
-import { insert } from '@woss/dali-orm';
-
-// Single record
-insert('user')
- .values({ name: 'John', email: 'john@example.com' })
- .returnAfter() // RETURN AFTER
- .returnBefore() // RETURN BEFORE
- .ignore(); // IGNORE on conflict
-
-// Multiple records
-insert('user').values([
- { name: 'John', email: 'john@example.com' },
- { name: 'Jane', email: 'jane@example.com' },
-]);
-```
-
-### UPDATE
-
-```typescript
-import { update, eq } from '@woss/dali-orm';
-
-update('user', 'user:123')
- .set('name', 'Jane')
- .set({ email: 'jane@example.com', age: 25 })
- .where(eq('active', true)) // Filter which records to update
- .returnAfter()
- .returnBefore();
-```
-
-### DELETE
-
-```typescript
-import { remove, eq } from '@woss/dali-orm';
-
-// Delete by ID
-remove('user', 'user:123').returnBefore().returnAfter();
-
-// Delete with condition
-remove('user').where(eq('active', false)).returnBefore();
-```
-
-### RELATE
-
-```typescript
-import { relate, eq } from '@woss/dali-orm';
-
-relate('wrote', 'user:123', 'article:456')
- .set('created_at', 'time::now()')
- .where(eq('active', true))
- .returnAfter()
- .returnBefore();
-```
-
-## Conditions
-
-### Comparison Operators
-
-```typescript
-import { eq, ne, gt, gte, lt, lte } from '@woss/dali-orm';
-
-eq('age', 18); // age = 18
-ne('status', 'active'); // status != 'active'
-gt('price', 100); // price > 100
-gte('age', 18); // age >= 18
-lt('price', 100); // price < 100
-lte('age', 18); // age <= 18
-```
-
-### String Operators
-
-```typescript
-import { like, contains, startsWith, endsWith } from '@woss/dali-orm';
-
-like('name', 'J%'); // name LIKE 'J%'
-contains('name', 'ohn'); // string::contains(name, 'ohn')
-startsWith('name', 'Jo'); // string::startsWith(name, 'Jo')
-endsWith('name', 'hn'); // string::endsWith(name, 'hn')
-```
-
-### Null & Array Checks
-
-```typescript
-import { isNull, isNotNull, inside, notInside, all, any } from '@woss/dali-orm';
-
-isNull('email'); // email = NONE
-isNotNull('email'); // email != NONE
-inside('status', ['active', 'pending']); // status IN [...]
-notInside('status', ['banned']); // status NOT IN [...]
-all('tags', ['featured', 'new']); // CONTAINSALL
-any('tags', ['sale', 'new']); // CONTAINSANY
-```
-
-### Combinators
-
-```typescript
-import { and, or, not } from '@woss/dali-orm';
-
-and(eq('age', 18), eq('active', true));
-or(eq('status', 'active'), eq('status', 'pending'));
-not(eq('active', false));
-```
-
-## Typed Conditions
-
-For full TypeScript type safety, import conditions from `dali-orm` and use with `SurrealColumn`:
-
-```typescript
-import {
- defineTable,
- string,
- int,
- array,
- select,
- eq,
- gt,
- and,
- or,
- like,
- contains,
- inside,
-} from '@woss/dali-orm';
-
-// Define schema with typed columns
-const users = defineTable('user', {
- id: string('id'),
- name: string('name'),
- email: string('email'),
- age: int('age'),
- status: string('status'),
- tags: array('tags'),
-});
-
-// Type-safe conditions with SurrealColumn
-select('user').where(eq(users.name, 'John')); // name = 'John'
-select('user').where(gt(users.age, 18)); // age > 18
-select('user').where(and(eq(users.status, 'active'), gt(users.age, 18)));
-select('user').where(inside(users.status, ['active', 'pending']));
-select('user').where(contains(users.tags, 'featured'));
-
-// String conditions
-select('user').where(like(users.name, 'J%'));
-```
-
-### Backwards Compatibility
-
-Conditions also accept string column names for backwards compatibility:
-
-```typescript
-select('user').where(eq('name', 'John')); // Still works
-```
-
-### SDK Integration
-
-The ORM conditions are built on top of the SurrealDB SDK's internal condition functions, providing:
-
-- Full TypeScript type inference
-- Proper escaping of parameter values
-- Consistent behavior with SDK methods
-
-## Database Functions
-
-Type-safe TypeScript wrappers for all SurrealDB built-in functions. Import from `@woss/dali-orm/sdk/functions`:
-
-```typescript
-import {
- count,
- math,
- string,
- vector,
- time,
- crypto,
- geo,
- meta,
- session,
- array,
- set,
- value,
- parse,
- type,
- sleep,
- record as rec,
- object,
- sequence,
- rand,
- search,
- bytes,
- duration,
- encoding,
- http,
- files,
- not,
- api,
- $,
- as_,
- col,
- expr,
-} from '@woss/dali-orm/sdk/functions';
-```
-
-### String & Math
-
-```typescript
-string.concat('a', 'b'); // string::concat('a', 'b')
-string.lowercase('HELLO'); // string::lowercase('HELLO')
-string.isEmail('a@b.com'); // string::is_email('a@b.com')
-string.html.encode('
'); // string::html::encode('
')
-string.distance('a', 'b'); // string::distance('a', 'b')
-
-math.round(4.7); // math::round(4.7)
-math.max(1, 2, 3); // math::max([1, 2, 3])
-math.sqrt(9); // math::sqrt(9)
-```
-
-### Crypto
-
-```typescript
-crypto.sha256('data'); // crypto::sha256('data')
-crypto.blake3('data'); // crypto::blake3('data')
-crypto.argon2.generate('pw'); // crypto::argon2::generate('pw')
-crypto.bcrypt.compare('pw', 'h'); // crypto::bcrypt::compare('pw', 'h')
-crypto.uuid.v4(); // crypto::uuid::v4()
-```
-
-### Vector & Geo
-
-```typescript
-vector.distance(v1, v2); // vector::distance(v1, v2)
-vector.similarity.cosine(v1, v2); // vector::similarity::cosine(v1, v2)
-
-geo.distance(p1, p2); // geo::distance(p1, p2)
-geo.hash.encode(lng, lat); // geo::hash::encode(lng, lat)
-```
-
-### Time & Type
-
-```typescript
-time.now(); // time::now()
-time.format(date, '%Y-%m-%d'); // time::format(date, '%Y-%m-%d')
-type.int('42'); // type::int('42')
-type.thing('user', 'abc'); // type::thing('user', 'abc')
-type.isArray(val); // type::is_array(val)
-```
-
-### Array & Object
+> SUPER EARLY BETA -- do not use in production yet! API is subject to change without warning.
-```typescript
-array.push(['a'], 'b'); // array::push(['a'], 'b')
-array.filter(arr, pred); // array::filter(arr, pred)
-object.keys({ a: 1 }); // object::keys({a: 1})
-object.entries({ a: 1 }); // object::entries({a: 1})
-```
-
-### HTTP, Rand, Sequence & More
-
-```typescript
-http.get('https://api.example.com'); // http::get(...)
-rand.int(1, 100); // rand::int(1, 100)
-sequence.next('my_seq'); // sequence::next(my_seq)
-sleep('1s'); // sleep(1s)
-count('*'); // count(*)
-```
-
-### SQL Expression Helpers
-
-```typescript
-$('age'); // Column reference: age
-as_(count(), 'total'); // Alias: count() AS total
-col('name'); // Column reference
-expr`${$('age')} + 1`; // Raw expression: age + 1
-```
-
-Functions compose naturally in query builders:
-
-```typescript
-const result = await select(driver, users)
- .fields(as_(mathRound($('score')), 'rounded'))
- .where((w) => w.eq('name', 'Alice'))
- .execute();
-```
-
-## Driver Connection
-
-### NodeDriver (Remote)
-
-```typescript
-import { DaliORM } from '@woss/dali-orm';
-
-const orm = await DaliORM.connect({
- driver: {
- url: 'ws://localhost:10101',
- namespace: 'test',
- database: 'test',
- auth: { user: 'root', pass: 'password' },
- },
-});
-```
-
-### Embedded Modes
-
-```typescript
-import { DaliORM, EmbeddedDriver } from '@woss/dali-orm';
-
-// Memory mode
-const orm = await DaliORM.connect({
- driver: new EmbeddedDriver({ mode: 'memory', namespace: 'test', database: 'test' }),
-});
-
-// SurrealKV mode (persistent key-value storage)
-const orm = await DaliORM.connect({
- driver: new EmbeddedDriver({
- mode: 'surrealkv',
- path: './db',
- namespace: 'test',
- database: 'test',
- }),
-});
-
-// RocksDB mode (alias for surrealkv)
-const orm = await DaliORM.connect({
- driver: new EmbeddedDriver({
- mode: 'rocksdb',
- path: './db',
- namespace: 'test',
- database: 'test',
- }),
-});
-```
-
-### DaliORM Methods
-
-```typescript
-// Execute raw SQL with parameters
-const result = await orm.query('SELECT * FROM user WHERE age > $age', { age: 18 });
-
-// Execute query builder
-const result = await orm.execute(select('user').where(eq('active', true)));
+## Packages
-// Transactions
-await orm.transaction(async (tx) => {
- await tx.query('CREATE user:john SET name = "John"');
- await tx.query('CREATE post:1 SET title = "Hello"');
- return { success: true };
-});
+| Package | Description | Readme |
+| ------------------- | --------------------------------------------------------------------------------- | --------------------------------------------- |
+| `@woss/dali-orm` | Schema definitions, query builders, conditions (merged core + driver) | [README.md](./packages/dali-orm/README.md) |
+| `@woss/dali-memory` | Agent memory with Dali ORM using embeddings, hooks, and tools backed by SurrealDB | [README.md](./packages/dali-memory/README.md) |
-// Live queries
-const subscriptionId = await orm.live('user', (data) => {
- console.log(data.action, data.result);
-});
+## Development
-// Kill a live query subscription
-await orm.kill(subscriptionId);
+### Prerequisites
-// Switch namespace/database
-await orm.use('new_namespace', 'new_database');
+- Node.js >=20 (managed via [mise](https://mise.jdx.dev)) — see [`.mise.toml`](./.mise.toml)
+- pnpm 11 — installed via mise or corepack
+- [SurrealDB](https://surrealdb.com) (for integration tests)
-// Get raw SurrealDB client for advanced operations
-const db = orm.client;
-await db.query('SELECT * FROM user');
+### Setup
-// Close connection
-await orm.disconnect();
+```bash
+pnpm install
```
-## Configuration Files
-
-The `@woss/dali-orm` package supports configuration files for connecting to databases.
-
-### Supported Formats
-
-- `.dali-orm.json` - JSON format
-- `.dali-orm.jsonc` - JSON with comments
-- `.dali-orm.ts` - TypeScript format
-
-### Example Config
+### Build
-Create `.dali-orm.json` in your project root:
-
-```json
-{
- "url": "ws://localhost:8000",
- "namespace": "test",
- "database": "test",
- "auth": {
- "type": "root",
- "user": "root",
- "pass": "root"
- }
-}
+```bash
+pnpm build
```
-### Usage
-
-```typescript
-import { DaliORM } from '@woss/dali-orm';
-
-// Load from config file
-const orm = await DaliORM.connect({
- config: './dali-orm.json',
-});
+Builds all packages in parallel (`pnpm -r build`).
-// Auto-discover config
-const orm = await DaliORM.connect({
- config: true,
-});
+### Test
-// Explicit options override config
-const orm = await DaliORM.connect({
- config: './dali-orm.json',
- driver: {
- url: 'ws://custom:8000', // Takes precedence
- },
-});
+```bash
+pnpm test # All unit tests
+pnpm test:coverage # With coverage report
+pnpm test:watch # Watch mode
+pnpm test:integration # Integration tests (requires SurrealDB)
```
-### Authentication Types
-
-| Type | Required Fields |
-| ----------- | --------------------------------------- |
-| `root` | `user`, `pass` |
-| `namespace` | `user`, `pass`, `namespace` |
-| `database` | `user`, `pass`, `namespace`, `database` |
-| `record` | `table` |
-
-### Shadow Database
-
-Optionally configure a shadow database for pre-validation:
+### Lint & Format
-```json
-{
- "shadow": {
- "namespace": "myapp_shadow",
- "database": "shadow_db"
- }
-}
+```bash
+pnpm lint # Check
+pnpm lint:fix # Auto-fix
+pnpm format # Format
```
-Guard: shadow ns/db must differ from target ns/db.
-
-## Migrations
-
-### CLI Commands
+### Clean
```bash
-# Dev workflow — generate migration + validate on shadow + apply
-npx dali-orm migrate dev add_users_table
-
-# Deploy to production — validate pending on shadow + apply (REQUIRES shadow config)
-npx dali-orm migrate deploy
-
-# Apply pending migrations to database
-npx dali-orm migrate up
-
-# Rollback last migration
-npx dali-orm migrate down --steps 1
-
-# Reset all migrations
-npx dali-orm migrate reset
-
-# Check migration status
-npx dali-orm migrate status
-
-# Generate migration from schema
-npx dali-orm generate add_users_table
-
-# Pull schema from database
-npx dali-orm pull
+pnpm clean # Remove all dist/ directories
```
-- `migrate dev ` — Generate migration file, validate on shadow DB, then apply to target
-- `migrate deploy` — Validate all pending migrations on shadow DB, then apply to target (requires `shadow` config)
-- `push` is removed — use `migrate dev` or `migrate deploy` instead
-
-### Shadow DB Pre-validation
+### Local Development Install (Link)
-DaliORM supports shadow database validation for safe migration deployment. Before applying changes to production, migrations are validated on an isolated shadow database. If validation fails, neither the shadow nor the target database is affected.
+To use these packages in another local project during development:
-**Configuration:**
+```bash
+# From the dali repo root, build first
+pnpm build
-Add `shadow` to your `dali-orm.config.ts`:
+# Link each package globally
+cd packages/dali-orm && pnpm link --global
+cd packages/dali-memory && pnpm link --global
-```typescript
-export default defineConfig({
- url: 'ws://localhost:10101',
- namespace: 'myapp',
- database: 'mydb',
- // ...
- shadow: {
- namespace: 'myapp_shadow', // Must differ from target namespace
- database: 'shadow_db', // Destroyed after each validation run
- },
-});
+# In your target project
+pnpm link --global @woss/dali-orm
+pnpm link --global @woss/dali-memory
```
-**Guard:** Shadow ns/db cannot match target ns/db — throws error immediately.
-
-### Programmatic API
-
-```typescript
-import { MigrationRunner, SurrealQLGenerator } from '@woss/dali-orm/migration/api';
-import { DaliORM } from '@woss/dali-orm';
-
-// Generate SQL from schema
-const generator = new SurrealQLGenerator();
-const sql = generator.generateMigration([userSchema]);
-
-// First connect to database
-const orm = await DaliORM.connect({
- driver: { url: 'ws://localhost:10101', namespace: 'test', database: 'test' },
-});
-
-// Create runner with the driver
-const runner = new MigrationRunner(orm.driver);
+The `dali-orm` CLI can also be used directly from source without linking:
-// Initialize migration tracking
-await runner.init();
-
-// Run pending migrations
-await runner.up();
-
-// Check status
-const status = await runner.status([]);
-
-// Revert last migration (1 step)
-await runner.down(1);
+```bash
+pnpm --filter @woss/dali-orm exec dali-orm --help
```
-## Demo Example
+### Deploy
-The `examples/demo` package provides a complete working demo of the ORM:
+Bump version on a branch, merge to main — CI auto-detects and publishes.
```bash
-cd examples/demo
-
-# Run with interactive prompts
-pnpm dev
-
-# Run with auto-accept defaults (no prompts)
-pnpm dev --yes
-
-# Show help
-pnpm dev --help
-
-# Generate migration from schema
-pnpm generate
+# On your feature branch, bump version (patch/minor/major)
+pnpm bump patch # 0.1.0 → 0.1.1
+pnpm bump minor # 0.1.0 → 0.2.0
+pnpm bump major # 0.1.0 → 1.0.0
-# Apply migrations
-pnpm dali-orm migrate up
+# This creates a commit via `but` with the version bump.
+# Push the branch and merge to main — CI handles the rest.
```
-The demo includes:
+On every push to `main`, the [Publish workflow](.github/workflows/publish.yml) reads the version from `packages/dali-orm/package.json`. If the `v*` tag for that version doesn't exist yet, it:
-- Schema definitions with tables and relations
-- Interactive CLI for data entry
-- Migration generation and execution
-- Complete CRUD operations with relations
-
-## TypeScript Types
-
-```typescript
-import { defineTable, string, int, InferSelectModel, InferInsertModel } from '@woss/dali-orm';
-
-const userSchema = defineTable('user', {
- id: string('id'),
- name: string('name'),
- email: string('email'),
- age: int('age'),
-});
-
-// Type for SELECT results
-type User = InferSelectModel;
-// { id?: string; name?: string; email?: string; age?: number | null }
-
-// Type for INSERT data
-type NewUser = InferInsertModel;
-// { name: string; email: string; age?: number }
-```
-
-## Packages
+- Builds both packages
+- Publishes `@woss/dali-orm` and `@woss/dali-memory` to npm
+- Creates a GitHub Release (with tag) with auto-generated notes
-| Package | Description |
-| ------------------------------ | --------------------------------------------------------------------- |
-| `@woss/dali-orm` | Schema definitions, query builders, conditions (merged core + driver) |
-| `@woss/dali-orm/migration/api` | CLI, migrations, schema generation, config management |
+No manual tag management needed.
## License
diff --git a/meta/_journal.json b/meta/_journal.json
deleted file mode 100644
index 52e0524..0000000
--- a/meta/_journal.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "version": 1,
- "dialect": "surrealdb",
- "id": "6477893e10f8",
- "entries": [
- {
- "idx": 1,
- "when": "2026-05-18T21:21:42.286728Z",
- "tag": "init_from_pull",
- "breakpoints": [
- true,
- true,
- true,
- true
- ],
- "hash": "8031cb1bf48eabe3fa0d70fc42aa21650d85ef8eea31bfd319833fb2960c05c5"
- }
- ]
-}
\ No newline at end of file
diff --git a/package.json b/package.json
index 72df86c..34ffa6f 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"build": "pnpm -r build",
+ "bump": "tsx scripts/bump-version.ts",
"clean": "pnpm -r clean",
"format": "vp fmt",
"lint": "vp check",
@@ -14,19 +15,15 @@
"test": "vp test run",
"test:coverage": "vp test run --coverage",
"test:integration": "pnpm --filter @woss/dali-memory test:integration",
- "version-packages": "pnpm changeset version",
- "version-packages:ci": "CI=true pnpm version-packages",
- "release": "pnpm changeset publish",
"test:watch": "vp test watch",
"typecheck": "pnpm -r typecheck"
},
"devDependencies": {
- "@changesets/changelog-github": "^0.7.0",
- "@changesets/cli": "^2.31.0",
"@types/node": "catalog:",
"@vitest/coverage-v8": "catalog:",
"dotenv": "^17.4.1",
"surrealdb": "catalog:",
+ "tsx": "catalog:",
"typescript": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
diff --git a/packages/dali-memory/README.md b/packages/dali-memory/README.md
new file mode 100644
index 0000000..856b165
--- /dev/null
+++ b/packages/dali-memory/README.md
@@ -0,0 +1,420 @@
+# @woss/dali-memory
+
+SurrealDB-backed memory plugin for OpenCode. Provides persistent memory with semantic search, fact extraction, session tracking, and message history for AI agents.
+
+## Features
+
+- **Persistent memory** — store and retrieve agent memories across sessions
+- **Semantic search** — vector-based similarity search via embeddings
+- **Facts** — structured knowledge extraction and verification
+- **Session tracking** — automatic session creation, updates, and model association
+- **Message history** — capture user and agent messages per session
+- **Two storage modes** — embedded (local) or remote SurrealDB
+- **Two embedding providers** — local HuggingFace pipeline or remote API
+- **DaliORM integration** — type-safe schema, migrations, query builders
+- **OpenCode plugin** — tool-based memory operations + lifecycle hooks
+
+## Architecture
+
+```
+dali-memory/
+├── src/
+│ ├── index.ts # Entry (empty export)
+│ ├── opencode.ts # OpenCode plugin definition
+│ ├── schema.ts # DaliORM schema (12 tables)
+│ ├── config.ts # Configuration loading + Zod validation
+│ ├── constants.ts # Plugin name constant
+│ ├── commands/
+│ │ └── commands.ts # CLI command templates
+│ ├── embedder/
+│ │ ├── embedder.ts # EmbedderService (provider dispatch)
+│ │ ├── schemas.ts # Embedder config + result Zod schemas
+│ │ ├── local-provider.ts # HuggingFace Transformers pipeline
+│ │ └── remote-provider.ts# OpenAI-compatible API client
+│ ├── tools/
+│ │ ├── hooks.ts # session.compacting + chat.message hooks
+│ │ ├── events.ts # session.created/updated/compacted events
+│ │ ├── memory-tool.ts # dali_memory tool executor
+│ │ ├── migrate-tool.ts # dali_migrate_oc_db tool executor
+│ │ └── types.ts # Shared type definitions
+│ └── utils/
+│ ├── argsParsing.ts # Zod argument validation + rehydration
+│ ├── logger.ts # LogTape logger (file rotation)
+│ ├── memory-service.ts # MemoryService (business logic layer)
+│ └── surreal-client.ts # SurrealClient (DaliORM data access)
+├── migrations/ # DaliORM migration files
+├── scripts/
+│ └── generate-schema.ts # JSON Schema generation script
+└── dali-memory.schema.json # Generated JSON Schema for config
+```
+
+### Data Flow
+
+```
+OpenCode Plugin
+ │
+ ├── tool(dali_memory) ──► MemoryTool ──► MemoryService ──► SurrealClient ──► DaliORM ──► SurrealDB
+ │
+ ├── tool(dali_migrate_oc_db) ──► MigrateTool ──► MemoryService ──► SurrealClient.applyPendingMigrations()
+ │
+ ├── chat.message hook ──► MemoryService.saveMessage()
+ │
+ ├── session.compacting hook ──► injects FACT: prompt into output
+ │
+ └── event handler ──► session.created → upsertSession()
+ session.updated → updateSession()
+ session.compacted → injectFactExtraction()
+```
+
+## Installation
+
+```bash
+pnpm add @woss/dali-memory
+```
+
+## Configuration
+
+Configuration is loaded from two locations (merged, project overrides user):
+
+1. **User config**: `~/.config/dali-memory/dali-memory.jsonc` or `.json`
+2. **Project config**: `/.opencode/dali-memory.jsonc` or `.json`
+
+### Default config
+
+```jsonc
+{
+ "storage": {
+ "mode": "embed",
+ "embed": {
+ "engine": "surrealkv",
+ "dataPath": "~/.config/dali-memory/data/",
+ },
+ },
+ "embedding": {
+ "provider": "remote",
+ "endpoint": "http://localhost:1234/v1",
+ "model": "text-embedding-qwen3-embedding-4b",
+ },
+}
+```
+
+### Configuration schema
+
+| Path | Type | Description |
+| ------------------------------ | --------------------------- | ------------------------------------------------------------- |
+| `storage.mode` | `"embed"` \| `"remote"` | Storage backend |
+| `storage.embed.engine` | `"surrealkv"` \| `"memory"` | Embedded engine (persistent or in-memory) |
+| `storage.embed.dataPath` | `string` | Path for embedded data (~ expanded) |
+| `storage.remote.url` | `string` | Remote SurrealDB URL (ws:// or http://) |
+| `storage.remote.auth.username` | `string` | Remote auth username |
+| `storage.remote.auth.password` | `string` | Remote auth password (supports `env://` / `file://` prefixes) |
+| `storage.remote.namespace` | `string` | SurrealDB namespace |
+| `storage.remote.database` | `string` | SurrealDB database |
+| `embedding.provider` | `"remote"` \| `"local"` | Embedding provider |
+| `embedding.endpoint` | `string` | API endpoint (remote) or cache dir (local) |
+| `embedding.model` | `string` | Model ID or HuggingFace model name |
+| `embedding.apiKey` | `string` | API key (supports `env://` / `file://` prefixes) |
+| `embedding.modelCacheDir` | `string` | Local model cache directory |
+| `plugin.chatMessage.enabled` | `boolean` | Capture chat messages |
+| `plugin.autoCapture.enabled` | `boolean` | Auto-capture from session activity |
+
+## Schema
+
+12 tables defined via DaliORM in `src/schema.ts`:
+
+### Tables
+
+| Table | Type | Description |
+| ------------ | ------- | -------------------------------------------------------------- |
+| `memories` | `TABLE` | Persistent memories with vector embeddings, tags, content hash |
+| `embeddings` | `TABLE` | Embedding dimension/model metadata |
+| `facts` | `TABLE` | Structured knowledge facts (verified/unverified) |
+| `projects` | `TABLE` | Project registrations (unique by directory_path) |
+| `sessions` | `TABLE` | OpenCode session records |
+| `messages` | `TABLE` | Per-session chat messages |
+| `models` | `TABLE` | AI model records (unique by provider_id + model_id) |
+
+### Relation tables
+
+| Table | Direction | Description |
+| ----------------- | ----------------------- | --------------------------- |
+| `part_of_project` | `projects → memories` | Memory belongs to project |
+| `part_of_session` | `sessions → memories` | Memory belongs to session |
+| `has_embedding` | `embeddings → memories` | Memory has vector embedding |
+| `relates_to` | `facts → memories` | Fact relates to memory |
+| `uses_model` | `sessions → models` | Session uses model |
+
+### Entity relationships
+
+```
+projects ──→ part_of_project ──→ memories ──→ has_embedding ──→ embeddings
+sessions ──→ part_of_session ──→ memories
+facts ────→ relates_to ──→ memories
+sessions ──→ uses_model ──→ models
+sessions ──── messages (record field)
+```
+
+## Storage Modes
+
+### Embedded (`mode: "embed"`)
+
+Runs SurrealDB embedded in-process. Two engines:
+
+- **`surrealkv`** (default) — persistent, file-based. Data stored at configured `dataPath`.
+- **`memory`** — in-memory only. Resets on restart. Useful for testing.
+
+```jsonc
+{
+ "storage": {
+ "mode": "embed",
+ "embed": {
+ "engine": "surrealkv",
+ "dataPath": "~/.config/dali-memory/data/",
+ },
+ },
+}
+```
+
+### Remote (`mode: "remote"`)
+
+Connects to an external SurrealDB server via WebSocket.
+
+```jsonc
+{
+ "storage": {
+ "mode": "remote",
+ "remote": {
+ "url": "ws://localhost:10101",
+ "auth": {
+ "username": "root",
+ "password": "env://SURREALDB_PASSWORD",
+ },
+ "namespace": "memory",
+ "database": "memory",
+ },
+ },
+}
+```
+
+## Embedding Providers
+
+### Remote provider
+
+Calls an OpenAI-compatible embeddings API (e.g., llama.cpp, vLLM, OpenAI).
+
+```jsonc
+{
+ "embedding": {
+ "provider": "remote",
+ "endpoint": "http://localhost:1234/v1",
+ "model": "text-embedding-qwen3-embedding-4b",
+ "apiKey": "file:///path/to/key",
+ },
+}
+```
+
+Features:
+
+- LRU cache (100 entries) with eviction
+- 30-second request timeout
+- API key via `env://` or `file://` prefixes
+
+### Local provider
+
+Runs HuggingFace Transformers via `@huggingface/transformers` (ONNX runtime).
+
+```jsonc
+{
+ "embedding": {
+ "provider": "local",
+ "model": "Xenova/bge-large-en-v1.5",
+ "modelCacheDir": "~/.config/dali-memory/model_cache/",
+ },
+}
+```
+
+Features:
+
+- Mean pooling + L2 normalization
+- Model cached to disk after first download
+- Default model: `Xenova/bge-large-en-v1.5` (1024 dimensions)
+
+## OpenCode Plugin
+
+The plugin registers itself as `DaliMemoryPlugin` and hooks into OpenCode lifecycle:
+
+### Tools
+
+| Tool | Description |
+| -------------------- | ----------------------------------------------------------- |
+| `dali_memory` | CRUD for memories + facts. Validated via Zod schema. |
+| `dali_migrate_oc_db` | Apply pending migration files from `migrations/` directory. |
+
+### Commands
+
+| Command | Description |
+| -------------------- | ----------------------------------------------------------------------- |
+| `dali_migrate_oc_db` | Runs the migrate tool (subtask). |
+| `dali_remember` | Proxy to `dali_memory` tool with argument forwarding (subtask). |
+| `dali_extract_facts` | Instructs agent to extract knowledge facts from conversation (subtask). |
+| `noop` | No-op command for termination. |
+
+### Hooks
+
+- **`chat.message`** — captures user and agent text from each message, persists to `messages` table.
+- **`experimental.session.compacting`** — injects a fact-extraction prompt into the compaction summary context, instructing the agent to output `FACT: ` lines.
+
+### Events
+
+- **`session.created`** — upserts session record, creates model record, links model to session via `uses_model` relation.
+- **`session.updated`** — updates session title/slug, upserts model, re-links.
+- **`session.compacted`** — injects fact extraction prompt into the session via `client.session.prompt()`.
+
+### Plugin initialization flow
+
+```
+Plugin load
+ └─► memoryService.initialize(directory)
+ ├── initConfig(directory) # Load user + project config
+ ├── embeddingService.configure() # Initialize embedding provider
+ ├── surrealClient.connect() # Connect to SurrealDB + apply migrations
+ └─► getOrCreateProject() # Register project by directory path
+ └─► projectId stored in memoryService.projectId
+```
+
+## Memory Tool
+
+The `dali_memory` tool supports multiple modes:
+
+### Memory operations
+
+| Mode | Args | Description |
+| -------- | ------------------------------------- | --------------------------------------------- |
+| `add` | `content`, `tags?`, `type?`, `scope?` | Store memory with auto-generated embedding |
+| `search` | `query`, `tags?`, `scope?` | Semantic search by vector similarity (cosine) |
+| `list` | `scope?` | List recent memories (limit 50) |
+| `forget` | `id` | Delete memory by ID |
+| `help` | — | Show available modes |
+
+### Fact operations
+
+| Mode | Args | Description |
+| ------------- | ---------------------- | ----------------------------------------------- |
+| `fact_add` | `content`, `memoryId?` | Store knowledge fact, optionally link to memory |
+| `fact_list` | `memoryId` | List facts linked to a memory |
+| `fact_verify` | `factId` | Mark fact as verified |
+
+### Scoping
+
+The `scope` parameter controls which container tag to use:
+
+- `"project"` (default) — scope to the current project directory
+- `"all-projects"` — scope to the current user across all projects
+
+Tags are generated deterministically:
+
+- **User tag**: `opencode_user_`
+- **Project tag**: `opencode_project_`
+
+### Memory deduplication
+
+Memories are content-deduplicated via:
+
+1. `content_hash` column with `DEFAULT crypto::blake3(content)`
+2. Unique index `idx_memories_content_hash`
+3. On duplicate: catches the constraint violation and selects the existing record by content instead
+
+## Migrations
+
+The `migrations/` directory contains 12 migration files covering schema evolution:
+
+```bash
+20260513162005_init/
+20260513162020_embedding/
+20260513162917_rm-user-embed/
+20260513232107_add/
+20260513235512_uses_model/
+20260514001453_uses_model_idx/
+20260514150050_add_indexes/
+20260514151644_add-content-hash/
+20260514162212_add_variant_to_model/
+20260514174946_cleanup_session/
+20260514182642_add_index_to_partofproject/
+20260515183000_content_hash_blake3/
+```
+
+Migrations auto-apply on connect via `migrateToDatabase()`. Run manually with the `dali_migrate_oc_db` tool or via CLI:
+
+```bash
+dali-orm migrate dev
+```
+
+## Logging
+
+Uses LogTape with rotating file sink. Configuration via environment variables:
+
+| Variable | Default | Description |
+| ------------------------------- | -------------------------------------------- | ------------------------------------------- |
+| `DALI_MEMORY_LOGGING_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
+| `DALI_MEMORY_LOGGING_ENABLED` | `true` | Set to `false` to disable file logging |
+| `DALI_MEMORY_LOGGING_FILE_PATH` | `~/.config/dali-memory/logs/dali-memory.log` | Log file path |
+
+Log rotation: 5 files × 10MB each.
+
+## Scripts
+
+| Script | Description |
+| ----------------------- | -------------------------------------------------- |
+| `pnpm build` | Build (generates JSON Schema + bundles) |
+| `pnpm test` | Run unit tests |
+| `pnpm test:all` | Run all tests (unit + integration, no parallelism) |
+| `pnpm test:integration` | Run integration tests only |
+| `pnpm orm` | Proxy to `dali-orm` CLI |
+
+## Development
+
+```bash
+# Build
+pnpm build
+
+# Tests
+pnpm test
+pnpm test:all # includes integration tests
+pnpm test:integration
+
+# Generate JSON Schema (from Zod config schema)
+pnpm generate:schema
+
+# Run migrations directly
+pnpm orm migrate dev
+```
+
+## Integration testing
+
+Integration tests in `src/__tests__/*.integration.test.ts` connect to an embedded SurrealDB in `memory` mode. They validate:
+
+- `memories` CRUD with vector search
+- Session lifecycle (create → update → model linking)
+- Message persistence
+- Project registration
+- Fact save/verify/retrieval
+- Full memory save → search → delete flow
+
+## Dependencies
+
+| Package | Purpose |
+| --------------------------- | ------------------------------------------ |
+| `@opencode-ai/plugin` | OpenCode plugin interface |
+| `@opencode-ai/sdk` | OpenCode SDK types (events) |
+| `@woss/dali-orm` | DaliORM schema, query builders, migrations |
+| `@surrealdb/node` | SurrealDB embedded driver |
+| `surrealdb` | SurrealDB client |
+| `@huggingface/transformers` | Local embedding inference (ONNX) |
+| `zod` | Schema validation |
+| `@logtape/logtape` | Logging framework |
+| `@logtape/file` | Rotating file sink |
+| `jsonc-parser` | JSONC config file parsing |
+
+## License
+
+GPL-3.0-only
diff --git a/packages/dali-memory/dali-memory.schema.json b/packages/dali-memory/dali-memory.schema.json
index 520cd71..f72a632 100644
--- a/packages/dali-memory/dali-memory.schema.json
+++ b/packages/dali-memory/dali-memory.schema.json
@@ -13,7 +13,10 @@
"properties": {
"mode": {
"type": "string",
- "enum": ["embed", "remote"],
+ "enum": [
+ "embed",
+ "remote"
+ ],
"description": "Storage mode: 'embed' for local embedded database, 'remote' for remote server"
},
"embed": {
@@ -22,7 +25,10 @@
"engine": {
"default": "surrealkv",
"type": "string",
- "enum": ["surrealkv", "memory"],
+ "enum": [
+ "surrealkv",
+ "memory"
+ ],
"description": "Embedded engine type: 'surrealkv' (persistent, default) or 'memory' (in-memory, resets on restart)"
},
"dataPath": {
@@ -30,7 +36,10 @@
"description": "Path to store embedded data (required when engine is 'surrealkv'). Supports ~ for home directory. Defaults to ~/.config/dali-memory/data/"
}
},
- "required": ["engine", "dataPath"],
+ "required": [
+ "engine",
+ "dataPath"
+ ],
"additionalProperties": false,
"description": "Embedded storage settings (required when mode is 'embed')"
},
@@ -69,12 +78,16 @@
"description": "SurrealDB database to use for memory storage"
}
},
- "required": ["url"],
+ "required": [
+ "url"
+ ],
"additionalProperties": false,
"description": "Remote storage settings (required when mode is 'remote')"
}
},
- "required": ["mode"],
+ "required": [
+ "mode"
+ ],
"additionalProperties": false,
"description": "Storage backend configuration"
},
@@ -95,7 +108,10 @@
},
"provider": {
"type": "string",
- "enum": ["remote", "local"],
+ "enum": [
+ "remote",
+ "local"
+ ],
"description": "Embedding provider: 'remote' (API endpoint) or 'local' (local HuggingFace pipeline). Defaults to 'remote' for backward compatibility"
},
"modelCacheDir": {
@@ -103,7 +119,10 @@
"description": "Directory to cache downloaded models (used by 'local' provider). Defaults to ~/.config/dali-memory/model_cache/"
}
},
- "required": ["endpoint", "model"],
+ "required": [
+ "endpoint",
+ "model"
+ ],
"additionalProperties": false,
"description": "Embedding model configuration"
},
@@ -118,7 +137,9 @@
"description": "Enable capturing chat messages for memory"
}
},
- "required": ["enabled"],
+ "required": [
+ "enabled"
+ ],
"additionalProperties": false,
"description": "Chat message memory capture settings"
},
@@ -130,7 +151,9 @@
"description": "Enable automatic memory capture from session activity"
}
},
- "required": ["enabled"],
+ "required": [
+ "enabled"
+ ],
"additionalProperties": false,
"description": "Automatic memory capture settings"
}
@@ -139,6 +162,8 @@
"description": "Plugin behavior configuration"
}
},
- "required": ["storage"],
+ "required": [
+ "storage"
+ ],
"additionalProperties": false
}
diff --git a/packages/dali-memory/package.json b/packages/dali-memory/package.json
index fe88423..644f8dd 100644
--- a/packages/dali-memory/package.json
+++ b/packages/dali-memory/package.json
@@ -1,6 +1,6 @@
{
"name": "@woss/dali-memory",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "SurrealDB-backed memory plugin for OpenCode",
"keywords": [
"bun",
diff --git a/packages/dali-orm/README.md b/packages/dali-orm/README.md
new file mode 100644
index 0000000..6281bd2
--- /dev/null
+++ b/packages/dali-orm/README.md
@@ -0,0 +1,787 @@
+# DaliORM
+
+> SUPER EARLY BETA -- do not use in production yet! API is subject to change without warning.
+
+A TypeScript ORM for SurrealDB with schema definitions, fluent query builders, and migrations. Built with 100% TypeScript for full type safety.
+
+## Table of Contents
+
+- [DaliORM](#daliorm)
+ - [Table of Contents](#table-of-contents)
+ - [Features](#features)
+ - [Installation](#installation)
+ - [Quick Start](#quick-start)
+ - [Schema Definitions](#schema-definitions)
+ - [Tables](#tables)
+ - [Column Types](#column-types)
+ - [Column Options](#column-options)
+ - [Query Builders](#query-builders)
+ - [SELECT](#select)
+ - [INSERT](#insert)
+ - [UPDATE](#update)
+ - [DELETE](#delete)
+ - [RELATE](#relate)
+ - [Conditions](#conditions)
+ - [Comparison Operators](#comparison-operators)
+ - [String Operators](#string-operators)
+ - [Null \& Array Checks](#null--array-checks)
+ - [Combinators](#combinators)
+ - [Typed Conditions](#typed-conditions)
+ - [Backwards Compatibility](#backwards-compatibility)
+ - [SDK Integration](#sdk-integration)
+ - [Database Functions](#database-functions)
+ - [String \& Math](#string--math)
+ - [Crypto](#crypto)
+ - [Vector \& Geo](#vector--geo)
+ - [Time \& Type](#time--type)
+ - [Array \& Object](#array--object)
+ - [HTTP, Rand, Sequence \& More](#http-rand-sequence--more)
+ - [SQL Expression Helpers](#sql-expression-helpers)
+ - [Driver Connection](#driver-connection)
+ - [NodeDriver (Remote)](#nodedriver-remote)
+ - [Embedded Modes](#embedded-modes)
+ - [DaliORM Methods](#daliorm-methods)
+ - [Configuration Files](#configuration-files)
+ - [Supported Formats](#supported-formats)
+ - [Example Config](#example-config)
+ - [Usage](#usage)
+ - [Authentication Types](#authentication-types)
+ - [Shadow Database](#shadow-database)
+ - [Migrations](#migrations)
+ - [CLI Commands](#cli-commands)
+ - [Shadow DB Pre-validation](#shadow-db-pre-validation)
+ - [Programmatic API](#programmatic-api)
+ - [Demo Example](#demo-example)
+ - [TypeScript Types](#typescript-types)
+ - [Packages](#packages)
+ - [License](#license)
+
+## Features
+
+- **TypeScript-First** - Full type inference for schema, queries, and results
+- **Schema Builder** - Define tables, columns, indexes, and relations programmatically
+- **Query Builders** - Fluent API for SELECT, INSERT, UPDATE, DELETE, and RELATE queries
+- **Migrations** - Generate and run database migrations with shadow DB pre-validation
+- **Multiple Drivers** - Support for remote (WebSocket) and embedded modes (memory, file, rocksdb)
+- **Config Files** - JSON, JSONC, and TypeScript configuration files with validation
+- **Database Functions** - Type-safe wrappers for all 28 SurrealDB function modules (array, math, string, crypto, geo, http, rand, vector, etc.)
+
+## Installation
+
+```bash
+pnpm add @woss/dali-orm
+```
+
+## Quick Start
+
+```typescript
+import { DaliORM, createOrmSchema, defineTable } from '@woss/dali-orm';
+import { string, int, bool, datetime } from '@woss/dali-orm/sdk/schema/column/simple-builders';
+import { select, insert } from '@woss/dali-orm/query';
+
+// Define schema
+const usersTable = defineTable('user', {
+ name: string('name'),
+ email: string('email'),
+ age: int('age').optional(),
+ active: bool('active').default(true),
+ created_at: datetime('created_at').defaultNow(),
+});
+
+// Wrap in OrmSchema
+const schema = createOrmSchema({ tables: { users: usersTable } });
+
+// Connect to SurrealDB
+const orm = await DaliORM.connect({
+ nodeDriver: { driver: 'node', url: 'ws://localhost:10101', namespace: 'test', database: 'test' },
+ schema,
+});
+
+const driver = orm.getDriver();
+
+// Insert a user
+const [newUser] = await insert(driver, usersTable)
+ .one({ name: 'John', email: 'john@example.com', age: 30 })
+ .execute();
+
+// Query users
+const users = await select(driver, usersTable)
+ .where((w) => w.eq('active', true))
+ .orderBy('name', 'ASC')
+ .limit(10)
+ .execute();
+
+await orm.disconnect();
+```
+
+## Schema Definitions
+
+### Tables
+
+```typescript
+import { defineTable, defineRelationTable, createOrmSchema, index } from '@woss/dali-orm';
+import {
+ string,
+ int,
+ bool,
+ datetime,
+ duration,
+ decimal,
+ array,
+ object,
+} from '@woss/dali-orm/sdk/schema/column/simple-builders';
+import { record } from '@woss/dali-orm/sdk/schema/column/record';
+
+// Basic table
+const userTable = defineTable('user', {
+ name: string('name'),
+ email: string('email'),
+ age: int('age'),
+});
+
+// Table with options
+const articleTable = defineTable(
+ 'article',
+ {
+ created_at: datetime('created_at').defaultNow(),
+ title: string('title'),
+ content: string('content'),
+ published_at: datetime('published_at').optional(),
+ author: string('author'),
+ },
+ {
+ schema: 'full', // 'full' or 'less'
+ type: 'normal', // 'normal' or 'relation'
+ permissions: {
+ select: 'WHERE true',
+ create: 'WHERE true',
+ update: 'WHERE true',
+ delete: 'WHERE true',
+ },
+ indexes: [
+ index('email_idx').on('email').unique(),
+ index('title_search').on('title').fulltext(),
+ index('embedding_idx').on('embedding').hnsw(1536, { distance: 'cosine' }),
+ ],
+ },
+);
+
+// Relation table
+const wroteTable = defineRelationTable(
+ 'wrote',
+ {
+ created_at: datetime('created_at').defaultNow(),
+ },
+ {
+ in: 'user',
+ out: 'article',
+ enforced: true,
+ },
+);
+
+// Wrap in OrmSchema for DaliORM.connect
+const schema = createOrmSchema({ tables: { users: userTable, articles: articleTable } });
+```
+
+### Column Types
+
+| Function | SurrealQL Type |
+| ------------ | -------------- |
+| `string()` | `string` |
+| `int()` | `int` |
+| `float()` | `float` |
+| `bool()` | `bool` |
+| `datetime()` | `datetime` |
+| `duration()` | `duration` |
+| `decimal()` | `decimal` |
+| `array()` | `array` |
+| `object()` | `object` |
+| `record()` | `record` |
+| `geometry()` | `geometry` |
+
+### Column Options
+
+```typescript
+string()
+ .optional() // Allow NULL values
+ .default('value') // Set default value
+ .assert('condition') // Add validation assertion
+ .readonly() // Mark as read-only
+ .flexible() // Allow flexible schema
+ .unique(); // Create unique index
+```
+
+## Query Builders
+
+### SELECT
+
+```typescript
+import { select, eq, and, or, not, like, contains, isNull } from '@woss/dali-orm/query';
+
+const driver = orm.getDriver();
+
+select(driver, userTable)
+ .where(eq('name', 'John')) // WHERE clause
+ .where((w) => w.eq('age', 18)) // Typed WHERE builder
+ .orderBy('name', 'ASC') // ORDER BY
+ .limit(10) // LIMIT
+ .start(20) // OFFSET/START
+ .groupBy('status') // GROUP BY
+ .fetch('posts') // FETCH related records
+ .parallel() // PARALLEL
+ .timeout(5) // TIMEOUT (seconds)
+ .execute();
+```
+
+### INSERT
+
+```typescript
+import { insert } from '@woss/dali-orm/query';
+
+// Single record
+const [result] = await insert(driver, userTable)
+ .one({ name: 'John', email: 'john@example.com' })
+ .execute();
+```
+
+### UPDATE
+
+```typescript
+import { update } from '@woss/dali-orm/query';
+
+const [result] = await update(driver, userTable)
+ .id('user:123')
+ .data({ name: 'Jane', email: 'jane@example.com' })
+ .execute();
+```
+
+### DELETE
+
+```typescript
+import { delete_ } from '@woss/dali-orm/query';
+
+// Delete by ID
+const [result] = await delete_(driver, userTable).id('user:123').execute();
+
+// Delete with condition
+const [result] = await delete_(driver, userTable).where(eq('active', false)).execute();
+```
+
+### RELATE
+
+```typescript
+import { relate } from '@woss/dali-orm/query';
+import { defineRelationTable } from '@woss/dali-orm';
+
+const wroteSchema = defineRelationTable('wrote', {}, { in: 'user', out: 'article' });
+
+const [result] = await relate(driver, wroteSchema)
+ .from('user:123')
+ .to('article:456')
+ .set({ created_at: new Date().toISOString() })
+ .execute();
+```
+
+## Conditions
+
+### Comparison Operators
+
+```typescript
+import { eq, ne, gt, gte, lt, lte } from '@woss/dali-orm/query';
+
+eq('age', 18); // age = 18
+ne('status', 'active'); // status != 'active'
+gt('price', 100); // price > 100
+gte('age', 18); // age >= 18
+lt('price', 100); // price < 100
+lte('age', 18); // age <= 18
+```
+
+### String Operators
+
+```typescript
+import { like, contains, startsWith, endsWith } from '@woss/dali-orm/query';
+
+like('name', 'J%'); // name LIKE 'J%'
+contains('name', 'ohn'); // string::contains(name, 'ohn')
+startsWith('name', 'Jo'); // string::startsWith(name, 'Jo')
+endsWith('name', 'hn'); // string::endsWith(name, 'hn')
+```
+
+### Null & Array Checks
+
+```typescript
+import { isNull, isNotNull, inside, notInside, all, any } from '@woss/dali-orm/query';
+
+isNull('email'); // email = NONE
+isNotNull('email'); // email != NONE
+inside('status', ['active', 'pending']); // status IN [...]
+notInside('status', ['banned']); // status NOT IN [...]
+all('tags', ['featured', 'new']); // CONTAINSALL
+any('tags', ['sale', 'new']); // CONTAINSANY
+```
+
+### Combinators
+
+```typescript
+import { and, or, not } from '@woss/dali-orm/query';
+
+and(eq('age', 18), eq('active', true));
+or(eq('status', 'active'), eq('status', 'pending'));
+not(eq('active', false));
+```
+
+## Typed Conditions
+
+For full TypeScript type safety, use conditions with typed columns from table definitions:
+
+```typescript
+import { defineTable, string, int, array } from '@woss/dali-orm';
+import { select, eq, gt, and, or, like, contains, inside } from '@woss/dali-orm/query';
+
+// Define schema with typed columns
+const users = defineTable('user', {
+ id: string('id'),
+ name: string('name'),
+ email: string('email'),
+ age: int('age'),
+ status: string('status'),
+ tags: array('tags'),
+});
+
+// Type-safe conditions with typed columns
+select(driver, users).where((w) => w.eq(users.name, 'John')); // name = 'John'
+select(driver, users).where((w) => w.gt(users.age, 18)); // age > 18
+select(driver, users).where((w) => w.and(w.eq(users.status, 'active'), w.gt(users.age, 18)));
+select(driver, users).where((w) => w.inside(users.status, ['active', 'pending']));
+select(driver, users).where((w) => w.contains(users.tags, 'featured'));
+
+// String conditions
+select(driver, users).where((w) => w.like(users.name, 'J%'));
+```
+
+### Backwards Compatibility
+
+Conditions also accept string column names for backwards compatibility:
+
+```typescript
+select(driver, users).where(eq('name', 'John')); // Still works
+```
+
+### SDK Integration
+
+The ORM conditions are built on top of the SurrealDB SDK's internal condition functions, providing:
+
+- Full TypeScript type inference
+- Proper escaping of parameter values
+- Consistent behavior with SDK methods
+
+## Database Functions
+
+Type-safe TypeScript wrappers for all SurrealDB built-in functions. Import from `@woss/dali-orm/sdk/functions`:
+
+```typescript
+import {
+ stringConcat,
+ stringLowercase,
+ stringIsEmail,
+ stringDistance,
+ stringHtmlEncode,
+ mathRound,
+ mathMax,
+ mathSqrt,
+ cryptoSha256,
+ cryptoBlake3,
+ cryptoArgon2Generate,
+ cryptoBcryptCompare,
+ cryptoUuidV4,
+ vectorDistance,
+ vectorSimilarity,
+ geoDistance,
+ geoHashEncode,
+ timeNow,
+ timeFormat,
+ typeInt,
+ typeThing,
+ typeIsArray,
+ arrayPush,
+ arrayFilter,
+ objectKeys,
+ objectEntries,
+ httpGet,
+ httpDelete,
+ randInt,
+ sequenceNext,
+ sleep,
+ count,
+ $,
+ as_,
+ col,
+ expr,
+} from '@woss/dali-orm/sdk/functions';
+```
+
+### String & Math
+
+```typescript
+stringConcat('a', 'b'); // string::concat('a', 'b')
+stringLowercase('HELLO'); // string::lowercase('HELLO')
+stringIsEmail('a@b.com'); // string::is_email('a@b.com')
+stringHtmlEncode('
'); // string::html::encode('
')
+stringDistance('a', 'b'); // string::distance('a', 'b')
+
+mathRound(4.7); // math::round(4.7)
+mathMax(1, 2, 3); // math::max([1, 2, 3])
+mathSqrt(9); // math::sqrt(9)
+```
+
+### Crypto
+
+```typescript
+cryptoSha256('data'); // crypto::sha256('data')
+cryptoBlake3('data'); // crypto::blake3('data')
+cryptoArgon2Generate('pw'); // crypto::argon2::generate('pw')
+cryptoBcryptCompare('pw', 'h'); // crypto::bcrypt::compare('pw', 'h')
+cryptoUuidV4(); // crypto::uuid::v4()
+```
+
+### Vector & Geo
+
+```typescript
+vectorDistance(v1, v2); // vector::distance(v1, v2)
+vectorSimilarity(v1, v2); // vector::similarity::cosine(v1, v2)
+
+geoDistance(p1, p2); // geo::distance(p1, p2)
+geoHashEncode(lng, lat); // geo::hash::encode(lng, lat)
+```
+
+### Time & Type
+
+```typescript
+timeNow(); // time::now()
+timeFormat(date, '%Y-%m-%d'); // time::format(date, '%Y-%m-%d')
+typeInt('42'); // type::int('42')
+typeThing('user', 'abc'); // type::thing('user', 'abc')
+typeIsArray(val); // type::is_array(val)
+```
+
+### Array & Object
+
+```typescript
+arrayPush(['a'], 'b'); // array::push(['a'], 'b')
+arrayFilter(arr, pred); // array::filter(arr, pred)
+objectKeys({ a: 1 }); // object::keys({a: 1})
+objectEntries({ a: 1 }); // object::entries({a: 1})
+```
+
+### HTTP, Rand, Sequence & More
+
+```typescript
+httpGet('https://api.example.com'); // http::get(...)
+randInt(1, 100); // rand::int(1, 100)
+sequenceNext('my_seq'); // sequence::next(my_seq)
+sleep('1s'); // sleep(1s)
+count('*'); // count(*)
+```
+
+### SQL Expression Helpers
+
+```typescript
+$('age'); // Column reference: age
+as_(count(), 'total'); // Alias: count() AS total
+col('name'); // Column reference
+expr`${$('age')} + 1`; // Raw expression: age + 1
+```
+
+Functions compose naturally in query builders:
+
+```typescript
+const result = await select(driver, users)
+ .fields(as_(mathRound($('score')), 'rounded'))
+ .where((w) => w.eq('name', 'Alice'))
+ .execute();
+```
+
+## Driver Connection
+
+### NodeDriver (Remote)
+
+```typescript
+import { DaliORM } from '@woss/dali-orm';
+
+const orm = await DaliORM.connect({
+ nodeDriver: { driver: 'node', url: 'ws://localhost:10101', namespace: 'test', database: 'test' },
+ schema,
+});
+
+// With authentication
+const orm = await DaliORM.connect({
+ nodeDriver: {
+ driver: 'node',
+ url: 'ws://localhost:10101',
+ auth: { username: 'root', password: 'root' },
+ },
+ schema,
+});
+```
+
+### Embedded Modes
+
+```typescript
+import { DaliORM } from '@woss/dali-orm';
+
+// Memory mode
+const orm = await DaliORM.connect({
+ embeddedDriver: { driver: 'embedded', mode: 'memory' },
+ schema,
+});
+
+// SurrealKV mode (persistent key-value storage)
+const orm = await DaliORM.connect({
+ embeddedDriver: { driver: 'embedded', mode: 'surrealkv', path: './db' },
+ schema,
+});
+```
+
+### DaliORM Methods
+
+```typescript
+// Execute raw SQL with parameters
+const result = await orm.query('SELECT * FROM user WHERE age > $age', { age: 18 });
+
+// Query builder — execute directly
+const driver = orm.getDriver();
+const users = await select(driver, userTable).where(eq('active', true)).execute();
+
+// Get driver for query builders
+const driver = orm.getDriver();
+
+// Check connection
+const connected = orm.isConnected();
+
+// Switch namespace/database
+await orm.use('new_namespace', 'new_database');
+
+// Close connection
+await orm.disconnect();
+```
+
+## Configuration Files
+
+The `@woss/dali-orm` package supports configuration files for connecting to databases.
+
+### Supported Formats
+
+- `.dali-orm.json` - JSON format
+- `.dali-orm.jsonc` - JSON with comments
+- `.dali-orm.ts` - TypeScript format
+
+### Example Config
+
+Create `.dali-orm.json` in your project root:
+
+```json
+{
+ "url": "ws://localhost:8000",
+ "namespace": "test",
+ "database": "test",
+ "auth": {
+ "type": "root",
+ "username": "root",
+ "password": "root"
+ }
+}
+```
+
+### Usage
+
+```typescript
+import { DaliORM } from '@woss/dali-orm';
+
+// Load from config file
+const orm = await DaliORM.connect({
+ config: './dali-orm.json',
+});
+
+// Auto-discover config
+const orm = await DaliORM.connect({
+ config: true,
+});
+
+// Explicit options override config
+const orm = await DaliORM.connect({
+ config: './dali-orm.json',
+ nodeDriver: {
+ url: 'ws://custom:8000', // Takes precedence
+ },
+});
+```
+
+### Authentication Types
+
+| Type | Required Fields |
+| ----------- | ----------------------------------------------- |
+| `root` | `username`, `password` |
+| `namespace` | `username`, `password`, `namespace` |
+| `database` | `username`, `password`, `namespace`, `database` |
+| `record` | `table` |
+
+### Shadow Database
+
+Optionally configure a shadow database for pre-validation:
+
+```json
+{
+ "shadow": {
+ "namespace": "myapp_shadow",
+ "database": "shadow_db"
+ }
+}
+```
+
+Guard: shadow ns/db must differ from target ns/db.
+
+## Migrations
+
+### CLI Commands
+
+```bash
+# Dev workflow — generate migration + validate on shadow + apply
+npx dali-orm migrate dev add_users_table
+
+# Deploy to production — validate pending on shadow + apply (REQUIRES shadow config)
+npx dali-orm migrate deploy
+
+# Apply pending migrations to database
+npx dali-orm migrate up
+
+# Rollback last migration
+npx dali-orm migrate down --steps 1
+
+# Reset all migrations
+npx dali-orm migrate reset
+
+# Check migration status
+npx dali-orm migrate status
+
+# Generate migration from schema
+npx dali-orm generate add_users_table
+
+# Pull schema from database
+npx dali-orm pull
+```
+
+- `migrate dev ` — Generate migration file, validate on shadow DB, then apply to target
+- `migrate deploy` — Validate all pending migrations on shadow DB, then apply to target (requires `shadow` config)
+- `push` is removed — use `migrate dev` or `migrate deploy` instead
+
+### Shadow DB Pre-validation
+
+DaliORM supports shadow database validation for safe migration deployment. Before applying changes to production, migrations are validated on an isolated shadow database. If validation fails, neither the shadow nor the target database is affected.
+
+**Configuration:**
+
+Add `shadow` to your `dali-orm.config.ts`:
+
+```typescript
+export default defineConfig({
+ url: 'ws://localhost:10101',
+ namespace: 'myapp',
+ database: 'mydb',
+ // ...
+ shadow: {
+ namespace: 'myapp_shadow', // Must differ from target namespace
+ database: 'shadow_db', // Destroyed after each validation run
+ },
+});
+```
+
+**Guard:** Shadow ns/db cannot match target ns/db — throws error immediately.
+
+### Programmatic API
+
+```typescript
+import { MigrationRunner, SurrealQLGenerator } from '@woss/dali-orm/migration/api';
+import { DaliORM } from '@woss/dali-orm';
+
+// Connect
+const orm = await DaliORM.connect({
+ nodeDriver: { driver: 'node', url: 'ws://localhost:10101', namespace: 'test', database: 'test' },
+});
+
+// Generate SQL from schema
+const generator = new SurrealQLGenerator();
+const sql = generator.generateMigration([userTable]);
+
+// Get driver from ORM
+const driver = orm.getDriver();
+
+// Create runner with the driver
+const runner = new MigrationRunner(driver);
+await runner.init();
+await runner.up();
+const status = await runner.status();
+```
+
+## Demo Example
+
+The `examples/demo` package provides a complete working demo of the ORM:
+
+```bash
+cd examples/demo
+
+# Run with interactive prompts
+pnpm dev
+
+# Run with auto-accept defaults (no prompts)
+pnpm dev --yes
+
+# Show help
+pnpm dev --help
+
+# Generate migration from schema
+pnpm generate
+
+# Apply migrations
+pnpm dali-orm migrate up
+```
+
+The demo includes:
+
+- Schema definitions with tables and relations
+- Interactive CLI for data entry
+- Migration generation and execution
+- Complete CRUD operations with relations
+
+## TypeScript Types
+
+```typescript
+import { defineTable, string, int } from '@woss/dali-orm';
+import { InferSelectResult, InferInsertInput } from '@woss/dali-orm/query/types';
+
+const userSchema = defineTable('user', {
+ id: string('id'),
+ name: string('name'),
+ email: string('email'),
+ age: int('age'),
+});
+
+// Type for SELECT results
+type User = InferSelectResult;
+// { id?: string; name?: string; email?: string; age?: number | null }
+
+// Type for INSERT data
+type NewUser = InferInsertInput;
+// { name: string; email: string; age?: number }
+```
+
+## Packages
+
+| Package | Description |
+| ------------------------------ | --------------------------------------------------------------------- |
+| `@woss/dali-orm` | Schema definitions, query builders, conditions (merged core + driver) |
+| `@woss/dali-orm/migration/api` | CLI, migrations, schema generation, config management |
+
+## License
+
+GPL-3.0-only
diff --git a/packages/dali-orm/meta/_journal.json b/packages/dali-orm/meta/_journal.json
index 4a3f88e..e69de29 100644
--- a/packages/dali-orm/meta/_journal.json
+++ b/packages/dali-orm/meta/_journal.json
@@ -1,19 +0,0 @@
-{
- "version": 1,
- "dialect": "surrealdb",
- "id": "6ddada34b489",
- "entries": [
- {
- "idx": 1,
- "when": "2026-05-17T22:20:25.151063Z",
- "tag": "init_from_pull",
- "breakpoints": [
- true,
- true,
- true,
- true
- ],
- "hash": "be5a03c9a503f73e928616f3cd9fa59737a090fe3f63d6cbd2ed88e488ae7fef"
- }
- ]
-}
\ No newline at end of file
diff --git a/packages/dali-orm/package.json b/packages/dali-orm/package.json
index 515b953..7516bd9 100644
--- a/packages/dali-orm/package.json
+++ b/packages/dali-orm/package.json
@@ -1,6 +1,6 @@
{
"name": "@woss/dali-orm",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "Type-safe ORM for SurrealDB with migration support",
"keywords": [
"migrations",
diff --git a/packages/dali-orm/src/migration/cli.ts b/packages/dali-orm/src/migration/cli.ts
index 83e08b3..a46f863 100644
--- a/packages/dali-orm/src/migration/cli.ts
+++ b/packages/dali-orm/src/migration/cli.ts
@@ -358,9 +358,9 @@ async function handleGenerate(args: string[], options: CLIOptions, config: Confi
}
// Determine snapshot directory - prefer CLI option, then config, then default
- // Resolve relative to cwd (consistent with migrations.dir and schema.dir)
- const snapshotDirOption = options.snapshots ?? config.snapshots?.dir ?? './snapshots';
- const resolvedSnapshotDir = path.resolve(snapshotDirOption);
+ const resolvedSnapshotDir = options.snapshots
+ ? path.resolve(options.snapshots)
+ : (config.snapshots?.dir ?? path.resolve('./snapshots'));
const outputPath = await generateMigration(
schemaFiles.tables,
diff --git a/packages/dali-orm/src/migration/cli/__tests__/migrate.test.ts b/packages/dali-orm/src/migration/cli/__tests__/migrate.test.ts
index b2d4301..8e70c2a 100644
--- a/packages/dali-orm/src/migration/cli/__tests__/migrate.test.ts
+++ b/packages/dali-orm/src/migration/cli/__tests__/migrate.test.ts
@@ -49,7 +49,6 @@ function makeConfig(overrides: Partial = {}): Config {
schema: { dir: './schema', pattern: '**/*.{js,ts}' },
migrations: {
dir: './migrations',
- journalDir: './meta',
table: '__test_migrations',
},
} as Config;
diff --git a/packages/dali-orm/src/migration/cli/__tests__/pull.test.ts b/packages/dali-orm/src/migration/cli/__tests__/pull.test.ts
index 28e4c07..74d5a0b 100644
--- a/packages/dali-orm/src/migration/cli/__tests__/pull.test.ts
+++ b/packages/dali-orm/src/migration/cli/__tests__/pull.test.ts
@@ -34,7 +34,6 @@ function makeConfig(overrides: Partial = {}): Config {
migrations: {
dir: './migrations',
table: '__test_pull_migrations',
- journalDir: './meta',
},
};
return {
diff --git a/packages/dali-orm/src/migration/cli/migrate.ts b/packages/dali-orm/src/migration/cli/migrate.ts
index ee30bb2..6ff58c2 100644
--- a/packages/dali-orm/src/migration/cli/migrate.ts
+++ b/packages/dali-orm/src/migration/cli/migrate.ts
@@ -78,6 +78,21 @@ export async function migrateUp(options: MigrateOptions, driver?: SurrealDriver)
console.log(` ○ ${m.version} — ${m.name}`);
}
}
+
+ // Apply pending migrations
+ if (status.pending.length > 0) {
+ console.log(`\n Applying ${status.pending.length} pending migration(s)...`);
+ const result = await runner.up(options.to);
+ console.log(`\n ✔ Applied ${result.applied.length} migration(s):`);
+ for (const name of result.applied) {
+ console.log(` ✓ ${name}`);
+ }
+ if (result.skipped.length > 0) {
+ console.log(`\n ○ Skipped ${result.skipped.length} migration(s) (beyond target)`);
+ }
+ } else {
+ console.log('\n No pending migrations to apply.');
+ }
} catch (error) {
console.error('migrateUp error:', error);
} finally {
diff --git a/packages/dali-orm/src/migration/config.ts b/packages/dali-orm/src/migration/config.ts
index 844a5f6..e0cd491 100644
--- a/packages/dali-orm/src/migration/config.ts
+++ b/packages/dali-orm/src/migration/config.ts
@@ -103,6 +103,10 @@ export function processConfigObject(
parsed.snapshots.dir = path.resolve(configDir, parsed.snapshots.dir);
}
+ if (!parsed.snapshots) {
+ parsed.snapshots = { dir: path.join(configDir, 'snapshots') };
+ }
+
log('Loaded successfully from:', resolvedPath);
return parsed;
}
diff --git a/packages/dali-orm/src/migration/core/__tests__/generator.integration.test.ts b/packages/dali-orm/src/migration/core/__tests__/generator.integration.test.ts
new file mode 100644
index 0000000..1278401
--- /dev/null
+++ b/packages/dali-orm/src/migration/core/__tests__/generator.integration.test.ts
@@ -0,0 +1,572 @@
+/**
+ * Integration tests for SurrealQLGenerator HNSW index SQL
+ *
+ * Validates generated HNSW index definitions against a live embedded SurrealDB
+ * engine. Catches syntax errors that pure string-matching tests cannot detect.
+ *
+ * Each test creates a unique table, defines a vector field, generates HNSW index
+ * SQL via SurrealQLGenerator, executes it against the engine, then cleans up.
+ */
+import { afterAll, beforeAll, describe, expect, it } from 'vite-plus/test';
+import { EmbeddedDriver } from '../../../sdk/driver/embedded-driver.js';
+import { SurrealQLGenerator } from '../generator.js';
+import * as fs from 'node:fs/promises';
+import os from 'node:os';
+import * as path from 'node:path';
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+const NS = 'generator_int';
+const DB = `test_${Date.now()}`;
+
+let _counter = 0;
+function uniqueTable(prefix = 'hnsw'): string {
+ _counter++;
+ return `${prefix}_${_counter}_${Date.now()}`;
+}
+
+// ============================================================================
+// Setup
+// ============================================================================
+
+let driver: EmbeddedDriver;
+let gen: SurrealQLGenerator;
+
+beforeAll(async () => {
+ // Try file-backed mode first (surrealkv) — HNSW may need it
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'generator-int-'));
+ try {
+ driver = new EmbeddedDriver({
+ driver: 'embedded',
+ namespace: NS,
+ database: DB,
+ mode: 'surrealkv',
+ path: tmpDir,
+ });
+ await driver.connect();
+ } catch {
+ // Fall back to memory mode — if HNSW fails here, tests will fail explicitly (no silent skip)
+ driver = new EmbeddedDriver({
+ driver: 'embedded',
+ namespace: NS,
+ database: DB,
+ mode: 'memory',
+ });
+ await driver.connect();
+ }
+ gen = new SurrealQLGenerator();
+});
+
+afterAll(async () => {
+ await driver.disconnect();
+});
+
+// ============================================================================
+// Tests
+// ============================================================================
+
+describe('SurrealQLGenerator HNSW (integration)', () => {
+ it('validates HNSW COSINE with float32 against engine', async () => {
+ const tn = uniqueTable('hnsw_cos');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD vec ON TABLE ${tn} TYPE float`);
+
+ const sql = gen.generateIndexDefinition(
+ {
+ name: 'idx_vec',
+ fields: ['vec'],
+ type: 'hnsw' as const,
+ dimension: 128,
+ vectorType: 'float32' as const,
+ distance: 'COSINE' as const,
+ },
+ tn,
+ );
+
+ // Expected SQL: HNSW DIMENSION 128 TYPE F32 DIST COSINE
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates HNSW with minimal params (dimension only)', async () => {
+ const tn = uniqueTable('hnsw_min');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD vec ON TABLE ${tn} TYPE float`);
+
+ const sql = gen.generateIndexDefinition(
+ {
+ name: 'idx_vec',
+ fields: ['vec'],
+ type: 'hnsw' as const,
+ dimension: 64,
+ },
+ tn,
+ );
+
+ // Expected SQL: HNSW DIMENSION 64
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates HNSW with MANHATTAN distance + float64', async () => {
+ const tn = uniqueTable('hnsw_man');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD vec ON TABLE ${tn} TYPE float`);
+
+ const sql = gen.generateIndexDefinition(
+ {
+ name: 'idx_vec',
+ fields: ['vec'],
+ type: 'hnsw' as const,
+ dimension: 256,
+ vectorType: 'float64' as const,
+ distance: 'MANHATTAN' as const,
+ },
+ tn,
+ );
+
+ // Expected SQL: DIMENSION 256 TYPE F64 DIST MANHATTAN
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates HNSW with EUCLIDEAN distance (no vectorType)', async () => {
+ const tn = uniqueTable('hnsw_euc');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD vec ON TABLE ${tn} TYPE float`);
+
+ const sql = gen.generateIndexDefinition(
+ {
+ name: 'idx_vec',
+ fields: ['vec'],
+ type: 'hnsw' as const,
+ dimension: 64,
+ distance: 'EUCLIDEAN' as const,
+ },
+ tn,
+ );
+
+ // Expected SQL: DIMENSION 64 DIST EUCLIDEAN
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates HNSW with float (deprecated alias, no distance)', async () => {
+ const tn = uniqueTable('hnsw_flt');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD vec ON TABLE ${tn} TYPE float`);
+
+ const sql = gen.generateIndexDefinition(
+ {
+ name: 'idx_vec',
+ fields: ['vec'],
+ type: 'hnsw' as const,
+ dimension: 128,
+ vectorType: 'float' as const,
+ distance: 'COSINE' as const,
+ },
+ tn,
+ );
+
+ // Expected SQL: TYPE F64 DIST COSINE
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+});
+
+// ============================================================================
+// 1. Table definitions
+// ============================================================================
+
+describe('generateTableDefinition (integration)', () => {
+ it('validates SCHEMAFULL table', async () => {
+ const tn = uniqueTable('tbl_sf');
+ const sql = gen.generateTableDefinition({
+ name: tn,
+ columns: [],
+ config: { schema: 'full' },
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates SCHEMALESS table', async () => {
+ const tn = uniqueTable('tbl_sl');
+ const sql = gen.generateTableDefinition({
+ name: tn,
+ columns: [],
+ config: { schema: 'less' },
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates TYPE RELATION with IN/OUT', async () => {
+ const refUser = uniqueTable('tbl_user');
+ const refPost = uniqueTable('tbl_post');
+ const tn = uniqueTable('tbl_rel');
+ // Create referenced tables first
+ await driver.query(`DEFINE TABLE ${refUser} SCHEMAFULL`);
+ await driver.query(`DEFINE TABLE ${refPost} SCHEMAFULL`);
+
+ const sql = gen.generateTableDefinition({
+ name: tn,
+ columns: [],
+ config: { type: 'relation', in: refUser, out: refPost },
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ await driver.query(`REMOVE TABLE ${refUser}`);
+ await driver.query(`REMOVE TABLE ${refPost}`);
+ });
+
+ it('validates PERMISSIONS on table', async () => {
+ const tn = uniqueTable('tbl_perm');
+ const sql = gen.generateTableDefinition({
+ name: tn,
+ columns: [],
+ config: {
+ permissions: {
+ select: 'WHERE published = true',
+ create: 'WHERE $auth.role = "admin"',
+ },
+ },
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates CHANGEFEED on table', async () => {
+ const tn = uniqueTable('tbl_cf');
+ const sql = gen.generateTableDefinition({
+ name: tn,
+ columns: [],
+ config: { changefeed: '7d' },
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates combined options on table', async () => {
+ const tn = uniqueTable('tbl_comb');
+ const sql = gen.generateTableDefinition({
+ name: tn,
+ columns: [],
+ config: {
+ schema: 'less',
+ permissions: {
+ select: 'FULL',
+ create: 'NONE',
+ update: 'NONE',
+ delete: 'NONE',
+ },
+ changefeed: '24h',
+ },
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+});
+
+// ============================================================================
+// 2. Field definitions
+// ============================================================================
+
+describe('generateFieldDefinition (integration)', () => {
+ it('validates basic string field', async () => {
+ const tn = uniqueTable('fd_str');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+
+ const sql = gen.generateFieldDefinition({
+ name: 'username',
+ config: { type: 'string' },
+ tableName: tn,
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates optional field', async () => {
+ const tn = uniqueTable('fd_opt');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+
+ const sql = gen.generateFieldDefinition({
+ name: 'nickname',
+ config: { type: 'string', optional: true },
+ tableName: tn,
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates record field', async () => {
+ const refUser = uniqueTable('fd_ref_user');
+ const tn = uniqueTable('fd_ref');
+ await driver.query(`DEFINE TABLE ${refUser} SCHEMAFULL`);
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+
+ const sql = gen.generateFieldDefinition({
+ name: 'author',
+ config: { type: 'record', linksTo: refUser },
+ tableName: tn,
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ await driver.query(`REMOVE TABLE ${refUser}`);
+ });
+
+ it('validates FLEXIBLE + READONLY + DEFAULT + ASSERT + PERMISSIONS', async () => {
+ const tn = uniqueTable('fd_all');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+
+ const sql = gen.generateFieldDefinition({
+ name: 'payload',
+ config: {
+ type: 'object',
+ flexible: true,
+ readonly: true,
+ default: '{}',
+ assert: '$value != none',
+ permissions: 'FOR select WHERE $auth.role = "admin"',
+ },
+ tableName: tn,
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates tuple field definition', async () => {
+ const tn = uniqueTable('fd_tup');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+
+ const sql = gen.generateFieldDefinition({
+ name: 'coords',
+ config: {
+ type: 'tuple',
+ size: 2,
+ elements: [{ type: 'float' }, { type: 'float' }],
+ },
+ tableName: tn,
+ });
+ // Tuple returns multiple statements joined by '; '
+ const statements = sql.split('; ').filter((s) => s.trim());
+ for (const stmt of statements) {
+ await expect(driver.query(stmt)).resolves.toBeDefined();
+ }
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+});
+
+// ============================================================================
+// 3. Field redefine
+// ============================================================================
+
+describe('generateFieldRedefine (integration)', () => {
+ it('validates field redefine overwrites existing field', async () => {
+ const tn = uniqueTable('fred');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD name ON TABLE ${tn} TYPE string`);
+
+ const sql = gen.generateFieldRedefine({
+ name: 'name',
+ config: { type: 'string' },
+ tableName: tn,
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+});
+
+// ============================================================================
+// 4. Index non-HNSW
+// ============================================================================
+
+describe('generateIndexDefinition non-HNSW (integration)', () => {
+ it('validates basic index (no type)', async () => {
+ const tn = uniqueTable('idx_basic');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD email ON TABLE ${tn} TYPE string`);
+
+ const sql = gen.generateIndexDefinition({ name: 'idx_email', fields: ['email'] }, tn);
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates UNIQUE index', async () => {
+ const tn = uniqueTable('idx_unq');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD email ON TABLE ${tn} TYPE string`);
+
+ const sql = gen.generateIndexDefinition(
+ { name: 'idx_email', fields: ['email'], type: 'unique' },
+ tn,
+ );
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates FULLTEXT index with analyzer', async () => {
+ const tn = uniqueTable('idx_ft');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD body ON TABLE ${tn} TYPE string`);
+
+ const sql = gen.generateIndexDefinition(
+ { name: 'idx_body', fields: ['body'], type: 'fulltext', analyzer: 'keyword' },
+ tn,
+ );
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates composite index', async () => {
+ const tn = uniqueTable('idx_comp');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD first_name ON TABLE ${tn} TYPE string`);
+ await driver.query(`DEFINE FIELD last_name ON TABLE ${tn} TYPE string`);
+
+ const sql = gen.generateIndexDefinition(
+ { name: 'idx_name', fields: ['first_name', 'last_name'] },
+ tn,
+ );
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+});
+
+// ============================================================================
+// 5. Access definitions
+// ============================================================================
+
+describe('generateAccessDefinition (integration)', () => {
+ it('validates RECORD access with signup/signin', async () => {
+ const userTbl = uniqueTable('acc_user');
+ await driver.query(`DEFINE TABLE ${userTbl} SCHEMAFULL`);
+
+ const sql = gen.generateAccessDefinition({
+ name: 'test_record_access',
+ type: 'RECORD',
+ signup: `CREATE ${userTbl} SET email = $email, pass = crypto::argon2::generate($pass)`,
+ signin: `SELECT * FROM ${userTbl} WHERE email = $email AND crypto::argon2::compare(pass, $pass)`,
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query('REMOVE ACCESS IF EXISTS test_record_access ON DATABASE');
+ await driver.query(`REMOVE TABLE ${userTbl}`);
+ });
+
+ it('validates JWT access with ALGORITHM and KEY', async () => {
+ const sql = gen.generateAccessDefinition({
+ name: 'test_jwt_access',
+ type: 'JWT',
+ algorithm: 'RS256',
+ key: 'mysecret',
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query('REMOVE ACCESS IF EXISTS test_jwt_access ON DATABASE');
+ });
+});
+
+// ============================================================================
+// 6. Events
+// ============================================================================
+
+describe('generateEventDefinition (integration)', () => {
+ it('validates basic event with WHEN and THEN', async () => {
+ const tn = uniqueTable('evt_main');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+
+ const sql = gen.generateEventDefinition({
+ name: 'on_create',
+ what: tn,
+ when: '$event = "CREATE"',
+ then: ['INSERT INTO audit SET action = "created"'],
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ // Cleanup: remove event then table
+ await driver.query(`REMOVE EVENT IF EXISTS on_create ON TABLE ${tn}`);
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+});
+
+// ============================================================================
+// 7. Functions
+// ============================================================================
+
+describe('generateFunctionDefinition (integration)', () => {
+ it('validates basic function with args', async () => {
+ const fnName = uniqueTable('fn_greet').replace(/-/g, '_');
+ const qualified = `fn::${fnName}`;
+
+ const sql = gen.generateFunctionDefinition({
+ name: qualified,
+ args: ['$name: string'],
+ body: 'RETURN "Hello " + $name',
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE FUNCTION IF EXISTS ${qualified}`);
+ });
+});
+
+// ============================================================================
+// 8. Alter operations
+// ============================================================================
+
+describe('Alter operations (integration)', () => {
+ it('validates ALTER FIELD TYPE', async () => {
+ const tn = uniqueTable('alt_type');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD label ON TABLE ${tn} TYPE string`);
+
+ const sql = gen.generateAlterFieldType(tn, 'label', 'int');
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates ALTER TABLE PERMISSIONS', async () => {
+ const tn = uniqueTable('alt_perm');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+
+ const sql = gen.generateAlterTablePermissions(tn, {
+ select: 'FULL',
+ create: 'NONE',
+ update: 'NONE',
+ delete: 'NONE',
+ });
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+
+ it('validates ALTER FIELD DEFAULT', async () => {
+ const tn = uniqueTable('alt_def');
+ await driver.query(`DEFINE TABLE ${tn} SCHEMAFULL`);
+ await driver.query(`DEFINE FIELD role ON TABLE ${tn} TYPE string`);
+
+ const sql = gen.generateAlterFieldDefault(tn, 'role', 'viewer');
+ await expect(driver.query(sql)).resolves.toBeDefined();
+
+ await driver.query(`REMOVE TABLE ${tn}`);
+ });
+});
diff --git a/packages/dali-orm/src/migration/core/__tests__/generator.test.ts b/packages/dali-orm/src/migration/core/__tests__/generator.test.ts
index 804dfeb..a3ad065 100644
--- a/packages/dali-orm/src/migration/core/__tests__/generator.test.ts
+++ b/packages/dali-orm/src/migration/core/__tests__/generator.test.ts
@@ -340,7 +340,7 @@ describe('generateFieldRedefine', () => {
}),
);
expect(sql).toContain(
- 'DEFINE FIELD OVERWRITE email ON TABLE test_table FLEXIBLE TYPE option',
+ 'DEFINE FIELD OVERWRITE email ON TABLE test_table TYPE option FLEXIBLE',
);
expect(sql).toContain('FLEXIBLE');
expect(sql).toContain('READONLY');
@@ -474,7 +474,7 @@ describe('generateIndexDefinition', () => {
tableName,
);
expect(sql).toBe(
- 'DEFINE INDEX idx_vec ON TABLE user COLUMNS vector HNSW DIMENSION 128 TYPE float32 DISTANCE COSINE',
+ 'DEFINE INDEX idx_vec ON TABLE user COLUMNS vector HNSW DIMENSION 128 TYPE F32 DIST COSINE',
);
});
@@ -1274,7 +1274,7 @@ describe('field type variations', () => {
}),
);
expect(sql).toBe(
- 'DEFINE FIELD IF NOT EXISTS email ON TABLE test_table FLEXIBLE TYPE option READONLY DEFAULT \'NONE\' ASSERT $value CONTAINS "@" PERMISSIONS FOR select FULL',
+ 'DEFINE FIELD IF NOT EXISTS email ON TABLE test_table TYPE option FLEXIBLE READONLY DEFAULT \'NONE\' ASSERT $value CONTAINS "@" PERMISSIONS FOR select FULL',
);
});
});
@@ -1327,7 +1327,7 @@ describe('index variations', () => {
'items',
);
expect(sql).toBe(
- 'DEFINE INDEX idx_vec ON TABLE items COLUMNS vec HNSW DIMENSION 256 TYPE float64 DISTANCE MANHATTAN',
+ 'DEFINE INDEX idx_vec ON TABLE items COLUMNS vec HNSW DIMENSION 256 TYPE F64 DIST MANHATTAN',
);
});
@@ -1342,7 +1342,7 @@ describe('index variations', () => {
}),
'items',
);
- expect(sql).toContain('DISTANCE EUCLIDEAN');
+ expect(sql).toContain('DIST EUCLIDEAN');
});
});
diff --git a/packages/dali-orm/src/migration/core/generator.ts b/packages/dali-orm/src/migration/core/generator.ts
index a3c56be..1ec8d47 100644
--- a/packages/dali-orm/src/migration/core/generator.ts
+++ b/packages/dali-orm/src/migration/core/generator.ts
@@ -192,10 +192,6 @@ export class SurrealQLGenerator {
if (baseType === 'record' && (column.config.recordTable || column.config.linksTo)) {
typeStr = `record<${column.config.recordTable || column.config.linksTo}>`;
}
- // FLEXIBLE before TYPE — SurrealDB requires FLEXIBLE TYPE, not TYPE FLEXIBLE
- if (column.config.flexible) {
- parts.push('FLEXIBLE');
- }
// FLEXIBLE only pairs with plain TYPE object, not option