Skip to content
Open
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
8 changes: 8 additions & 0 deletions src/app/(outerbase)/new-resource-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
SQLiteIcon,
} from "@/components/icons/outerbase-icon";
import {
ClickHouseIcon,
CloudflareIcon,
RQLiteIcon,
StarbaseIcon,
Expand Down Expand Up @@ -63,6 +64,13 @@ export function getCreateResourceTypeList(
? `/w/${workspaceId}/new-base/mysql`
: "/local/new-base/mysql",
},
{
name: "ClickHouse",
icon: ClickHouseIcon,
href: workspaceId
? `/w/${workspaceId}/new-base/clickhouse`
: "/local/new-base/clickhouse",
},
{
name: "SQLite",
icon: SQLiteIcon,
Expand Down
13 changes: 13 additions & 0 deletions src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { Studio } from "@/components/gui/studio";
import PageLoading from "@/components/page-loading";
import { StudioExtensionManager } from "@/core/extension-manager";
import {
createClickHouseExtensions,
createMySQLExtensions,
createPostgreSQLExtensions,
createSQLiteExtensions,
} from "@/core/standard-extension";
import ClickHouseLikeDriver from "@/drivers/clickhouse/clickhouse-driver";
import MySQLLikeDriver from "@/drivers/mysql/mysql-driver";
import PostgresLikeDriver from "@/drivers/postgres/postgres-driver";
import { SqliteLikeBaseDriver } from "@/drivers/sqlite-base-driver";
Expand Down Expand Up @@ -94,6 +96,17 @@ export default function OuterbaseSourcePage() {
...outerbaseSpecifiedDrivers,
]),
];
} else if (dialect === "clickhouse") {
return [
new ClickHouseLikeDriver(
new OuterbaseQueryable(outerbaseConfig),
credential.database
),
new StudioExtensionManager([
...createClickHouseExtensions(),
...outerbaseSpecifiedDrivers,
]),
];
}

return [
Expand Down
3 changes: 3 additions & 0 deletions src/app/(theme)/client/s/[[...driver]]/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { Studio } from "@/components/gui/studio";
import { StudioExtensionManager } from "@/core/extension-manager";
import {
createClickHouseExtensions,
createMySQLExtensions,
createPostgreSQLExtensions,
createSQLiteExtensions,
Expand Down Expand Up @@ -52,6 +53,8 @@ export default function ClientPageBody() {
return new StudioExtensionManager(createSQLiteExtensions());
} else if (dialet === "postgres") {
return new StudioExtensionManager(createPostgreSQLExtensions());
} else if (dialet === "clickhouse") {
return new StudioExtensionManager(createClickHouseExtensions());
}

return new StudioExtensionManager(createStandardExtensions());
Expand Down
3 changes: 2 additions & 1 deletion src/app/(theme)/connect/saved-connection-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export type SupportedDriver =
| "starbase"
| "cloudflare-d1"
| "cloudflare-wae"
| "sqlite-filehandler";
| "sqlite-filehandler"
| "clickhouse";

export type SavedConnectionStorage = "remote" | "local";
export type SavedConnectionLabel = "gray" | "red" | "yellow" | "green" | "blue";
Expand Down
82 changes: 82 additions & 0 deletions src/components/connection-config-editor/template/clickhouse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ConnectionTemplateList } from "@/app/(outerbase)/base-template";
import { GENERIC_CONNECTION_TEMPLATE } from "./generic";

function buildClickHouseUrl(
host?: string,
port?: string,
ssl?: boolean | string
): string {
const useSsl = ssl === true || ssl === "true";
const protocol = useSsl ? "https" : "http";
const defaultPort = useSsl ? "8443" : "8123";
const actualHost = (host ?? "localhost").replace(/^https?:\/\//, "");
const actualPort = port && port.length > 0 ? port : defaultPort;
return `${protocol}://${actualHost}:${actualPort}`;
}

function parseClickHouseUrl(url: string | undefined): {
host: string;
port: string;
ssl: boolean;
} {
if (!url) return { host: "", port: "8123", ssl: false };
try {
const parsed = new URL(url);
return {
host: parsed.hostname,
port: parsed.port || (parsed.protocol === "https:" ? "8443" : "8123"),
ssl: parsed.protocol === "https:",
};
} catch {
return { host: url, port: "8123", ssl: false };
}
}

export const ClickHouseConnectionTemplate: ConnectionTemplateList = {
template: GENERIC_CONNECTION_TEMPLATE,
remoteFrom: (value) => {
return {
name: value.name,
host: value.source.host,
username: value.source.user,
database: value.source.database,
port: value.source.port,
};
},
remoteTo: (value) => {
return {
name: value.name,
source: {
host: value.host,
user: value.username,
password: value.password,
database: value.database,
port: value.port,
type: "clickhouse",
base_id: "",
},
};
},
localFrom: (value) => {
const parsed = parseClickHouseUrl(value.url);
return {
name: value.name,
host: parsed.host,
port: parsed.port,
username: value.username,
password: value.password,
database: value.database,
ssl: parsed.ssl,
};
},
localTo: (value) => {
return {
name: value.name,
driver: "clickhouse",
url: buildClickHouseUrl(value.host, value.port, value.ssl),
username: value.username,
password: value.password,
database: value.database,
};
},
};
2 changes: 2 additions & 0 deletions src/components/connection-config-editor/template/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ConnectionTemplateList } from "@/app/(outerbase)/base-template";
import { ClickHouseConnectionTemplate } from "./clickhouse";
import { CloudflareConnectionTemplate } from "./cloudflare";
import { CloudflareWAEConnectionTemplate } from "./cloudflare-wae";
import { MySQLConnectionTemplate } from "./mysql";
Expand All @@ -25,4 +26,5 @@ export const ConnectionTemplateDictionary: Record<

mysql: MySQLConnectionTemplate,
postgres: PostgresConnectionTemplate,
clickhouse: ClickHouseConnectionTemplate,
};
17 changes: 15 additions & 2 deletions src/components/gui/tabs/query-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ import {
} from "@/lib/sql/multiple-query";
import { sendAnalyticEvents } from "@/lib/tracking";
import { cn } from "@/lib/utils";
import type { SupportedDialect as SdkSupportedDialect } from "@outerbase/sdk-transform";
import { tokenizeSql } from "@outerbase/sdk-transform";
import type { SupportedDialect } from "@/drivers/base-driver";

