Skip to content

feat: add @objectstack/driver-turso extending SqlDriver with zero CRUD duplication#963

Merged
hotlong merged 6 commits intomainfrom
copilot/standardize-migration-architecture-upgrade
Mar 23, 2026
Merged

feat: add @objectstack/driver-turso extending SqlDriver with zero CRUD duplication#963
hotlong merged 6 commits intomainfrom
copilot/standardize-migration-architecture-upgrade

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 23, 2026

Migrates the Turso/libSQL driver into the spec repo as a proper SqlDriver subclass. All query, schema, filter, and introspection logic is inherited — TursoDriver only adds connection lifecycle, embedded replica sync, and multi-tenant routing.

SqlDriver extensibility

  • privateprotected for all internal properties and methods (19 members: knex, config, getBuilder, applyFilters, formatInput, formatOutput, introspection helpers, etc.)
  • Widened name/version type annotations from literal to string to support clean subclass override in DTS builds
  • Fixed count() returning NaN for zero results: row.count || row['count(*)']row.count ?? row['count(*)'] ?? 0 (logical OR treats 0 as falsy)
  • Removed legacy tuple-style [field, dir] indexing in orderBy processing — only standard { field, order } (SortNodeSchema) is supported

TursoDriver (packages/plugins/driver-turso/)

  • Extends SqlDriver — passes better-sqlite3 Knex config derived from TursoDriverConfig
  • Overrides: connect() / disconnect() for @libsql/client lifecycle, name, version, supports
  • Adds: sync() for embedded replica, isSyncEnabled(), getLibsqlClient()
  • Two supported connection modes: local file (file:./data/app.db or :memory:) and embedded replica (url + syncUrl)
  • Remote-only URLs (libsql:// without syncUrl) throw a clear error — prevents silent data loss
  • Config options encryptionKey, concurrency, timeout documented as requiring syncUrl (embedded replica mode only)
  • Enhanced capabilities vs SqlDriver: FTS5, JSON1, CTE, savepoints, indexes
const driver = new TursoDriver({
  url: 'file:./data/replica.db',
  syncUrl: 'libsql://my-db-orgname.turso.io',
  authToken: process.env.TURSO_AUTH_TOKEN,
  sync: { intervalSeconds: 60 },
});
await driver.connect();
// All SqlDriver methods (find, create, update, syncSchema, ...) inherited

Multi-tenant router

  • createMultiTenantRouter({ urlTemplate: 'file:./data/{tenant}.db' }) — database-per-tenant with TTL cache
  • {tenant} placeholder interpolated in both urlTemplate and driverConfigOverrides.syncUrl
  • Concurrent getDriverForTenant() calls for the same tenant are deduplicated via in-flight promise map — prevents connection leaks
  • Tenant ID validation, onTenantCreate / onTenantEvict lifecycle callbacks
  • Serverless-safe: lazy expiration, no global intervals

Test coverage

  • 55 new tests (40 driver, 15 multi-tenant) — CRUD, upsert, bulk ops, transactions, schema sync, streaming, pagination, capabilities, remote URL validation, concurrent access guard, edge cases
  • 78 driver-sql regression tests pass unchanged
  • Tests use os.tmpdir() + random suffix for cross-platform safety
  • All builds succeed (ESM + CJS + DTS)
Original prompt

This section details on the original issue you should resolve

<issue_title>Phase 2: 标准化迁移和架构升级 driver-turso(libSQL/Turso 驱动插件)</issue_title>
<issue_description>## 目标
将 objectql Turso(libSQL)驱动 https://github.com/objectstack-ai/objectql/tree/main/packages/drivers/turso 迁移进 spec 仓库,真正实现长期可维护、极简升级、协议完全对齐的插件化架构。


长远与"可持续架构"原则

  • 彻底消除"复制-粘贴"或 type hack 迁移;基础能力一切基于抽象父类/组合/注入,不走两套实现、两份 schema、两份 filter。
  • 不只追求阶段性"搬家",要保证 2 年、5 年后依然易于迭代和扩展。
  • 保证 turso 后续能力扩展只需 override 极少代码。

最优迁移方案设计

1. 施工前:制定归一架构目标

  • 对比 driver-sql(Phase 1 成果)、objectql/driver-turso、以及平台协议 spec,先确定 100% 行为与类型的父类继承链、能力分类与跨插件接口点。不要急于动手迁移。
  • 凡是 turso 独有特性(如同步、多租户、透明加密)一律抽象成可覆写的「扩展点」或 service 层,而非重复 CRUD/find 逻辑。
  • 在设计层面,将所有共性能力沉淀到父类 driver-sql,分层组织代码,调整父类 API 使 Turso/SqlDriver/memory 等可等价协作。

2. 代码迁移与最小实现

  • spec/packages/plugins/driver-turso 下创建项目。
  • 只迁移 turso "独特值",完全复用 Phase 1 SqlDriver:
    • 连接与 client 初始化:切换到 libSQL,区分 local/remote/hybrid,以及 sync 机制,事务特性等都基于 libsql client 做必要覆写。
    • 多租户能力用组合而非多 driver 实例(推荐服务对象单例+隔离配置)
    • 其余一切 query/schema 相关,全依赖父类,不留复制代码
  • 共性能力沉淀到父类 driver-sql 并补测试(如发现有 filter/schema 仍只能靠复制覆盖,必须优化抽象允许 child 0 覆写)。

3. 协议与开发体验对齐

  • type 校验与实际返回值一律可静态推断、IDE 智能提示可全补齐,所有 hack 和 as any 语法须解除/消灭。
  • 发现协议分歧及时同步推进 spec(如同步、事务 context、连接属性等),不能"先用 any 后补 specs"。
  • 设计层到测试用例到文档,全部与 phase 1/driver-sql/driver-memory 保持体验一致和极简。

4. 测试和交付标准

  • 单元测试与 CI 全链路跑通,与 memory/sql 驱动交叉测试。
  • CI 包括多租户、sync、加密的典型覆盖(如有平台 E2E 测试建议接入或文档标明调用示例)。
  • README/CHANGELOG/ROADMAP 文档说明 driver-turso 的继承关系与使用方式。

验收 & 里程碑

  • PR 评论中说明已沉淀最大程度共性到父类
  • CI 必须无绿 -> 任何多余复制或冗余实现拒绝合并
  • 与 Platform 侧典型场景均测通,技术债务清零

如需补充详细分任务/编码参考,可继续拆分。任务完成后需按要求自测、同步 changelog 和相关文档,杜绝短视搬运方案。
</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Mar 23, 2026 6:35am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
spec Ignored Ignored Mar 23, 2026 6:35am

Request Review

Copilot AI and others added 2 commits March 23, 2026 05:18
…bSQL support

- Refactor SqlDriver: private → protected for extensibility
- Fix count() bug: use ?? instead of || for zero-count handling
- Implement TursoDriver extending SqlDriver (zero CRUD duplication)
- Add multi-tenant router with TTL cache
- 53 tests passing (39 driver + 14 multi-tenant)
- 78 driver-sql regression tests passing

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/f3d58acb-4d15-42db-a9ef-f318b266842f
@hotlong hotlong marked this pull request as ready for review March 23, 2026 05:20
Copilot AI review requested due to automatic review settings March 23, 2026 05:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the Turso/libSQL driver into this monorepo as a first-class plugin that extends the existing Knex-based SqlDriver, plus adds a multi-tenant router and updates project docs/changelog/roadmap accordingly.

Changes:

  • Added new @objectstack/driver-turso plugin package with TursoDriver, multi-tenant router, exports, README, and tests.
  • Refactored @objectstack/driver-sql internals from privateprotected to support subclassing, and fixed count() zero-handling (||??).
  • Updated lockfile and project docs (CHANGELOG/ROADMAP) to reflect the new driver/plugin.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds @libsql/client and related transitive deps for the new Turso driver package.
packages/plugins/driver-turso/tsconfig.json Adds package TS config (aligned with other plugins).
packages/plugins/driver-turso/src/turso-driver.ts Implements TursoDriver extending SqlDriver with optional libsql sync lifecycle.
packages/plugins/driver-turso/src/turso-driver.test.ts Adds Turso driver integration tests over in-memory SQLite.
packages/plugins/driver-turso/src/multi-tenant.ts Adds TTL-cached per-tenant driver router.
packages/plugins/driver-turso/src/multi-tenant.test.ts Adds router tests (creation/caching/CRUD/callbacks).
packages/plugins/driver-turso/src/index.ts Exports driver/router/factory and provides default plugin manifest.
packages/plugins/driver-turso/package.json Introduces new plugin package metadata and dependencies.
packages/plugins/driver-turso/README.md Documents architecture, connection modes, multi-tenant usage, and plugin registration.
packages/plugins/driver-sql/src/sql-driver.ts Enables subclass extensibility (protected members) and fixes count() for zero results.
ROADMAP.md Lists SQL + Turso drivers as shipped components.
CHANGELOG.md Records Turso driver addition, SQL driver extensibility change, and count() fix.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +159 to +174
private static toKnexConfig(config: TursoDriverConfig): SqlDriverConfig {
let filename = ':memory:';

if (config.url === ':memory:') {
filename = ':memory:';
} else if (config.url.startsWith('file:')) {
filename = config.url.replace(/^file:/, '');
}
// For remote-only URLs (libsql://, https://), use :memory: as the local backend.
// Writes will be local; use embedded replica mode (syncUrl) for remote persistence.

return {
client: 'better-sqlite3',
connection: { filename },
useNullAsDefault: true,
};
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toKnexConfig() silently falls back to :memory: for remote-only URLs (libsql:///https://). This makes TursoDriver appear to work but all CRUD hits an in-memory SQLite DB and nothing is persisted to the remote Turso database (high risk of data loss). Consider either (a) implementing remote execution via @libsql/client for remote-only mode, or (b) throwing a clear error in connect() when url is remote and syncUrl is not provided, so the unsupported mode can't be used accidentally.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +59
export interface TursoDriverConfig {
/** Database URL (`file:`, `:memory:`, `libsql://`, `https://`) */
url: string;

/** JWT auth token for remote Turso database */
authToken?: string;

/** AES-256 encryption key for local files */
encryptionKey?: string;

/** Maximum concurrent requests. Default: 20 */
concurrency?: number;

/** Remote sync URL for embedded replica mode */
syncUrl?: string;

/** Sync configuration for embedded replica mode */
sync?: {
/** Periodic sync interval in seconds (0 = manual only). Default: 60 */
intervalSeconds?: number;
/** Sync immediately on connect. Default: true */
onConnect?: boolean;
};

/** Operation timeout in milliseconds */
timeout?: number;
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encryptionKey, concurrency, and timeout are exposed on TursoDriverConfig, but in local/in-memory modes (no syncUrl) they are never applied because the driver only uses Knex/better-sqlite3. This can mislead callers into thinking encryption/limits/timeouts are active when they're ignored. Either validate these options (e.g., require syncUrl/libsql mode) or document/implement their behavior for the Knex path.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +195
async function getDriverForTenant(tenantId: string): Promise<TursoDriver> {
validateTenantId(tenantId);

const existing = cache.get(tenantId);
if (existing) {
if (Date.now() < existing.expiresAt) {
return existing.driver;
}
// Expired — evict and recreate
await evictEntry(tenantId, existing);
}

// Create new driver
const url = buildUrl(tenantId);
const driverConfig: TursoDriverConfig = {
...config.driverConfigOverrides,
url,
authToken: config.groupAuthToken ?? config.driverConfigOverrides?.authToken,
};

const driver = new TursoDriver(driverConfig);

if (config.onTenantCreate) {
await config.onTenantCreate(tenantId);
}

await driver.connect();

cache.set(tenantId, {
driver,
expiresAt: Date.now() + ttl,
});

return driver;
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createMultiTenantRouter() does not guard against concurrent getDriverForTenant() calls for the same tenant. Two overlapping calls can create/connect two drivers, with the later one overwriting the cache entry and the earlier one leaking an open connection. Consider storing an in-flight Promise per tenant (or a simple per-tenant mutex) so concurrent calls share the same creation/connect work and only one driver instance is created.

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +179
// Create new driver
const url = buildUrl(tenantId);
const driverConfig: TursoDriverConfig = {
...config.driverConfigOverrides,
url,
authToken: config.groupAuthToken ?? config.driverConfigOverrides?.authToken,
};
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

driverConfigOverrides.syncUrl can contain a {tenant} placeholder (the README example uses this), but only urlTemplate is interpolated. As written, syncUrl will be passed through literally and embedded-replica sync will fail. Consider applying the same {tenant} replacement to syncUrl (and any other templated string fields you intend to support) when building driverConfig.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +55
it('should create a TursoDriver for a tenant', async () => {
router = createMultiTenantRouter({
urlTemplate: 'file:/tmp/test-turso-mt-{tenant}.db',
});

const driver = await router.getDriverForTenant('acme');
expect(driver).toBeInstanceOf(TursoDriver);
expect(driver).toBeInstanceOf(SqlDriver);
expect(driver.name).toBe('com.objectstack.driver.turso');
});

it('should cache drivers and return same instance', async () => {
router = createMultiTenantRouter({
urlTemplate: 'file:/tmp/test-turso-cache-{tenant}.db',
});

const driver1 = await router.getDriverForTenant('acme');
const driver2 = await router.getDriverForTenant('acme');
expect(driver1).toBe(driver2);
});

it('should create separate drivers per tenant', async () => {
router = createMultiTenantRouter({
urlTemplate: 'file:/tmp/test-turso-sep-{tenant}.db',
});

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests hardcode /tmp/... paths in urlTemplate. This will fail on Windows runners and can also collide across parallel test runs. Prefer using node:os tmpdir() + node:path to build a per-test temporary directory/file (and clean it up), or use Vitest's temp facilities if available.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +40
"nanoid": "^3.3.11"
},
"devDependencies": {
"@types/node": "^25.5.0",
"better-sqlite3": "^11.9.1",
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better-sqlite3 is required at runtime because TursoDriver always instantiates Knex with client: 'better-sqlite3', but it is listed under devDependencies. Consumers installing @objectstack/driver-turso won't get better-sqlite3 installed, causing runtime failures. Move it to dependencies (or declare it as a peerDependency and document the requirement).

Suggested change
"nanoid": "^3.3.11"
},
"devDependencies": {
"@types/node": "^25.5.0",
"better-sqlite3": "^11.9.1",
"nanoid": "^3.3.11",
"better-sqlite3": "^11.9.1"
},
"devDependencies": {
"@types/node": "^25.5.0",

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +106
### Multi-Tenant with Turso Cloud

```typescript
const router = createMultiTenantRouter({
urlTemplate: 'file:./data/{tenant}-replica.db',
groupAuthToken: process.env.TURSO_GROUP_TOKEN,
driverConfigOverrides: {
syncUrl: 'libsql://{tenant}-myorg.turso.io',
sync: { intervalSeconds: 30 },
},
});
```
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README shows driverConfigOverrides.syncUrl: 'libsql://{tenant}-myorg.turso.io', but the router currently only interpolates {tenant} in urlTemplate, not in syncUrl. Either update the README example or implement placeholder replacement for syncUrl so the documented configuration works.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +37
* Supports three connection modes:
* 1. **Local (Embedded):** `url: 'file:./data/local.db'` or `url: ':memory:'`
* 2. **Remote (Cloud):** `url: 'libsql://my-db-orgname.turso.io'`
* 3. **Embedded Replica (Hybrid):** `url` (local file) + `syncUrl` (remote)
*
* For local and in-memory modes, the driver uses better-sqlite3 via Knex
* (inherited from SqlDriver). For embedded replica mode, sync operations
* are handled via `@libsql/client`.
*/
export interface TursoDriverConfig {
/** Database URL (`file:`, `:memory:`, `libsql://`, `https://`) */
url: string;

/** JWT auth token for remote Turso database */
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README describes a "Remote (Cloud)" connection mode (url: 'libsql://...'), but the current implementation falls back to an in-memory SQLite backend for remote-only URLs and does not execute queries against the remote Turso database unless syncUrl is configured. Please either implement true remote mode via @libsql/client or remove/clarify the remote-mode documentation to avoid users believing their data is persisted remotely when it isn't.

Suggested change
* Supports three connection modes:
* 1. **Local (Embedded):** `url: 'file:./data/local.db'` or `url: ':memory:'`
* 2. **Remote (Cloud):** `url: 'libsql://my-db-orgname.turso.io'`
* 3. **Embedded Replica (Hybrid):** `url` (local file) + `syncUrl` (remote)
*
* For local and in-memory modes, the driver uses better-sqlite3 via Knex
* (inherited from SqlDriver). For embedded replica mode, sync operations
* are handled via `@libsql/client`.
*/
export interface TursoDriverConfig {
/** Database URL (`file:`, `:memory:`, `libsql://`, `https://`) */
url: string;
/** JWT auth token for remote Turso database */
* Supports the following connection modes:
* 1. **Local (Embedded):** `url: 'file:./data/local.db'`
* 2. **In-memory (Ephemeral):** `url: ':memory:'`
* 3. **Embedded Replica (Hybrid):** `url` (local file or `:memory:`) +
* `syncUrl` (remote `libsql://` / `https://` Turso endpoint)
*
* In all modes, the primary query engine runs against a local SQLite
* database (via SqlDriver). In embedded replica mode, `syncUrl` and
* `authToken` configure synchronization with a remote Turso database;
* a bare `url: 'libsql://...'` remote-only connection is not supported
* by this driver and will not persist data remotely.
*/
export interface TursoDriverConfig {
/** Database URL for the local store (`file:` or `:memory:`) */
url: string;
/** JWT auth token for remote Turso database (used with `syncUrl`) */

Copilot uses AI. Check for mistakes.
Copilot AI changed the title [WIP] Migrate objectql Turso driver to spec repository feat: add @objectstack/driver-turso extending SqlDriver with zero CRUD duplication Mar 23, 2026
Copilot AI requested a review from hotlong March 23, 2026 05:26
@github-actions github-actions bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/xl labels Mar 23, 2026
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Mar 23, 2026

@copilot fix all ci build and test errors

- Fix DTS build: remove legacy tuple indexing in orderBy (item[0]/item[1])
- Fix DTS build: widen SqlDriver name/version types to string for subclass override
- Fix test: update orderBy test to use standard {field, order} format
- Move better-sqlite3 from devDependencies to dependencies (runtime dep)
- Throw error for unsupported remote-only URLs without syncUrl
- Clarify TursoDriverConfig docs: encryptionKey/concurrency/timeout need syncUrl
- Add in-flight promise dedup in multi-tenant router (prevents concurrent duplicates)
- Interpolate {tenant} in syncUrl and other string config fields
- Use os.tmpdir() + random suffix in tests (cross-platform, no collisions)
- Update README: clarify connection modes, syncUrl interpolation, concurrency safety

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/e1258eb3-0b40-4278-af78-15e3ede22d41
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot fix all ci build and test errors

Fixed all CI build and test errors in 670d565:

DTS build failures (driver-sql):

  • Removed legacy tuple indexing item[0]/item[1] in orderBy — only item.field/item.order (SortNodeSchema) is supported
  • Widened SqlDriver.name/version type from literal to string so subclass override passes strict DTS

Test failure:

  • Updated orderBy test to use standard { field, order } format instead of legacy tuple ['price', 'asc']

PR review fixes:

  • Moved better-sqlite3 from devDeps to deps (runtime requirement)
  • Remote-only URLs without syncUrl now throw a clear error
  • {tenant} placeholder interpolated in syncUrl (not just urlTemplate)
  • Added concurrent getDriverForTenant deduplication via inflight promise map
  • Tests use os.tmpdir() + random suffix (cross-platform safe)
  • Config docs clarify encryptionKey/concurrency/timeout require syncUrl

All 133 tests pass (78 driver-sql + 55 driver-turso). All builds succeed (ESM + CJS + DTS). CodeQL: 0 alerts.

@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Mar 23, 2026

@copilot fix all

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot fix all

All CI build and test workflows are now passing on 670d565:

  • CI (Build Core + Build Docs + Test Core): success
  • Lint & Type Check: success
  • CodeQL Security Analysis: success
  • PR Automation: success

The only failing check is Validate Dependencies, which reports pre-existing transitive dependency vulnerabilities (undici via @vscode/vsce, kysely/effect via better-auth in plugin-auth). These are not related to driver-turso changes and exist on the main branch.

@hotlong hotlong merged commit e84befe into main Mar 23, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 2: 标准化迁移和架构升级 driver-turso(libSQL/Turso 驱动插件)

3 participants