Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c633d10e74 | ||
|
|
178840489f | ||
|
|
71423b9a58 | ||
|
|
b3d6f93148 | ||
|
|
dcbd244dd7 | ||
|
|
dc55ece384 | ||
|
|
8b38c9b999 | ||
|
|
2743504e60 | ||
|
|
479b49ffa6 | ||
|
|
110d4091e5 | ||
|
|
e674c242e1 | ||
|
|
5381795844 | ||
|
|
7b9d7a7b7d | ||
|
|
7b0fd9fed2 | ||
|
|
b4a01cc3cd | ||
|
|
a6e1aa085f | ||
|
|
6a9cbe7de0 |
@@ -305,6 +305,20 @@
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
},
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.15.5",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.15.6",
|
||||
@@ -415,12 +429,14 @@
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/llm": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
@@ -717,6 +733,7 @@
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@effect/opentelemetry": "4.0.0-beta.66",
|
||||
"@effect/platform-node": "4.0.0-beta.66",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.66",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
@@ -1114,6 +1131,8 @@
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.66", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-+ymrhBnESv/hmn5SKTe2//IY9Ox/hGPeoogEWhW47ZGyhFI5eMYFxdEUBa+3IAV05rrBzrxON9lynu68n0DM7w=="],
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.66", "", { "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-UYsrAb/5T0ZRypeN9Kmv3/ZInibGCjM6dtoiAWtfG+xKyuq8N05wmuVCXB0+XgVmUBxDWjw/S1fu4ivS0vZVuw=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||
|
||||
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
||||
@@ -1542,6 +1561,8 @@
|
||||
|
||||
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
|
||||
|
||||
"@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"],
|
||||
|
||||
"@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"],
|
||||
|
||||
"@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"],
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-beta.66",
|
||||
"@effect/platform-node": "4.0.0-beta.66",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.66",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
IconAnthropic,
|
||||
IconArcee,
|
||||
IconGemini,
|
||||
IconDeepSeek,
|
||||
IconMiniMax,
|
||||
IconMoonshotAI,
|
||||
IconNvidia,
|
||||
@@ -27,6 +28,7 @@ const getModelLab = (modelId: string) => {
|
||||
if (modelId.startsWith("claude")) return "Anthropic"
|
||||
if (modelId.startsWith("gpt")) return "OpenAI"
|
||||
if (modelId.startsWith("gemini")) return "Google"
|
||||
if (modelId.startsWith("deepseek")) return "DeepSeek"
|
||||
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
||||
if (modelId.startsWith("glm")) return "Z.ai"
|
||||
if (modelId.startsWith("qwen")) return "Alibaba"
|
||||
@@ -47,7 +49,19 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
||||
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
||||
.filter(([id, _model]) => !id.endsWith(":global"))
|
||||
.sort(([idA, modelA], [idB, modelB]) => {
|
||||
const priority = ["big-pickle", "minimax", "grok", "claude", "gpt", "gemini"]
|
||||
const priority = [
|
||||
"big-pickle",
|
||||
"claude",
|
||||
"gpt",
|
||||
"gemini",
|
||||
"deepseek",
|
||||
"glm",
|
||||
"kimi",
|
||||
"qwen",
|
||||
"grok",
|
||||
"minimax",
|
||||
"mimo",
|
||||
]
|
||||
const getPriority = (id: string) => {
|
||||
const index = priority.findIndex((p) => id.startsWith(p))
|
||||
return index === -1 ? Infinity : index
|
||||
@@ -136,6 +150,8 @@ export function ModelSection() {
|
||||
return <IconAnthropic width={16} height={16} />
|
||||
case "Google":
|
||||
return <IconGemini width={16} height={16} />
|
||||
case "DeepSeek":
|
||||
return <IconDeepSeek width={16} height={16} />
|
||||
case "Moonshot AI":
|
||||
return <IconMoonshotAI width={16} height={16} />
|
||||
case "Z.ai":
|
||||
|
||||
@@ -13,7 +13,7 @@ export function createRateLimiter(
|
||||
if (!zenApiKey) return
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const LIMIT = rateLimit ?? 500
|
||||
const LIMIT = rateLimit ?? 1000
|
||||
const yyyyMMddHHmm = new Date(Date.now())
|
||||
.toISOString()
|
||||
.replace(/[^0-9]/g, "")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Effect Drizzle SQLite
|
||||
|
||||
This package vendors a Drizzle Effect SQLite adapter for this repo.
|
||||
|
||||
- Keep this package generic: Drizzle + Effect + SQLite only.
|
||||
- Do not add opencode-specific tables, paths, migrations, post-commit hooks, or domain storage APIs here.
|
||||
- Runtime code should depend on generic `effect/unstable/sql/SqlClient`, not a specific SQLite driver.
|
||||
- Concrete SQLite clients such as `@effect/sql-sqlite-bun` belong in tests or examples unless this package intentionally adds a driver-specific helper.
|
||||
- Preserve Drizzle adapter naming and behavior where possible so this can be replaced by upstream `drizzle-orm/effect-sqlite` later.
|
||||
- If touching copied Drizzle internals, compare with current `drizzle-orm@1.0.0-rc.2` declarations and runtime JS.
|
||||
- If touching Effect APIs, verify against `/Users/kit/code/open-source/effect-smol`.
|
||||
|
||||
Useful entry points:
|
||||
|
||||
- `src/effect-sqlite/driver.ts`: creates the Effect-backed Drizzle database with `make` and `makeWithDefaults`.
|
||||
- `src/effect-sqlite/session.ts`: adapts generic Effect `SqlClient` execution and transactions to Drizzle SQLite sessions.
|
||||
- `src/sqlite-core/effect/*`: Effect-yieldable SQLite query builders.
|
||||
- `src/internal/drizzle-utils.ts`: local typed shims for Drizzle runtime internals that RC2 does not expose in declarations.
|
||||
- `examples/basic.ts`: minimal usage example with Bun SQLite.
|
||||
@@ -0,0 +1,92 @@
|
||||
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Schema from "effect/Schema"
|
||||
import { EffectDrizzleSqlite } from "../src"
|
||||
|
||||
const users = sqliteTable("users", {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
name: text().notNull(),
|
||||
})
|
||||
|
||||
type User = typeof users.$inferSelect
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
|
||||
const sqliteLayer = SqliteClient.layer({ filename: ":memory:", disableWAL: true })
|
||||
|
||||
class Database extends Context.Service<Database, DatabaseShape>()("@opencode/example/Database") {
|
||||
static layer = Layer.effect(Database, makeDatabase).pipe(Layer.provide(sqliteLayer))
|
||||
}
|
||||
|
||||
class UserStoreError extends Schema.TaggedErrorClass<UserStoreError>()("UserStoreError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
const mapStoreError = (message: string) => (cause: unknown) => new UserStoreError({ message, cause })
|
||||
|
||||
interface UserStoreShape {
|
||||
migrate(): Effect.Effect<void, UserStoreError>
|
||||
create(name: string): Effect.Effect<void, UserStoreError>
|
||||
rename(from: string, to: string): Effect.Effect<void, UserStoreError>
|
||||
list(): Effect.Effect<User[], UserStoreError>
|
||||
}
|
||||
|
||||
class UserStore extends Context.Service<UserStore, UserStoreShape>()("@opencode/example/UserStore") {
|
||||
static layer = Layer.effect(
|
||||
UserStore,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* Database
|
||||
|
||||
return UserStore.of({
|
||||
migrate: Effect.fn("UserStore.migrate")(function* () {
|
||||
yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: `${import.meta.dirname}/migrations` }).pipe(
|
||||
Effect.mapError((cause) => new UserStoreError({ message: "Failed to migrate users", cause })),
|
||||
)
|
||||
}),
|
||||
create: Effect.fn("UserStore.create")(function* (name: string) {
|
||||
yield* db
|
||||
.insert(users)
|
||||
.values({ name })
|
||||
.pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to create user")))
|
||||
}),
|
||||
rename: Effect.fn("UserStore.rename")(function* (from: string, to: string) {
|
||||
yield* db
|
||||
.transaction(
|
||||
Effect.fnUntraced(function* (tx) {
|
||||
yield* tx.insert(users).values({ name: from })
|
||||
yield* tx.update(users).set({ name: to }).where(eq(users.name, from))
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
.pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to rename user")))
|
||||
}),
|
||||
list: Effect.fn("UserStore.list")(function* () {
|
||||
return yield* db
|
||||
.select()
|
||||
.from(users)
|
||||
.pipe(Effect.mapError(mapStoreError("Failed to list users")))
|
||||
}),
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(Database.layer))
|
||||
}
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const userStore = yield* UserStore
|
||||
|
||||
yield* userStore.migrate()
|
||||
yield* userStore.create("Ada")
|
||||
yield* userStore.rename("Grace", "Grace Hopper")
|
||||
|
||||
return yield* userStore.list()
|
||||
})
|
||||
|
||||
const rows = await Effect.runPromise(program.pipe(Effect.provide(UserStore.layer)))
|
||||
|
||||
console.log(rows)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE users (
|
||||
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name text NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.15.5",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "bun test --timeout 30000",
|
||||
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effect-sqlite": "./src/effect-sqlite/index.ts",
|
||||
"./effect-sqlite/migrator": "./src/effect-sqlite/migrator.ts",
|
||||
"./sqlite-core/effect": "./src/sqlite-core/effect/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/* oxlint-disable */
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Layer from "effect/Layer"
|
||||
import { SqlClient } from "effect/unstable/sql/SqlClient"
|
||||
import { EffectCache } from "drizzle-orm/cache/core/cache-effect"
|
||||
import { EffectLogger } from "drizzle-orm/effect-core"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
|
||||
import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import { SQLiteEffectDatabase } from "../sqlite-core/effect/db"
|
||||
import type { DrizzleConfig } from "drizzle-orm/utils"
|
||||
import { jitCompatCheck } from "../internal/drizzle-utils"
|
||||
import { type EffectSQLiteQueryEffectHKT, type EffectSQLiteRunResult, EffectSQLiteSession } from "./session"
|
||||
|
||||
export class EffectSQLiteDatabase<TRelations extends AnyRelations = EmptyRelations> extends SQLiteEffectDatabase<
|
||||
EffectSQLiteQueryEffectHKT,
|
||||
EffectSQLiteRunResult,
|
||||
TRelations
|
||||
> {
|
||||
static override readonly [entityKind]: string = "EffectSQLiteDatabase"
|
||||
}
|
||||
|
||||
export type EffectDrizzleSQLiteConfig<TRelations extends AnyRelations = EmptyRelations> = Omit<
|
||||
DrizzleConfig<Record<string, never>, TRelations>,
|
||||
"cache" | "logger" | "schema"
|
||||
>
|
||||
|
||||
export const DefaultServices = Layer.merge(EffectCache.Default, EffectLogger.Default)
|
||||
|
||||
/**
|
||||
* Creates an EffectSQLiteDatabase instance.
|
||||
*
|
||||
* Requires a generic Effect `SqlClient`, `EffectLogger`, and `EffectCache` services to be provided.
|
||||
* Drizzle only depends on the generic `SqlClient`; install and provide a compatible SQLite provider such as
|
||||
* `@effect/sql-sqlite-node`, `@effect/sql-sqlite-bun`, or another package that exposes `SqlClient`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { SqliteClient } from '@effect/sql-sqlite-node';
|
||||
* import * as SQLiteDrizzle from 'drizzle-orm/effect-sqlite';
|
||||
* import * as Effect from 'effect/Effect';
|
||||
*
|
||||
* const db = yield* SQLiteDrizzle.make({ relations }).pipe(
|
||||
* Effect.provide(SQLiteDrizzle.DefaultServices),
|
||||
* Effect.provide(SqliteClient.layer({ filename: 'sqlite.db' })),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export const make = Effect.fn("SQLiteDrizzle.make")(function* <TRelations extends AnyRelations = EmptyRelations>(
|
||||
config: EffectDrizzleSQLiteConfig<TRelations> = {},
|
||||
) {
|
||||
const client = yield* SqlClient
|
||||
const cache = yield* EffectCache
|
||||
const logger = yield* EffectLogger
|
||||
|
||||
const dialect = new SQLiteAsyncDialect()
|
||||
const relations = config.relations ?? ({} as TRelations)
|
||||
const session = new EffectSQLiteSession(client, dialect, relations, {
|
||||
logger,
|
||||
cache,
|
||||
useJitMappers: jitCompatCheck(config.jit),
|
||||
})
|
||||
const db = new EffectSQLiteDatabase(dialect, session, relations) as EffectSQLiteDatabase<TRelations> & {
|
||||
$client: SqlClient
|
||||
}
|
||||
db.$client = client
|
||||
db.$cache.invalidate = cache.onMutate
|
||||
|
||||
return db
|
||||
})
|
||||
|
||||
/**
|
||||
* Convenience function that creates an EffectSQLiteDatabase with `DefaultServices` already provided.
|
||||
*/
|
||||
export const makeWithDefaults = <TRelations extends AnyRelations = EmptyRelations>(
|
||||
config: EffectDrizzleSQLiteConfig<TRelations> = {},
|
||||
) => make(config).pipe(Effect.provide(DefaultServices))
|
||||
@@ -0,0 +1,4 @@
|
||||
/* oxlint-disable */
|
||||
export { EffectLogger } from "drizzle-orm/effect-core"
|
||||
export * from "./driver"
|
||||
export * from "./session"
|
||||
@@ -0,0 +1,14 @@
|
||||
/* oxlint-disable */
|
||||
import type { MigrationConfig } from "drizzle-orm/migrator"
|
||||
import { readMigrationFiles } from "drizzle-orm/migrator"
|
||||
import type { AnyRelations } from "drizzle-orm/relations"
|
||||
import { migrate as coreMigrate } from "../sqlite-core/effect/session"
|
||||
import type { EffectSQLiteDatabase } from "./driver"
|
||||
|
||||
export function migrate<TRelations extends AnyRelations>(
|
||||
db: EffectSQLiteDatabase<TRelations>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = readMigrationFiles(config)
|
||||
return coreMigrate(migrations, db.session, config)
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/* oxlint-disable */
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Exit from "effect/Exit"
|
||||
import * as Scope from "effect/Scope"
|
||||
import type { SqlClient } from "effect/unstable/sql/SqlClient"
|
||||
import type { SqlError } from "effect/unstable/sql/SqlError"
|
||||
import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
|
||||
import type { WithCacheConfig } from "drizzle-orm/cache/core/types"
|
||||
import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors"
|
||||
import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger"
|
||||
import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import type { AnyRelations } from "drizzle-orm/relations"
|
||||
import type { RelationalQueryMapperConfig } from "drizzle-orm/relations"
|
||||
import type { Query } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import { SQLiteEffectPreparedQuery, SQLiteEffectSession, SQLiteEffectTransaction } from "../sqlite-core/effect/session"
|
||||
import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
|
||||
|
||||
export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase {
|
||||
readonly error: EffectDrizzleQueryError
|
||||
readonly context: never
|
||||
}
|
||||
|
||||
export type EffectSQLiteRunResult = readonly never[]
|
||||
|
||||
export interface EffectSQLiteSessionOptions {
|
||||
logger: EffectLoggerShape
|
||||
cache: EffectCacheShape
|
||||
useJitMappers?: boolean
|
||||
}
|
||||
|
||||
export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLiteEffectSession<
|
||||
EffectSQLiteQueryEffectHKT,
|
||||
EffectSQLiteRunResult,
|
||||
TRelations
|
||||
> {
|
||||
static override readonly [entityKind]: string = "EffectSQLiteSession"
|
||||
|
||||
constructor(
|
||||
private client: SqlClient,
|
||||
dialect: SQLiteAsyncDialect,
|
||||
protected relations: TRelations,
|
||||
private options: EffectSQLiteSessionOptions,
|
||||
) {
|
||||
super(dialect)
|
||||
}
|
||||
|
||||
override prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
queryMetadata?: {
|
||||
type: "select" | "update" | "delete" | "insert"
|
||||
tables: string[]
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT> {
|
||||
return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT>(
|
||||
(params, method) => this.execute(query, params, method),
|
||||
query,
|
||||
this.options.logger,
|
||||
this.options.cache,
|
||||
queryMetadata,
|
||||
cacheConfig,
|
||||
fields,
|
||||
executeMethod,
|
||||
this.options.useJitMappers,
|
||||
customResultMapper,
|
||||
undefined,
|
||||
undefined,
|
||||
this.isInTransaction(),
|
||||
)
|
||||
}
|
||||
|
||||
override prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
config: RelationalQueryMapperConfig,
|
||||
): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true> {
|
||||
return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true>(
|
||||
(params, method) => this.execute(query, params, method),
|
||||
query,
|
||||
this.options.logger,
|
||||
this.options.cache,
|
||||
undefined,
|
||||
undefined,
|
||||
fields,
|
||||
executeMethod,
|
||||
this.options.useJitMappers,
|
||||
customResultMapper,
|
||||
true,
|
||||
config,
|
||||
this.isInTransaction(),
|
||||
)
|
||||
}
|
||||
|
||||
private execute(query: Query, params: unknown[], method: SQLiteExecuteMethod | "values") {
|
||||
const statement = this.client.unsafe(query.sql, params)
|
||||
if (method === "values") return statement.values
|
||||
if (method === "get") return statement.withoutTransform.pipe(Effect.map((rows) => rows[0]))
|
||||
return statement.withoutTransform
|
||||
}
|
||||
|
||||
private isInTransaction() {
|
||||
return Effect.serviceOption(this.client.transactionService).pipe(Effect.map((option) => option._tag === "Some"))
|
||||
}
|
||||
|
||||
private executeTransactionStatement(connection: Effect.Success<SqlClient["reserve"]>, query: string) {
|
||||
return connection.executeUnprepared(query, [], undefined).pipe(Effect.asVoid)
|
||||
}
|
||||
|
||||
private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) {
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
Effect.withFiber<A, E | SqlError, R>((fiber) => {
|
||||
const services = fiber.context
|
||||
const connectionOption = Context.getOption(services, this.client.transactionService)
|
||||
const connection: Effect.Effect<
|
||||
readonly [Scope.Closeable | undefined, Effect.Success<SqlClient["reserve"]>],
|
||||
SqlError
|
||||
> =
|
||||
connectionOption._tag === "Some"
|
||||
? Effect.succeed([undefined, connectionOption.value[0]] as const)
|
||||
: Scope.make().pipe(
|
||||
Effect.flatMap((scope) =>
|
||||
Scope.provide(this.client.reserve, scope).pipe(
|
||||
Effect.map((connection) => [scope, connection] as const),
|
||||
Effect.catch((error) =>
|
||||
Scope.close(scope, Exit.fail(error)).pipe(Effect.andThen(Effect.fail(error))),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
const id = connectionOption._tag === "Some" ? connectionOption.value[1] + 1 : 0
|
||||
|
||||
return connection.pipe(
|
||||
Effect.flatMap(([scope, connection]) =>
|
||||
this.executeTransactionStatement(
|
||||
connection,
|
||||
id === 0 ? `begin ${config?.behavior ?? "deferred"}` : `savepoint effect_sql_${id}`,
|
||||
).pipe(
|
||||
Effect.flatMap(() =>
|
||||
Effect.provideContext(
|
||||
restore(effect),
|
||||
Context.add(services, this.client.transactionService, [connection, id]),
|
||||
),
|
||||
),
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => {
|
||||
const finalize = Exit.isSuccess(exit)
|
||||
? id === 0
|
||||
? this.executeTransactionStatement(connection, "commit").pipe(
|
||||
// SQLite keeps the transaction open after deferred constraint commit failures.
|
||||
Effect.catch((error) =>
|
||||
this.executeTransactionStatement(connection, "rollback").pipe(
|
||||
Effect.catch(() => Effect.void),
|
||||
Effect.andThen(Effect.fail(error)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`)
|
||||
: id === 0
|
||||
? this.executeTransactionStatement(connection, "rollback")
|
||||
: this.executeTransactionStatement(connection, `rollback to savepoint effect_sql_${id}`).pipe(
|
||||
Effect.andThen(
|
||||
this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`),
|
||||
),
|
||||
)
|
||||
const scoped = scope === undefined ? finalize : Effect.ensuring(finalize, Scope.close(scope, exit))
|
||||
|
||||
return scoped.pipe(Effect.flatMap(() => exit))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
override transaction<A, E, R>(
|
||||
transaction: (tx: EffectSQLiteTransaction<TRelations>) => Effect.Effect<A, E, R>,
|
||||
config?: SQLiteTransactionConfig,
|
||||
): Effect.Effect<A, E | SqlError, R> {
|
||||
const { dialect, relations } = this
|
||||
|
||||
return this.withTransaction(
|
||||
Effect.gen({ self: this }, function* () {
|
||||
const tx = new EffectSQLiteTransaction<TRelations>(dialect, this, relations)
|
||||
|
||||
return yield* transaction(tx)
|
||||
}),
|
||||
config,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class EffectSQLiteTransaction<TRelations extends AnyRelations> extends SQLiteEffectTransaction<
|
||||
EffectSQLiteQueryEffectHKT,
|
||||
EffectSQLiteRunResult,
|
||||
TRelations
|
||||
> {
|
||||
static override readonly [entityKind]: string = "EffectSQLiteTransaction"
|
||||
|
||||
override transaction: <A, E, R>(
|
||||
transaction: (
|
||||
tx: SQLiteEffectTransaction<EffectSQLiteQueryEffectHKT, EffectSQLiteRunResult, TRelations>,
|
||||
) => Effect.Effect<A, E, R>,
|
||||
) => Effect.Effect<A, SqlError | E, R> = (tx) => this.session.transaction(tx)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { EffectLogger } from "drizzle-orm/effect-core"
|
||||
export * from "./effect-sqlite/driver"
|
||||
export * from "./effect-sqlite/session"
|
||||
export { migrate } from "./effect-sqlite/migrator"
|
||||
|
||||
export * as EffectDrizzleSqlite from "."
|
||||
@@ -0,0 +1,127 @@
|
||||
/* oxlint-disable */
|
||||
import { Column, getColumnTable } from "drizzle-orm/column"
|
||||
import { is } from "drizzle-orm/entity"
|
||||
import type { JoinNullability } from "drizzle-orm/query-builders/select.types"
|
||||
import { Param, SQL } from "drizzle-orm/sql/sql"
|
||||
import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
|
||||
import { Subquery } from "drizzle-orm/subquery"
|
||||
import { Table, getTableName } from "drizzle-orm/table"
|
||||
import type { UpdateSet } from "drizzle-orm/utils"
|
||||
import { ViewBaseConfig } from "drizzle-orm/view-common"
|
||||
|
||||
const TableSymbol = (
|
||||
Table as unknown as {
|
||||
Symbol: { Columns: symbol; IsAlias: symbol; Name: symbol; BaseName: symbol }
|
||||
}
|
||||
).Symbol
|
||||
|
||||
export function getTableColumnsRuntime(table: SQLiteTable) {
|
||||
return (table as unknown as Record<symbol, Record<string, Column>>)[TableSymbol.Columns]
|
||||
}
|
||||
|
||||
export function getViewSelectedFieldsRuntime(view: SQLiteViewBase) {
|
||||
return (view as unknown as Record<symbol, { selectedFields: Record<string, unknown>; name: string }>)[ViewBaseConfig]
|
||||
}
|
||||
|
||||
export function jitCompatCheck(isEnabled: boolean | undefined) {
|
||||
if (!isEnabled) return false
|
||||
try {
|
||||
return new Function("input", '"use strict"; return input;')(true) === true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function orderSelectedFields<TColumn extends Column>(
|
||||
fields: Record<string, unknown>,
|
||||
pathPrefix?: string[],
|
||||
): SelectedFieldsOrdered {
|
||||
return Object.entries(fields).flatMap(([name, field]) => {
|
||||
const path = pathPrefix ? [...pathPrefix, name] : [name]
|
||||
if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased) || is(field, Subquery)) {
|
||||
return [{ path, field }] as SelectedFieldsOrdered
|
||||
}
|
||||
if (is(field, Table)) return orderSelectedFields(getTableColumnsRuntime(field as SQLiteTable), path)
|
||||
return orderSelectedFields(field as Record<string, unknown>, path)
|
||||
}) as SelectedFieldsOrdered
|
||||
}
|
||||
|
||||
export function mapUpdateSet<TTable extends SQLiteTable>(table: TTable, values: SQLiteUpdateSetSource<TTable>) {
|
||||
const entries = Object.entries(values).filter(([, value]) => value !== undefined)
|
||||
if (entries.length === 0) throw new Error("No values to set")
|
||||
|
||||
return Object.fromEntries(
|
||||
entries.map(([key, value]) => [
|
||||
key,
|
||||
is(value, SQL) || is(value, Column) ? value : new Param(value, getTableColumnsRuntime(table)[key]),
|
||||
]),
|
||||
) as UpdateSet
|
||||
}
|
||||
|
||||
export function mapResultRow(
|
||||
columns: SelectedFieldsOrdered,
|
||||
row: unknown[],
|
||||
joinsNotNullableMap: Record<string, boolean> | undefined,
|
||||
) {
|
||||
const nullifyMap: Record<string, string | false> = {}
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
columns.forEach((column, columnIndex) => {
|
||||
const decoder = (
|
||||
is(column.field, Column)
|
||||
? column.field
|
||||
: is(column.field, SQL)
|
||||
? (column.field as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
|
||||
: is(column.field, Subquery)
|
||||
? (column.field._.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
|
||||
: (column.field.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
|
||||
) as {
|
||||
mapFromDriverValue(value: unknown): unknown
|
||||
}
|
||||
const rawValue = row[columnIndex]
|
||||
const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue)
|
||||
const objectName = column.path[0]
|
||||
let node = result
|
||||
|
||||
column.path.forEach((pathChunk, pathChunkIndex) => {
|
||||
if (pathChunkIndex === column.path.length - 1) {
|
||||
node[pathChunk] = value
|
||||
return
|
||||
}
|
||||
node[pathChunk] = (node[pathChunk] ?? {}) as Record<string, unknown>
|
||||
node = node[pathChunk] as Record<string, unknown>
|
||||
})
|
||||
|
||||
if (joinsNotNullableMap && is(column.field, Column) && column.path.length === 2 && objectName) {
|
||||
const tableName = getTableName(getColumnTable(column.field))
|
||||
nullifyMap[objectName] =
|
||||
!(objectName in nullifyMap) && value === null
|
||||
? tableName
|
||||
: typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== tableName
|
||||
? false
|
||||
: nullifyMap[objectName]
|
||||
}
|
||||
})
|
||||
|
||||
Object.entries(nullifyMap).forEach(([objectName, tableName]) => {
|
||||
if (typeof tableName === "string" && !joinsNotNullableMap?.[tableName]) result[objectName] = null
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getTableLikeName(table: SQLiteTable | Subquery | SQLiteViewBase | SQL) {
|
||||
if (is(table, Subquery)) return table._.alias
|
||||
if (is(table, SQLiteViewBase)) return getViewSelectedFieldsRuntime(table).name
|
||||
if (is(table, SQL)) return undefined
|
||||
return (table as unknown as Record<symbol, string | boolean>)[
|
||||
(table as unknown as Record<symbol, string | boolean>)[TableSymbol.IsAlias]
|
||||
? TableSymbol.Name
|
||||
: TableSymbol.BaseName
|
||||
] as string
|
||||
}
|
||||
|
||||
export type { JoinNullability }
|
||||
@@ -0,0 +1,58 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import { SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import type { SQLiteView } from "drizzle-orm/sqlite-core/view"
|
||||
import type { SQLiteEffectSession } from "./session"
|
||||
|
||||
function buildSQLiteEmbeddedCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
|
||||
return sql<number>`(select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters})`
|
||||
}
|
||||
|
||||
function buildSQLiteCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
|
||||
return sql<number>`select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters}`
|
||||
}
|
||||
|
||||
export interface SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
|
||||
extends SQL<number>,
|
||||
SQLWrapper<number>,
|
||||
Effect.Effect<number, TEffectHKT["error"], TEffectHKT["context"]> {}
|
||||
|
||||
export class SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> extends SQL<number> {
|
||||
static override readonly [entityKind]: string = "SQLiteEffectCountBuilder"
|
||||
|
||||
private sql: SQL<number>
|
||||
private session: SQLiteEffectSession<TEffectHKT, any, any>
|
||||
|
||||
constructor(params: {
|
||||
source: SQLiteTable | SQLiteView | SQL | SQLWrapper
|
||||
filters?: SQL<unknown>
|
||||
session: SQLiteEffectSession<TEffectHKT, any, any>
|
||||
}) {
|
||||
super(buildSQLiteEmbeddedCount(params.source, params.filters).queryChunks)
|
||||
|
||||
this.session = params.session
|
||||
this.sql = buildSQLiteCount(params.source, params.filters)
|
||||
}
|
||||
|
||||
execute(placeholderValues?: Record<string, unknown>) {
|
||||
return this.session
|
||||
.prepareQuery<{
|
||||
type: "async"
|
||||
execute: number
|
||||
run: unknown
|
||||
all: unknown
|
||||
get: unknown
|
||||
values: unknown
|
||||
}>(this.session.dialect.sqlToQuery(this.sql), undefined, "all", (rows) => {
|
||||
const v = rows[0]?.[0]
|
||||
if (typeof v === "number") return v
|
||||
return v ? Number(v) : 0
|
||||
})
|
||||
.execute(placeholderValues)
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectCountBuilder)
|
||||
@@ -0,0 +1,296 @@
|
||||
/* oxlint-disable */
|
||||
import { Effect } from "effect"
|
||||
import type { SqlError } from "effect/unstable/sql/SqlError"
|
||||
import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
|
||||
import type { MutationOption } from "drizzle-orm/cache/core/cache"
|
||||
import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import type { TypedQueryBuilder } from "drizzle-orm/query-builders/query-builder"
|
||||
import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
|
||||
import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
|
||||
import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder"
|
||||
import type { SelectedFields } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import type { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
|
||||
import { WithSubquery } from "drizzle-orm/subquery"
|
||||
import type { WithBuilder } from "drizzle-orm/sqlite-core/subquery"
|
||||
import { SQLiteEffectCountBuilder } from "./count"
|
||||
import { SQLiteEffectDeleteBase } from "./delete"
|
||||
import { SQLiteEffectInsertBuilder } from "./insert"
|
||||
import { SQLiteEffectRelationalQueryBuilder } from "./query"
|
||||
import { SQLiteEffectRaw } from "./raw"
|
||||
import { SQLiteEffectSelectBuilder } from "./select"
|
||||
import type { SQLiteEffectSelectBase } from "./select"
|
||||
import type { SQLiteEffectSession, SQLiteEffectTransaction } from "./session"
|
||||
import { SQLiteEffectUpdateBuilder } from "./update"
|
||||
|
||||
export class SQLiteEffectDatabase<
|
||||
TEffectHKT extends QueryEffectHKTBase,
|
||||
TRunResult,
|
||||
TRelations extends AnyRelations = EmptyRelations,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectDatabase"
|
||||
|
||||
declare readonly _: {
|
||||
readonly relations: TRelations
|
||||
readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>
|
||||
}
|
||||
|
||||
query: {
|
||||
[K in keyof TRelations]: SQLiteEffectRelationalQueryBuilder<TRelations, TRelations[K], TEffectHKT>
|
||||
}
|
||||
|
||||
constructor(
|
||||
/** @internal */
|
||||
readonly dialect: SQLiteAsyncDialect,
|
||||
/** @internal */
|
||||
readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>,
|
||||
relations: TRelations,
|
||||
readonly rowModeRQB?: boolean,
|
||||
readonly forbidJsonb?: boolean,
|
||||
) {
|
||||
this._ = {
|
||||
relations,
|
||||
session,
|
||||
}
|
||||
|
||||
this.query = {} as (typeof this)["query"]
|
||||
for (const [tableName, relation] of Object.entries(relations)) {
|
||||
;(this.query as SQLiteEffectDatabase<TEffectHKT, TRunResult, AnyRelations>["query"])[tableName] =
|
||||
new SQLiteEffectRelationalQueryBuilder(
|
||||
relations,
|
||||
relations[relation.name]!.table as SQLiteTable,
|
||||
relation,
|
||||
dialect,
|
||||
session,
|
||||
rowModeRQB,
|
||||
forbidJsonb,
|
||||
)
|
||||
}
|
||||
|
||||
this.$cache = {
|
||||
invalidate: (_params: MutationOption) => Effect.void,
|
||||
}
|
||||
}
|
||||
|
||||
$with: WithBuilder = (alias: string, selection?: ColumnsSelection) => {
|
||||
const self = this
|
||||
const as = (
|
||||
qb:
|
||||
| TypedQueryBuilder<ColumnsSelection | undefined>
|
||||
| SQL
|
||||
| ((qb: QueryBuilder) => TypedQueryBuilder<ColumnsSelection | undefined> | SQL),
|
||||
) => {
|
||||
if (typeof qb === "function") {
|
||||
qb = qb(new QueryBuilder(self.dialect))
|
||||
}
|
||||
|
||||
return new Proxy(
|
||||
new WithSubquery(
|
||||
qb.getSQL(),
|
||||
selection ??
|
||||
(("getSelectedFields" in qb
|
||||
? ((qb as { getSelectedFields(): SelectedFields | undefined }).getSelectedFields() ?? {})
|
||||
: {}) as SelectedFields),
|
||||
alias,
|
||||
true,
|
||||
),
|
||||
new SelectionProxyHandler({ alias, sqlAliasedBehavior: "alias", sqlBehavior: "error" }),
|
||||
)
|
||||
}
|
||||
return { as }
|
||||
}
|
||||
|
||||
$cache: { invalidate: EffectCacheShape["onMutate"] }
|
||||
|
||||
$count(source: SQLiteTable | SQLiteViewBase | SQL | SQLWrapper, filters?: SQL<unknown>) {
|
||||
return new SQLiteEffectCountBuilder({ source, filters, session: this.session })
|
||||
}
|
||||
|
||||
with(...queries: WithSubquery[]) {
|
||||
const self = this
|
||||
|
||||
function select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
|
||||
function select<TSelection extends SelectedFields>(
|
||||
fields: TSelection,
|
||||
): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
|
||||
function select(
|
||||
fields?: SelectedFields,
|
||||
): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: self.session,
|
||||
dialect: self.dialect,
|
||||
withList: queries,
|
||||
})
|
||||
}
|
||||
|
||||
function selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
|
||||
function selectDistinct<TSelection extends SelectedFields>(
|
||||
fields: TSelection,
|
||||
): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
|
||||
function selectDistinct(
|
||||
fields?: SelectedFields,
|
||||
): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: self.session,
|
||||
dialect: self.dialect,
|
||||
withList: queries,
|
||||
distinct: true,
|
||||
})
|
||||
}
|
||||
|
||||
function update<TTable extends SQLiteTable>(
|
||||
table: TTable,
|
||||
): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectUpdateBuilder(table, self.session, self.dialect, queries)
|
||||
}
|
||||
|
||||
function insert<TTable extends SQLiteTable>(
|
||||
into: TTable,
|
||||
): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectInsertBuilder(into, self.session, self.dialect, queries)
|
||||
}
|
||||
|
||||
function delete_<TTable extends SQLiteTable>(
|
||||
from: TTable,
|
||||
): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
|
||||
return new SQLiteEffectDeleteBase(from, self.session, self.dialect, queries)
|
||||
}
|
||||
|
||||
return { select, selectDistinct, update, insert, delete: delete_ }
|
||||
}
|
||||
|
||||
select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
|
||||
select<TSelection extends SelectedFields>(
|
||||
fields: TSelection,
|
||||
): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
|
||||
select(fields?: SelectedFields): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect })
|
||||
}
|
||||
|
||||
selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
|
||||
selectDistinct<TSelection extends SelectedFields>(
|
||||
fields: TSelection,
|
||||
): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
|
||||
selectDistinct(
|
||||
fields?: SelectedFields,
|
||||
): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: this.session,
|
||||
dialect: this.dialect,
|
||||
distinct: true,
|
||||
})
|
||||
}
|
||||
|
||||
update<TTable extends SQLiteTable>(table: TTable): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectUpdateBuilder(table, this.session, this.dialect)
|
||||
}
|
||||
|
||||
insert<TTable extends SQLiteTable>(into: TTable): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> {
|
||||
return new SQLiteEffectInsertBuilder(into, this.session, this.dialect)
|
||||
}
|
||||
|
||||
delete<TTable extends SQLiteTable>(
|
||||
from: TTable,
|
||||
): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
|
||||
return new SQLiteEffectDeleteBase(from, this.session, this.dialect)
|
||||
}
|
||||
|
||||
private raw<TResult>(
|
||||
query: SQLWrapper | string,
|
||||
action: "all" | "get" | "run" | "values",
|
||||
execute: (query: SQL) => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
|
||||
): SQLiteEffectRaw<TResult, TEffectHKT> {
|
||||
const sequel = typeof query === "string" ? sql.raw(query) : query.getSQL()
|
||||
return new SQLiteEffectRaw(
|
||||
() => execute(sequel),
|
||||
() => sequel,
|
||||
action,
|
||||
this.dialect,
|
||||
(result) => result,
|
||||
)
|
||||
}
|
||||
|
||||
run(query: SQLWrapper | string): SQLiteEffectRaw<TRunResult, TEffectHKT> {
|
||||
return this.raw(query, "run", (sequel) => this.session.run(sequel))
|
||||
}
|
||||
|
||||
all<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> {
|
||||
return this.raw(query, "all", (sequel) => this.session.all(sequel))
|
||||
}
|
||||
|
||||
get<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T | undefined, TEffectHKT> {
|
||||
return this.raw(query, "get", (sequel) => this.session.get(sequel))
|
||||
}
|
||||
|
||||
values<T extends unknown[] = unknown[]>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> {
|
||||
return this.raw(query, "values", (sequel) => this.session.values(sequel))
|
||||
}
|
||||
|
||||
transaction: <A, E, R>(
|
||||
transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>,
|
||||
config?: SQLiteTransactionConfig,
|
||||
) => Effect.Effect<A, E | SqlError, R> = (tx, config) => this.session.transaction(tx, config)
|
||||
}
|
||||
|
||||
export type SQLiteEffectWithReplicas<Q> = Q & { $primary: Q; $replicas: Q[] }
|
||||
|
||||
export const withReplicas = <
|
||||
TEffectHKT extends QueryEffectHKTBase,
|
||||
TRunResult,
|
||||
TRelations extends AnyRelations,
|
||||
Q extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations>,
|
||||
>(
|
||||
primary: Q,
|
||||
replicas: [Q, ...Q[]],
|
||||
getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!,
|
||||
): SQLiteEffectWithReplicas<Q> => {
|
||||
const select: Q["select"] = (...args: []) => getReplica(replicas).select(...args)
|
||||
const selectDistinct: Q["selectDistinct"] = (...args: []) => getReplica(replicas).selectDistinct(...args)
|
||||
const $count: Q["$count"] = (...args: [any]) => getReplica(replicas).$count(...args)
|
||||
const _with: Q["with"] = (...args: []) => getReplica(replicas).with(...args)
|
||||
const $with = ((...args: [string] | [string, ColumnsSelection]) =>
|
||||
args.length === 1
|
||||
? getReplica(replicas).$with(args[0])
|
||||
: getReplica(replicas).$with(args[0], args[1])) as Q["$with"]
|
||||
|
||||
const update: Q["update"] = (...args: [any]) => primary.update(...args)
|
||||
const insert: Q["insert"] = (...args: [any]) => primary.insert(...args)
|
||||
const $delete: Q["delete"] = (...args: [any]) => primary.delete(...args)
|
||||
const run: Q["run"] = (...args: [any]) => primary.run(...args)
|
||||
const all: Q["all"] = (...args: [any]) => primary.all(...args)
|
||||
const get: Q["get"] = (...args: [any]) => primary.get(...args)
|
||||
const values: Q["values"] = (...args: [any]) => primary.values(...args)
|
||||
const transaction: Q["transaction"] = (...args: [any]) => primary.transaction(...args)
|
||||
|
||||
return {
|
||||
...primary,
|
||||
update,
|
||||
insert,
|
||||
delete: $delete,
|
||||
run,
|
||||
all,
|
||||
get,
|
||||
values,
|
||||
transaction,
|
||||
$primary: primary,
|
||||
$replicas: replicas,
|
||||
select,
|
||||
selectDistinct,
|
||||
$count,
|
||||
$with,
|
||||
with: _with,
|
||||
get query() {
|
||||
return getReplica(replicas).query
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type AnySQLiteEffectDatabase = SQLiteEffectDatabase<any, any, any>
|
||||
export type AnySQLiteEffectSelectBase = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any>
|
||||
@@ -0,0 +1,261 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
|
||||
import type { RunnableQuery } from "drizzle-orm/runnable-query"
|
||||
import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
|
||||
import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import type { SQLiteDeleteConfig } from "drizzle-orm/sqlite-core/query-builders/delete"
|
||||
import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
|
||||
import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
|
||||
import type { Subquery } from "drizzle-orm/subquery"
|
||||
import { type DrizzleTypeError, type ValueOrArray } from "drizzle-orm/utils"
|
||||
import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
|
||||
import { getTableColumnsRuntime, orderSelectedFields } from "../../internal/drizzle-utils"
|
||||
import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
|
||||
|
||||
export type SQLiteEffectDeleteWithout<
|
||||
T extends AnySQLiteEffectDelete,
|
||||
TDynamic extends boolean,
|
||||
K extends keyof T & string,
|
||||
> = TDynamic extends true
|
||||
? T
|
||||
: Omit<
|
||||
SQLiteEffectDeleteBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["returning"],
|
||||
TDynamic,
|
||||
T["_"]["excludedMethods"] | K,
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
T["_"]["excludedMethods"] | K
|
||||
>
|
||||
|
||||
export type SQLiteEffectDeleteReturningAll<
|
||||
T extends AnySQLiteEffectDelete,
|
||||
TDynamic extends boolean,
|
||||
> = SQLiteEffectDeleteWithout<
|
||||
SQLiteEffectDeleteBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["table"]["$inferSelect"],
|
||||
T["_"]["dynamic"],
|
||||
T["_"]["excludedMethods"],
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
TDynamic,
|
||||
"returning"
|
||||
>
|
||||
|
||||
export type SQLiteEffectDeleteReturning<
|
||||
T extends AnySQLiteEffectDelete,
|
||||
TDynamic extends boolean,
|
||||
TSelectedFields extends SelectedFieldsFlat,
|
||||
> = SQLiteEffectDeleteWithout<
|
||||
SQLiteEffectDeleteBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
SelectResultFields<TSelectedFields>,
|
||||
T["_"]["dynamic"],
|
||||
T["_"]["excludedMethods"],
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
TDynamic,
|
||||
"returning"
|
||||
>
|
||||
|
||||
export type SQLiteEffectDeleteExecute<T extends AnySQLiteEffectDelete> = T["_"]["returning"] extends undefined
|
||||
? T["_"]["runResult"]
|
||||
: T["_"]["returning"][]
|
||||
|
||||
export type SQLiteEffectDeletePrepare<
|
||||
T extends AnySQLiteEffectDelete,
|
||||
TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
|
||||
> = SQLiteEffectPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
run: T["_"]["runResult"]
|
||||
all: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".all() cannot be used without .returning()">
|
||||
: T["_"]["returning"][]
|
||||
get: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".get() cannot be used without .returning()">
|
||||
: T["_"]["returning"] | undefined
|
||||
values: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".values() cannot be used without .returning()">
|
||||
: any[][]
|
||||
execute: SQLiteEffectDeleteExecute<T>
|
||||
},
|
||||
TEffectHKT
|
||||
>
|
||||
|
||||
export type SQLiteEffectDeleteDynamic<T extends AnySQLiteEffectDelete> = SQLiteEffectDelete<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["returning"],
|
||||
T["_"]["effectHKT"]
|
||||
>
|
||||
|
||||
export type SQLiteEffectDelete<
|
||||
TTable extends SQLiteTable = SQLiteTable,
|
||||
TRunResult = unknown,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> = SQLiteEffectDeleteBase<TTable, TRunResult, TReturning, true, never, TEffectHKT>
|
||||
|
||||
export type AnySQLiteEffectDelete = SQLiteEffectDeleteBase<any, any, any, any, any, any>
|
||||
|
||||
export interface SQLiteEffectDeleteBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
|
||||
SQLWrapper,
|
||||
Effect.Effect<
|
||||
TReturning extends undefined ? TRunResult : TReturning[],
|
||||
TEffectHKT["error"],
|
||||
TEffectHKT["context"]
|
||||
> {
|
||||
readonly _: {
|
||||
dialect: "sqlite"
|
||||
readonly table: TTable
|
||||
readonly resultType: "async"
|
||||
readonly runResult: TRunResult
|
||||
readonly returning: TReturning
|
||||
readonly dynamic: TDynamic
|
||||
readonly excludedMethods: _TExcludedMethods
|
||||
readonly result: TReturning extends undefined ? TRunResult : TReturning[]
|
||||
readonly effectHKT: TEffectHKT
|
||||
}
|
||||
}
|
||||
|
||||
export class SQLiteEffectDeleteBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
|
||||
{
|
||||
static readonly [entityKind]: string = "SQLiteEffectDelete"
|
||||
|
||||
/** @internal */
|
||||
config: SQLiteDeleteConfig
|
||||
|
||||
constructor(
|
||||
private table: TTable,
|
||||
private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
|
||||
private effectDialect: SQLiteDialect,
|
||||
withList?: Subquery[],
|
||||
) {
|
||||
this.config = { table, withList }
|
||||
}
|
||||
|
||||
where(where: SQL | undefined): SQLiteEffectDeleteWithout<this, TDynamic, "where"> {
|
||||
this.config.where = where
|
||||
return this as any
|
||||
}
|
||||
|
||||
orderBy(
|
||||
builder: (deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>,
|
||||
): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy">
|
||||
orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy">
|
||||
orderBy(
|
||||
...columns:
|
||||
| [(deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>]
|
||||
| (SQLiteColumn | SQL | SQL.Aliased)[]
|
||||
): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy"> {
|
||||
if (typeof columns[0] === "function") {
|
||||
const orderBy = columns[0](
|
||||
new Proxy(
|
||||
getTableColumnsRuntime(this.config.table),
|
||||
new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }),
|
||||
) as any,
|
||||
)
|
||||
|
||||
this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]
|
||||
return this as any
|
||||
}
|
||||
|
||||
this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]
|
||||
return this as any
|
||||
}
|
||||
|
||||
limit(limit: number | Placeholder): SQLiteEffectDeleteWithout<this, TDynamic, "limit"> {
|
||||
this.config.limit = limit
|
||||
return this as any
|
||||
}
|
||||
|
||||
returning(): SQLiteEffectDeleteReturningAll<this, TDynamic>
|
||||
returning<TSelectedFields extends SelectedFieldsFlat>(
|
||||
fields: TSelectedFields,
|
||||
): SQLiteEffectDeleteReturning<this, TDynamic, TSelectedFields>
|
||||
returning(
|
||||
fields: SelectedFieldsFlat = getTableColumnsRuntime(this.table),
|
||||
): SQLiteEffectDeleteReturning<this, TDynamic, any> | SQLiteEffectDeleteReturningAll<this, TDynamic> {
|
||||
this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
|
||||
return this as any
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.effectDialect.buildDeleteQuery(this.config)
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
return this.effectDialect.sqlToQuery(this.getSQL())
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(isOneTimeQuery = true): SQLiteEffectDeletePrepare<this, TEffectHKT> {
|
||||
return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
|
||||
this.effectDialect.sqlToQuery(this.getSQL()),
|
||||
this.config.returning,
|
||||
this.config.returning ? "all" : "run",
|
||||
undefined,
|
||||
{
|
||||
type: "delete",
|
||||
tables: extractUsedTable(this.config.table),
|
||||
},
|
||||
) as SQLiteEffectDeletePrepare<this, TEffectHKT>
|
||||
}
|
||||
|
||||
prepare(): SQLiteEffectDeletePrepare<this, TEffectHKT> {
|
||||
return this._prepare(false)
|
||||
}
|
||||
|
||||
run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
|
||||
return this._prepare().run(placeholderValues)
|
||||
}
|
||||
|
||||
all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
|
||||
return this._prepare().all(placeholderValues)
|
||||
}
|
||||
|
||||
get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
|
||||
return this._prepare().get(placeholderValues)
|
||||
}
|
||||
|
||||
values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
|
||||
return this._prepare().values(placeholderValues)
|
||||
}
|
||||
|
||||
execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
|
||||
return this._prepare().execute(placeholderValues)
|
||||
}
|
||||
|
||||
$dynamic(): SQLiteEffectDeleteDynamic<this> {
|
||||
return this as any
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectDeleteBase)
|
||||
@@ -0,0 +1,10 @@
|
||||
/* oxlint-disable */
|
||||
export * from "./count"
|
||||
export * from "./db"
|
||||
export * from "./delete"
|
||||
export * from "./insert"
|
||||
export * from "./query"
|
||||
export * from "./raw"
|
||||
export * from "./select"
|
||||
export * from "./session"
|
||||
export * from "./update"
|
||||
@@ -0,0 +1,349 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind, is } from "drizzle-orm/entity"
|
||||
import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
|
||||
import type { RunnableQuery } from "drizzle-orm/runnable-query"
|
||||
import type { Query, SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import { Param, SQL, sql } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import type { IndexColumn } from "drizzle-orm/sqlite-core/indexes"
|
||||
import type {
|
||||
SQLiteInsertConfig,
|
||||
SQLiteInsertSelectQueryBuilder,
|
||||
SQLiteInsertValue,
|
||||
} from "drizzle-orm/sqlite-core/query-builders/insert"
|
||||
import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
|
||||
import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
|
||||
import type { Subquery } from "drizzle-orm/subquery"
|
||||
import { type DrizzleTypeError, haveSameKeys } from "drizzle-orm/utils"
|
||||
import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
|
||||
import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder"
|
||||
import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
|
||||
import { getTableColumnsRuntime, mapUpdateSet, orderSelectedFields } from "../../internal/drizzle-utils"
|
||||
import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
|
||||
|
||||
export type SQLiteEffectInsertWithout<
|
||||
T extends AnySQLiteEffectInsert,
|
||||
TDynamic extends boolean,
|
||||
K extends keyof T & string,
|
||||
> = TDynamic extends true
|
||||
? T
|
||||
: Omit<
|
||||
SQLiteEffectInsertBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["returning"],
|
||||
TDynamic,
|
||||
T["_"]["excludedMethods"] | K,
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
T["_"]["excludedMethods"] | K
|
||||
>
|
||||
|
||||
export type SQLiteEffectInsertReturning<
|
||||
T extends AnySQLiteEffectInsert,
|
||||
TDynamic extends boolean,
|
||||
TSelectedFields extends SelectedFieldsFlat,
|
||||
> = SQLiteEffectInsertWithout<
|
||||
SQLiteEffectInsertBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
SelectResultFields<TSelectedFields>,
|
||||
TDynamic,
|
||||
T["_"]["excludedMethods"],
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
TDynamic,
|
||||
"returning"
|
||||
>
|
||||
|
||||
export type SQLiteEffectInsertReturningAll<
|
||||
T extends AnySQLiteEffectInsert,
|
||||
TDynamic extends boolean,
|
||||
> = SQLiteEffectInsertWithout<
|
||||
SQLiteEffectInsertBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["table"]["$inferSelect"],
|
||||
TDynamic,
|
||||
T["_"]["excludedMethods"],
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
TDynamic,
|
||||
"returning"
|
||||
>
|
||||
|
||||
export type SQLiteEffectInsertDynamic<T extends AnySQLiteEffectInsert> = SQLiteEffectInsert<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["returning"],
|
||||
T["_"]["effectHKT"]
|
||||
>
|
||||
|
||||
export type SQLiteEffectInsertOnConflictDoUpdateConfig<T extends AnySQLiteEffectInsert> = {
|
||||
target: IndexColumn | IndexColumn[]
|
||||
/** @deprecated - use either `targetWhere` or `setWhere` */
|
||||
where?: SQL
|
||||
targetWhere?: SQL
|
||||
setWhere?: SQL
|
||||
set: SQLiteUpdateSetSource<T["_"]["table"]>
|
||||
}
|
||||
|
||||
export type SQLiteEffectInsertExecute<T extends AnySQLiteEffectInsert> = T["_"]["returning"] extends undefined
|
||||
? T["_"]["runResult"]
|
||||
: T["_"]["returning"][]
|
||||
|
||||
export type SQLiteEffectInsertPrepare<
|
||||
T extends AnySQLiteEffectInsert,
|
||||
TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
|
||||
> = SQLiteEffectPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
run: T["_"]["runResult"]
|
||||
all: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".all() cannot be used without .returning()">
|
||||
: T["_"]["returning"][]
|
||||
get: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".get() cannot be used without .returning()">
|
||||
: T["_"]["returning"]
|
||||
values: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".values() cannot be used without .returning()">
|
||||
: any[][]
|
||||
execute: SQLiteEffectInsertExecute<T>
|
||||
},
|
||||
TEffectHKT
|
||||
>
|
||||
|
||||
export type SQLiteEffectInsert<
|
||||
TTable extends SQLiteTable = SQLiteTable,
|
||||
TRunResult = unknown,
|
||||
TReturning = any,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> = SQLiteEffectInsertBase<TTable, TRunResult, TReturning, true, never, TEffectHKT>
|
||||
|
||||
export type AnySQLiteEffectInsert = SQLiteEffectInsertBase<any, any, any, any, any, any>
|
||||
|
||||
export class SQLiteEffectInsertBuilder<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectInsertBuilder"
|
||||
|
||||
constructor(
|
||||
protected table: TTable,
|
||||
protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
|
||||
protected dialect: SQLiteDialect,
|
||||
private withList?: Subquery[],
|
||||
) {}
|
||||
|
||||
values(
|
||||
value: SQLiteInsertValue<TTable>,
|
||||
): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
|
||||
values(
|
||||
values: SQLiteInsertValue<TTable>[],
|
||||
): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
|
||||
values(
|
||||
values: SQLiteInsertValue<TTable> | SQLiteInsertValue<TTable>[],
|
||||
): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
|
||||
values = Array.isArray(values) ? values : [values]
|
||||
if (values.length === 0) {
|
||||
throw new Error("values() must be called with at least one value")
|
||||
}
|
||||
const mappedValues = values.map((entry) => {
|
||||
const result: Record<string, Param | SQL> = {}
|
||||
const cols = getTableColumnsRuntime(this.table)
|
||||
for (const colKey of Object.keys(entry)) {
|
||||
const colValue = entry[colKey as keyof typeof entry]
|
||||
result[colKey] = is(colValue, SQL) ? colValue : new Param(colValue, cols[colKey])
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
return new SQLiteEffectInsertBase(this.table, mappedValues, this.session, this.dialect, this.withList)
|
||||
}
|
||||
|
||||
select(
|
||||
selectQuery: (qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable>,
|
||||
): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
|
||||
select(
|
||||
selectQuery: (qb: QueryBuilder) => SQL,
|
||||
): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
|
||||
select(selectQuery: SQL): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
|
||||
select(
|
||||
selectQuery: SQLiteInsertSelectQueryBuilder<TTable>,
|
||||
): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
|
||||
select(
|
||||
selectQuery:
|
||||
| SQL
|
||||
| SQLiteInsertSelectQueryBuilder<TTable>
|
||||
| ((qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable> | SQL),
|
||||
): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
|
||||
const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery
|
||||
|
||||
if (!is(select, SQL) && !haveSameKeys(getTableColumnsRuntime(this.table), select._.selectedFields)) {
|
||||
throw new Error(
|
||||
"Insert select error: selected fields are not the same or are in a different order compared to the table definition",
|
||||
)
|
||||
}
|
||||
|
||||
return new SQLiteEffectInsertBase(this.table, select, this.session, this.dialect, this.withList, true)
|
||||
}
|
||||
}
|
||||
|
||||
export interface SQLiteEffectInsertBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends SQLWrapper,
|
||||
RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
|
||||
Effect.Effect<
|
||||
TReturning extends undefined ? TRunResult : TReturning[],
|
||||
TEffectHKT["error"],
|
||||
TEffectHKT["context"]
|
||||
> {
|
||||
readonly _: {
|
||||
readonly dialect: "sqlite"
|
||||
readonly table: TTable
|
||||
readonly resultType: "async"
|
||||
readonly runResult: TRunResult
|
||||
readonly returning: TReturning
|
||||
readonly dynamic: TDynamic
|
||||
readonly excludedMethods: _TExcludedMethods
|
||||
readonly result: TReturning extends undefined ? TRunResult : TReturning[]
|
||||
readonly effectHKT: TEffectHKT
|
||||
}
|
||||
}
|
||||
|
||||
export class SQLiteEffectInsertBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
|
||||
{
|
||||
static readonly [entityKind]: string = "SQLiteEffectInsert"
|
||||
|
||||
/** @internal */
|
||||
config: SQLiteInsertConfig<TTable>
|
||||
|
||||
constructor(
|
||||
private table: TTable,
|
||||
values: SQLiteInsertConfig["values"],
|
||||
private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
|
||||
private effectDialect: SQLiteDialect,
|
||||
withList?: Subquery[],
|
||||
select?: boolean,
|
||||
) {
|
||||
this.config = { table, values: values as any, withList, select }
|
||||
}
|
||||
|
||||
returning(): SQLiteEffectInsertReturningAll<this, TDynamic>
|
||||
returning<TSelectedFields extends SelectedFieldsFlat>(
|
||||
fields: TSelectedFields,
|
||||
): SQLiteEffectInsertReturning<this, TDynamic, TSelectedFields>
|
||||
returning(
|
||||
fields: SelectedFieldsFlat = getTableColumnsRuntime(this.config.table),
|
||||
): SQLiteEffectInsertWithout<AnySQLiteEffectInsert, TDynamic, "returning"> {
|
||||
this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
|
||||
return this as any
|
||||
}
|
||||
|
||||
onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this {
|
||||
if (!this.config.onConflict) this.config.onConflict = []
|
||||
|
||||
if (config.target === undefined) {
|
||||
this.config.onConflict.push(sql` on conflict do nothing`)
|
||||
return this
|
||||
}
|
||||
|
||||
const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`
|
||||
const whereSql = config.where ? sql` where ${config.where}` : sql``
|
||||
this.config.onConflict.push(sql` on conflict ${targetSql} do nothing${whereSql}`)
|
||||
return this
|
||||
}
|
||||
|
||||
onConflictDoUpdate(config: SQLiteEffectInsertOnConflictDoUpdateConfig<this>): this {
|
||||
if (config.where && (config.targetWhere || config.setWhere)) {
|
||||
throw new Error(
|
||||
'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.',
|
||||
)
|
||||
}
|
||||
|
||||
if (!this.config.onConflict) this.config.onConflict = []
|
||||
|
||||
const whereSql = config.where ? sql` where ${config.where}` : undefined
|
||||
const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined
|
||||
const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined
|
||||
const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`
|
||||
const setSql = this.effectDialect.buildUpdateSet(
|
||||
this.config.table,
|
||||
mapUpdateSet(this.config.table, config.set as SQLiteUpdateSetSource<TTable>),
|
||||
)
|
||||
this.config.onConflict.push(
|
||||
sql` on conflict ${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`,
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.effectDialect.buildInsertQuery(this.config)
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
return this.effectDialect.sqlToQuery(this.getSQL())
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(isOneTimeQuery = true): SQLiteEffectInsertPrepare<this, TEffectHKT> {
|
||||
return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
|
||||
this.effectDialect.sqlToQuery(this.getSQL()),
|
||||
this.config.returning,
|
||||
this.config.returning ? "all" : "run",
|
||||
undefined,
|
||||
{
|
||||
type: "insert",
|
||||
tables: extractUsedTable(this.config.table),
|
||||
},
|
||||
) as SQLiteEffectInsertPrepare<this, TEffectHKT>
|
||||
}
|
||||
|
||||
prepare(): SQLiteEffectInsertPrepare<this, TEffectHKT> {
|
||||
return this._prepare(false)
|
||||
}
|
||||
|
||||
run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
|
||||
return this._prepare().run(placeholderValues)
|
||||
}
|
||||
|
||||
all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
|
||||
return this._prepare().all(placeholderValues)
|
||||
}
|
||||
|
||||
get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
|
||||
return this._prepare().get(placeholderValues)
|
||||
}
|
||||
|
||||
values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
|
||||
return this._prepare().values(placeholderValues)
|
||||
}
|
||||
|
||||
execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
|
||||
return this._prepare().execute(placeholderValues)
|
||||
}
|
||||
|
||||
$dynamic(): SQLiteEffectInsertDynamic<this> {
|
||||
return this as any
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectInsertBase)
|
||||
@@ -0,0 +1,198 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import {
|
||||
type BuildQueryResult,
|
||||
type BuildRelationalQueryResult,
|
||||
type DBQueryConfig,
|
||||
makeDefaultRqbMapper,
|
||||
type TableRelationalConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from "drizzle-orm/relations"
|
||||
import type { RunnableQuery } from "drizzle-orm/runnable-query"
|
||||
import { type Query, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { KnownKeysOnly } from "drizzle-orm/utils"
|
||||
import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
|
||||
|
||||
export class SQLiteEffectRelationalQueryBuilder<
|
||||
TSchema extends TablesRelationalConfig,
|
||||
TFields extends TableRelationalConfig,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectRelationalQueryBuilderV2"
|
||||
|
||||
constructor(
|
||||
private schema: TSchema,
|
||||
private table: SQLiteTable,
|
||||
private tableConfig: TableRelationalConfig,
|
||||
private dialect: SQLiteDialect,
|
||||
private session: SQLiteEffectSession<TEffectHKT, any, any>,
|
||||
private rowMode?: boolean,
|
||||
private forbidJsonb?: boolean,
|
||||
) {}
|
||||
|
||||
findMany<TConfig extends DBQueryConfig<"many", TSchema, TFields>>(
|
||||
config?: KnownKeysOnly<TConfig, DBQueryConfig<"many", TSchema, TFields>>,
|
||||
): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig>[], TEffectHKT> {
|
||||
return new SQLiteEffectRelationalQuery(
|
||||
this.schema,
|
||||
this.table,
|
||||
this.tableConfig,
|
||||
this.dialect,
|
||||
this.session,
|
||||
(config as DBQueryConfig<"many"> | undefined) ?? true,
|
||||
"many",
|
||||
this.rowMode,
|
||||
this.forbidJsonb,
|
||||
)
|
||||
}
|
||||
|
||||
findFirst<TConfig extends DBQueryConfig<"one", TSchema, TFields>>(
|
||||
config?: KnownKeysOnly<TConfig, DBQueryConfig<"one", TSchema, TFields>>,
|
||||
): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig> | undefined, TEffectHKT> {
|
||||
return new SQLiteEffectRelationalQuery(
|
||||
this.schema,
|
||||
this.table,
|
||||
this.tableConfig,
|
||||
this.dialect,
|
||||
this.session,
|
||||
(config as DBQueryConfig<"one"> | undefined) ?? true,
|
||||
"first",
|
||||
this.rowMode,
|
||||
this.forbidJsonb,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export interface SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
|
||||
extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
|
||||
RunnableQuery<TResult, "sqlite">,
|
||||
SQLWrapper {}
|
||||
|
||||
export class SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
|
||||
implements RunnableQuery<TResult, "sqlite">, SQLWrapper
|
||||
{
|
||||
static readonly [entityKind]: string = "SQLiteEffectRelationalQueryV2"
|
||||
|
||||
declare readonly _: {
|
||||
readonly dialect: "sqlite"
|
||||
readonly type: "async"
|
||||
readonly result: TResult
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
mode: "many" | "first"
|
||||
/** @internal */
|
||||
table: SQLiteTable
|
||||
|
||||
constructor(
|
||||
private schema: TablesRelationalConfig,
|
||||
table: SQLiteTable,
|
||||
private tableConfig: TableRelationalConfig,
|
||||
private dialect: SQLiteDialect,
|
||||
private session: SQLiteEffectSession<TEffectHKT, any, any>,
|
||||
private config: DBQueryConfig<"many" | "one"> | true,
|
||||
mode: "many" | "first",
|
||||
private rowMode?: boolean,
|
||||
private forbidJsonb?: boolean,
|
||||
) {
|
||||
this.mode = mode
|
||||
this.table = table
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this._getQuery().sql
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(
|
||||
isOneTimeQuery = true,
|
||||
): SQLiteEffectPreparedQuery<
|
||||
PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
|
||||
TEffectHKT,
|
||||
true
|
||||
> {
|
||||
const { query, builtQuery } = this._toSQL()
|
||||
const mapperConfig = {
|
||||
isFirst: this.mode === "first",
|
||||
parseJson: !this.rowMode,
|
||||
parseJsonIfString: false,
|
||||
rootJsonMappers: true,
|
||||
selection: query.selection,
|
||||
}
|
||||
|
||||
return this.session[isOneTimeQuery ? "prepareOneTimeRelationalQuery" : "prepareRelationalQuery"](
|
||||
builtQuery,
|
||||
undefined,
|
||||
this.mode === "first" ? "get" : "all",
|
||||
makeDefaultRqbMapper(mapperConfig),
|
||||
mapperConfig,
|
||||
) as SQLiteEffectPreparedQuery<
|
||||
PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
|
||||
TEffectHKT,
|
||||
true
|
||||
>
|
||||
}
|
||||
|
||||
prepare(): SQLiteEffectPreparedQuery<
|
||||
PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
|
||||
TEffectHKT,
|
||||
true
|
||||
> {
|
||||
return this._prepare(false)
|
||||
}
|
||||
|
||||
private _getQuery() {
|
||||
const jsonb = this.forbidJsonb ? sql`json` : sql`jsonb`
|
||||
|
||||
const query = this.dialect.buildRelationalQuery({
|
||||
schema: this.schema,
|
||||
table: this.table,
|
||||
tableConfig: this.tableConfig,
|
||||
queryConfig: this.config,
|
||||
mode: this.mode,
|
||||
isNested: this.rowMode,
|
||||
jsonb,
|
||||
})
|
||||
|
||||
if (this.rowMode) {
|
||||
const jsonColumns = sql.join(
|
||||
query.selection.map((s) => {
|
||||
return sql`${sql.raw(this.dialect.escapeString(s.key))}, ${
|
||||
s.selection ? sql`${jsonb}(${sql.identifier(s.key)})` : sql.identifier(s.key)
|
||||
}`
|
||||
}),
|
||||
sql`, `,
|
||||
)
|
||||
|
||||
query.sql = sql`select json_object(${jsonColumns}) as ${sql.identifier("r")} from (${query.sql}) as ${sql.identifier(
|
||||
"t",
|
||||
)}`
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: Query } {
|
||||
const query = this._getQuery()
|
||||
|
||||
const builtQuery = this.dialect.sqlToQuery(query.sql)
|
||||
|
||||
return { query, builtQuery }
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
return this._toSQL().builtQuery
|
||||
}
|
||||
|
||||
execute(placeholderValues?: Record<string, unknown>) {
|
||||
return this.mode === "first" ? this._prepare().get(placeholderValues) : this._prepare().all(placeholderValues)
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectRelationalQuery)
|
||||
@@ -0,0 +1,49 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind } from "drizzle-orm/entity"
|
||||
import type { RunnableQuery } from "drizzle-orm/runnable-query"
|
||||
import type { PreparedQuery } from "drizzle-orm/session"
|
||||
import type { Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
|
||||
type SQLiteEffectRawAction = "all" | "get" | "values" | "run"
|
||||
|
||||
export interface SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
|
||||
extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
|
||||
RunnableQuery<TResult, "sqlite">,
|
||||
SQLWrapper {}
|
||||
|
||||
export class SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
|
||||
implements RunnableQuery<TResult, "sqlite">, SQLWrapper, PreparedQuery
|
||||
{
|
||||
static readonly [entityKind]: string = "SQLiteEffectRaw"
|
||||
|
||||
declare readonly _: {
|
||||
readonly dialect: "sqlite"
|
||||
readonly result: TResult
|
||||
}
|
||||
|
||||
constructor(
|
||||
public execute: () => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
|
||||
/** @internal */
|
||||
public getSQL: () => SQL,
|
||||
private action: SQLiteEffectRawAction,
|
||||
private dialect: SQLiteAsyncDialect,
|
||||
private mapBatchResult: (result: unknown) => unknown,
|
||||
) {}
|
||||
|
||||
getQuery(): Query & { method: SQLiteEffectRawAction } {
|
||||
return { ...this.dialect.sqlToQuery(this.getSQL()), method: this.action }
|
||||
}
|
||||
|
||||
mapResult(result: unknown, isFromBatch?: boolean) {
|
||||
return isFromBatch ? this.mapBatchResult(result) : result
|
||||
}
|
||||
|
||||
_prepare(): PreparedQuery {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectRaw)
|
||||
@@ -0,0 +1,279 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import type { CacheConfig } from "drizzle-orm/cache/core/types"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind, is } from "drizzle-orm/entity"
|
||||
import type {
|
||||
BuildSubquerySelection,
|
||||
GetSelectTableName,
|
||||
GetSelectTableSelection,
|
||||
JoinNullability,
|
||||
SelectMode,
|
||||
SelectResult,
|
||||
} from "drizzle-orm/query-builders/select.types"
|
||||
import { SQL } from "drizzle-orm/sql/sql"
|
||||
import type { ColumnsSelection, SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns"
|
||||
import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import { SQLiteSelectQueryBuilderBase } from "drizzle-orm/sqlite-core/query-builders/select"
|
||||
import type {
|
||||
CreateSQLiteSelectFromBuilderMode,
|
||||
SelectedFields,
|
||||
SQLiteSelectConfig,
|
||||
SQLiteSelectHKTBase,
|
||||
} from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
|
||||
import { Subquery } from "drizzle-orm/subquery"
|
||||
import { type Assume, getTableColumns } from "drizzle-orm/utils"
|
||||
import { getViewSelectedFieldsRuntime, orderSelectedFields } from "../../internal/drizzle-utils"
|
||||
import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
|
||||
|
||||
export type SQLiteEffectSelectPrepare<
|
||||
T extends AnySQLiteEffectSelect,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> = SQLiteEffectPreparedQuery<
|
||||
{
|
||||
type: "async"
|
||||
run: T["_"]["runResult"]
|
||||
all: T["_"]["result"]
|
||||
get: T["_"]["result"][number] | undefined
|
||||
values: any[][]
|
||||
execute: T["_"]["result"]
|
||||
},
|
||||
TEffectHKT
|
||||
>
|
||||
|
||||
export class SQLiteEffectSelectBuilder<
|
||||
TSelection extends SelectedFields | undefined,
|
||||
TRunResult,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
TBuilderMode extends "db" | "qb" = "db",
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectSelectBuilder"
|
||||
|
||||
private fields: TSelection
|
||||
private session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined
|
||||
private dialect: SQLiteDialect
|
||||
private withList: Subquery[] | undefined
|
||||
private distinct: boolean | undefined
|
||||
|
||||
constructor(config: {
|
||||
fields: TSelection
|
||||
session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined
|
||||
dialect: SQLiteDialect
|
||||
withList?: Subquery[]
|
||||
distinct?: boolean
|
||||
}) {
|
||||
this.fields = config.fields
|
||||
this.session = config.session
|
||||
this.dialect = config.dialect
|
||||
this.withList = config.withList
|
||||
this.distinct = config.distinct
|
||||
}
|
||||
|
||||
from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>(
|
||||
source: TFrom,
|
||||
): TBuilderMode extends "db"
|
||||
? SQLiteEffectSelectBase<
|
||||
GetSelectTableName<TFrom>,
|
||||
TRunResult,
|
||||
TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
|
||||
TSelection extends undefined ? "single" : "partial",
|
||||
GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {},
|
||||
false,
|
||||
never,
|
||||
SelectResult<
|
||||
TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
|
||||
TSelection extends undefined ? "single" : "partial",
|
||||
GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {}
|
||||
>[],
|
||||
BuildSubquerySelection<
|
||||
TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
|
||||
GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {}
|
||||
>,
|
||||
TEffectHKT
|
||||
>
|
||||
: CreateSQLiteSelectFromBuilderMode<
|
||||
TBuilderMode,
|
||||
GetSelectTableName<TFrom>,
|
||||
"async",
|
||||
TRunResult,
|
||||
TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
|
||||
TSelection extends undefined ? "single" : "partial"
|
||||
> {
|
||||
const isPartialSelect = !!this.fields
|
||||
|
||||
let fields: SelectedFields
|
||||
if (this.fields) {
|
||||
fields = this.fields
|
||||
} else if (is(source, Subquery)) {
|
||||
fields = Object.fromEntries(
|
||||
Object.keys(source._.selectedFields).map((key) => [
|
||||
key,
|
||||
source[key as unknown as keyof typeof source] as unknown as SelectedFields[string],
|
||||
]),
|
||||
)
|
||||
} else if (is(source, SQLiteViewBase)) {
|
||||
fields = getViewSelectedFieldsRuntime(source).selectedFields as SelectedFields
|
||||
} else if (is(source, SQL)) {
|
||||
fields = {}
|
||||
} else {
|
||||
fields = getTableColumns<SQLiteTable>(source)
|
||||
}
|
||||
|
||||
return new SQLiteEffectSelectBase({
|
||||
table: source,
|
||||
fields,
|
||||
isPartialSelect,
|
||||
session: this.session as any,
|
||||
dialect: this.dialect,
|
||||
withList: this.withList,
|
||||
distinct: this.distinct,
|
||||
}) as any
|
||||
}
|
||||
}
|
||||
|
||||
export interface SQLiteEffectSelectHKT<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
|
||||
extends SQLiteSelectHKTBase {
|
||||
_type: SQLiteEffectSelectBase<
|
||||
this["tableName"],
|
||||
this["runResult"],
|
||||
Assume<this["selection"], ColumnsSelection>,
|
||||
this["selectMode"],
|
||||
Assume<this["nullabilityMap"], Record<string, JoinNullability>>,
|
||||
this["dynamic"],
|
||||
this["excludedMethods"],
|
||||
Assume<this["result"], any[]>,
|
||||
Assume<this["selectedFields"], ColumnsSelection>,
|
||||
TEffectHKT
|
||||
>
|
||||
}
|
||||
|
||||
export interface SQLiteEffectSelectBase<
|
||||
TTableName extends string | undefined,
|
||||
TRunResult,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode = "single",
|
||||
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
|
||||
? Record<TTableName, "not-null">
|
||||
: {},
|
||||
TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends SQLiteSelectQueryBuilderBase<
|
||||
SQLiteEffectSelectHKT<TEffectHKT>,
|
||||
TTableName,
|
||||
"async",
|
||||
TRunResult,
|
||||
TSelection,
|
||||
TSelectMode,
|
||||
TNullabilityMap,
|
||||
TDynamic,
|
||||
TExcludedMethods,
|
||||
TResult,
|
||||
TSelectedFields
|
||||
>,
|
||||
Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]> {}
|
||||
|
||||
export class SQLiteEffectSelectBase<
|
||||
TTableName extends string | undefined,
|
||||
TRunResult,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode = "single",
|
||||
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
|
||||
? Record<TTableName, "not-null">
|
||||
: {},
|
||||
TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
extends SQLiteSelectQueryBuilderBase<
|
||||
SQLiteEffectSelectHKT<TEffectHKT>,
|
||||
TTableName,
|
||||
"async",
|
||||
TRunResult,
|
||||
TSelection,
|
||||
TSelectMode,
|
||||
TNullabilityMap,
|
||||
TDynamic,
|
||||
TExcludedMethods,
|
||||
TResult,
|
||||
TSelectedFields
|
||||
>
|
||||
implements SQLWrapper
|
||||
{
|
||||
static override readonly [entityKind]: string = "SQLiteEffectSelect"
|
||||
|
||||
private get effectConfig() {
|
||||
return (this as unknown as { config: SQLiteSelectConfig }).config
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.dialect.buildSelectQuery(this.effectConfig)
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(isOneTimeQuery = true): SQLiteEffectSelectPrepare<this, TEffectHKT> {
|
||||
if (!this.session) {
|
||||
throw new Error("Cannot execute a query on a query builder. Please use a database instance instead.")
|
||||
}
|
||||
const session = this.session as unknown as SQLiteEffectSession<TEffectHKT, TRunResult, any>
|
||||
const query = session[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
|
||||
this.dialect.sqlToQuery(this.getSQL()),
|
||||
orderSelectedFields<SQLiteColumn>(this.effectConfig.fields),
|
||||
"all",
|
||||
undefined,
|
||||
{
|
||||
type: "select",
|
||||
tables: [...this.usedTables],
|
||||
},
|
||||
this.cacheConfig,
|
||||
)
|
||||
query.joinsNotNullableMap = this.joinsNotNullableMap
|
||||
return query as ReturnType<this["prepare"]>
|
||||
}
|
||||
|
||||
$withCache(config?: { config?: CacheConfig; tag?: string; autoInvalidate?: boolean } | false) {
|
||||
this.cacheConfig =
|
||||
config === undefined
|
||||
? { config: {}, enabled: true, autoInvalidate: true }
|
||||
: config === false
|
||||
? { enabled: false }
|
||||
: { enabled: true, autoInvalidate: true, ...config }
|
||||
return this
|
||||
}
|
||||
|
||||
prepare(): SQLiteEffectSelectPrepare<this, TEffectHKT> {
|
||||
return this._prepare(false)
|
||||
}
|
||||
|
||||
run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
|
||||
return this._prepare().run(placeholderValues)
|
||||
}
|
||||
|
||||
all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
|
||||
return this._prepare().all(placeholderValues)
|
||||
}
|
||||
|
||||
get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
|
||||
return this._prepare().get(placeholderValues)
|
||||
}
|
||||
|
||||
values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
|
||||
return this._prepare().values(placeholderValues)
|
||||
}
|
||||
|
||||
execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
|
||||
return this._prepare().execute(placeholderValues)
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectSelectBase)
|
||||
|
||||
export type AnySQLiteEffectSelect = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any>
|
||||
@@ -0,0 +1,490 @@
|
||||
/* oxlint-disable */
|
||||
import * as Cause from "effect/Cause"
|
||||
import * as Effect from "effect/Effect"
|
||||
import type { SqlError } from "effect/unstable/sql/SqlError"
|
||||
import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
|
||||
import { NoopCache, strategyFor } from "drizzle-orm/cache/core/cache"
|
||||
import type { WithCacheConfig } from "drizzle-orm/cache/core/types"
|
||||
import { MigratorInitError } from "drizzle-orm/effect-core/errors"
|
||||
import { EffectDrizzleQueryError, EffectTransactionRollbackError } from "drizzle-orm/effect-core/errors"
|
||||
import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger"
|
||||
import type { QueryEffectHKTBase, QueryEffectKind } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind, is } from "drizzle-orm/entity"
|
||||
import type { MigrationConfig, MigrationMeta } from "drizzle-orm/migrator"
|
||||
import { getMigrationsToRun } from "drizzle-orm/migrator.utils"
|
||||
import type {
|
||||
AnyRelations,
|
||||
EmptyRelations,
|
||||
RelationalQueryMapperConfig,
|
||||
RelationalRowsMapper,
|
||||
} from "drizzle-orm/relations"
|
||||
import { makeJitRqbMapper } from "drizzle-orm/relations"
|
||||
import type { PreparedQuery } from "drizzle-orm/session"
|
||||
import { fillPlaceholders, type Query, type SQL, sql } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
|
||||
import { upgradeIfNeeded } from "../../up-migrations/effect-sqlite"
|
||||
import { assertUnreachable, makeJitQueryMapper, type RowsMapper } from "drizzle-orm/utils"
|
||||
import { mapResultRow } from "../../internal/drizzle-utils"
|
||||
import { SQLiteEffectDatabase } from "./db"
|
||||
|
||||
type MigrationConfigWithInit = MigrationConfig & { init?: boolean }
|
||||
|
||||
type SQLiteEffectExecuteMethod = SQLiteExecuteMethod | "values"
|
||||
|
||||
export class SQLiteEffectPreparedQuery<
|
||||
T extends PreparedQueryConfig,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
TIsRqbV2 extends boolean = false,
|
||||
> implements PreparedQuery
|
||||
{
|
||||
static readonly [entityKind]: string = "SQLiteEffectPreparedQuery"
|
||||
|
||||
/** @internal */
|
||||
joinsNotNullableMap?: Record<string, boolean>
|
||||
private jitMapper?: RowsMapper<any> | RelationalRowsMapper<any>
|
||||
private cacheConfig: WithCacheConfig | undefined
|
||||
private effectExecuteMethod: SQLiteExecuteMethod
|
||||
|
||||
constructor(
|
||||
private executor: (
|
||||
params: unknown[],
|
||||
executeMethod: SQLiteEffectExecuteMethod,
|
||||
) => Effect.Effect<unknown, unknown, unknown>,
|
||||
protected query: Query,
|
||||
private logger: EffectLoggerShape,
|
||||
private cache: EffectCacheShape,
|
||||
private queryMetadata:
|
||||
| {
|
||||
type: "select" | "update" | "delete" | "insert"
|
||||
tables: string[]
|
||||
}
|
||||
| undefined,
|
||||
cacheConfig: WithCacheConfig | undefined,
|
||||
private fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
private useJitMappers: boolean | undefined,
|
||||
private customResultMapper?: (
|
||||
rows: TIsRqbV2 extends true ? Record<string, unknown>[] : unknown[][],
|
||||
mapColumnValue?: (value: unknown) => unknown,
|
||||
) => unknown,
|
||||
private isRqbV2Query?: TIsRqbV2,
|
||||
private rqbConfig?: RelationalQueryMapperConfig,
|
||||
private isInTransaction: Effect.Effect<boolean> = Effect.succeed(false),
|
||||
) {
|
||||
this.effectExecuteMethod = executeMethod
|
||||
this.cacheConfig =
|
||||
cache.strategy() === "all" && cacheConfig === undefined ? { enabled: true, autoInvalidate: true } : cacheConfig
|
||||
if (!this.cacheConfig?.enabled) {
|
||||
this.cacheConfig = undefined
|
||||
}
|
||||
}
|
||||
|
||||
run(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["run"]>
|
||||
run(placeholderValues?: Record<string, unknown>): any {
|
||||
return this.executeWithCache<T["run"]>(placeholderValues, "run")
|
||||
}
|
||||
|
||||
all(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["all"]>
|
||||
all(placeholderValues?: Record<string, unknown>): any {
|
||||
if (this.isRqbV2Query) return this.allRqbV2(placeholderValues)
|
||||
|
||||
if (!this.fields && !this.customResultMapper) {
|
||||
return this.executeWithCache<T["all"]>(placeholderValues, "all")
|
||||
}
|
||||
|
||||
return this.executeWithCache<T["values"], T["all"]>(
|
||||
placeholderValues,
|
||||
"values",
|
||||
(rows) => this.mapAllResult(rows) as T["all"],
|
||||
)
|
||||
}
|
||||
|
||||
get(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["get"]>
|
||||
get(placeholderValues?: Record<string, unknown>): any {
|
||||
if (this.isRqbV2Query) return this.getRqbV2(placeholderValues)
|
||||
|
||||
if (!this.fields && !this.customResultMapper) {
|
||||
return this.executeWithCache<T["get"]>(placeholderValues, "get")
|
||||
}
|
||||
|
||||
return this.executeWithCache<T["values"], T["get"]>(
|
||||
placeholderValues,
|
||||
"values",
|
||||
(rows) => this.mapGetResult(rows) as T["get"],
|
||||
)
|
||||
}
|
||||
|
||||
values(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["values"]>
|
||||
values(placeholderValues?: Record<string, unknown>): any {
|
||||
return this.executeWithCache<T["values"]>(placeholderValues, "values")
|
||||
}
|
||||
|
||||
execute(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["execute"]>
|
||||
execute(placeholderValues?: Record<string, unknown>): any {
|
||||
return this[this.effectExecuteMethod](placeholderValues) as QueryEffectKind<TEffectHKT, T["execute"]>
|
||||
}
|
||||
|
||||
mapRunResult(result: unknown, _isFromBatch?: boolean): unknown {
|
||||
return result
|
||||
}
|
||||
|
||||
mapAllResult(rows: unknown, isFromBatch?: boolean): unknown {
|
||||
if (isFromBatch) {
|
||||
rows = Array.isArray(rows) ? rows : []
|
||||
}
|
||||
|
||||
if (!this.fields && !this.customResultMapper) {
|
||||
return rows
|
||||
}
|
||||
|
||||
if (this.isRqbV2Query) {
|
||||
return this.useJitMappers
|
||||
? (this.jitMapper =
|
||||
(this.jitMapper as RelationalRowsMapper<T["all"]>) ?? makeJitRqbMapper<T["all"]>(this.rqbConfig!))(
|
||||
rows as Record<string, unknown>[],
|
||||
)
|
||||
: (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)(rows as Record<string, unknown>[])
|
||||
}
|
||||
|
||||
if (this.customResultMapper) {
|
||||
return (this.customResultMapper as (rows: unknown[][]) => unknown)(rows as unknown[][]) as T["all"]
|
||||
}
|
||||
|
||||
return this.useJitMappers
|
||||
? (this.jitMapper =
|
||||
(this.jitMapper as RowsMapper<T["all"]>) ??
|
||||
makeJitQueryMapper<T["all"]>(this.fields!, this.joinsNotNullableMap))(rows as unknown[][])
|
||||
: (rows as unknown[][]).map((row) => mapResultRow(this.fields!, row, this.joinsNotNullableMap))
|
||||
}
|
||||
|
||||
mapGetResult(rows: unknown, isFromBatch?: boolean): unknown {
|
||||
if (isFromBatch) {
|
||||
rows = Array.isArray(rows) ? rows : []
|
||||
}
|
||||
|
||||
if (!this.fields && !this.customResultMapper) {
|
||||
return Array.isArray(rows) ? rows[0] : rows
|
||||
}
|
||||
|
||||
const row = Array.isArray(rows) ? rows[0] : rows
|
||||
if (!row) return undefined
|
||||
|
||||
if (this.isRqbV2Query) {
|
||||
return this.useJitMappers
|
||||
? (this.jitMapper =
|
||||
(this.jitMapper as RelationalRowsMapper<T["get"][]>) ?? makeJitRqbMapper<T["get"][]>(this.rqbConfig!))([
|
||||
row as Record<string, unknown>,
|
||||
])
|
||||
: (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)([row as Record<string, unknown>])
|
||||
}
|
||||
|
||||
if (this.customResultMapper) {
|
||||
return (this.customResultMapper as (rows: unknown[][]) => unknown)([row as unknown[]]) as T["get"]
|
||||
}
|
||||
|
||||
return this.useJitMappers
|
||||
? (this.jitMapper =
|
||||
(this.jitMapper as RowsMapper<T["get"][]>) ??
|
||||
makeJitQueryMapper<T["get"][]>(this.fields!, this.joinsNotNullableMap))([row as unknown[]])[0]
|
||||
: mapResultRow(this.fields!, row as unknown[], this.joinsNotNullableMap)
|
||||
}
|
||||
|
||||
private allRqbV2(placeholderValues?: Record<string, unknown>) {
|
||||
return this.executeWithCache<unknown[], T["all"]>(
|
||||
placeholderValues,
|
||||
"all",
|
||||
(rows) => this.mapAllResult(rows) as T["all"],
|
||||
)
|
||||
}
|
||||
|
||||
private getRqbV2(placeholderValues?: Record<string, unknown>) {
|
||||
return this.executeWithCache<unknown, T["get"] | undefined>(placeholderValues, "get", (row) =>
|
||||
row === undefined ? undefined : (this.mapGetResult(row) as T["get"]),
|
||||
)
|
||||
}
|
||||
|
||||
private executeWithCache<A, B = A>(
|
||||
placeholderValues: Record<string, unknown> | undefined,
|
||||
executeMethod: SQLiteEffectExecuteMethod,
|
||||
mapResult?: (result: A) => B,
|
||||
) {
|
||||
return Effect.gen({ self: this }, function* () {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {})
|
||||
|
||||
yield* this.logger.logQuery(this.query.sql, params)
|
||||
|
||||
return yield* this.queryWithCache(
|
||||
this.query.sql,
|
||||
params,
|
||||
Effect.suspend(() => this.executor(params, executeMethod) as Effect.Effect<A, unknown, unknown>),
|
||||
mapResult,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private mapCachedResult<A, B>(result: A, mapResult: ((result: A) => B) | undefined) {
|
||||
if (!mapResult) return Effect.succeed(result as unknown as B)
|
||||
return Effect.try({
|
||||
try: () => mapResult(result),
|
||||
catch: (cause) => cause,
|
||||
})
|
||||
}
|
||||
|
||||
private queryWithCache<A, E, R, B = A>(
|
||||
queryString: string,
|
||||
params: unknown[],
|
||||
query: Effect.Effect<A, E, R>,
|
||||
mapResult?: (result: A) => B,
|
||||
) {
|
||||
return Effect.gen({ self: this }, function* () {
|
||||
if (this.queryMetadata?.type === "select" && this.cacheConfig?.enabled && (yield* this.isInTransaction)) {
|
||||
return yield* this.mapCachedResult(yield* query, mapResult)
|
||||
}
|
||||
|
||||
const cacheStrat: Awaited<ReturnType<typeof strategyFor>> = !is(this.cache.cache, NoopCache)
|
||||
? yield* Effect.tryPromise(() => strategyFor(queryString, params, this.queryMetadata, this.cacheConfig))
|
||||
: { type: "skip" as const }
|
||||
|
||||
if (cacheStrat.type === "skip") {
|
||||
return yield* this.mapCachedResult(yield* query, mapResult)
|
||||
}
|
||||
|
||||
if (cacheStrat.type === "invalidate") {
|
||||
const result = yield* query
|
||||
yield* this.cache.onMutate({ tables: cacheStrat.tables })
|
||||
return yield* this.mapCachedResult(result, mapResult)
|
||||
}
|
||||
|
||||
if (cacheStrat.type === "try") {
|
||||
if (yield* this.isInTransaction) {
|
||||
return yield* this.mapCachedResult(yield* query, mapResult)
|
||||
}
|
||||
|
||||
const { tables, key, isTag, autoInvalidate, config } = cacheStrat
|
||||
const fromCache: any[] | undefined = yield* this.cache.get(key, tables, isTag, autoInvalidate)
|
||||
|
||||
if (typeof fromCache !== "undefined") {
|
||||
return yield* this.mapCachedResult(fromCache as unknown as A, mapResult)
|
||||
}
|
||||
|
||||
const result = yield* query
|
||||
|
||||
yield* this.cache.put(key, result, autoInvalidate ? tables : [], isTag, config)
|
||||
|
||||
return yield* this.mapCachedResult(result, mapResult)
|
||||
}
|
||||
|
||||
assertUnreachable(cacheStrat)
|
||||
}).pipe(
|
||||
Effect.catch((e) => {
|
||||
return Effect.fail(new EffectDrizzleQueryError({ query: queryString, params, cause: Cause.fail(e) }))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
getQuery(): Query {
|
||||
return this.query
|
||||
}
|
||||
|
||||
mapResult(response: unknown, isFromBatch?: boolean) {
|
||||
switch (this.effectExecuteMethod) {
|
||||
case "run": {
|
||||
return this.mapRunResult(response, isFromBatch)
|
||||
}
|
||||
case "all": {
|
||||
return this.mapAllResult(response, isFromBatch)
|
||||
}
|
||||
case "get": {
|
||||
return this.mapGetResult(response, isFromBatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class SQLiteEffectSession<
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
TRunResult = unknown,
|
||||
TRelations extends AnyRelations = EmptyRelations,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectSession"
|
||||
|
||||
constructor(readonly dialect: SQLiteAsyncDialect) {}
|
||||
|
||||
abstract prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
queryMetadata?: {
|
||||
type: "select" | "update" | "delete" | "insert"
|
||||
tables: string[]
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): SQLiteEffectPreparedQuery<T, TEffectHKT>
|
||||
|
||||
prepareOneTimeQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
queryMetadata?: {
|
||||
type: "select" | "update" | "delete" | "insert"
|
||||
tables: string[]
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): SQLiteEffectPreparedQuery<T, TEffectHKT> {
|
||||
return this.prepareQuery(query, fields, executeMethod, customResultMapper, queryMetadata, cacheConfig)
|
||||
}
|
||||
|
||||
abstract prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
config: RelationalQueryMapperConfig,
|
||||
): SQLiteEffectPreparedQuery<T, TEffectHKT, true>
|
||||
|
||||
prepareOneTimeRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
|
||||
config: RelationalQueryMapperConfig,
|
||||
): SQLiteEffectPreparedQuery<T, TEffectHKT, true> {
|
||||
return this.prepareRelationalQuery(query, fields, executeMethod, customResultMapper, config)
|
||||
}
|
||||
|
||||
run(query: SQL): QueryEffectKind<TEffectHKT, TRunResult>
|
||||
run(query: SQL): any {
|
||||
return this.prepareQuery<PreparedQueryConfig & { run: TRunResult; execute: TRunResult }>(
|
||||
this.dialect.sqlToQuery(query),
|
||||
undefined,
|
||||
"run",
|
||||
).run()
|
||||
}
|
||||
|
||||
all<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T[]>
|
||||
all<T = unknown>(query: SQL): any {
|
||||
return this.prepareQuery<PreparedQueryConfig & { all: T[]; execute: T[] }>(
|
||||
this.dialect.sqlToQuery(query),
|
||||
undefined,
|
||||
"all",
|
||||
).all()
|
||||
}
|
||||
|
||||
get<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T | undefined>
|
||||
get<T = unknown>(query: SQL): any {
|
||||
return this.prepareQuery<PreparedQueryConfig & { get: T | undefined; execute: T | undefined }>(
|
||||
this.dialect.sqlToQuery(query),
|
||||
undefined,
|
||||
"get",
|
||||
).get()
|
||||
}
|
||||
|
||||
values<T extends unknown[] = unknown[]>(query: SQL): QueryEffectKind<TEffectHKT, T[]>
|
||||
values<T extends unknown[] = unknown[]>(query: SQL): any {
|
||||
return this.prepareQuery<PreparedQueryConfig & { values: T[]; execute: T[] }>(
|
||||
this.dialect.sqlToQuery(query),
|
||||
undefined,
|
||||
"all",
|
||||
).values()
|
||||
}
|
||||
|
||||
count(query: SQL): QueryEffectKind<TEffectHKT, number>
|
||||
count(query: SQL): any {
|
||||
return this.values<[number]>(query).pipe(Effect.map((result) => result[0]?.[0] ?? 0))
|
||||
}
|
||||
|
||||
abstract transaction<A, E, R>(
|
||||
transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>,
|
||||
config?: SQLiteTransactionConfig,
|
||||
): Effect.Effect<A, E | SqlError, R>
|
||||
}
|
||||
|
||||
export abstract class SQLiteEffectTransaction<
|
||||
TEffectHKT extends QueryEffectHKTBase,
|
||||
TRunResult,
|
||||
TRelations extends AnyRelations = EmptyRelations,
|
||||
> extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations> {
|
||||
static override readonly [entityKind]: string = "SQLiteEffectTransaction"
|
||||
|
||||
constructor(
|
||||
dialect: SQLiteAsyncDialect,
|
||||
session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>,
|
||||
protected relations: TRelations,
|
||||
) {
|
||||
super(dialect, session, relations)
|
||||
}
|
||||
|
||||
rollback() {
|
||||
return new EffectTransactionRollbackError()
|
||||
}
|
||||
}
|
||||
|
||||
export const migrate = Effect.fn("migrate")(function* <TEffectHKT extends QueryEffectHKTBase>(
|
||||
migrations: MigrationMeta[],
|
||||
session: SQLiteEffectSession<TEffectHKT>,
|
||||
config: string | MigrationConfigWithInit,
|
||||
) {
|
||||
const migrationsTable =
|
||||
typeof config === "string" ? "__drizzle_migrations" : (config.migrationsTable ?? "__drizzle_migrations")
|
||||
|
||||
const { newDb } = yield* upgradeIfNeeded(migrationsTable, session, migrations)
|
||||
|
||||
if (newDb) {
|
||||
yield* session.run(sql`
|
||||
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
hash text NOT NULL,
|
||||
created_at numeric,
|
||||
name text,
|
||||
applied_at TEXT
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
const dbMigrations = yield* session.all<{ id: number; hash: string; created_at: string; name: string | null }>(
|
||||
sql`SELECT id, hash, created_at, name FROM ${sql.identifier(migrationsTable)}`,
|
||||
)
|
||||
|
||||
if (typeof config === "object" && config.init) {
|
||||
if (dbMigrations.length) {
|
||||
return yield* new MigratorInitError({ exitCode: "databaseMigrations" })
|
||||
}
|
||||
|
||||
if (migrations.length > 1) {
|
||||
return yield* new MigratorInitError({ exitCode: "localMigrations" })
|
||||
}
|
||||
|
||||
const [migration] = migrations
|
||||
if (!migration) return
|
||||
|
||||
yield* session.run(
|
||||
sql`insert into ${sql.identifier(
|
||||
migrationsTable,
|
||||
)} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const migrationsToRun = getMigrationsToRun({ localMigrations: migrations, dbMigrations })
|
||||
if (migrationsToRun.length === 0) return
|
||||
|
||||
yield* session.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
for (const migration of migrationsToRun) {
|
||||
for (const stmt of migration.sql) {
|
||||
yield* tx.run(sql.raw(stmt))
|
||||
}
|
||||
yield* tx.run(
|
||||
sql`insert into ${sql.identifier(
|
||||
migrationsTable,
|
||||
)} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,402 @@
|
||||
/* oxlint-disable */
|
||||
import type * as Effect from "effect/Effect"
|
||||
import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import { entityKind, is } from "drizzle-orm/entity"
|
||||
import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
|
||||
import type { RunnableQuery } from "drizzle-orm/runnable-query"
|
||||
import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
|
||||
import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
|
||||
import type { SelectedFields, SQLiteSelectJoinConfig } from "drizzle-orm/sqlite-core/query-builders/select.types"
|
||||
import type { SQLiteUpdateConfig, SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
|
||||
import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
|
||||
import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
|
||||
import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
|
||||
import { Subquery } from "drizzle-orm/subquery"
|
||||
import { type DrizzleTypeError, type UpdateSet, type ValueOrArray } from "drizzle-orm/utils"
|
||||
import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
|
||||
import {
|
||||
getTableColumnsRuntime,
|
||||
getTableLikeName,
|
||||
getViewSelectedFieldsRuntime,
|
||||
mapUpdateSet,
|
||||
orderSelectedFields,
|
||||
} from "../../internal/drizzle-utils"
|
||||
import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
|
||||
|
||||
export type SQLiteEffectUpdateWithout<
|
||||
T extends AnySQLiteEffectUpdate,
|
||||
TDynamic extends boolean,
|
||||
K extends keyof T & string,
|
||||
> = TDynamic extends true
|
||||
? T
|
||||
: Omit<
|
||||
SQLiteEffectUpdateBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["from"],
|
||||
T["_"]["returning"],
|
||||
TDynamic,
|
||||
T["_"]["excludedMethods"] | K,
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
T["_"]["excludedMethods"] | K
|
||||
>
|
||||
|
||||
export type SQLiteEffectUpdateWithJoins<
|
||||
T extends AnySQLiteEffectUpdate,
|
||||
TDynamic extends boolean,
|
||||
TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL,
|
||||
> = TDynamic extends true
|
||||
? T
|
||||
: Omit<
|
||||
SQLiteEffectUpdateBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
TFrom,
|
||||
T["_"]["returning"],
|
||||
TDynamic,
|
||||
Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">,
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">
|
||||
>
|
||||
|
||||
export type SQLiteEffectUpdateReturningAll<
|
||||
T extends AnySQLiteEffectUpdate,
|
||||
TDynamic extends boolean,
|
||||
> = SQLiteEffectUpdateWithout<
|
||||
SQLiteEffectUpdateBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["from"],
|
||||
T["_"]["table"]["$inferSelect"],
|
||||
TDynamic,
|
||||
T["_"]["excludedMethods"],
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
TDynamic,
|
||||
"returning"
|
||||
>
|
||||
|
||||
export type SQLiteEffectUpdateReturning<
|
||||
T extends AnySQLiteEffectUpdate,
|
||||
TDynamic extends boolean,
|
||||
TSelectedFields extends SelectedFields,
|
||||
> = SQLiteEffectUpdateWithout<
|
||||
SQLiteEffectUpdateBase<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["from"],
|
||||
SelectResultFields<TSelectedFields>,
|
||||
TDynamic,
|
||||
T["_"]["excludedMethods"],
|
||||
T["_"]["effectHKT"]
|
||||
>,
|
||||
TDynamic,
|
||||
"returning"
|
||||
>
|
||||
|
||||
export type SQLiteEffectUpdateExecute<T extends AnySQLiteEffectUpdate> = T["_"]["returning"] extends undefined
|
||||
? T["_"]["runResult"]
|
||||
: T["_"]["returning"][]
|
||||
|
||||
export type SQLiteEffectUpdatePrepare<
|
||||
T extends AnySQLiteEffectUpdate,
|
||||
TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
|
||||
> = SQLiteEffectPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
run: T["_"]["runResult"]
|
||||
all: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".all() cannot be used without .returning()">
|
||||
: T["_"]["returning"][]
|
||||
get: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".get() cannot be used without .returning()">
|
||||
: T["_"]["returning"]
|
||||
values: T["_"]["returning"] extends undefined
|
||||
? DrizzleTypeError<".values() cannot be used without .returning()">
|
||||
: any[][]
|
||||
execute: SQLiteEffectUpdateExecute<T>
|
||||
},
|
||||
TEffectHKT
|
||||
>
|
||||
|
||||
export type SQLiteEffectUpdateDynamic<T extends AnySQLiteEffectUpdate> = SQLiteEffectUpdate<
|
||||
T["_"]["table"],
|
||||
T["_"]["runResult"],
|
||||
T["_"]["from"],
|
||||
T["_"]["returning"],
|
||||
T["_"]["effectHKT"]
|
||||
>
|
||||
|
||||
export type SQLiteEffectUpdate<
|
||||
TTable extends SQLiteTable = SQLiteTable,
|
||||
TRunResult = unknown,
|
||||
TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
|
||||
TReturning extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> = SQLiteEffectUpdateBase<TTable, TRunResult, TFrom, TReturning, true, never, TEffectHKT>
|
||||
|
||||
export type AnySQLiteEffectUpdate = SQLiteEffectUpdateBase<any, any, any, any, any, any, any>
|
||||
|
||||
export type SQLiteEffectUpdateJoinFn<T extends AnySQLiteEffectUpdate> = <
|
||||
TJoinedTable extends SQLiteTable | Subquery | SQLiteViewBase | SQL,
|
||||
>(
|
||||
table: TJoinedTable,
|
||||
on:
|
||||
| ((
|
||||
updateTable: T["_"]["table"]["_"]["columns"],
|
||||
from: T["_"]["from"] extends SQLiteTable
|
||||
? T["_"]["from"]["_"]["columns"]
|
||||
: T["_"]["from"] extends Subquery | SQLiteViewBase
|
||||
? T["_"]["from"]["_"]["selectedFields"]
|
||||
: never,
|
||||
) => SQL | undefined)
|
||||
| SQL
|
||||
| undefined,
|
||||
) => T
|
||||
|
||||
export class SQLiteEffectUpdateBuilder<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectUpdateBuilder"
|
||||
|
||||
declare readonly _: {
|
||||
readonly table: TTable
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected table: TTable,
|
||||
protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
|
||||
protected dialect: SQLiteDialect,
|
||||
private withList?: Subquery[],
|
||||
) {}
|
||||
|
||||
set(
|
||||
values: SQLiteUpdateSetSource<TTable>,
|
||||
): SQLiteEffectUpdateWithout<
|
||||
SQLiteEffectUpdateBase<TTable, TRunResult, undefined, undefined, false, never, TEffectHKT>,
|
||||
false,
|
||||
"leftJoin" | "rightJoin" | "innerJoin" | "fullJoin"
|
||||
> {
|
||||
return new SQLiteEffectUpdateBase(
|
||||
this.table,
|
||||
mapUpdateSet(this.table, values),
|
||||
this.session,
|
||||
this.dialect,
|
||||
this.withList,
|
||||
) as any
|
||||
}
|
||||
}
|
||||
|
||||
export interface SQLiteEffectUpdateBase<
|
||||
TTable extends SQLiteTable = SQLiteTable,
|
||||
TRunResult = unknown,
|
||||
TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends SQLWrapper,
|
||||
RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
|
||||
Effect.Effect<
|
||||
TReturning extends undefined ? TRunResult : TReturning[],
|
||||
TEffectHKT["error"],
|
||||
TEffectHKT["context"]
|
||||
> {
|
||||
readonly _: {
|
||||
readonly dialect: "sqlite"
|
||||
readonly table: TTable
|
||||
readonly resultType: "async"
|
||||
readonly runResult: TRunResult
|
||||
readonly from: TFrom
|
||||
readonly returning: TReturning
|
||||
readonly dynamic: TDynamic
|
||||
readonly excludedMethods: _TExcludedMethods
|
||||
readonly result: TReturning extends undefined ? TRunResult : TReturning[]
|
||||
readonly effectHKT: TEffectHKT
|
||||
}
|
||||
}
|
||||
|
||||
export class SQLiteEffectUpdateBase<
|
||||
TTable extends SQLiteTable = SQLiteTable,
|
||||
TRunResult = unknown,
|
||||
TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
|
||||
{
|
||||
static readonly [entityKind]: string = "SQLiteEffectUpdate"
|
||||
|
||||
/** @internal */
|
||||
config: SQLiteUpdateConfig
|
||||
|
||||
constructor(
|
||||
table: TTable,
|
||||
set: UpdateSet,
|
||||
private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
|
||||
private effectDialect: SQLiteDialect,
|
||||
withList?: Subquery[],
|
||||
) {
|
||||
this.config = { set, table, withList, joins: [] }
|
||||
}
|
||||
|
||||
from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>(
|
||||
source: TFrom,
|
||||
): SQLiteEffectUpdateWithJoins<this, TDynamic, TFrom> {
|
||||
this.config.from = source
|
||||
return this as any
|
||||
}
|
||||
|
||||
private createJoin<TJoinType extends SQLiteSelectJoinConfig["joinType"]>(
|
||||
joinType: TJoinType,
|
||||
): SQLiteEffectUpdateJoinFn<this> {
|
||||
return ((
|
||||
table: SQLiteTable | Subquery | SQLiteViewBase | SQL,
|
||||
on: ((updateTable: TTable, from: TFrom) => SQL | undefined) | SQL | undefined,
|
||||
) => {
|
||||
const tableName = getTableLikeName(table)
|
||||
|
||||
if (typeof tableName === "string" && this.config.joins.some((join) => join.alias === tableName)) {
|
||||
throw new Error(`Alias "${tableName}" is already used in this query`)
|
||||
}
|
||||
|
||||
if (typeof on === "function") {
|
||||
const from = this.config.from
|
||||
? is(table, SQLiteTable)
|
||||
? getTableColumnsRuntime(table)
|
||||
: is(table, Subquery)
|
||||
? table._.selectedFields
|
||||
: is(table, SQLiteViewBase)
|
||||
? getViewSelectedFieldsRuntime(table).selectedFields
|
||||
: undefined
|
||||
: undefined
|
||||
on = on(
|
||||
new Proxy(
|
||||
this.config.table._.columns,
|
||||
new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }),
|
||||
) as any,
|
||||
from &&
|
||||
(new Proxy(from, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" })) as any),
|
||||
)
|
||||
}
|
||||
|
||||
this.config.joins.push({ on, table, joinType, alias: tableName })
|
||||
|
||||
return this as any
|
||||
}) as any
|
||||
}
|
||||
|
||||
leftJoin = this.createJoin("left")
|
||||
|
||||
rightJoin = this.createJoin("right")
|
||||
|
||||
innerJoin = this.createJoin("inner")
|
||||
|
||||
fullJoin = this.createJoin("full")
|
||||
|
||||
where(where: SQL | undefined): SQLiteEffectUpdateWithout<this, TDynamic, "where"> {
|
||||
this.config.where = where
|
||||
return this as any
|
||||
}
|
||||
|
||||
orderBy(
|
||||
builder: (updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>,
|
||||
): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy">
|
||||
orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy">
|
||||
orderBy(
|
||||
...columns:
|
||||
| [(updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>]
|
||||
| (SQLiteColumn | SQL | SQL.Aliased)[]
|
||||
): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy"> {
|
||||
if (typeof columns[0] === "function") {
|
||||
const orderBy = columns[0](
|
||||
new Proxy(
|
||||
getTableColumnsRuntime(this.config.table),
|
||||
new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }),
|
||||
) as any,
|
||||
)
|
||||
|
||||
this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]
|
||||
return this as any
|
||||
}
|
||||
|
||||
this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]
|
||||
return this as any
|
||||
}
|
||||
|
||||
limit(limit: number | Placeholder): SQLiteEffectUpdateWithout<this, TDynamic, "limit"> {
|
||||
this.config.limit = limit
|
||||
return this as any
|
||||
}
|
||||
|
||||
returning(): SQLiteEffectUpdateReturningAll<this, TDynamic>
|
||||
returning<TSelectedFields extends SelectedFields>(
|
||||
fields: TSelectedFields,
|
||||
): SQLiteEffectUpdateReturning<this, TDynamic, TSelectedFields>
|
||||
returning(
|
||||
fields: SelectedFields = getTableColumnsRuntime(this.config.table),
|
||||
): SQLiteEffectUpdateWithout<AnySQLiteEffectUpdate, TDynamic, "returning"> {
|
||||
this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
|
||||
return this as any
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.effectDialect.buildUpdateQuery(this.config)
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
return this.effectDialect.sqlToQuery(this.getSQL())
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(isOneTimeQuery = true): SQLiteEffectUpdatePrepare<this, TEffectHKT> {
|
||||
return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
|
||||
this.effectDialect.sqlToQuery(this.getSQL()),
|
||||
this.config.returning,
|
||||
this.config.returning ? "all" : "run",
|
||||
undefined,
|
||||
{
|
||||
type: "update",
|
||||
tables: extractUsedTable(this.config.table),
|
||||
},
|
||||
) as SQLiteEffectUpdatePrepare<this, TEffectHKT>
|
||||
}
|
||||
|
||||
prepare(): SQLiteEffectUpdatePrepare<this, TEffectHKT> {
|
||||
return this._prepare(false)
|
||||
}
|
||||
|
||||
run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
|
||||
return this._prepare().run(placeholderValues)
|
||||
}
|
||||
|
||||
all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
|
||||
return this._prepare().all(placeholderValues)
|
||||
}
|
||||
|
||||
get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
|
||||
return this._prepare().get(placeholderValues)
|
||||
}
|
||||
|
||||
values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
|
||||
return this._prepare().values(placeholderValues)
|
||||
}
|
||||
|
||||
execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
|
||||
return this._prepare().execute(placeholderValues)
|
||||
}
|
||||
|
||||
$dynamic(): SQLiteEffectUpdateDynamic<this> {
|
||||
return this as any
|
||||
}
|
||||
}
|
||||
|
||||
applyEffectWrapper(SQLiteEffectUpdateBase)
|
||||
@@ -0,0 +1,102 @@
|
||||
/* oxlint-disable */
|
||||
import * as Effect from "effect/Effect"
|
||||
import type { SqlError } from "effect/unstable/sql/SqlError"
|
||||
import { EffectDrizzleError } from "drizzle-orm/effect-core/errors"
|
||||
import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
|
||||
import type { MigrationMeta } from "drizzle-orm/migrator"
|
||||
import { sql } from "drizzle-orm/sql/sql"
|
||||
import type { SQLiteEffectSession } from "../sqlite-core/effect/session"
|
||||
import {
|
||||
buildSQLiteMigrationBackfillStatements,
|
||||
prepareSQLiteMigrationBackfill,
|
||||
type SQLiteMigrationTableRow,
|
||||
} from "./sqlite"
|
||||
import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils"
|
||||
|
||||
const migrationUpgradeError = (cause: unknown) =>
|
||||
new EffectDrizzleError({
|
||||
message:
|
||||
typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string"
|
||||
? cause.message
|
||||
: String(cause),
|
||||
cause,
|
||||
})
|
||||
|
||||
export const upgradeIfNeeded: <TEffectHKT extends QueryEffectHKTBase>(
|
||||
migrationsTable: string,
|
||||
session: SQLiteEffectSession<TEffectHKT>,
|
||||
localMigrations: MigrationMeta[],
|
||||
) => Effect.Effect<UpgradeResult, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> =
|
||||
Effect.fn("upgradeIfNeeded")(function* <TEffectHKT extends QueryEffectHKTBase>(
|
||||
migrationsTable: string,
|
||||
session: SQLiteEffectSession<TEffectHKT>,
|
||||
localMigrations: MigrationMeta[],
|
||||
) {
|
||||
const tableExists = yield* session.all(
|
||||
sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`,
|
||||
)
|
||||
|
||||
if (tableExists.length === 0) {
|
||||
return { newDb: true }
|
||||
}
|
||||
|
||||
const rows = yield* session.all<{ column_name: string }>(
|
||||
sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`,
|
||||
)
|
||||
|
||||
const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name))
|
||||
|
||||
for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) {
|
||||
const upgradeFn = upgradeFunctions[v]
|
||||
if (!upgradeFn) {
|
||||
return yield* new EffectDrizzleError({
|
||||
message: `No upgrade path from migration table version ${v} to ${v + 1}`,
|
||||
cause: { version: v },
|
||||
})
|
||||
}
|
||||
yield* upgradeFn(migrationsTable, session, localMigrations)
|
||||
}
|
||||
|
||||
return { newDb: false }
|
||||
})
|
||||
|
||||
const upgradeFunctions: Record<
|
||||
number,
|
||||
<TEffectHKT extends QueryEffectHKTBase>(
|
||||
migrationsTable: string,
|
||||
session: SQLiteEffectSession<TEffectHKT>,
|
||||
localMigrations: MigrationMeta[],
|
||||
) => Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]>
|
||||
> = {
|
||||
0: upgradeFromV0,
|
||||
}
|
||||
|
||||
function upgradeFromV0<TEffectHKT extends QueryEffectHKTBase>(
|
||||
migrationsTable: string,
|
||||
session: SQLiteEffectSession<TEffectHKT>,
|
||||
localMigrations: MigrationMeta[],
|
||||
): Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> {
|
||||
return Effect.gen(function* () {
|
||||
const table = sql`${sql.identifier(migrationsTable)}`
|
||||
|
||||
const dbRows = yield* session.all<SQLiteMigrationTableRow>(
|
||||
sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`,
|
||||
)
|
||||
const statements = yield* Effect.try({
|
||||
try: () =>
|
||||
buildSQLiteMigrationBackfillStatements(
|
||||
migrationsTable,
|
||||
prepareSQLiteMigrationBackfill(dbRows, localMigrations),
|
||||
),
|
||||
catch: migrationUpgradeError,
|
||||
})
|
||||
|
||||
yield* session.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
for (const statement of statements) {
|
||||
yield* tx.run(statement)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/* oxlint-disable */
|
||||
import type { TablesRelationalConfig } from "drizzle-orm/_relations"
|
||||
import type { MigrationMeta } from "drizzle-orm/migrator"
|
||||
import type { AnyRelations } from "drizzle-orm/relations"
|
||||
import { type SQL, sql } from "drizzle-orm/sql/sql"
|
||||
import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"
|
||||
import type { SQLiteSession } from "drizzle-orm/sqlite-core/session"
|
||||
import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils"
|
||||
|
||||
/** @internal */
|
||||
export type SQLiteMigrationTableRow = { id: number | null; hash: string; created_at: number }
|
||||
|
||||
type AsyncSQLiteDatabaseWithSession = BaseSQLiteDatabase<"async", unknown, Record<string, unknown>> & {
|
||||
session: {
|
||||
all<T>(query: SQL): Promise<T[]>
|
||||
}
|
||||
transaction<T>(transaction: (tx: { run(query: SQL): Promise<unknown> }) => Promise<T>): Promise<T>
|
||||
}
|
||||
|
||||
type SQLiteMigrationBackfillEntry = {
|
||||
name: string
|
||||
selector:
|
||||
| { column: "id"; value: number }
|
||||
| { column: "created_at"; value: number }
|
||||
| { column: "hash"; value: string }
|
||||
}
|
||||
|
||||
function unmatchedMigrationError(unmatched: SQLiteMigrationTableRow[]) {
|
||||
return new Error(
|
||||
`While upgrading your database migrations table we found ${unmatched.length} (${unmatched
|
||||
.map((it) => `[id: ${it.id}, created_at: ${it.created_at}]`)
|
||||
.join(
|
||||
", ",
|
||||
)}) migrations in the database that do not match any local migration. This means that some migrations were applied to the database but are missing from the local environment`,
|
||||
)
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function prepareSQLiteMigrationBackfill(
|
||||
dbRows: SQLiteMigrationTableRow[],
|
||||
localMigrations: MigrationMeta[],
|
||||
): SQLiteMigrationBackfillEntry[] {
|
||||
const sortedLocalMigrations = [...localMigrations].sort((a, b) =>
|
||||
a.folderMillis !== b.folderMillis ? a.folderMillis - b.folderMillis : (a.name ?? "").localeCompare(b.name ?? ""),
|
||||
)
|
||||
const byMillis = new Map<number, MigrationMeta[]>()
|
||||
const byHash = new Map<string, MigrationMeta>()
|
||||
for (const migration of sortedLocalMigrations) {
|
||||
if (!byMillis.has(migration.folderMillis)) {
|
||||
byMillis.set(migration.folderMillis, [])
|
||||
}
|
||||
byMillis.get(migration.folderMillis)!.push(migration)
|
||||
byHash.set(migration.hash, migration)
|
||||
}
|
||||
|
||||
const toApply: SQLiteMigrationBackfillEntry[] = []
|
||||
const unmatched: SQLiteMigrationTableRow[] = []
|
||||
|
||||
for (const dbRow of dbRows) {
|
||||
const stringified = String(dbRow.created_at)
|
||||
const millis = Number(stringified.substring(0, stringified.length - 3) + "000")
|
||||
const candidates = byMillis.get(millis)
|
||||
|
||||
const matchedByMillis = candidates?.length === 1 ? candidates[0] : undefined
|
||||
const matchedByCandidateHash =
|
||||
candidates && candidates.length > 1
|
||||
? candidates.find((candidate) => candidate.hash && dbRow.hash && candidate.hash === dbRow.hash)
|
||||
: undefined
|
||||
const matchedByHash = matchedByMillis || matchedByCandidateHash ? undefined : byHash.get(dbRow.hash)
|
||||
const matched = matchedByMillis ?? matchedByCandidateHash ?? matchedByHash
|
||||
|
||||
if (matched) {
|
||||
toApply.push({
|
||||
name: matched.name,
|
||||
selector:
|
||||
dbRow.id !== null
|
||||
? { column: "id", value: dbRow.id }
|
||||
: matchedByMillis
|
||||
? { column: "created_at", value: dbRow.created_at }
|
||||
: { column: "hash", value: dbRow.hash },
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
unmatched.push(dbRow)
|
||||
}
|
||||
|
||||
if (unmatched.length > 0) {
|
||||
throw unmatchedMigrationError(unmatched)
|
||||
}
|
||||
|
||||
return toApply
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function buildSQLiteMigrationBackfillStatements(
|
||||
migrationsTable: string,
|
||||
backfillEntries: SQLiteMigrationBackfillEntry[],
|
||||
) {
|
||||
const table = sql`${sql.identifier(migrationsTable)}`
|
||||
const statements: SQL[] = [
|
||||
sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("name")} text`,
|
||||
sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("applied_at")} TEXT`,
|
||||
]
|
||||
|
||||
for (const backfillEntry of backfillEntries) {
|
||||
const updateQuery = sql`UPDATE ${table} SET ${sql.identifier("name")} = ${backfillEntry.name}, ${sql.identifier(
|
||||
"applied_at",
|
||||
)} = NULL WHERE`
|
||||
|
||||
updateQuery.append(sql` ${sql.identifier(backfillEntry.selector.column)} = ${backfillEntry.selector.value}`)
|
||||
|
||||
statements.push(updateQuery)
|
||||
}
|
||||
|
||||
return statements
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the current version of the migrations table schema and upgrades it if needed.
|
||||
*
|
||||
* Version 0: Original schema (id, hash, created_at)
|
||||
* Version 1: Extended schema (id, hash, created_at, name, applied_at)
|
||||
*/
|
||||
export function upgradeSyncIfNeeded(
|
||||
migrationsTable: string,
|
||||
session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>,
|
||||
localMigrations: MigrationMeta[],
|
||||
): UpgradeResult {
|
||||
const tableExists = session.all(sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`)
|
||||
|
||||
if (tableExists.length === 0) {
|
||||
return { newDb: true }
|
||||
}
|
||||
|
||||
// Table exists, check table shape
|
||||
const rows = session.all<{ column_name: string }>(
|
||||
sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`,
|
||||
)
|
||||
|
||||
const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name))
|
||||
|
||||
for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) {
|
||||
const upgradeFn = upgradeSyncFunctions[v]
|
||||
if (!upgradeFn) {
|
||||
throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`)
|
||||
}
|
||||
upgradeFn(migrationsTable, session, localMigrations)
|
||||
}
|
||||
|
||||
return { newDb: false }
|
||||
}
|
||||
|
||||
const upgradeSyncFunctions: Record<
|
||||
number,
|
||||
(
|
||||
migrationsTable: string,
|
||||
session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>,
|
||||
localMigrations: MigrationMeta[],
|
||||
) => void
|
||||
> = {
|
||||
/**
|
||||
* Upgrade from version 0 to version 1:
|
||||
* 1. Read all existing DB migrations
|
||||
* 2. Sort localMigrations ASC by millis and if the same - sort by name
|
||||
* 3. Match each DB row to a local migration
|
||||
* If multiple migrations share the same second, use hash matching as a tiebreaker
|
||||
* Not implemented for now -> If hash matching fails, fall back to serial id ordering
|
||||
* 5. Create extra column and backfill names for matched migrations
|
||||
*/
|
||||
0: (migrationsTable, session, localMigrations) => {
|
||||
const table = sql`${sql.identifier(migrationsTable)}`
|
||||
const dbRows = session.all<SQLiteMigrationTableRow>(sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`)
|
||||
const statements = buildSQLiteMigrationBackfillStatements(
|
||||
migrationsTable,
|
||||
prepareSQLiteMigrationBackfill(dbRows, localMigrations),
|
||||
)
|
||||
|
||||
session.transaction((tx) => {
|
||||
for (const statement of statements) {
|
||||
tx.run(statement)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the current version of the migrations table schema and upgrades it if needed.
|
||||
*
|
||||
* Version 0: Original schema (id, hash, created_at)
|
||||
* Version 1: Extended schema (id, hash, created_at, name, applied_at)
|
||||
*/
|
||||
export async function upgradeAsyncIfNeeded(
|
||||
migrationsTable: string,
|
||||
db: AsyncSQLiteDatabaseWithSession,
|
||||
localMigrations: MigrationMeta[],
|
||||
): Promise<UpgradeResult> {
|
||||
// Check if the table exists at all
|
||||
const tableExists = await db.session.all(
|
||||
sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`,
|
||||
)
|
||||
|
||||
if (tableExists.length === 0) {
|
||||
return { newDb: true }
|
||||
}
|
||||
|
||||
const rows = await db.session.all<{ column_name: string }>(
|
||||
sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`,
|
||||
)
|
||||
|
||||
const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name))
|
||||
|
||||
for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) {
|
||||
const upgradeFn = upgradeAsyncFunctions[v]
|
||||
if (!upgradeFn) {
|
||||
throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`)
|
||||
}
|
||||
await upgradeFn(migrationsTable, db, localMigrations)
|
||||
}
|
||||
|
||||
return { newDb: false }
|
||||
}
|
||||
|
||||
const upgradeAsyncFunctions: Record<
|
||||
number,
|
||||
(migrationsTable: string, db: AsyncSQLiteDatabaseWithSession, localMigrations: MigrationMeta[]) => Promise<void>
|
||||
> = {
|
||||
/**
|
||||
* Upgrade from version 0 to version 1:
|
||||
* 1. Read all existing DB migrations
|
||||
* 2. Sort localMigrations ASC by millis and if the same - sort by name
|
||||
* 3. Match each DB row to a local migration
|
||||
* If multiple migrations share the same second, use hash matching as a tiebreaker
|
||||
* Not implemented for now -> If hash matching fails, fall back to serial id ordering
|
||||
* 5. Create extra column and backfill names for matched migrations
|
||||
*/
|
||||
0: async (migrationsTable, db, localMigrations) => {
|
||||
const table = sql`${sql.identifier(migrationsTable)}`
|
||||
const dbRows = await db.session.all<SQLiteMigrationTableRow>(
|
||||
sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`,
|
||||
)
|
||||
const statements = buildSQLiteMigrationBackfillStatements(
|
||||
migrationsTable,
|
||||
prepareSQLiteMigrationBackfill(dbRows, localMigrations),
|
||||
)
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
for (const statement of statements) {
|
||||
await tx.run(statement)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/* oxlint-disable */
|
||||
export interface UpgradeResult {
|
||||
newDb: boolean
|
||||
}
|
||||
|
||||
export const MIGRATIONS_TABLE_VERSIONS = {
|
||||
sqlite: 1,
|
||||
pg: 1,
|
||||
effect: 1,
|
||||
mysql: 1,
|
||||
mssql: 1,
|
||||
cockroach: 1,
|
||||
singlestore: 1,
|
||||
} as const
|
||||
|
||||
export const GET_VERSION_FOR = {
|
||||
mysql: (columns: string[]): number => {
|
||||
if (columns.includes("name")) return 1
|
||||
return 0
|
||||
},
|
||||
pg: (columns: string[]): number => {
|
||||
if (columns.includes("name")) return 1
|
||||
return 0
|
||||
},
|
||||
effect: (columns: string[]): number => {
|
||||
if (columns.includes("name")) return 1
|
||||
return 0
|
||||
},
|
||||
mssql: (columns: string[]): number => {
|
||||
if (columns.includes("name")) return 1
|
||||
return 0
|
||||
},
|
||||
cockroach: (columns: string[]): number => {
|
||||
if (columns.includes("name")) return 1
|
||||
return 0
|
||||
},
|
||||
singlestore: (columns: string[]): number => {
|
||||
if (columns.includes("name")) return 1
|
||||
return 0
|
||||
},
|
||||
sqlite: (columns: string[]): number => {
|
||||
if (columns.includes("name")) return 1
|
||||
return 0
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,139 @@
|
||||
import { mkdir, mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { expect, test } from "bun:test"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { Effect } from "effect"
|
||||
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
|
||||
import { EffectDrizzleSqlite } from "../src"
|
||||
|
||||
const users = sqliteTable("users", {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
name: text().notNull(),
|
||||
})
|
||||
|
||||
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
|
||||
Effect.runPromise(
|
||||
effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
|
||||
)
|
||||
|
||||
const makeDb = Effect.gen(function* () {
|
||||
const db = yield* EffectDrizzleSqlite.makeWithDefaults()
|
||||
yield* db.run(sql`create table users (id integer primary key autoincrement, name text not null)`)
|
||||
return db
|
||||
})
|
||||
|
||||
const createMigrationsFolder = async () => {
|
||||
const migrationsFolder = await mkdtemp(join(tmpdir(), "effect-drizzle-sqlite-"))
|
||||
await mkdir(join(migrationsFolder, "20240101000000_create_migrated_users"), { recursive: true })
|
||||
await Bun.write(
|
||||
join(migrationsFolder, "20240101000000_create_migrated_users", "migration.sql"),
|
||||
"create table migrated_users (id integer primary key autoincrement, name text not null);",
|
||||
)
|
||||
return migrationsFolder
|
||||
}
|
||||
|
||||
test("selects rows through Effect-yieldable query builders", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.insert(users).values({ name: "Ada" })
|
||||
|
||||
expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Ada" }])
|
||||
expect(yield* db.select({ id: users.id }).from(users).where(eq(users.name, "Ada")).get()).toEqual({ id: 1 })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("commits successful transactions", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
|
||||
yield* db.transaction((tx) => tx.insert(users).values({ name: "Grace" }), { behavior: "immediate" })
|
||||
|
||||
expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Grace" }])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("rolls back failed transactions", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
tx
|
||||
.insert(users)
|
||||
.values({ name: "Linus" })
|
||||
.pipe(Effect.andThen(Effect.fail("boom"))),
|
||||
)
|
||||
.pipe(Effect.ignore)
|
||||
|
||||
expect(yield* db.select().from(users)).toEqual([])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("rolls back explicit transaction rollback", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
tx
|
||||
.insert(users)
|
||||
.values({ name: "Barbara" })
|
||||
.pipe(Effect.andThen(Effect.fail(tx.rollback()))),
|
||||
)
|
||||
.pipe(Effect.ignore)
|
||||
|
||||
expect(yield* db.select().from(users)).toEqual([])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("supports returning and rejects empty update sets", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
|
||||
const inserted = yield* db.insert(users).values({ name: "Ada" }).returning({ id: users.id, name: users.name })
|
||||
expect(inserted).toEqual([{ id: 1, name: "Ada" }])
|
||||
|
||||
const updated = yield* db.update(users).set({ name: "Grace" }).where(eq(users.id, 1)).returning()
|
||||
expect(updated).toEqual([{ id: 1, name: "Grace" }])
|
||||
|
||||
const deleted = yield* db.delete(users).where(eq(users.id, 1)).returning({ id: users.id })
|
||||
expect(deleted).toEqual([{ id: 1 }])
|
||||
|
||||
expect(() => db.update(users).set({ name: undefined })).toThrow("No values to set")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("runs migrations once and records migration metadata", async () => {
|
||||
const migrationsFolder = await createMigrationsFolder()
|
||||
try {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* EffectDrizzleSqlite.makeWithDefaults()
|
||||
|
||||
yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder })
|
||||
yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder })
|
||||
yield* db.run(sql`insert into migrated_users (name) values ('Margaret')`)
|
||||
|
||||
expect(yield* db.all<{ name: string }>(sql`select name from migrated_users`)).toEqual([{ name: "Margaret" }])
|
||||
expect(yield* db.all<{ name: string | null }>(sql`select name from __drizzle_migrations`)).toEqual([
|
||||
{ name: "20240101000000_create_migrated_users" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
await rm(migrationsFolder, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@effect/language-service",
|
||||
"transform": "@effect/language-service/transform",
|
||||
"namespaceImportPackages": ["effect", "@effect/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -96,12 +96,14 @@
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/llm": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
|
||||
@@ -64,7 +64,6 @@ import { useTuiConfig } from "../../context/tui-config"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
workspaceID?: string
|
||||
visible?: boolean
|
||||
disabled?: boolean
|
||||
onSubmit?: () => void
|
||||
@@ -201,7 +200,6 @@ export function Prompt(props: PromptProps) {
|
||||
const [cursorVersion, setCursorVersion] = createSignal(0)
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
const defaultWorkspaceID = createMemo(() => props.workspaceID ?? project.workspace.current())
|
||||
|
||||
function selectWorkspace(selection: WorkspaceSelection | undefined) {
|
||||
setWorkspaceSelection(selection)
|
||||
@@ -1061,7 +1059,7 @@ export function Prompt(props: PromptProps) {
|
||||
if (sessionID == null) {
|
||||
const workspace = workspaceSelection()
|
||||
const workspaceID = iife(() => {
|
||||
if (!workspace) return defaultWorkspaceID()
|
||||
if (!workspace) return undefined
|
||||
if (workspace.type === "none") return undefined
|
||||
if (workspace.type === "existing") return workspace.workspaceID
|
||||
return undefined
|
||||
@@ -1422,17 +1420,7 @@ export function Prompt(props: PromptProps) {
|
||||
| undefined
|
||||
>(() => {
|
||||
const selected = workspaceSelection()
|
||||
if (!selected) {
|
||||
const workspaceID = defaultWorkspaceID()
|
||||
if (props.sessionID || !workspaceID) return
|
||||
const workspace = project.workspace.get(workspaceID)
|
||||
return {
|
||||
type: "existing",
|
||||
workspaceType: workspace?.type ?? "unknown",
|
||||
workspaceName: workspace?.name ?? workspaceID,
|
||||
status: project.workspace.status(workspaceID) ?? "error",
|
||||
}
|
||||
}
|
||||
if (!selected) return
|
||||
if (selected.type === "none") return
|
||||
if (props.sessionID && !workspaceCreating()) return
|
||||
if (selected.type === "new") {
|
||||
|
||||
@@ -277,7 +277,6 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
||||
return (
|
||||
<Prompt
|
||||
sessionID={props.sessionID}
|
||||
workspaceID={props.workspaceID}
|
||||
visible={props.visible}
|
||||
disabled={props.disabled}
|
||||
onSubmit={props.onSubmit}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import { createEffect, createSignal, onMount } from "solid-js"
|
||||
import { Logo } from "../component/logo"
|
||||
import { useProject } from "../context/project"
|
||||
import { useSync } from "../context/sync"
|
||||
import { Toast } from "../ui/toast"
|
||||
import { useArgs } from "../context/args"
|
||||
@@ -19,7 +18,6 @@ const placeholder = {
|
||||
|
||||
export function Home() {
|
||||
const sync = useSync()
|
||||
const project = useProject()
|
||||
const route = useRouteData("home")
|
||||
const promptRef = usePromptRef()
|
||||
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
||||
@@ -70,18 +68,8 @@ export function Home() {
|
||||
</box>
|
||||
<box height={1} minHeight={0} flexShrink={1} />
|
||||
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
|
||||
<TuiPluginRuntime.Slot
|
||||
name="home_prompt"
|
||||
mode="replace"
|
||||
workspace_id={project.workspace.current()}
|
||||
ref={bind}
|
||||
>
|
||||
<Prompt
|
||||
ref={bind}
|
||||
workspaceID={project.workspace.current()}
|
||||
right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={project.workspace.current()} />}
|
||||
placeholders={placeholder}
|
||||
/>
|
||||
<TuiPluginRuntime.Slot name="home_prompt" mode="replace" ref={bind}>
|
||||
<Prompt ref={bind} right={<TuiPluginRuntime.Slot name="home_prompt_right" />} placeholders={placeholder} />
|
||||
</TuiPluginRuntime.Slot>
|
||||
</box>
|
||||
<TuiPluginRuntime.Slot name="home_bottom" />
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { SessionMessageTable, SessionTable } from "@/session/session.sql"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db"
|
||||
import * as Database from "@/storage/db"
|
||||
import { Context, DateTime, Effect, Layer, Option, Schema } from "effect"
|
||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import type { Prompt } from "@opencode-ai/core/session-prompt"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
@@ -13,7 +10,8 @@ import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { SessionStorage } from "./storage/session"
|
||||
import { SessionStorageSql } from "./storage/session-sql"
|
||||
|
||||
export const Delivery = Schema.Literals(["immediate", "deferred"]).annotate({
|
||||
identifier: "Session.Delivery",
|
||||
@@ -73,40 +71,17 @@ export interface Interface {
|
||||
workspaceID?: WorkspaceID
|
||||
}) => Effect.Effect<Info>
|
||||
readonly get: (sessionID: SessionID) => Effect.Effect<Info, NotFoundError>
|
||||
readonly list: (input: {
|
||||
limit?: number
|
||||
order?: "asc" | "desc"
|
||||
directory?: string
|
||||
path?: string
|
||||
workspaceID?: WorkspaceID
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
cursor?: {
|
||||
id: SessionID
|
||||
time: number
|
||||
direction: "previous" | "next"
|
||||
}
|
||||
}) => Effect.Effect<Info[], never>
|
||||
readonly messages: (input: {
|
||||
sessionID: SessionID
|
||||
limit?: number
|
||||
order?: "asc" | "desc"
|
||||
cursor?: {
|
||||
id: SessionMessage.ID
|
||||
time: number
|
||||
direction: "previous" | "next"
|
||||
}
|
||||
}) => Effect.Effect<SessionMessage.Message[], never>
|
||||
readonly context: (sessionID: SessionID) => Effect.Effect<SessionMessage.Message[], never>
|
||||
readonly list: (input: SessionStorage.SessionListInput) => Effect.Effect<Info[]>
|
||||
readonly messages: (input: SessionStorage.MessageListInput) => Effect.Effect<SessionMessage.Message[]>
|
||||
readonly context: (sessionID: SessionID) => Effect.Effect<SessionMessage.Message[]>
|
||||
readonly prompt: (input: {
|
||||
id?: EventV2.ID
|
||||
sessionID: SessionID
|
||||
prompt: Prompt
|
||||
delivery?: Delivery
|
||||
}) => Effect.Effect<SessionMessage.User, never>
|
||||
readonly shell: (input: { id?: EventV2.ID; sessionID: SessionID; command: string }) => Effect.Effect<void, never>
|
||||
readonly skill: (input: { id?: EventV2.ID; sessionID: SessionID; skill: string }) => Effect.Effect<void, never>
|
||||
}) => Effect.Effect<SessionMessage.User>
|
||||
readonly shell: (input: { id?: EventV2.ID; sessionID: SessionID; command: string }) => Effect.Effect<void>
|
||||
readonly skill: (input: { id?: EventV2.ID; sessionID: SessionID; skill: string }) => Effect.Effect<void>
|
||||
readonly subagent: (input: {
|
||||
id?: EventV2.ID
|
||||
parentID: SessionID
|
||||
@@ -114,10 +89,10 @@ export interface Interface {
|
||||
agent: string
|
||||
model?: ModelV2.Ref
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect<void, never>
|
||||
readonly switchModel: (input: { sessionID: SessionID; model: ModelV2.Ref }) => Effect.Effect<void, never>
|
||||
readonly compact: (sessionID: SessionID) => Effect.Effect<void, never>
|
||||
readonly wait: (sessionID: SessionID) => Effect.Effect<void, never>
|
||||
readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect<void>
|
||||
readonly switchModel: (input: { sessionID: SessionID; model: ModelV2.Ref }) => Effect.Effect<void>
|
||||
readonly compact: (sessionID: SessionID) => Effect.Effect<void>
|
||||
readonly wait: (sessionID: SessionID) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
|
||||
@@ -126,170 +101,28 @@ export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
||||
|
||||
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
|
||||
function fromRow(row: typeof SessionTable.$inferSelect): Info {
|
||||
return new Info({
|
||||
id: SessionID.make(row.id),
|
||||
projectID: ProjectID.make(row.project_id),
|
||||
workspaceID: row.workspace_id ? WorkspaceID.make(row.workspace_id) : undefined,
|
||||
title: row.title,
|
||||
parentID: row.parent_id ? SessionID.make(row.parent_id) : undefined,
|
||||
path: row.path ?? "",
|
||||
agent: row.agent ?? undefined,
|
||||
model: row.model
|
||||
? {
|
||||
id: ModelV2.ID.make(row.model.id),
|
||||
providerID: ProviderV2.ID.make(row.model.providerID),
|
||||
variant: ModelV2.VariantID.make(row.model.variant ?? "default"),
|
||||
}
|
||||
: undefined,
|
||||
cost: row.cost,
|
||||
tokens: {
|
||||
input: row.tokens_input,
|
||||
output: row.tokens_output,
|
||||
reasoning: row.tokens_reasoning,
|
||||
cache: {
|
||||
read: row.tokens_cache_read,
|
||||
write: row.tokens_cache_write,
|
||||
},
|
||||
},
|
||||
time: {
|
||||
created: DateTime.makeUnsafe(row.time_created),
|
||||
updated: DateTime.makeUnsafe(row.time_updated),
|
||||
archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
const storage = yield* SessionStorage.Service
|
||||
|
||||
const result = Service.of({
|
||||
create: Effect.fn("V2Session.create")(function* (_input) {
|
||||
return {} as any
|
||||
return yield* Effect.die(new Error("V2Session.create is not implemented"))
|
||||
}),
|
||||
get: Effect.fn("V2Session.get")(function* (sessionID) {
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get())
|
||||
const row = yield* storage.get(sessionID).pipe(Effect.orDie)
|
||||
if (!row) return yield* new NotFoundError({ sessionID })
|
||||
return fromRow(row)
|
||||
return new Info(row)
|
||||
}),
|
||||
list: Effect.fn("V2Session.list")(function* (input) {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
let order = input.order ?? "desc"
|
||||
// This is a load bearing sort, desktop relies on this
|
||||
const sortColumn = SessionTable.time_updated
|
||||
// Query the adjacent rows in reverse, then flip them back into the requested order below.
|
||||
if (direction === "previous" && order === "asc") order = "desc"
|
||||
if (direction === "previous" && order === "desc") order = "asc"
|
||||
const conditions: SQL[] = []
|
||||
if (input.directory) conditions.push(eq(SessionTable.directory, input.directory))
|
||||
if (input.path)
|
||||
conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!)
|
||||
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
||||
if (input.roots) conditions.push(isNull(SessionTable.parent_id))
|
||||
if (input.start) conditions.push(gte(sortColumn, input.start))
|
||||
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||
if (input.cursor) {
|
||||
conditions.push(
|
||||
order === "asc"
|
||||
? or(
|
||||
gt(sortColumn, input.cursor.time),
|
||||
and(eq(sortColumn, input.cursor.time), gt(SessionTable.id, input.cursor.id)),
|
||||
)!
|
||||
: or(
|
||||
lt(sortColumn, input.cursor.time),
|
||||
and(eq(sortColumn, input.cursor.time), lt(SessionTable.id, input.cursor.id)),
|
||||
)!,
|
||||
)
|
||||
}
|
||||
const query = Database.Client()
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(
|
||||
order === "asc" ? asc(sortColumn) : desc(sortColumn),
|
||||
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
|
||||
)
|
||||
|
||||
const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all()
|
||||
return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
|
||||
return (yield* storage.list(input).pipe(Effect.orDie)).map((row) => new Info(row))
|
||||
}),
|
||||
messages: Effect.fn("V2Session.messages")(function* (input) {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
let order = input.order ?? "desc"
|
||||
// Query the adjacent rows in reverse, then flip them back into the requested order below.
|
||||
if (direction === "previous" && order === "asc") order = "desc"
|
||||
if (direction === "previous" && order === "desc") order = "asc"
|
||||
const boundary = input.cursor
|
||||
? order === "asc"
|
||||
? or(
|
||||
gt(SessionMessageTable.time_created, input.cursor.time),
|
||||
and(
|
||||
eq(SessionMessageTable.time_created, input.cursor.time),
|
||||
gt(SessionMessageTable.id, input.cursor.id),
|
||||
),
|
||||
)
|
||||
: or(
|
||||
lt(SessionMessageTable.time_created, input.cursor.time),
|
||||
and(
|
||||
eq(SessionMessageTable.time_created, input.cursor.time),
|
||||
lt(SessionMessageTable.id, input.cursor.id),
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
const where = boundary
|
||||
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
|
||||
: eq(SessionMessageTable.session_id, input.sessionID)
|
||||
|
||||
const rows = Database.use((db) => {
|
||||
const query = db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(where)
|
||||
.orderBy(
|
||||
order === "asc" ? asc(SessionMessageTable.time_created) : desc(SessionMessageTable.time_created),
|
||||
order === "asc" ? asc(SessionMessageTable.id) : desc(SessionMessageTable.id),
|
||||
)
|
||||
const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all()
|
||||
return direction === "previous" ? rows.toReversed() : rows
|
||||
})
|
||||
return rows.map((row) => decode(row))
|
||||
return yield* storage.messages(input).pipe(Effect.orDie)
|
||||
}),
|
||||
context: Effect.fn("V2Session.context")(function* (sessionID) {
|
||||
const rows = Database.use((db) => {
|
||||
const compaction = db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction")))
|
||||
.orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id))
|
||||
.limit(1)
|
||||
.get()
|
||||
|
||||
return db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, sessionID),
|
||||
compaction
|
||||
? or(
|
||||
gt(SessionMessageTable.time_created, compaction.time_created),
|
||||
and(
|
||||
eq(SessionMessageTable.time_created, compaction.time_created),
|
||||
gte(SessionMessageTable.id, compaction.id),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id))
|
||||
.all()
|
||||
})
|
||||
return rows.map((row) => decode(row))
|
||||
return yield* storage.context(sessionID).pipe(Effect.orDie)
|
||||
}),
|
||||
prompt: Effect.fn("V2Session.prompt")(function* (_input) {
|
||||
return {} as any
|
||||
return yield* Effect.die(new Error("V2Session.prompt is not implemented"))
|
||||
}),
|
||||
shell: Effect.fn("V2Session.shell")(function* (_input) {}),
|
||||
skill: Effect.fn("V2Session.skill")(function* (_input) {}),
|
||||
@@ -336,6 +169,8 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Layer.mergeAll(EventV2Bridge.defaultLayer, SessionStorageSql.defaultLayer)),
|
||||
)
|
||||
|
||||
export * as SessionV2 from "./session"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Database as LegacyDatabase } from "@/storage/db"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-bun"
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
|
||||
export class Service extends Context.Service<Service, DatabaseShape>()("@opencode/v2/storage/Database") {}
|
||||
|
||||
export const layerForPath = (filename: string) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDatabase
|
||||
yield* db.run("PRAGMA journal_mode = WAL")
|
||||
yield* db.run("PRAGMA synchronous = NORMAL")
|
||||
yield* db.run("PRAGMA busy_timeout = 5000")
|
||||
yield* db.run("PRAGMA cache_size = -64000")
|
||||
yield* db.run("PRAGMA foreign_keys = ON")
|
||||
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
return db
|
||||
}),
|
||||
).pipe(Layer.provide(SqliteClient.layer({ filename })))
|
||||
|
||||
export const layer = Layer.unwrap(
|
||||
Effect.sync(() => {
|
||||
// TODO: Extract migration/bootstrap from the legacy Database.Client() so V2 storage
|
||||
// can ensure the schema exists without opening the old global Drizzle connection.
|
||||
LegacyDatabase.Client()
|
||||
return layerForPath(LegacyDatabase.getPath())
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
export * as StorageDatabase from "./database"
|
||||
@@ -0,0 +1,101 @@
|
||||
import { DateTime, Effect, Layer } from "effect"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { SessionStorage } from "./session"
|
||||
|
||||
export const make = (state: {
|
||||
readonly sessions: Map<string, SessionStorage.SessionRow>
|
||||
readonly messages: Map<string, SessionMessage.Message[]>
|
||||
}) =>
|
||||
SessionStorage.Service.of({
|
||||
get: (sessionID) => Effect.sync(() => state.sessions.get(sessionID)),
|
||||
list: (input) =>
|
||||
Effect.sync(() => {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
|
||||
const rows = Array.from(state.sessions.values())
|
||||
.filter((row) => {
|
||||
if (input.directory && row.directory !== input.directory) return false
|
||||
if (input.path && row.path !== input.path && !row.path?.startsWith(`${input.path}/`)) return false
|
||||
if (input.workspaceID && row.workspaceID !== input.workspaceID) return false
|
||||
if (input.roots && row.parentID) return false
|
||||
if (input.start && DateTime.toEpochMillis(row.time.updated) < input.start) return false
|
||||
if (input.search && !row.title.includes(input.search)) return false
|
||||
if (!input.cursor) return true
|
||||
return compareCursor(row.id, DateTime.toEpochMillis(row.time.updated), input.cursor, order)
|
||||
})
|
||||
.toSorted((a, b) =>
|
||||
compareRows(
|
||||
a.id,
|
||||
DateTime.toEpochMillis(a.time.updated),
|
||||
b.id,
|
||||
DateTime.toEpochMillis(b.time.updated),
|
||||
order,
|
||||
),
|
||||
)
|
||||
const limited = input.limit === undefined ? rows : rows.slice(0, input.limit)
|
||||
return direction === "previous" ? limited.toReversed() : limited
|
||||
}),
|
||||
messages: (input) =>
|
||||
Effect.sync(() => {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
|
||||
const rows = (state.messages.get(input.sessionID) ?? [])
|
||||
.filter((message) => {
|
||||
if (!input.cursor) return true
|
||||
return compareCursor(message.id, DateTime.toEpochMillis(message.time.created), input.cursor, order)
|
||||
})
|
||||
.toSorted((a, b) =>
|
||||
compareRows(
|
||||
a.id,
|
||||
DateTime.toEpochMillis(a.time.created),
|
||||
b.id,
|
||||
DateTime.toEpochMillis(b.time.created),
|
||||
order,
|
||||
),
|
||||
)
|
||||
const limited = input.limit === undefined ? rows : rows.slice(0, input.limit)
|
||||
return direction === "previous" ? limited.toReversed() : limited
|
||||
}),
|
||||
context: (sessionID) =>
|
||||
Effect.sync(() => {
|
||||
const messages = (state.messages.get(sessionID) ?? []).toSorted((a, b) =>
|
||||
compareRows(
|
||||
a.id,
|
||||
DateTime.toEpochMillis(a.time.created),
|
||||
b.id,
|
||||
DateTime.toEpochMillis(b.time.created),
|
||||
"asc",
|
||||
),
|
||||
)
|
||||
const index = messages.findLastIndex((message) => message.type === "compaction")
|
||||
return index === -1 ? messages : messages.slice(index)
|
||||
}),
|
||||
})
|
||||
|
||||
const storageLayer = Layer.sync(SessionStorage.Service, () => {
|
||||
return make({
|
||||
sessions: new Map(),
|
||||
messages: new Map(),
|
||||
})
|
||||
})
|
||||
|
||||
export const layer = storageLayer
|
||||
|
||||
export const defaultLayer = layer
|
||||
|
||||
function compareCursor(
|
||||
id: string,
|
||||
time: number,
|
||||
cursor: { readonly id: string; readonly time: number },
|
||||
order: SessionStorage.SortOrder,
|
||||
) {
|
||||
if (order === "asc") return time > cursor.time || (time === cursor.time && id > cursor.id)
|
||||
return time < cursor.time || (time === cursor.time && id < cursor.id)
|
||||
}
|
||||
|
||||
function compareRows(aID: string, aTime: number, bID: string, bTime: number, order: SessionStorage.SortOrder) {
|
||||
const result = aTime === bTime ? aID.localeCompare(bID) : aTime - bTime
|
||||
return order === "asc" ? result : -result
|
||||
}
|
||||
|
||||
export * as SessionStorageMemory from "./session-memory"
|
||||
@@ -0,0 +1,164 @@
|
||||
import { SessionMessageTable, SessionTable } from "@/session/session.sql"
|
||||
import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { StorageDatabase } from "./database"
|
||||
import { SessionStorage } from "./session"
|
||||
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
||||
const decodeSessionRow = Schema.decodeUnknownSync(SessionStorage.SessionRow)
|
||||
const mapStorageError = Effect.mapError(
|
||||
(cause) => new SessionStorage.StorageError({ message: "Session storage SQL operation failed", cause }),
|
||||
)
|
||||
|
||||
export const layer = Layer.effect(
|
||||
SessionStorage.Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* StorageDatabase.Service
|
||||
|
||||
const get: SessionStorage.Interface["get"] = Effect.fn("SessionStorageSql.get")(function* (sessionID) {
|
||||
const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()
|
||||
return row ? fromSessionRow(row) : undefined
|
||||
}, mapStorageError)
|
||||
|
||||
const list: SessionStorage.Interface["list"] = Effect.fn("SessionStorageSql.list")(function* (input) {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
|
||||
const sortColumn = SessionTable.time_updated
|
||||
const conditions: SQL[] = []
|
||||
if (input.directory) conditions.push(eq(SessionTable.directory, input.directory))
|
||||
if (input.path)
|
||||
conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!)
|
||||
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
||||
if (input.roots) conditions.push(isNull(SessionTable.parent_id))
|
||||
if (input.start) conditions.push(gte(sortColumn, input.start))
|
||||
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||
if (input.cursor) conditions.push(sessionCursorBoundary(input.cursor, order))
|
||||
|
||||
const query = db
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(
|
||||
order === "asc" ? asc(sortColumn) : desc(sortColumn),
|
||||
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
|
||||
)
|
||||
const rows = yield* input.limit === undefined ? query : query.limit(input.limit)
|
||||
return (direction === "previous" ? rows.toReversed() : rows).map(fromSessionRow)
|
||||
}, mapStorageError)
|
||||
|
||||
const messages: SessionStorage.Interface["messages"] = Effect.fn("SessionStorageSql.messages")(function* (input) {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
|
||||
const boundary = input.cursor ? messageCursorBoundary(input.cursor, order) : undefined
|
||||
const where = boundary
|
||||
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
|
||||
: eq(SessionMessageTable.session_id, input.sessionID)
|
||||
|
||||
const query = db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(where)
|
||||
.orderBy(
|
||||
order === "asc" ? asc(SessionMessageTable.time_created) : desc(SessionMessageTable.time_created),
|
||||
order === "asc" ? asc(SessionMessageTable.id) : desc(SessionMessageTable.id),
|
||||
)
|
||||
const rows = yield* input.limit === undefined ? query : query.limit(input.limit)
|
||||
return (direction === "previous" ? rows.toReversed() : rows).map((row) =>
|
||||
decodeMessage({ ...row.data, id: row.id, type: row.type }),
|
||||
)
|
||||
}, mapStorageError)
|
||||
|
||||
const context: SessionStorage.Interface["context"] = Effect.fn("SessionStorageSql.context")((sessionID) =>
|
||||
Effect.gen(function* () {
|
||||
const compaction = yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction")))
|
||||
.orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id))
|
||||
.limit(1)
|
||||
.get()
|
||||
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, sessionID),
|
||||
compaction
|
||||
? or(
|
||||
gt(SessionMessageTable.time_created, compaction.time_created),
|
||||
and(
|
||||
eq(SessionMessageTable.time_created, compaction.time_created),
|
||||
gte(SessionMessageTable.id, compaction.id),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id))
|
||||
|
||||
return rows.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type }))
|
||||
}).pipe(mapStorageError),
|
||||
)
|
||||
|
||||
return SessionStorage.Service.of({ get, list, messages, context })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(StorageDatabase.defaultLayer.pipe(Layer.orDie)))
|
||||
|
||||
function sessionCursorBoundary(cursor: SessionStorage.SessionCursor, order: SessionStorage.SortOrder) {
|
||||
if (order === "asc")
|
||||
return or(
|
||||
gt(SessionTable.time_updated, cursor.time),
|
||||
and(eq(SessionTable.time_updated, cursor.time), gt(SessionTable.id, cursor.id)),
|
||||
)!
|
||||
return or(
|
||||
lt(SessionTable.time_updated, cursor.time),
|
||||
and(eq(SessionTable.time_updated, cursor.time), lt(SessionTable.id, cursor.id)),
|
||||
)!
|
||||
}
|
||||
|
||||
function messageCursorBoundary(cursor: SessionStorage.MessageCursor, order: SessionStorage.SortOrder) {
|
||||
if (order === "asc")
|
||||
return or(
|
||||
gt(SessionMessageTable.time_created, cursor.time),
|
||||
and(eq(SessionMessageTable.time_created, cursor.time), gt(SessionMessageTable.id, cursor.id)),
|
||||
)!
|
||||
return or(
|
||||
lt(SessionMessageTable.time_created, cursor.time),
|
||||
and(eq(SessionMessageTable.time_created, cursor.time), lt(SessionMessageTable.id, cursor.id)),
|
||||
)!
|
||||
}
|
||||
|
||||
function fromSessionRow(row: typeof SessionTable.$inferSelect) {
|
||||
return decodeSessionRow({
|
||||
id: row.id,
|
||||
parentID: row.parent_id ?? undefined,
|
||||
projectID: row.project_id,
|
||||
workspaceID: row.workspace_id ?? undefined,
|
||||
directory: row.directory,
|
||||
title: row.title,
|
||||
path: row.path ?? "",
|
||||
agent: row.agent ?? undefined,
|
||||
model: row.model ? { ...row.model, variant: row.model.variant ?? "default" } : undefined,
|
||||
cost: row.cost,
|
||||
tokens: {
|
||||
input: row.tokens_input,
|
||||
output: row.tokens_output,
|
||||
reasoning: row.tokens_reasoning,
|
||||
cache: {
|
||||
read: row.tokens_cache_read,
|
||||
write: row.tokens_cache_write,
|
||||
},
|
||||
},
|
||||
time: {
|
||||
created: row.time_created,
|
||||
updated: row.time_updated,
|
||||
archived: row.time_archived ?? undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export * as SessionStorageSql from "./session-sql"
|
||||
@@ -0,0 +1,100 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { V2Schema } from "@opencode-ai/core/v2-schema"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Context, Effect, Schema } from "effect"
|
||||
|
||||
export const SortOrder = Schema.Literals(["asc", "desc"]).annotate({
|
||||
identifier: "SortOrder",
|
||||
})
|
||||
export type SortOrder = typeof SortOrder.Type
|
||||
|
||||
export const PageDirection = Schema.Literals(["previous", "next"]).annotate({
|
||||
identifier: "PageDirection",
|
||||
})
|
||||
export type PageDirection = typeof PageDirection.Type
|
||||
|
||||
export class StorageError extends Schema.TaggedErrorClass<StorageError>()("StorageError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.Defect,
|
||||
}) {}
|
||||
|
||||
export class SessionRow extends Schema.Class<SessionRow>("SessionRow")({
|
||||
id: SessionID,
|
||||
parentID: Schema.optional(SessionID),
|
||||
projectID: ProjectID,
|
||||
workspaceID: Schema.optional(WorkspaceID),
|
||||
directory: Schema.optional(Schema.String),
|
||||
path: Schema.optional(Schema.String),
|
||||
agent: Schema.optional(Schema.String),
|
||||
model: Schema.optional(ModelV2.Ref),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
time: Schema.Struct({
|
||||
created: V2Schema.DateTimeUtcFromMillis,
|
||||
updated: V2Schema.DateTimeUtcFromMillis,
|
||||
archived: Schema.optional(V2Schema.DateTimeUtcFromMillis),
|
||||
}),
|
||||
title: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const SessionCursor = Schema.Struct({
|
||||
id: SessionID,
|
||||
time: Schema.Finite,
|
||||
direction: PageDirection,
|
||||
}).annotate({ identifier: "SessionCursor" })
|
||||
export type SessionCursor = typeof SessionCursor.Type
|
||||
|
||||
export const SessionListInput = Schema.Struct({
|
||||
limit: Schema.optional(Schema.Finite),
|
||||
order: Schema.optional(SortOrder),
|
||||
directory: Schema.optional(Schema.String),
|
||||
path: Schema.optional(Schema.String),
|
||||
workspaceID: Schema.optional(WorkspaceID),
|
||||
roots: Schema.optional(Schema.Boolean),
|
||||
start: Schema.optional(Schema.Finite),
|
||||
search: Schema.optional(Schema.String),
|
||||
cursor: Schema.optional(SessionCursor),
|
||||
}).annotate({ identifier: "SessionListInput" })
|
||||
export type SessionListInput = typeof SessionListInput.Type
|
||||
|
||||
export const MessageCursor = Schema.Struct({
|
||||
id: SessionMessage.ID,
|
||||
time: Schema.Finite,
|
||||
direction: PageDirection,
|
||||
}).annotate({ identifier: "MessageCursor" })
|
||||
export type MessageCursor = typeof MessageCursor.Type
|
||||
|
||||
export const MessageListInput = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
limit: Schema.optional(Schema.Finite),
|
||||
order: Schema.optional(SortOrder),
|
||||
cursor: Schema.optional(MessageCursor),
|
||||
}).annotate({ identifier: "MessageListInput" })
|
||||
export type MessageListInput = typeof MessageListInput.Type
|
||||
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: SessionID) => Effect.Effect<SessionRow | undefined, StorageError>
|
||||
readonly list: (input: SessionListInput) => Effect.Effect<SessionRow[], StorageError>
|
||||
readonly messages: (input: MessageListInput) => Effect.Effect<SessionMessage.Message[], StorageError>
|
||||
readonly context: (sessionID: SessionID) => Effect.Effect<SessionMessage.Message[], StorageError>
|
||||
}
|
||||
|
||||
export function pageOrder(order: SortOrder, direction: PageDirection) {
|
||||
if (direction !== "previous") return order
|
||||
return order === "asc" ? "desc" : "asc"
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/session/Storage") {}
|
||||
|
||||
export * as SessionStorage from "./session"
|
||||
@@ -36,6 +36,7 @@ import { SessionRunState } from "../../src/session/run-state"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionV2 } from "../../src/v2/session"
|
||||
import { SessionStorageSql } from "../../src/v2/storage/session-sql"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { Shell } from "../../src/shell/shell"
|
||||
@@ -507,6 +508,7 @@ noLLMServer.instance(
|
||||
|
||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||
Effect.provide(SessionV2.layer),
|
||||
Effect.provide(SessionStorageSql.defaultLayer),
|
||||
)
|
||||
const row = Database.use((db) =>
|
||||
db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(),
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectTable } from "@/project/project.sql"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionMessageTable, SessionTable } from "@/session/session.sql"
|
||||
import { SessionStorage } from "@/v2/storage/session"
|
||||
import { StorageDatabase } from "@/v2/storage/database"
|
||||
import { SessionStorageMemory } from "@/v2/storage/session-memory"
|
||||
import { SessionStorageSql } from "@/v2/storage/session-sql"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||
import { eq, or } from "@/storage/db"
|
||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const projectID = ProjectID.make("project-session-storage")
|
||||
const sessionA = SessionID.make("ses_storage_a")
|
||||
const sessionB = SessionID.make("ses_storage_b")
|
||||
const sessionC = SessionID.make("ses_storage_c")
|
||||
const sessionD = SessionID.make("ses_storage_d")
|
||||
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
||||
const memoryState = {
|
||||
sessions: new Map(),
|
||||
messages: new Map(),
|
||||
}
|
||||
const memoryLayer = Layer.sync(SessionStorage.Service, () => SessionStorageMemory.make(memoryState))
|
||||
|
||||
interface Seeds<R> {
|
||||
readonly reset: Effect.Effect<void, never, R>
|
||||
readonly project: Effect.Effect<void, never, R>
|
||||
readonly session: (input: {
|
||||
id: SessionID
|
||||
title: string
|
||||
directory?: string
|
||||
path: string
|
||||
updated: number
|
||||
}) => Effect.Effect<void, never, R>
|
||||
readonly userMessage: (input: { id: SessionMessage.ID; text: string; time: number }) => Effect.Effect<void, never, R>
|
||||
readonly compaction: (input: {
|
||||
id: SessionMessage.ID
|
||||
summary: string
|
||||
time: number
|
||||
}) => Effect.Effect<void, never, R>
|
||||
}
|
||||
|
||||
function sessionStorageContract<R, E>(name: string, layer: Layer.Layer<SessionStorage.Service | R, E>, seed: Seeds<R>) {
|
||||
const it = testEffect(layer)
|
||||
|
||||
const setup = Effect.gen(function* () {
|
||||
yield* seed.reset
|
||||
yield* Effect.addFinalizer(() => seed.reset)
|
||||
yield* seed.project
|
||||
})
|
||||
|
||||
it.effect(`${name}: gets and lists sessions with filters and cursors`, () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
yield* seed.session({
|
||||
id: sessionA,
|
||||
title: "Alpha",
|
||||
directory: "/tmp/project-session-storage",
|
||||
path: "apps/api",
|
||||
updated: 1000,
|
||||
})
|
||||
yield* seed.session({
|
||||
id: sessionB,
|
||||
title: "Beta",
|
||||
directory: "/tmp/project-session-storage",
|
||||
path: "apps/web",
|
||||
updated: 2000,
|
||||
})
|
||||
yield* seed.session({
|
||||
id: sessionC,
|
||||
title: "Gamma",
|
||||
path: "docs",
|
||||
updated: 3000,
|
||||
})
|
||||
yield* seed.session({
|
||||
id: sessionD,
|
||||
title: "Delta",
|
||||
directory: "/tmp/other-project",
|
||||
path: "other",
|
||||
updated: 4000,
|
||||
})
|
||||
|
||||
const storage = yield* SessionStorage.Service
|
||||
const found = yield* storage.get(sessionB)
|
||||
expect(found?.title).toBe("Beta")
|
||||
expect(found ? DateTime.toEpochMillis(found.time.updated) : undefined).toBe(2000)
|
||||
|
||||
expect(
|
||||
(yield* storage.list({ directory: "/tmp/project-session-storage", path: "apps", order: "asc" })).map(
|
||||
(row) => row.id,
|
||||
),
|
||||
).toEqual([sessionA, sessionB])
|
||||
expect(
|
||||
(yield* storage.list({ directory: "/tmp/project-session-storage", order: "asc" })).map((row) => row.id),
|
||||
).toEqual([sessionA, sessionB, sessionC])
|
||||
expect(
|
||||
(yield* storage.list({
|
||||
directory: "/tmp/project-session-storage",
|
||||
order: "asc",
|
||||
cursor: { id: sessionA, time: 1000, direction: "next" },
|
||||
})).map((row) => row.id),
|
||||
).toEqual([sessionB, sessionC])
|
||||
expect(
|
||||
(yield* storage.list({
|
||||
directory: "/tmp/project-session-storage",
|
||||
order: "asc",
|
||||
cursor: { id: sessionC, time: 3000, direction: "previous" },
|
||||
})).map((row) => row.id),
|
||||
).toEqual([sessionA, sessionB])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect(`${name}: lists session messages with cursor direction`, () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
yield* seed.session({ id: sessionA, title: "Alpha", path: "apps/api", updated: 1000 })
|
||||
yield* seed.userMessage({ id: EventV2.ID.make("evt_msg_1"), text: "one", time: 1000 })
|
||||
yield* seed.userMessage({ id: EventV2.ID.make("evt_msg_2"), text: "two", time: 2000 })
|
||||
yield* seed.userMessage({ id: EventV2.ID.make("evt_msg_3"), text: "three", time: 3000 })
|
||||
|
||||
const storage = yield* SessionStorage.Service
|
||||
|
||||
expect((yield* storage.messages({ sessionID: sessionA, order: "asc", limit: 2 })).map((row) => row.id)).toEqual([
|
||||
EventV2.ID.make("evt_msg_1"),
|
||||
EventV2.ID.make("evt_msg_2"),
|
||||
])
|
||||
expect(
|
||||
(yield* storage.messages({
|
||||
sessionID: sessionA,
|
||||
order: "asc",
|
||||
cursor: { id: EventV2.ID.make("evt_msg_3"), time: 3000, direction: "previous" },
|
||||
})).map((row) => row.id),
|
||||
).toEqual([EventV2.ID.make("evt_msg_1"), EventV2.ID.make("evt_msg_2")])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect(`${name}: returns context from the latest compaction boundary`, () =>
|
||||
Effect.gen(function* () {
|
||||
yield* setup
|
||||
yield* seed.session({ id: sessionA, title: "Alpha", path: "apps/api", updated: 1000 })
|
||||
yield* seed.userMessage({ id: EventV2.ID.make("evt_context_1"), text: "before", time: 1000 })
|
||||
yield* seed.compaction({ id: EventV2.ID.make("evt_context_2"), summary: "compact", time: 2000 })
|
||||
yield* seed.userMessage({ id: EventV2.ID.make("evt_context_3"), text: "after", time: 3000 })
|
||||
|
||||
const storage = yield* SessionStorage.Service
|
||||
const context = yield* storage.context(sessionA)
|
||||
|
||||
expect(context.map((message) => message.id)).toEqual([
|
||||
EventV2.ID.make("evt_context_2"),
|
||||
EventV2.ID.make("evt_context_3"),
|
||||
])
|
||||
expect(context.map((message) => message.type)).toEqual(["compaction", "user"])
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const testDatabaseLayer = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||
)
|
||||
return Layer.effect(
|
||||
StorageDatabase.Service,
|
||||
Effect.gen(function* () {
|
||||
const db = yield* StorageDatabase.Service
|
||||
yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: path.join(import.meta.dirname, "../../migration") })
|
||||
return db
|
||||
}),
|
||||
).pipe(Layer.provide(StorageDatabase.layerForPath(path.join(dir.path, "storage.db"))))
|
||||
}),
|
||||
)
|
||||
|
||||
const sqlLayer = SessionStorageSql.layer.pipe(Layer.provideMerge(testDatabaseLayer))
|
||||
|
||||
const sqlSeeds: Seeds<StorageDatabase.Service> = {
|
||||
reset: resetSqlSeeds(),
|
||||
project: seedProject(),
|
||||
session: seedSession,
|
||||
userMessage: seedUserMessage,
|
||||
compaction: seedCompaction,
|
||||
}
|
||||
|
||||
sessionStorageContract("SessionStorageSql", sqlLayer, sqlSeeds)
|
||||
|
||||
const memorySeeds: Seeds<never> = {
|
||||
reset: Effect.sync(() => {
|
||||
memoryState.sessions.clear()
|
||||
memoryState.messages.clear()
|
||||
}),
|
||||
project: Effect.void,
|
||||
session: (input) =>
|
||||
Effect.sync(() => {
|
||||
memoryState.sessions.set(input.id, makeSessionRow(input))
|
||||
}),
|
||||
userMessage: (input) =>
|
||||
Effect.sync(() => {
|
||||
appendMemoryMessage(makeUserMessage(input))
|
||||
}),
|
||||
compaction: (input) =>
|
||||
Effect.sync(() => {
|
||||
appendMemoryMessage(makeCompaction(input))
|
||||
}),
|
||||
}
|
||||
|
||||
sessionStorageContract("SessionStorageMemory", memoryLayer, memorySeeds)
|
||||
|
||||
function seedProject() {
|
||||
return Effect.gen(function* () {
|
||||
const db = yield* StorageDatabase.Service
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({
|
||||
id: projectID,
|
||||
worktree: "/tmp/project-session-storage",
|
||||
time_created: 1,
|
||||
time_updated: 1,
|
||||
sandboxes: [],
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
}
|
||||
|
||||
function resetSqlSeeds() {
|
||||
return Effect.gen(function* () {
|
||||
const db = yield* StorageDatabase.Service
|
||||
yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, sessionA)).run().pipe(Effect.orDie)
|
||||
yield* db
|
||||
.delete(SessionTable)
|
||||
.where(
|
||||
or(
|
||||
eq(SessionTable.id, sessionA),
|
||||
eq(SessionTable.id, sessionB),
|
||||
eq(SessionTable.id, sessionC),
|
||||
eq(SessionTable.id, sessionD),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db.delete(ProjectTable).where(eq(ProjectTable.id, projectID)).run().pipe(Effect.orDie)
|
||||
})
|
||||
}
|
||||
|
||||
function seedSession(input: { id: SessionID; title: string; directory?: string; path: string; updated: number }) {
|
||||
return Effect.gen(function* () {
|
||||
const db = yield* StorageDatabase.Service
|
||||
yield* db
|
||||
.insert(SessionTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
project_id: projectID,
|
||||
slug: input.title.toLowerCase(),
|
||||
directory: input.directory ?? "/tmp/project-session-storage",
|
||||
path: input.path,
|
||||
title: input.title,
|
||||
version: "test",
|
||||
cost: 0,
|
||||
tokens_input: 0,
|
||||
tokens_output: 0,
|
||||
tokens_reasoning: 0,
|
||||
tokens_cache_read: 0,
|
||||
tokens_cache_write: 0,
|
||||
time_created: input.updated,
|
||||
time_updated: input.updated,
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
}
|
||||
|
||||
function seedUserMessage(input: { id: SessionMessage.ID; text: string; time: number }) {
|
||||
const encoded = encodeMessage(makeUserMessage(input))
|
||||
const { id: _, type: __, ...data } = encoded
|
||||
return seedMessage(input.id, "user", input.time, data)
|
||||
}
|
||||
|
||||
function seedCompaction(input: { id: SessionMessage.ID; summary: string; time: number }) {
|
||||
const encoded = encodeMessage(makeCompaction(input))
|
||||
const { id: _, type: __, ...data } = encoded
|
||||
return seedMessage(input.id, "compaction", input.time, data)
|
||||
}
|
||||
|
||||
function makeSessionRow(input: { id: SessionID; title: string; directory?: string; path: string; updated: number }) {
|
||||
return new SessionStorage.SessionRow({
|
||||
id: input.id,
|
||||
projectID,
|
||||
title: input.title,
|
||||
directory: input.directory ?? "/tmp/project-session-storage",
|
||||
path: input.path,
|
||||
cost: 0,
|
||||
tokens: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
reasoning: 0,
|
||||
cache: {
|
||||
read: 0,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
time: {
|
||||
created: DateTime.makeUnsafe(input.updated),
|
||||
updated: DateTime.makeUnsafe(input.updated),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function makeUserMessage(input: { id: SessionMessage.ID; text: string; time: number }) {
|
||||
return new SessionMessage.User({
|
||||
id: input.id,
|
||||
type: "user",
|
||||
text: input.text,
|
||||
files: [],
|
||||
agents: [],
|
||||
references: [],
|
||||
time: { created: DateTime.makeUnsafe(input.time) },
|
||||
})
|
||||
}
|
||||
|
||||
function makeCompaction(input: { id: SessionMessage.ID; summary: string; time: number }) {
|
||||
return new SessionMessage.Compaction({
|
||||
id: input.id,
|
||||
type: "compaction",
|
||||
reason: "manual",
|
||||
summary: input.summary,
|
||||
time: { created: DateTime.makeUnsafe(input.time) },
|
||||
})
|
||||
}
|
||||
|
||||
function seedMessage(
|
||||
id: SessionMessage.ID,
|
||||
type: SessionMessage.Type,
|
||||
time: number,
|
||||
data: typeof SessionMessageTable.$inferInsert.data,
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const db = yield* StorageDatabase.Service
|
||||
yield* db
|
||||
.insert(SessionMessageTable)
|
||||
.values({
|
||||
id,
|
||||
session_id: sessionA,
|
||||
type,
|
||||
time_created: time,
|
||||
time_updated: time,
|
||||
data,
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
}
|
||||
|
||||
function appendMemoryMessage(message: SessionMessage.Message) {
|
||||
const current = memoryState.messages.get(sessionA) ?? []
|
||||
current.push(message)
|
||||
memoryState.messages.set(sessionA, current)
|
||||
}
|
||||
@@ -210,7 +210,6 @@ export type TuiPromptRef = {
|
||||
|
||||
export type TuiPromptProps = {
|
||||
sessionID?: string
|
||||
workspaceID?: string
|
||||
visible?: boolean
|
||||
disabled?: boolean
|
||||
onSubmit?: () => void
|
||||
@@ -458,12 +457,9 @@ export type TuiHostSlotMap = {
|
||||
app_bottom: {}
|
||||
home_logo: {}
|
||||
home_prompt: {
|
||||
workspace_id?: string
|
||||
ref?: (ref: TuiPromptRef | undefined) => void
|
||||
}
|
||||
home_prompt_right: {
|
||||
workspace_id?: string
|
||||
}
|
||||
home_prompt_right: {}
|
||||
session_prompt: {
|
||||
session_id: string
|
||||
visible?: boolean
|
||||
|
||||
@@ -87,12 +87,12 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -119,7 +119,6 @@ https://opencode.ai/zen/v1/models
|
||||
| --------------------------------- | ------- | ------- | --------------- | --------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -129,6 +128,7 @@ https://opencode.ai/zen/v1/models
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -172,7 +172,6 @@ https://opencode.ai/zen/v1/models
|
||||
النماذج المجانية:
|
||||
|
||||
- DeepSeek V4 Flash Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
|
||||
- MiniMax M2.5 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
|
||||
- Nemotron 3 Super Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
|
||||
- Big Pickle نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
|
||||
|
||||
@@ -224,7 +223,6 @@ https://opencode.ai/zen/v1/models
|
||||
|
||||
- Big Pickle: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
|
||||
- DeepSeek V4 Flash Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
|
||||
- MiniMax M2.5 Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
|
||||
- Nemotron 3 Super Free (نقاط نهاية NVIDIA المجانية): يُقدَّم بموجب [شروط خدمة النسخة التجريبية من واجهة NVIDIA API](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). للاستخدام التجريبي فقط، وليس للإنتاج أو البيانات الحساسة. تقوم NVIDIA بتسجيل المطالبات والمخرجات لتحسين نماذجها وخدماتها. لا ترسل بيانات شخصية أو سرية.
|
||||
- OpenAI APIs: يتم الاحتفاظ بالطلبات لمدة 30 يوما وفقا لـ [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: يتم الاحتفاظ بالطلبات لمدة 30 يوما وفقا لـ [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -92,12 +92,12 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa.
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -179,7 +179,6 @@ Naknade za kreditne kartice prosljeđujemo po stvarnom trošku (4.4% + $0.30 po
|
||||
Besplatni modeli:
|
||||
|
||||
- DeepSeek V4 Flash Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
|
||||
- MiniMax M2.5 Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
|
||||
- Nemotron 3 Super Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
|
||||
- Big Pickle je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
|
||||
|
||||
@@ -236,7 +235,6 @@ i ne koriste vaše podatke za treniranje modela, uz sljedeće izuzetke:
|
||||
|
||||
- Big Pickle: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
|
||||
- DeepSeek V4 Flash Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
|
||||
- MiniMax M2.5 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
|
||||
- Nemotron 3 Super Free (besplatni NVIDIA endpointi): Dostupan je prema [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Samo za probnu upotrebu, nije za produkciju niti osjetljive podatke. NVIDIA bilježi promptove i izlaze radi poboljšanja svojih modela i usluga. Nemojte slati lične ili povjerljive podatke.
|
||||
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -92,12 +92,12 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**.
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**.
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -179,7 +179,6 @@ Kreditkortgebyrer videregives til kostpris (4.4% + $0.30 pr. transaktion); vi op
|
||||
De gratis modeller:
|
||||
|
||||
- DeepSeek V4 Flash Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
|
||||
- MiniMax M2.5 Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
|
||||
- Nemotron 3 Super Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
|
||||
- Big Pickle er en stealth-model, som er gratis på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
|
||||
|
||||
@@ -234,7 +233,6 @@ Alle vores modeller hostes i US. Vores udbydere følger en nul-opbevaringspoliti
|
||||
|
||||
- Big Pickle: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
|
||||
- DeepSeek V4 Flash Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
|
||||
- MiniMax M2.5 Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
|
||||
- Nemotron 3 Super Free (gratis NVIDIA-endpoints): Leveres under [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Kun til prøvebrug, ikke til produktion eller følsomme data. Prompts og outputs logges af NVIDIA for at forbedre deres modeller og tjenester. Indsend ikke personlige eller fortrolige data.
|
||||
- OpenAI APIs: Anmodninger opbevares i 30 dage i overensstemmelse med [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Anmodninger opbevares i 30 dage i overensstemmelse med [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -83,12 +83,12 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen.
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -115,7 +115,6 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -125,6 +124,7 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -168,7 +168,6 @@ Kreditkartengebühren werden zum Selbstkostenpreis weitergegeben (4.4% + $0.30 p
|
||||
Die kostenlosen Modelle:
|
||||
|
||||
- DeepSeek V4 Flash Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
|
||||
- MiniMax M2.5 Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
|
||||
- Nemotron 3 Super Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
|
||||
- Big Pickle ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
|
||||
|
||||
@@ -220,7 +219,6 @@ Alle unsere Modelle werden in den USA gehostet. Unsere Provider folgen einer Zer
|
||||
|
||||
- Big Pickle: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
|
||||
- DeepSeek V4 Flash Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
|
||||
- MiniMax M2.5 Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
|
||||
- Nemotron 3 Super Free (kostenlose NVIDIA-Endpunkte): Bereitgestellt gemäß den [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Nur für Testzwecke, nicht für Produktion oder sensible Daten. Eingaben und Ausgaben werden von NVIDIA protokolliert, um seine Modelle und Dienste zu verbessern. Übermitteln Sie keine personenbezogenen oder vertraulichen Daten.
|
||||
- OpenAI APIs: Anfragen werden in Übereinstimmung mit [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 30 Tage lang gespeichert.
|
||||
- Anthropic APIs: Anfragen werden in Übereinstimmung mit [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage) 30 Tage lang gespeichert.
|
||||
|
||||
@@ -92,12 +92,12 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p
|
||||
| --------------------------------- | ------- | ------- | ---------------- | ------------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -179,7 +179,6 @@ Las comisiones de tarjeta de crédito se trasladan al costo (4.4% + $0.30 por tr
|
||||
Los modelos gratuitos:
|
||||
|
||||
- DeepSeek V4 Flash Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
|
||||
- MiniMax M2.5 Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
|
||||
- Nemotron 3 Super Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
|
||||
- Big Pickle es un modelo stealth que es gratuito en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
|
||||
|
||||
@@ -234,7 +233,6 @@ Todos nuestros modelos están alojados en US. Nuestros proveedores siguen una po
|
||||
|
||||
- Big Pickle: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
|
||||
- DeepSeek V4 Flash Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
|
||||
- MiniMax M2.5 Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
|
||||
- Nemotron 3 Super Free (endpoints gratuitos de NVIDIA): Se ofrece bajo los [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Solo para uso de prueba, no para producción ni datos sensibles. NVIDIA registra los prompts y las salidas para mejorar sus modelos y servicios. No envíes datos personales ni confidenciales.
|
||||
- OpenAI APIs: Las solicitudes se conservan durante 30 días de acuerdo con [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Las solicitudes se conservan durante 30 días de acuerdo con [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -83,12 +83,12 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -115,7 +115,6 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -125,6 +124,7 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -168,7 +168,6 @@ Les frais de carte de crédit sont répercutés au prix coûtant (4.4% + $0.30 p
|
||||
Les modèles gratuits :
|
||||
|
||||
- DeepSeek V4 Flash Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
|
||||
- MiniMax M2.5 Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
|
||||
- Nemotron 3 Super Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
|
||||
- Big Pickle est un modèle stealth gratuit sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
|
||||
|
||||
@@ -220,7 +219,6 @@ Tous nos modèles sont hébergés aux US. Nos fournisseurs suivent une politique
|
||||
|
||||
- Big Pickle : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
|
||||
- DeepSeek V4 Flash Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
|
||||
- MiniMax M2.5 Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
|
||||
- Nemotron 3 Super Free (endpoints NVIDIA gratuits) : Fourni dans le cadre des [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Réservé à un usage d'essai, pas à la production ni aux données sensibles. Les prompts et les sorties sont journalisés par NVIDIA pour améliorer ses modèles et services. N'envoyez pas de données personnelles ou confidentielles.
|
||||
- OpenAI APIs : Les requêtes sont conservées pendant 30 jours conformément à [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs : Les requêtes sont conservées pendant 30 jours conformément à [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -92,12 +92,12 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API.
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -179,7 +179,6 @@ Le commissioni della carta di credito vengono trasferite al costo (4.4% + $0.30
|
||||
I modelli gratuiti:
|
||||
|
||||
- DeepSeek V4 Flash Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
|
||||
- MiniMax M2.5 Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
|
||||
- Nemotron 3 Super Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
|
||||
- Big Pickle è un modello stealth che è gratuito su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
|
||||
|
||||
@@ -234,7 +233,6 @@ Tutti i nostri modelli sono ospitati negli US. I nostri provider seguono una pol
|
||||
|
||||
- Big Pickle: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
|
||||
- DeepSeek V4 Flash Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
|
||||
- MiniMax M2.5 Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
|
||||
- Nemotron 3 Super Free (endpoint NVIDIA gratuiti): fornito secondo i [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Solo per uso di prova, non per produzione o dati sensibili. NVIDIA registra prompt e output per migliorare i propri modelli e servizi. Non inviare dati personali o riservati.
|
||||
- OpenAI APIs: le richieste vengono conservate per 30 giorni in conformità con [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: le richieste vengono conservate per 30 giorni in conformità con [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -83,12 +83,12 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -115,7 +115,6 @@ https://opencode.ai/zen/v1/models
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -125,6 +124,7 @@ https://opencode.ai/zen/v1/models
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -168,7 +168,6 @@ https://opencode.ai/zen/v1/models
|
||||
無料モデル:
|
||||
|
||||
- DeepSeek V4 Flash Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
|
||||
- MiniMax M2.5 Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
|
||||
- Nemotron 3 Super Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
|
||||
- Big Pickle はステルスモデルで、期間限定で OpenCode で無料提供されています。チームはこの期間中にフィードバックを集め、モデルを改善しています。
|
||||
|
||||
@@ -220,7 +219,6 @@ https://opencode.ai/zen/v1/models
|
||||
|
||||
- Big Pickle: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
|
||||
- DeepSeek V4 Flash Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
|
||||
- MiniMax M2.5 Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
|
||||
- Nemotron 3 Super Free(NVIDIA の無料エンドポイント): [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) に基づいて提供されます。試用専用であり、本番環境や機密性の高いデータには使用しないでください。プロンプトと出力は、NVIDIA が自社のモデルとサービスを改善するために記録します。個人情報や機密データは送信しないでください。
|
||||
- OpenAI APIs: リクエストは [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) に従って 30 日間保持されます。
|
||||
- Anthropic APIs: リクエストは [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage) に従って 30 日間保持されます。
|
||||
|
||||
@@ -83,12 +83,12 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다.
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -115,7 +115,6 @@ https://opencode.ai/zen/v1/models
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -125,6 +124,7 @@ https://opencode.ai/zen/v1/models
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -168,7 +168,6 @@ https://opencode.ai/zen/v1/models
|
||||
무료 모델:
|
||||
|
||||
- DeepSeek V4 Flash Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
|
||||
- MiniMax M2.5 Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
|
||||
- Nemotron 3 Super Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
|
||||
- Big Pickle은 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
|
||||
|
||||
@@ -220,7 +219,6 @@ https://opencode.ai/zen/v1/models
|
||||
|
||||
- Big Pickle: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
|
||||
- DeepSeek V4 Flash Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
|
||||
- MiniMax M2.5 Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
|
||||
- Nemotron 3 Super Free(NVIDIA 무료 엔드포인트): [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)에 따라 제공됩니다. 평가판 전용이며 프로덕션 환경이나 민감한 데이터에는 사용할 수 없습니다. NVIDIA는 자사 모델과 서비스를 개선하기 위해 프롬프트와 출력을 기록합니다. 개인 정보나 기밀 데이터는 제출하지 마세요.
|
||||
- OpenAI APIs: 요청은 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data)에 따라 30일 동안 보관됩니다.
|
||||
- Anthropic APIs: 요청은 [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage)에 따라 30일 동안 보관됩니다.
|
||||
|
||||
@@ -92,12 +92,12 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter.
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**.
|
||||
| --------------------------------- | ------- | ------- | ------------- | --------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**.
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -179,7 +179,6 @@ Kredittkortgebyrer videreføres til kostpris (4.4% + $0.30 per transaction); vi
|
||||
Gratis-modellene:
|
||||
|
||||
- DeepSeek V4 Flash Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
|
||||
- MiniMax M2.5 Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
|
||||
- Nemotron 3 Super Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
|
||||
- Big Pickle er en stealth-modell som er gratis på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
|
||||
|
||||
@@ -234,7 +233,6 @@ Alle modellene våre hostes i US. Leverandørene våre følger en policy for zer
|
||||
|
||||
- Big Pickle: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
|
||||
- DeepSeek V4 Flash Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
|
||||
- MiniMax M2.5 Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
|
||||
- Nemotron 3 Super Free (gratis NVIDIA-endepunkter): Leveres under [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Kun for prøvebruk, ikke for produksjon eller sensitive data. Prompter og svar logges av NVIDIA for å forbedre modellene og tjenestene deres. Ikke send inn personopplysninger eller konfidensielle data.
|
||||
- OpenAI APIs: Forespørsler lagres i 30 dager i samsvar med [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Forespørsler lagres i 30 dager i samsvar med [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -93,11 +93,11 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API.
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów*
|
||||
| --------------------------------- | ------- | ------- | -------------- | -------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów*
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -180,7 +180,6 @@ Opłaty za karty kredytowe są przenoszone po kosztach (4.4% + $0.30 per transac
|
||||
Darmowe modele:
|
||||
|
||||
- DeepSeek V4 Flash Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
|
||||
- MiniMax M2.5 Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
|
||||
- Nemotron 3 Super Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
|
||||
- Big Pickle to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
|
||||
|
||||
@@ -235,7 +234,6 @@ Wszystkie nasze modele są hostowane w US. Nasi dostawcy stosują politykę zero
|
||||
|
||||
- Big Pickle: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
|
||||
- DeepSeek V4 Flash Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
|
||||
- MiniMax M2.5 Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
|
||||
- Nemotron 3 Super Free (darmowe endpointy NVIDIA): Udostępniany zgodnie z [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Tylko do użytku próbnego, nie do produkcji ani danych wrażliwych. NVIDIA rejestruje prompty i odpowiedzi, aby ulepszać swoje modele i usługi. Nie przesyłaj danych osobowych ani poufnych.
|
||||
- OpenAI APIs: Żądania są przechowywane przez 30 dni zgodnie z [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Żądania są przechowywane przez 30 dni zgodnie z [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -84,11 +84,11 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API.
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -115,7 +115,6 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**.
|
||||
| --------------------------------- | ------- | ------- | ---------------- | ---------------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -125,6 +124,7 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**.
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -168,7 +168,6 @@ As taxas de cartão de crédito são repassadas a preço de custo (4.4% + $0.30
|
||||
Os modelos gratuitos:
|
||||
|
||||
- DeepSeek V4 Flash Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
|
||||
- MiniMax M2.5 Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
|
||||
- Nemotron 3 Super Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
|
||||
- Big Pickle é um modelo stealth que está gratuito no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
|
||||
|
||||
@@ -220,7 +219,6 @@ Todos os nossos modelos são hospedados nos US. Nossos provedores seguem uma pol
|
||||
|
||||
- Big Pickle: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
|
||||
- DeepSeek V4 Flash Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
|
||||
- MiniMax M2.5 Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
|
||||
- Nemotron 3 Super Free (endpoints gratuitos da NVIDIA): Fornecido sob os [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Apenas para uso de avaliação, não para produção nem dados sensíveis. A NVIDIA registra prompts e saídas para melhorar seus modelos e serviços. Não envie dados pessoais ou confidenciais.
|
||||
- OpenAI APIs: As solicitações são retidas por 30 dias de acordo com [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: As solicitações são retidas por 30 dias de acordo com [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -93,11 +93,11 @@ OpenCode Zen работает как любой другой провайдер
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ https://opencode.ai/zen/v1/models
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ https://opencode.ai/zen/v1/models
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -179,7 +179,6 @@ https://opencode.ai/zen/v1/models
|
||||
Бесплатные модели:
|
||||
|
||||
- DeepSeek V4 Flash Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
|
||||
- MiniMax M2.5 Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
|
||||
- Nemotron 3 Super Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
|
||||
- Big Pickle — это скрытая модель, которая доступна бесплатно в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
|
||||
|
||||
@@ -234,7 +233,6 @@ https://opencode.ai/zen/v1/models
|
||||
|
||||
- Big Pickle: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
|
||||
- DeepSeek V4 Flash Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
|
||||
- MiniMax M2.5 Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
|
||||
- Nemotron 3 Super Free (бесплатные эндпоинты NVIDIA): предоставляется в соответствии с [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Только для пробного использования, не для продакшена и не для чувствительных данных. NVIDIA логирует запросы и ответы, чтобы улучшать свои модели и сервисы. Не отправляйте персональные или конфиденциальные данные.
|
||||
- OpenAI APIs: запросы хранятся 30 дней в соответствии с [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: запросы хранятся 30 дней в соответствии с [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -85,12 +85,12 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -117,7 +117,6 @@ https://opencode.ai/zen/v1/models
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -127,6 +126,7 @@ https://opencode.ai/zen/v1/models
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -170,7 +170,6 @@ https://opencode.ai/zen/v1/models
|
||||
โมเดลฟรี:
|
||||
|
||||
- DeepSeek V4 Flash Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
|
||||
- MiniMax M2.5 Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
|
||||
- Nemotron 3 Super Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
|
||||
- Big Pickle เป็น stealth model ที่ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
|
||||
|
||||
@@ -222,7 +221,6 @@ https://opencode.ai/zen/v1/models
|
||||
|
||||
- Big Pickle: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
|
||||
- DeepSeek V4 Flash Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
|
||||
- MiniMax M2.5 Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
|
||||
- Nemotron 3 Super Free (endpoint ฟรีของ NVIDIA): ให้บริการภายใต้ [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) ใช้สำหรับการทดลองเท่านั้น ไม่เหมาะสำหรับ production หรือข้อมูลที่อ่อนไหว NVIDIA จะบันทึก prompt และ output เพื่อนำไปปรับปรุงโมเดลและบริการของตน โปรดอย่าส่งข้อมูลส่วนบุคคลหรือข้อมูลลับ.
|
||||
- OpenAI APIs: คำขอจะถูกเก็บไว้เป็นเวลา 30 วันตาม [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: คำขอจะถูกเก็บไว้เป็นเวลา 30 วันตาม [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -83,12 +83,12 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin
|
||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -115,7 +115,6 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -125,6 +124,7 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -168,7 +168,6 @@ Kredi kartı ücretleri maliyet üzerinden yansıtılır (%4.4 + işlem başına
|
||||
Ücretsiz modeller:
|
||||
|
||||
- DeepSeek V4 Flash Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
|
||||
- MiniMax M2.5 Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
|
||||
- Nemotron 3 Super Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
|
||||
- Big Pickle, sınırlı bir süre için OpenCode'da ücretsiz olan gizli bir modeldir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
|
||||
|
||||
@@ -220,7 +219,6 @@ Tüm modellerimiz US'de barındırılıyor. Sağlayıcılarımız zero-retention
|
||||
|
||||
- Big Pickle: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
|
||||
- DeepSeek V4 Flash Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
|
||||
- MiniMax M2.5 Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
|
||||
- Nemotron 3 Super Free (ücretsiz NVIDIA uç noktaları): [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) kapsamında sunulur. Yalnızca deneme amaçlıdır; üretim veya hassas veriler için uygun değildir. NVIDIA, modellerini ve hizmetlerini geliştirmek için promptları ve çıktıları kaydeder. Kişisel veya gizli veri göndermeyin.
|
||||
- OpenAI APIs: İstekler [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) uyarınca 30 gün boyunca saklanır.
|
||||
- Anthropic APIs: İstekler [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage) uyarınca 30 gün boyunca saklanır.
|
||||
|
||||
@@ -93,11 +93,11 @@ You can also access our models through the following API endpoints.
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -126,7 +126,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| --------------------------------- | ------ | ------- | ----------- | ------------ |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -136,6 +135,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -179,7 +179,6 @@ Credit card fees are passed along at cost (4.4% + $0.30 per transaction); we don
|
||||
The free models:
|
||||
|
||||
- DeepSeek V4 Flash Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- MiniMax M2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- Nemotron 3 Super Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
|
||||
@@ -234,7 +233,6 @@ All our models are hosted in the US. Our providers follow a zero-retention polic
|
||||
|
||||
- Big Pickle: During its free period, collected data may be used to improve the model.
|
||||
- DeepSeek V4 Flash Free: During its free period, collected data may be used to improve the model.
|
||||
- MiniMax M2.5 Free: During its free period, collected data may be used to improve the model.
|
||||
- Nemotron 3 Super Free (NVIDIA free endpoints): Provided under the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Trial use only — not for production or sensitive data. Prompts and outputs are logged by NVIDIA to improve its models and services. Do not submit personal or confidential data.
|
||||
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
@@ -84,11 +84,11 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -115,7 +115,6 @@ https://opencode.ai/zen/v1/models
|
||||
| --------------------------------- | ------ | ------- | -------- | -------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -125,6 +124,7 @@ https://opencode.ai/zen/v1/models
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -168,7 +168,6 @@ https://opencode.ai/zen/v1/models
|
||||
免费模型:
|
||||
|
||||
- DeepSeek V4 Flash Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
|
||||
- MiniMax M2.5 Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
|
||||
- Nemotron 3 Super Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
|
||||
- Big Pickle 是一个隐身模型,目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
|
||||
|
||||
@@ -220,7 +219,6 @@ https://opencode.ai/zen/v1/models
|
||||
|
||||
- Big Pickle:在免费期间,收集的数据可能会被用于改进模型。
|
||||
- DeepSeek V4 Flash Free:在免费期间,收集的数据可能会被用于改进模型。
|
||||
- MiniMax M2.5 Free:在免费期间,收集的数据可能会被用于改进模型。
|
||||
- Nemotron 3 Super Free(NVIDIA 免费端点):根据 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) 提供。仅供试用,不适用于生产环境或敏感数据。NVIDIA 会记录提示词和输出内容,以改进其模型和服务。请勿提交个人或机密数据。
|
||||
- OpenAI APIs:请求会根据 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 保留 30 天。
|
||||
- Anthropic APIs:请求会根据 [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage) 保留 30 天。
|
||||
|
||||
@@ -88,11 +88,11 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。
|
||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -120,7 +120,6 @@ https://opencode.ai/zen/v1/models
|
||||
| --------------------------------- | ------ | ------- | -------- | -------- |
|
||||
| Big Pickle | Free | Free | Free | - |
|
||||
| DeepSeek V4 Flash Free | Free | Free | Free | - |
|
||||
| MiniMax M2.5 Free | Free | Free | Free | - |
|
||||
| Nemotron 3 Super Free | Free | Free | Free | - |
|
||||
| MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
|
||||
@@ -130,6 +129,7 @@ https://opencode.ai/zen/v1/models
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 |
|
||||
| Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 |
|
||||
| Grok Build 0.1 | 1.00 | $2.00 | $0.20 | - |
|
||||
| Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -174,7 +174,6 @@ https://opencode.ai/zen/v1/models
|
||||
免費模型:
|
||||
|
||||
- DeepSeek V4 Flash Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
|
||||
- MiniMax M2.5 Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
|
||||
- Nemotron 3 Super Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
|
||||
- Big Pickle 是一個隱身模型,在 OpenCode 上限時免費提供。團隊正在利用這段時間收集回饋並改進模型。
|
||||
|
||||
@@ -227,7 +226,6 @@ https://opencode.ai/zen/v1/models
|
||||
|
||||
- Big Pickle: 在免費期間,收集到的資料可能會用於改進模型。
|
||||
- DeepSeek V4 Flash Free: 在免費期間,收集到的資料可能會用於改進模型。
|
||||
- MiniMax M2.5 Free: 在免費期間,收集到的資料可能會用於改進模型。
|
||||
- Nemotron 3 Super Free(NVIDIA 免費端點):依據 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) 提供。僅供試用,不適用於正式環境或敏感資料。NVIDIA 會記錄提示詞與輸出內容,以改進其模型與服務。請勿提交個人或機密資料。
|
||||
- OpenAI APIs: 請求會依據 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 保留 30 天。
|
||||
- Anthropic APIs: 請求會依據 [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage) 保留 30 天。
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# Effect Drizzle SQLite Package
|
||||
|
||||
## Goal
|
||||
|
||||
Create a small workspace package that vendors the Drizzle `effect-sqlite` adapter shape for our repo. This is not an opencode storage abstraction. It is a local package that ports the Drizzle Effect SQLite implementation so we can use it before/independently of upstream release timing.
|
||||
|
||||
`packages/opencode` will use it internally, but the package itself should be generic: Drizzle + Effect + SQLite. No opencode paths, migrations, tables, transaction hooks, post-commit behavior, or domain language should live in this package.
|
||||
|
||||
## Package Shape
|
||||
|
||||
Add a package similar in style to `packages/http-recorder`:
|
||||
|
||||
- `packages/effect-drizzle-sqlite/package.json`
|
||||
- `packages/effect-drizzle-sqlite/src/index.ts`
|
||||
- `packages/effect-drizzle-sqlite/src/effect-sqlite/*`
|
||||
- `packages/effect-drizzle-sqlite/src/sqlite-core/effect/*`
|
||||
- `packages/effect-drizzle-sqlite/test/sqlite.test.ts`
|
||||
|
||||
Package name:
|
||||
|
||||
- `@opencode-ai/effect-drizzle-sqlite`
|
||||
|
||||
Initial exports:
|
||||
|
||||
```ts
|
||||
export { EffectLogger } from "drizzle-orm/effect-core"
|
||||
export * from "./effect-sqlite/driver"
|
||||
export * from "./effect-sqlite/session"
|
||||
export { migrate } from "./effect-sqlite/migrator"
|
||||
export * as EffectDrizzleSqlite from "."
|
||||
```
|
||||
|
||||
The package should follow Drizzle's adapter naming and semantics as closely as possible. Think of it as a vendored `drizzle-orm/effect-sqlite` package surface, not as a new storage service API.
|
||||
|
||||
## Upstream References
|
||||
|
||||
Use these as implementation references instead of inventing a custom API:
|
||||
|
||||
- Drizzle Effect Postgres current RC:
|
||||
- `/Users/kit/code/open-source/drizzle-orm-rc4-pr/drizzle-orm/src/effect-core/query-effect.ts`
|
||||
- `/Users/kit/code/open-source/drizzle-orm-rc4-pr/integration-tests/tests/pg/effect-sql.test.ts`
|
||||
- SQLite Effect branch/reference:
|
||||
- `/Users/kit/code/open-source/drizzle-orm-beta16/drizzle-orm/src/up-migrations/effect-sqlite.ts`
|
||||
- `/Users/kit/code/open-source/drizzle-orm-beta16/integration-tests/tests/sqlite/effect-sql.test.ts`
|
||||
- `/Users/kit/code/open-source/drizzle-orm-beta16/drizzle-orm/type-tests/sqlite/effect.ts`
|
||||
- Effect SQLite client source of truth:
|
||||
- `/Users/kit/code/open-source/effect-smol/packages/sql/sqlite-bun/src/SqliteClient.ts`
|
||||
- `/Users/kit/code/open-source/effect-smol/packages/sql/sqlite-node/test/Client.test.ts`
|
||||
- `/Users/kit/code/open-source/effect-smol/packages/sql/sqlite-node/test/SqliteMigrator.test.ts`
|
||||
|
||||
Important API patterns from those references:
|
||||
|
||||
- Drizzle queries are Effect-yieldable: `yield* db.select().from(table)`.
|
||||
- Transactions are Effect values: `yield* db.transaction((tx) => Effect.gen(...), { behavior: "immediate" })`.
|
||||
- SQLite clients come from Effect layers such as `SqliteClient.layer({ filename })`.
|
||||
- Migrations can run through Effect SQL/SQLite migrator mechanisms or Drizzle's `effect-sqlite/migrator` when available.
|
||||
|
||||
## Public Surface
|
||||
|
||||
Do not invent an `Interface<TDatabase>` abstraction unless the Drizzle port already has one. The public surface should mirror Drizzle's Effect adapters:
|
||||
|
||||
```ts
|
||||
const db = yield * EffectDrizzleSqlite.make({ relations }).pipe(Effect.provide(EffectDrizzleSqlite.DefaultServices))
|
||||
|
||||
yield * db.select().from(users)
|
||||
yield *
|
||||
db.transaction(
|
||||
(tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* tx.insert(users).values({ name: "Ada" })
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `make` / `makeWithDefaults` should match the Drizzle Effect SQLite branch as much as possible.
|
||||
- `DefaultServices` should provide Drizzle's default logger/cache services, same as Effect Postgres.
|
||||
- The package should depend on Effect SQL SQLite clients (`@effect/sql-sqlite-bun` and/or node) the same way the Drizzle branch does.
|
||||
- Opencode-specific path/channel selection stays in `packages/opencode`.
|
||||
|
||||
## Opencode Adoption Notes
|
||||
|
||||
These are not package requirements, but they matter for the later opencode adoption PR.
|
||||
|
||||
The current `packages/opencode/src/storage/db.ts` has two non-obvious semantics that the opencode wrapper must preserve when it consumes this adapter:
|
||||
|
||||
- Nested `Database.use` inside `Database.transaction` sees the current transaction, not the root client.
|
||||
- `Database.effect` queues post-commit side effects while inside a transaction, and runs immediately outside a transaction.
|
||||
|
||||
The opencode wrapper can implement that using Effect context instead of `LocalContext`:
|
||||
|
||||
- A private transaction context holding `{ tx, afterCommit }`.
|
||||
- `withDb`/`db` methods read the current transaction context if present, otherwise use the root db.
|
||||
- `transaction` installs a transaction context around the effect.
|
||||
- Nested transactions can either reuse the existing tx initially, matching current behavior, or later use explicit savepoints if needed.
|
||||
|
||||
Do not remove this behavior while moving opencode to Effect SQLite. `SyncEvent.run` depends on transaction composability and `behavior: "immediate"` for sequencing correctness.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. Add `@opencode-ai/effect-drizzle-sqlite` with a minimal in-memory/file SQLite test schema.
|
||||
2. Port the Drizzle Effect SQLite adapter from the SQLite branch into the package, preserving upstream names and API shape.
|
||||
3. Test adapter-level guarantees:
|
||||
- query builders are yieldable Effect values,
|
||||
- `transaction(..., { behavior: "immediate" })` commits successful writes,
|
||||
- failed transaction rolls back,
|
||||
- migrations run once and in order,
|
||||
- close finalizer closes the underlying SQLite database.
|
||||
4. Add `@opencode-ai/effect-drizzle-sqlite` as a dependency of `packages/opencode`.
|
||||
5. Port `packages/opencode/src/storage/db.ts` to be a thin compatibility wrapper over the adapter plus opencode-specific transaction/post-commit context.
|
||||
6. Keep existing call sites working first:
|
||||
- `Database.Client()`
|
||||
- `Database.use(...)`
|
||||
- `Database.transaction(...)`
|
||||
- `Database.effect(...)`
|
||||
7. After compatibility is stable, migrate call sites from callback-style `Database.use` to yielding Effect Drizzle queries directly.
|
||||
8. Only then build domain stores like session/message/project stores on top of opencode's storage wrapper.
|
||||
|
||||
## Why This Is Cleaner Than Starting With SessionStorage
|
||||
|
||||
`SessionStorage` is a useful domain seam, but it does not answer the core adapter problem: how to make Drizzle SQLite Effect-native in this repo.
|
||||
|
||||
An Effect Drizzle SQLite package lets us vendor the adapter once. Then opencode can build its own storage wrapper on top, and `SessionStorage`, `MessageStorage`, event store, and projector writes can all share the same transaction and migration model.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Which client should the first package target: `@effect/sql-sqlite-bun`, `@effect/sql-sqlite-node`, or both behind separate layers?
|
||||
- How much source should we copy from the Drizzle branch versus import from catalog `drizzle-orm` internals?
|
||||
- What is the update path once Drizzle upstream ships `effect-sqlite`?
|
||||
- Should `afterCommit` stay opencode-specific until event publishing moves? Default answer: yes.
|
||||
- Should the compatibility wrapper preserve synchronous return types temporarily, or should the migration intentionally force Effect call sites?
|
||||
- Do CLI/admin raw SQL and sqlite shell stay in `packages/opencode`, or does the storage package expose backend capabilities for them?
|
||||
|
||||
## Recommended First PR
|
||||
|
||||
Make the first PR package-only and intentionally boring:
|
||||
|
||||
- Add `packages/effect-drizzle-sqlite`.
|
||||
- Use a tiny test schema, not opencode domain tables.
|
||||
- Prove Effect Drizzle SQLite queries, transactions, and migrations.
|
||||
- Do not migrate `packages/opencode` yet except possibly adding the dependency if needed for typechecking.
|
||||
|
||||
That gives us a focused place to validate the Effect SQLite approach before disturbing opencode's current database runtime.
|
||||
@@ -0,0 +1,474 @@
|
||||
# V2 Storage Service
|
||||
|
||||
This note inventories the current SQLite/Drizzle query shapes used by opencode and sketches a swappable Effect service boundary for V2 storage. The goal is not to expose a generic SQL abstraction. The goal is to collect the domain operations we actually need behind an Effect service so the implementation can remain Drizzle SQLite today and move to Drizzle Effect SQLite, Effect SQL, a remote store, or a test implementation later.
|
||||
|
||||
## Current Shape
|
||||
|
||||
The current database module is `packages/opencode/src/storage/db.ts`.
|
||||
|
||||
It provides:
|
||||
|
||||
- `Database.Client()` as a lazy singleton Drizzle client.
|
||||
- `Database.use(callback)` as an ambient callback against the current transaction or global client.
|
||||
- `Database.transaction(callback, { behavior })` for synchronous SQLite transactions.
|
||||
- `Database.effect(fn)` for side effects queued until after the surrounding transaction.
|
||||
- SQLite lifecycle concerns: path selection, PRAGMAs, migrations, close/reset.
|
||||
|
||||
This API is convenient but leaks the concrete database everywhere. Most call sites import Drizzle operators and tables directly, build SQL in feature modules, and depend on synchronous callback execution.
|
||||
|
||||
## Tables In Scope
|
||||
|
||||
The regularly queried tables are:
|
||||
|
||||
- `project`: project identity, worktree, sandbox list, commands, icon metadata.
|
||||
- `workspace`: workspace records associated with projects.
|
||||
- `session`: session metadata, hierarchy, workspace/project links, usage totals, archive/revert/permission fields.
|
||||
- `message`: legacy V2 message rows with JSON payloads.
|
||||
- `part`: legacy V2 message part rows with JSON payloads.
|
||||
- `session_message`: new V2 event-derived session message rows.
|
||||
- `todo`: ordered per-session todo rows.
|
||||
- `event_sequence`: per-aggregate high-water mark and owner claim.
|
||||
- `event`: ordered sync/event history rows.
|
||||
- `account` and `account_state`: auth accounts and singleton active account state.
|
||||
- `permission`: project-scoped permission rules.
|
||||
- `session_share`: share records by session.
|
||||
- `data_migration`: resumable data migration completion markers.
|
||||
|
||||
## Query Shapes
|
||||
|
||||
### Lifecycle And Migrations
|
||||
|
||||
- Open database at the channel/user-selected path.
|
||||
- Apply SQLite PRAGMAs for WAL, sync mode, busy timeout, cache size, foreign keys, and checkpointing.
|
||||
- Apply schema migrations from bundled or dev migration files.
|
||||
- Run data migrations in resumable background fibers.
|
||||
- Run high-throughput JSON import using bulk inserts and explicit transactions.
|
||||
- Provide a readonly/raw admin path for `opencode db` diagnostics.
|
||||
|
||||
### Transactions
|
||||
|
||||
- Run synchronous transactions with SQLite behavior options: `deferred`, `immediate`, `exclusive`.
|
||||
- Preserve transaction context across nested helpers and projectors.
|
||||
- Queue post-commit side effects for bus/global event publication.
|
||||
- Need write-lock semantics for event sequencing. `SyncEvent.run` depends on `immediate` to avoid another writer changing the aggregate sequence between read and write.
|
||||
|
||||
### Event Store
|
||||
|
||||
- Read latest sequence and owner by aggregate id.
|
||||
- Claim an aggregate by updating `event_sequence.owner_id`.
|
||||
- Remove all sync state for an aggregate by deleting `event_sequence` and `event` rows transactionally.
|
||||
- Append a projected event:
|
||||
- read latest sequence inside an immediate transaction,
|
||||
- run the domain projector,
|
||||
- upsert `event_sequence`,
|
||||
- insert `event`,
|
||||
- publish only after commit.
|
||||
- Read sync fence state for all aggregates or a supplied aggregate id set.
|
||||
- Read event history with per-aggregate high-water exclusions.
|
||||
- Read replay history for one aggregate ordered by sequence.
|
||||
|
||||
### Sessions
|
||||
|
||||
- Get one session by id.
|
||||
- List sessions with dynamic filters:
|
||||
- project id,
|
||||
- workspace id,
|
||||
- directory,
|
||||
- path or path prefix,
|
||||
- root sessions only,
|
||||
- updated since timestamp,
|
||||
- title search,
|
||||
- archived/non-archived,
|
||||
- cursor pagination by `(time_updated, id)`,
|
||||
- asc/desc order and previous/next page direction.
|
||||
- List child sessions by parent id.
|
||||
- List global sessions, then hydrate project metadata for distinct project ids.
|
||||
- Create/update/delete sessions through event projectors.
|
||||
- Update session usage counters using atomic increments.
|
||||
- Patch sessions with partial row updates and read the updated row.
|
||||
|
||||
### Messages And Parts
|
||||
|
||||
- Page legacy `message` rows by session, descending by `(time_created, id)`, with `limit + 1` pagination.
|
||||
- Hydrate page results with all `part` rows for returned message ids ordered by `(message_id, id)`.
|
||||
- Get one message by `(session_id, message_id)`.
|
||||
- Get one part by `(session_id, message_id, part_id)`.
|
||||
- List parts by message id ordered by id.
|
||||
- Upsert messages by id, updating JSON payload on conflict.
|
||||
- Upsert parts by id, updating JSON payload on conflict.
|
||||
- Delete messages and parts by session-scoped keys.
|
||||
- Read previous part usage before update/delete so usage counters can be adjusted.
|
||||
- Ignore late writes that fail foreign-key constraints when the session/message has already been removed.
|
||||
|
||||
### Session Message Timeline
|
||||
|
||||
- List session messages by session id with cursor pagination by `(time_created, id)`.
|
||||
- Load context since latest compaction:
|
||||
- read latest `type = compaction` message,
|
||||
- read all messages after or at that compaction boundary ordered ascending.
|
||||
- Read current assistant/compaction/shell messages by session and type, newest first, then apply in-memory predicates.
|
||||
- Update message JSON data by `(id, session_id, type)`.
|
||||
- Append session message rows.
|
||||
- Update session metadata for agent/model switch events.
|
||||
|
||||
### Projects And Workspaces
|
||||
|
||||
- Upsert projects by id.
|
||||
- List all projects and get one project by id.
|
||||
- Update project fields and return the updated row.
|
||||
- Update initialized timestamp.
|
||||
- Mutate JSON-ish sandbox arrays by read/update-returning.
|
||||
- Repair global sessions into a discovered project by matching `(project_id = global, directory = worktree)`.
|
||||
- Workspace CRUD by project and workspace id.
|
||||
- Read sessions associated with a workspace.
|
||||
- Read distinct workspaces for a project when needed by control-plane flows.
|
||||
|
||||
### Accounts, Permissions, Shares, Todos
|
||||
|
||||
- Account repository:
|
||||
- read active singleton state then active account,
|
||||
- list accounts,
|
||||
- upsert account,
|
||||
- upsert singleton active state,
|
||||
- update tokens,
|
||||
- transactionally clear state and delete account.
|
||||
- Permissions:
|
||||
- load rules by project id during per-instance state initialization.
|
||||
- persist rules when approval state changes.
|
||||
- Shares:
|
||||
- get share by session id,
|
||||
- upsert share by session id,
|
||||
- delete share by session id.
|
||||
- Todos:
|
||||
- transactionally replace all todos for a session with ordered rows,
|
||||
- list todos by session ordered by position.
|
||||
|
||||
### Imports And Admin
|
||||
|
||||
- Import sessions/messages/parts idempotently using `onConflictDoNothing`.
|
||||
- Import sessions with conflict update for project/directory/path.
|
||||
- Run CLI diagnostics:
|
||||
- readonly raw SQL query,
|
||||
- sqlite shell for local SQLite backend,
|
||||
- print database path.
|
||||
|
||||
## Proposed Boundary
|
||||
|
||||
Use concrete service interfaces at the level higher services actually consume. We do not need a broad abstract hierarchy of sub-interfaces up front.
|
||||
|
||||
For V2, the first useful boundary is a session storage service consumed by `SessionV2.Service`:
|
||||
|
||||
```ts
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: SessionID) => Effect.Effect<SessionRow | undefined, StorageError>
|
||||
readonly list: (input: SessionListInput) => Effect.Effect<SessionRow[], StorageError>
|
||||
readonly messages: (input: SessionMessageListInput) => Effect.Effect<SessionMessageRow[], StorageError>
|
||||
readonly context: (sessionID: SessionID) => Effect.Effect<SessionMessageRow[], StorageError>
|
||||
}
|
||||
```
|
||||
|
||||
That interface should be shaped by `v2/session.ts`, not by the underlying tables. Internally it can use whatever helpers make sense: Drizzle query builders, transaction helpers, mapper functions, or smaller private modules. Those internals do not need to be stable service boundaries until another higher-level service needs to consume them.
|
||||
|
||||
The important design choice is that public boundaries are domain-level and demand-driven. The implementation may still use Drizzle internally, but V2 session code should stop importing Drizzle tables/operators for normal app behavior.
|
||||
|
||||
## Service Breakdown
|
||||
|
||||
We can safely break this into multiple services when there is a real consumer boundary, but we should not invent a full repository graph before the higher services ask for it. Start with the services that map to product/domain services, then let implementation helpers stay private.
|
||||
|
||||
### Foundation Helper
|
||||
|
||||
`StorageConnection` can exist as an implementation helper that owns the backend and cross-cutting mechanics:
|
||||
|
||||
- open/close lifecycle,
|
||||
- migrations,
|
||||
- transaction context,
|
||||
- transaction behavior (`deferred`, `immediate`, `exclusive`),
|
||||
- post-commit callbacks,
|
||||
- backend capabilities such as raw SQL or sqlite shell support.
|
||||
|
||||
This should be the only place that knows whether the implementation is current Drizzle SQLite, future Drizzle Effect SQLite, Effect SQL, or something else. It does not necessarily need to be exposed directly to feature services.
|
||||
|
||||
### Public Services
|
||||
|
||||
Public services should map to higher-level consumers:
|
||||
|
||||
- `SessionV2Storage`: the first public storage service for `v2/session.ts` reads and eventually V2 session writes.
|
||||
- `SyncEventStorage`: later, if we extract event sequencing/projection out of `sync/index.ts`.
|
||||
- `StorageAdmin`: later, if CLI diagnostics need a backend-neutral story.
|
||||
- Smaller stores only when their current owner service needs a swappable dependency.
|
||||
|
||||
Splitting this way is safe because public services share one internal connection/transaction helper. A transaction can compose operations across implementation helpers without each public service opening its own client.
|
||||
|
||||
The unsafe split would be one independent service per table with independent clients/lifecycles. That would make cross-table transactions, event projection, and post-commit publication harder to reason about.
|
||||
|
||||
## V2 Session Today
|
||||
|
||||
`packages/opencode/src/v2/session.ts` is currently a thin experimental service. It mixes domain API shape, row decoding, cursor query construction, and event publishing.
|
||||
|
||||
Implemented methods:
|
||||
|
||||
- `get(sessionID)`: reads `session` by id and maps the row to `SessionV2.Info`.
|
||||
- `list(input)`: lists sessions from `session` with filters and cursor pagination by `(time_updated, id)`.
|
||||
- `messages(input)`: lists projected `session_message` rows with cursor pagination by `(time_created, id)` and decodes them to `SessionMessage.Message`.
|
||||
- `context(sessionID)`: finds the latest compaction message, then returns all `session_message` rows at or after that boundary ordered ascending.
|
||||
- `switchAgent(input)`: publishes `SessionEvent.AgentSwitched` through `EventV2Bridge`.
|
||||
- `switchModel(input)`: publishes `SessionEvent.ModelSwitched` through `EventV2Bridge`.
|
||||
- `subagent(input)`: partially implemented orchestration that calls `create`, `prompt`, `wait`, and `messages`.
|
||||
|
||||
Stubbed or incomplete methods:
|
||||
|
||||
- `create`: currently returns `{}` via `any`.
|
||||
- `prompt`: currently returns `{}` via `any`.
|
||||
- `shell`: no-op.
|
||||
- `skill`: no-op.
|
||||
- `compact`: no-op.
|
||||
- `wait`: no-op.
|
||||
|
||||
Current direct storage needs for implemented reads:
|
||||
|
||||
- Get session row by id.
|
||||
- List sessions with filters: directory, path prefix, workspace id, roots-only, updated-start, title search.
|
||||
- Cursor sessions by `(time_updated, id)` with `previous`/`next` page semantics.
|
||||
- List session messages by session id with cursor by `(time_created, id)`.
|
||||
- Load active context since latest compaction.
|
||||
|
||||
Current write path for implemented commands:
|
||||
|
||||
- V2 session commands publish core `SessionEvent` definitions through `EventV2Bridge`.
|
||||
- `EventV2Bridge` maps versioned aggregate events to legacy `SyncEvent.run`.
|
||||
- `SyncEvent.run` applies `session/projectors-next.ts`, which updates `session` and `session_message` transactionally.
|
||||
|
||||
That means V2 session reads and V2 event projection are coupled through storage, but they are two distinct seams.
|
||||
|
||||
## Best First Slice
|
||||
|
||||
The best first storage service to extract is `SessionV2Storage`, scoped to what `v2/session.ts` currently needs, not a general storage layer.
|
||||
|
||||
Reasons:
|
||||
|
||||
- It is central to the new V2 API.
|
||||
- It has a small current query surface: `get`, `list`, `messages`, `context`.
|
||||
- It is shaped by the V2 session service API, not by table names.
|
||||
- It can replace all direct Drizzle reads in `v2/session.ts` without touching legacy `message`/`part` yet.
|
||||
- It can grow to include V2 writes (`create`, `prompt`, `compact`, etc.) as those methods become real.
|
||||
|
||||
Possible first interface:
|
||||
|
||||
```ts
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: SessionID) => Effect.Effect<SessionRow | undefined, StorageError>
|
||||
readonly list: (input: SessionListInput) => Effect.Effect<SessionRow[], StorageError>
|
||||
readonly messages: (input: SessionMessageListInput) => Effect.Effect<SessionMessageRow[], StorageError>
|
||||
readonly context: (sessionID: SessionID) => Effect.Effect<SessionMessageRow[], StorageError>
|
||||
}
|
||||
```
|
||||
|
||||
The second slice should be the write side for V2 session behavior once the currently stubbed methods are designed:
|
||||
|
||||
- `create`,
|
||||
- `prompt`,
|
||||
- `shell`,
|
||||
- `skill`,
|
||||
- `compact`,
|
||||
- `wait`.
|
||||
|
||||
The third slice should be event sequencing/projection if the V2 write path continues to publish through `EventV2Bridge` and `SyncEvent.run`. That is more valuable but riskier, so doing `SessionV2Storage` first gives us the service pattern before touching event sequencing.
|
||||
|
||||
Recommended sequence:
|
||||
|
||||
1. Add `SessionV2Storage` implemented with current Drizzle SQLite internals.
|
||||
2. Refactor `V2Session.get`, `V2Session.list`, `V2Session.messages`, and `V2Session.context` to use it.
|
||||
3. Keep private implementation helpers for session rows, session-message rows, cursor predicates, and row decoding near the storage implementation.
|
||||
4. Add write methods to `SessionV2Storage` only as `V2Session.create/prompt/shell/skill/compact/wait` become real.
|
||||
5. Extract `SyncEventStorage` later if event sequencing/projection needs its own swappable boundary.
|
||||
|
||||
## Suggested Sub-Interfaces
|
||||
|
||||
### `EventStore`
|
||||
|
||||
```ts
|
||||
interface EventStore {
|
||||
readonly getSequence: (
|
||||
aggregateID: string,
|
||||
) => Effect.Effect<{ seq: number; ownerID?: string } | undefined, StorageError>
|
||||
readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect<void, StorageError>
|
||||
readonly removeAggregate: (aggregateID: string) => Effect.Effect<void, StorageError>
|
||||
readonly appendProjected: <A, E>(input: {
|
||||
definition: SyncDefinition
|
||||
aggregateID: string
|
||||
data: unknown
|
||||
project: Effect.Effect<A, E, Transaction>
|
||||
publish: Effect.Effect<void>
|
||||
}) => Effect.Effect<A, E | StorageError>
|
||||
readonly fence: (aggregateIDs?: string[]) => Effect.Effect<Record<string, number>, StorageError>
|
||||
readonly history: (input: { since?: Record<string, number> }) => Effect.Effect<StoredEvent[], StorageError>
|
||||
readonly replay: (aggregateID: string) => Effect.Effect<StoredEvent[], StorageError>
|
||||
}
|
||||
```
|
||||
|
||||
This is the most important seam. It owns sequence correctness, immediate transaction behavior, and post-commit publication.
|
||||
|
||||
### `SessionStore`
|
||||
|
||||
```ts
|
||||
interface SessionStore {
|
||||
readonly get: (id: SessionID) => Effect.Effect<SessionRow | undefined, StorageError>
|
||||
readonly list: (input: SessionListInput) => Effect.Effect<SessionRow[], StorageError>
|
||||
readonly listGlobal: (
|
||||
input: GlobalSessionListInput,
|
||||
) => Effect.Effect<Array<{ session: SessionRow; project?: ProjectSummary }>, StorageError>
|
||||
readonly children: (parentID: SessionID) => Effect.Effect<SessionRow[], StorageError>
|
||||
readonly insert: (row: SessionInsert) => Effect.Effect<void, StorageError>
|
||||
readonly patch: (id: SessionID, patch: SessionPatch) => Effect.Effect<SessionRow | undefined, StorageError>
|
||||
readonly delete: (id: SessionID) => Effect.Effect<void, StorageError>
|
||||
readonly incrementUsage: (id: SessionID, usage: UsageDelta) => Effect.Effect<void, StorageError>
|
||||
}
|
||||
```
|
||||
|
||||
This removes query construction from `session.ts` and projectors while preserving the list semantics that clients rely on.
|
||||
|
||||
### `MessageStore`
|
||||
|
||||
```ts
|
||||
interface MessageStore {
|
||||
readonly page: (
|
||||
input: MessagePageInput,
|
||||
) => Effect.Effect<{ rows: MessageRow[]; more: boolean; cursor?: MessageCursor }, StorageError>
|
||||
readonly hydrate: (
|
||||
rows: MessageRow[],
|
||||
) => Effect.Effect<Array<{ message: MessageRow; parts: PartRow[] }>, StorageError>
|
||||
readonly get: (input: {
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
}) => Effect.Effect<MessageRow | undefined, StorageError>
|
||||
readonly parts: (messageID: MessageID) => Effect.Effect<PartRow[], StorageError>
|
||||
readonly getPart: (input: {
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
partID: PartID
|
||||
}) => Effect.Effect<PartRow | undefined, StorageError>
|
||||
readonly upsertMessage: (row: MessageInsert) => Effect.Effect<void, StorageError | ForeignKeyError>
|
||||
readonly upsertPart: (row: PartInsert) => Effect.Effect<{ previous?: PartRow }, StorageError | ForeignKeyError>
|
||||
readonly deleteMessage: (input: {
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
}) => Effect.Effect<PartRow[], StorageError>
|
||||
readonly deletePart: (input: {
|
||||
sessionID: SessionID
|
||||
partID: PartID
|
||||
}) => Effect.Effect<PartRow | undefined, StorageError>
|
||||
}
|
||||
```
|
||||
|
||||
The delete/update methods expose previous rows where callers need usage compensation.
|
||||
|
||||
### `SessionMessageStore`
|
||||
|
||||
```ts
|
||||
interface SessionMessageStore {
|
||||
readonly list: (input: SessionMessageListInput) => Effect.Effect<SessionMessageRow[], StorageError>
|
||||
readonly context: (sessionID: SessionID) => Effect.Effect<SessionMessageRow[], StorageError>
|
||||
readonly currentByType: (input: {
|
||||
sessionID: SessionID
|
||||
type: SessionMessage.Type
|
||||
}) => Effect.Effect<SessionMessageRow[], StorageError>
|
||||
readonly append: (row: SessionMessageInsert) => Effect.Effect<void, StorageError>
|
||||
readonly updateData: (input: {
|
||||
id: SessionMessage.ID
|
||||
sessionID: SessionID
|
||||
type: SessionMessage.Type
|
||||
data: SessionMessageData
|
||||
}) => Effect.Effect<void, StorageError>
|
||||
}
|
||||
```
|
||||
|
||||
This gives the V2 message updater a storage adapter without exposing Drizzle.
|
||||
|
||||
### Smaller Stores
|
||||
|
||||
- `ProjectStore`: upsert, get, list, patch-returning, set initialized, sandbox mutation, repair global sessions.
|
||||
- `WorkspaceStore`: create/update/delete/get/list by project, list session ids, distinct project workspaces.
|
||||
- `AccountStore`: active, list, get row, persist account, persist token, use account/org, remove.
|
||||
- `PermissionStore`: load and save project rules.
|
||||
- `ShareStore`: get, upsert, delete by session id.
|
||||
- `TodoStore`: replace all for session, list by session.
|
||||
- `MigrationStore`: completed marker get/insert, data migration helpers, JSON import helpers.
|
||||
- `AdminStore`: path, readonly raw query, local sqlite shell support flag.
|
||||
|
||||
## Transaction Model
|
||||
|
||||
There are two viable implementations.
|
||||
|
||||
### Option A: One Top-Level Service With Fiber-Local Transaction
|
||||
|
||||
The service exposes stores directly. Store methods inspect a fiber-local/current transaction and use it if present. `transaction(effect)` installs the transaction in that context.
|
||||
|
||||
Pros:
|
||||
|
||||
- Closest to current `Database.use` semantics.
|
||||
- Domain services do not need explicit transaction parameters.
|
||||
- Projectors can call the same store methods inside and outside transactions.
|
||||
|
||||
Cons:
|
||||
|
||||
- Requires careful implementation of fiber-local context and post-commit queues.
|
||||
- More ambient than explicit dependency passing.
|
||||
|
||||
### Option B: Explicit `Transaction` Service
|
||||
|
||||
`transaction(effect)` provides a separate `Transaction` context. Projector-only methods require `Transaction` in their environment.
|
||||
|
||||
Pros:
|
||||
|
||||
- More explicit. A method that must be transactional says so in the type.
|
||||
- Easier to prevent accidental out-of-transaction writes for projectors.
|
||||
|
||||
Cons:
|
||||
|
||||
- More churn at call sites.
|
||||
- Some methods may need both transactional and non-transactional variants.
|
||||
|
||||
Recommendation: start with Option A for migration ergonomics, but keep the internal implementation structured so projector methods can later move to explicit `Transaction` if needed.
|
||||
|
||||
## Error Model
|
||||
|
||||
Use typed storage errors at the boundary:
|
||||
|
||||
- `StorageError`: unknown backend failure, includes cause.
|
||||
- `NotFoundError`: domain-specific absence only where absence is exceptional.
|
||||
- `ConflictError`: uniqueness or stale write conflicts, if exposed.
|
||||
- `ForeignKeyError`: late event/projector writes where parent rows are already gone.
|
||||
- `UnsupportedAdminOperation`: non-SQLite backend cannot open sqlite shell or run raw SQL.
|
||||
|
||||
Avoid leaking Drizzle or SQLite error codes above the storage implementation. For current behavior, `ForeignKeyError` should allow projectors to keep ignoring late message/part updates intentionally.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. Add the storage service as a thin wrapper over existing Drizzle SQLite.
|
||||
2. Move V2 session/session-message read APIs behind `SessionStorage`. This is the smallest reversible slice and establishes the service pattern.
|
||||
3. Add write methods to `SessionStorage` only as `V2Session.create/prompt/shell/skill/compact/wait` become real.
|
||||
4. Move event-store operations after the read seam is proven. This is higher value but riskier because it owns sequencing, transactions, and post-commit side effects.
|
||||
5. Move projector writes behind stores, preserving transaction semantics.
|
||||
6. Move small repositories: share, todo, permission, account.
|
||||
7. Leave CLI/admin raw SQL as an explicit `AdminStore` escape hatch.
|
||||
8. Only after the domain boundary is in place, evaluate replacing the implementation with Drizzle Effect SQLite or another backend.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should V2 storage be per instance/workspace via `InstanceState`, or global per data directory like the current DB?
|
||||
- Should event projection become the only write path for sessions/messages, or should import/migration retain direct write APIs permanently?
|
||||
- Do we need a remote-capable store soon? If yes, raw admin SQL and SQLite shell must stay backend-specific from day one.
|
||||
- Should `session_message` replace legacy `message`/`part` for V2 context entirely, or do both need first-class APIs for the medium term?
|
||||
- Should cursor encoding live in the storage service or stay in domain modules while storage accepts decoded cursor structs?
|
||||
|
||||
## Initial Implementation Target
|
||||
|
||||
The first implementation PR should be small and reversible:
|
||||
|
||||
- Add `packages/opencode/src/v2/storage/session.ts` with the `SessionStorage` service shape.
|
||||
- Implement SQL and in-memory read backends for `get`, `list`, `messages`, and `context`.
|
||||
- Refactor `v2/session.ts` to consume `SessionStorage.Service` instead of raw Drizzle calls.
|
||||
- Keep table schemas and existing migrations unchanged.
|
||||
- Add generic contract tests that run against both storage implementations.
|
||||
|
||||
That gives us a concrete swappable seam without forcing every event/session/project/account query to move at once. Event-store extraction remains the next broader storage slice.
|
||||
Reference in New Issue
Block a user