// The SDK's tokenizer/parser does not ship a ClickHouse grammar yet.
// ClickHouse's SQL surface — backtick-quoted identifiers, `--` comments,
// standard string literals — is closest to MySQL, so fall back to MySQL
// tokenization rules for dialects the SDK doesn't know about.
function toSdkDialect(d: SupportedDialect): SdkSupportedDialect {
return d === "clickhouse" ? "mysql" : d;
}
import { CaretDown } from "@phosphor-icons/react";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import {
Expand Down Expand Up @@ -96,7 +106,10 @@ export default function QueryWindow({
const timer = setTimeout(() => {
setPlaceholders((prev) => {
const newPlaceholders: Record<string, string> = {};
const token = tokenizeSql(code, databaseDriver.getFlags().dialect);
const token = tokenizeSql(
code,
toSdkDialect(databaseDriver.getFlags().dialect)
);

const foundPlaceholders = token
.filter((t) => t.type === "PLACEHOLDER")
Expand Down Expand Up @@ -168,7 +181,7 @@ export default function QueryWindow({
for (let i = 0; i < finalStatements.length; i++) {
const token = tokenizeSql(
finalStatements[i],
databaseDriver.getFlags().dialect
toSdkDialect(databaseDriver.getFlags().dialect)
);

// Defensive measurement
Expand Down
20 changes: 20 additions & 0 deletions src/components/resource-card/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ export const SupabaseIcon = ({ className }: IconProps) => (
</svg>
);

export const ClickHouseIcon = ({ className }: IconProps) => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
{/* Simplified placeholder glyph: 5 vertical bars evoking the
ClickHouse brand mark. Replace with the official SVG when
brand assets are added. */}
<rect x="3" y="3" width="3" height="18" fill="currentColor" />
<rect x="7.5" y="3" width="3" height="18" fill="currentColor" />
<rect x="12" y="3" width="3" height="18" fill="currentColor" />
<rect x="16.5" y="3" width="3" height="18" fill="currentColor" />
<rect x="21" y="10.5" width="3" height="3" fill="currentColor" />
</svg>
);

export const ValTownIcon = ({ className }: IconProps) => (
<svg
width="24"
Expand Down
9 changes: 8 additions & 1 deletion src/components/resource-card/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
SQLiteIcon,
TursoIcon,
} from "../icons/outerbase-icon";
import { CloudflareIcon, StarbaseIcon, ValTownIcon } from "./icon";
import {
ClickHouseIcon,
CloudflareIcon,
StarbaseIcon,
ValTownIcon,
} from "./icon";
import {
BoardVisual,
GeneralVisual,
Expand All @@ -33,6 +38,7 @@ export function getDatabaseFriendlyName(type: string) {
if (type === "bigquery") return "BigQuery";
if (type === "valtown") return "ValTown";
if (type === "board") return "Board";
if (type === "clickhouse") return "ClickHouse";

return type;
}
Expand All @@ -52,6 +58,7 @@ export function getDatabaseIcon(type: string) {
if (type === "rqlite") return RqliteIcon;
if (type === "sqlite") return SQLiteIcon;
if (type === "board") return ChartBar;
if (type === "clickhouse") return ClickHouseIcon;

return Database;
}
Expand Down
5 changes: 5 additions & 0 deletions src/core/standard-extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ export function createMySQLExtensions() {
export function createPostgreSQLExtensions() {
return createStandardExtensions();
}

export function createClickHouseExtensions() {
// ClickHouse has no trigger system, so skip TriggerEditorExtension.
return createStandardExtensions();
}
7 changes: 6 additions & 1 deletion src/drivers/base-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export function describeTableColumnType(type: ColumnType) {
}
}

export type SupportedDialect = "sqlite" | "mysql" | "postgres" | "dolt";
export type SupportedDialect =
| "sqlite"
| "mysql"
| "postgres"
| "dolt"
| "clickhouse";
export type SqlOrder = "ASC" | "DESC";
export type DatabaseRow = Record<string, unknown>;

Expand Down
Loading