Skip to content

feat: sqlite support #796

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dev": "npx turbo watch dev",
"build": "npx turbo build",
"test": "npx turbo test",
"test:watch": "npx turbo watch test",
"check-types": "npx turbo check-types",
"fmt": "yarn biome check --write .",
"dev-docs": "cd docs && yarn dlx mintlify@latest dev",
Expand All @@ -28,6 +29,7 @@
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/ws": "^8.5.14",
"better-sqlite3": "^11.9.1",
"dedent": "^1.5.3",
"lefthook": "^1.6.12",
"turbo": "^2.0.1",
Expand Down
14 changes: 13 additions & 1 deletion packages/actor-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@
"default": "./dist/actor/errors.cjs"
}
},
"./sql": {
"import": {
"types": "./dist/actor/sql/mod.d.ts",
"default": "./dist/actor/sql/mod.js"
},
"require": {
"types": "./dist/actor/sql/mod.d.cts",
"default": "./dist/actor/sql/mod.cjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils.d.ts",
Expand Down Expand Up @@ -135,7 +145,7 @@
},
"sideEffects": false,
"scripts": {
"build": "tsup src/mod.ts src/client/mod.ts src/common/log.ts src/actor/errors.ts src/topologies/coordinate/mod.ts src/topologies/partition/mod.ts src/utils.ts src/driver-helpers/mod.ts src/cli/mod.ts src/actor/protocol/inspector/mod.ts src/actor/protocol/http/rpc.ts src/test/mod.ts",
"build": "tsup src/mod.ts src/client/mod.ts src/common/log.ts src/actor/errors.ts src/actor/sql/mod.ts src/topologies/coordinate/mod.ts src/topologies/partition/mod.ts src/utils.ts src/driver-helpers/mod.ts src/cli/mod.ts src/actor/protocol/inspector/mod.ts src/actor/protocol/http/rpc.ts src/test/mod.ts",
"check-types": "tsc --noEmit",
"boop": "tsc --outDir dist/test -d",
"test": "vitest run",
Expand All @@ -153,6 +163,8 @@
"@types/invariant": "^2",
"@types/node": "^22.13.1",
"@types/ws": "^8",
"drizzle-kit": "^0.30.5",
"drizzle-orm": "^0.41.0",
"eventsource": "^3.0.5",
"tsup": "^8.4.0",
"typescript": "^5.7.3",
Expand Down
6 changes: 5 additions & 1 deletion packages/actor-core/src/actor/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { ActorTags } from "@/common/utils";
import type { Schedule } from "./schedule";
import type { ConnId } from "./connection";
import type { SaveStateOptions } from "./instance";
import { Actions } from "./config";
import { ActorContext } from "./context";
import { SqlConnection } from "@/actor/sql/mod";

/**
* Context for a remote procedure call.
Expand Down Expand Up @@ -36,6 +36,10 @@ export class ActionContext<S, CP, CS, V> {
return this.#actorContext.state;
}

get sql(): SqlConnection {
return this.#actorContext.sql
}

/**
* Get the actor variables
*/
Expand Down
12 changes: 10 additions & 2 deletions packages/actor-core/src/actor/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const ActorConfigSchema = z
onBeforeActionResponse: z.function().optional(),
actions: z.record(z.function()),
state: z.any().optional(),
sql: z.boolean().default(false),
createState: z.function().optional(),
connState: z.any().optional(),
createConnState: z.function().optional(),
Expand Down Expand Up @@ -81,7 +82,11 @@ export interface OnConnectOptions<CP> {
// This must have only one or the other or else S will not be able to be inferred
type CreateState<S, CP, CS, V> =
| { state: S }
| { createState: (c: ActorContext<undefined, undefined, undefined, undefined>) => S | Promise<S> }
| {
createState: (
c: ActorContext<undefined, undefined, undefined, undefined>,
) => S | Promise<S>;
}
| Record<never, never>;

// Creates connection state config
Expand Down Expand Up @@ -114,7 +119,10 @@ type CreateVars<S, CP, CS, V> =
/**
* @experimental
*/
createVars: (c: ActorContext<undefined, undefined, undefined, undefined>, driverCtx: unknown) => V | Promise<V>;
createVars: (
c: ActorContext<undefined, undefined, undefined, undefined>,
driverCtx: unknown,
) => V | Promise<V>;
}
| Record<never, never>;

Expand Down
5 changes: 5 additions & 0 deletions packages/actor-core/src/actor/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ActorInstance, SaveStateOptions } from "./instance";
import { Conn, ConnId } from "./connection";
import { ActorTags } from "@/common/utils";
import { Schedule } from "./schedule";
import { SqlConnection } from "@/actor/sql/mod";


/**
Expand All @@ -23,6 +24,10 @@ export class ActorContext<S, CP, CS, V> {
return this.#actor.state;
}

get sql(): SqlConnection {
return this.#actor.sql;
}

/**
* Get the actor variables
*/
Expand Down
5 changes: 4 additions & 1 deletion packages/actor-core/src/actor/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import type * as messageToClient from "@/actor/protocol/message/to-client";
import type { CachedSerializer } from "@/actor/protocol/serde";
import type { AnyActorInstance } from "./instance";
import { AnyConn } from "./connection";
import type { SqlConnection } from "./sql/mod";

export type ConnDrivers = Record<string, ConnDriver>;

export type KvKey = unknown[];
export type KvValue = unknown;


export interface ActorDriver {
//load(): Promise<LoadOutput>;
get context(): unknown;

// SQL connection
createSqlConnection?(): SqlConnection;

// HACK: Clean these up
kvGet(actorId: string, key: KvKey): Promise<KvValue | undefined>;
kvGetBatch(actorId: string, key: KvKey[]): Promise<(KvValue | undefined)[]>;
Expand Down
18 changes: 18 additions & 0 deletions packages/actor-core/src/actor/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ export class StateNotEnabled extends ActorError {
}
}

export class SqlNotEnabled extends ActorError {
constructor() {
super(
"sql_not_enabled",
"SQL not enabled.",
);
}
}

export class ConnStateNotEnabled extends ActorError {
constructor() {
super(
Expand Down Expand Up @@ -164,6 +173,15 @@ export class Unsupported extends ActorError {
}
}

export class DriverDoesNotSupportSql extends ActorError {
constructor() {
super(
"driver_does_not_support_sql",
"Driver does not support SQL. Check https://actorcore.org for supported platforms.",
);
}
}

/**
* Options for the UserError class.
*/
Expand Down
26 changes: 26 additions & 0 deletions packages/actor-core/src/actor/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CachedSerializer } from "./protocol/serde";
import { Inspector } from "@/actor/inspect";
import { ActorContext } from "./context";
import invariant from "invariant";
import { SqlConnection } from "./sql/mod";

/**
* Options for the `_saveState` method.
Expand Down Expand Up @@ -97,6 +98,8 @@ export class ActorInstance<S, CP, CS, V> {
/** Raw state without the proxy wrapper */
#persistRaw!: PersistedActor<S, CP, CS>;

#sqlConn?: SqlConnection;

#writePersistLock = new Lock<void>(void 0);

#lastSaveTime = 0;
Expand Down Expand Up @@ -158,6 +161,13 @@ export class ActorInstance<S, CP, CS, V> {
this.#schedule = new Schedule(this, actorDriver);
this.inspector = new Inspector(this);

// Create SQL connection
if (this.#config.sql) {
if (!this.#actorDriver.createSqlConnection) throw new errors.DriverDoesNotSupportSql();
this.#sqlConn = this.#actorDriver.createSqlConnection();
}


// Initialize server
//
// Store the promise so network requests can await initialization
Expand Down Expand Up @@ -214,6 +224,16 @@ export class ActorInstance<S, CP, CS, V> {
}
}

get sqlEnabled() {
return this.#config.sql;
}

#validateSqlEnabled() {
if (!this.sqlEnabled) {
throw new errors.SqlNotEnabled();
}
}

get #connStateEnabled() {
return "createConnState" in this.#config || "connState" in this.#config;
}
Expand Down Expand Up @@ -846,6 +866,12 @@ export class ActorInstance<S, CP, CS, V> {
return this.#persist.s;
}

get sql(): SqlConnection {
this.#validateSqlEnabled();
invariant(this.#sqlConn !== undefined, "#sqlConn is undefined");
return this.#sqlConn;
}

/**
* Sets the current state.
*
Expand Down
8 changes: 8 additions & 0 deletions packages/actor-core/src/actor/sql/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface SqlConnection {
// TODO: Remove this
HACK_raw: unknown;

// TODO: Find methods that are required for Drizzle-compat
// TODO: Support both sync and async
}

14 changes: 13 additions & 1 deletion packages/actor-core/src/test/driver/actor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { ActorDriver, KvKey, KvValue, AnyActorInstance } from "@/driver-helpers/mod";
import type {
ActorDriver,
KvKey,
KvValue,
AnyActorInstance,
} from "@/driver-helpers/mod";
import type { TestGlobalState } from "./global_state";
import { SqlConnection } from "@/actor/sql/mod";
import Database from "better-sqlite3";
import { TestSqlConnection } from "./sql";

export interface ActorDriverContext {
state: TestGlobalState;
Expand All @@ -16,6 +24,10 @@ export class TestActorDriver implements ActorDriver {
return { state: this.#state };
}

createSqlConnection(): SqlConnection {
return new TestSqlConnection(new Database(":memory:"));
}

async kvGet(actorId: string, key: KvKey): Promise<KvValue | undefined> {
const serializedKey = this.#serializeKey(key);
const value = this.#state.getKv(actorId, serializedKey);
Expand Down
15 changes: 15 additions & 0 deletions packages/actor-core/src/test/driver/sql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SqlConnection } from "@/actor/sql/mod";
import { Database } from "better-sqlite3";

export class TestSqlConnection implements SqlConnection {
#raw: Database;

// TODO: This is a temporary hack for Drizzle
get HACK_raw(): unknown {
return this.#raw;
}

constructor(db: Database) {
this.#raw = db;
}
}
7 changes: 7 additions & 0 deletions packages/actor-core/tests/schemas/chat-room/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
out: "./tests/schemas/chat-room/drizzle",
dialect: "sqlite",
schema: "./tests/schemas/chat-room/schema.ts",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE `messages` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`username` text,
`message` text
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_messages` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`username` text NOT NULL,
`message` text NOT NULL
);
--> statement-breakpoint
INSERT INTO `__new_messages`("id", "username", "message") SELECT "id", "username", "message" FROM `messages`;--> statement-breakpoint
DROP TABLE `messages`;--> statement-breakpoint
ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint
PRAGMA foreign_keys=ON;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `messages` ADD `createdAt` integer DEFAULT (unixepoch() * 1000) NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX `created_at_idx` ON `messages` (`createdAt`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"version": "6",
"dialect": "sqlite",
"id": "78d399d7-9b51-4e04-ad31-1c3dc909e24a",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"messages": {
"name": "messages",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"message": {
"name": "message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
Loading
Loading