Start With Your ClickHouse Database
Viewing
Early Access Feature
We’re actively developing this feature and would love your feedback. While it’s ready for most use cases, check out Known Limitations for details on what’s still in progress.
Why Use Moose with ClickHouse?
Moose provides a layer on top of your existing ClickHouse database to make it simpler to manage your tables, views, pipelines, and APIs in plain TypeScriptPython code:
Manually Managing ClickHouse?
Copy-pasting DDL between environments
Running the same CREATE TABLE and CREATE MATERIALIZED VIEW statements in dev, staging, and prod, hoping you didn't miss a field
Forgetting dependency order
Creating a materialized view before its source table exists, then scrambling to figure out what broke
Lost schema changes
That ALTER TABLE you ran last week? Good luck remembering what it was when you need to apply it to production
How Moose Can Speed Things Up:
Reduce boilerplate
Generate typed models from your existing tables instead of hand-coding everything
Build APIs in code, not Jinja templates
Write TypeScript/Python functions with proper IDE support instead of templated SQL
Add streaming with one line of code
Turn any table into a real-time streaming destination without configuring Kafka topics, serialization, or message schemas
Version control your database
Manage schema changes, materialized views, and transformations like actual code
Leverage CI/CD built for ClickHouse
See a plan for your database changes before promoting to production, and apply them automatically when you do
Quick Setup
Prerequisites
- Node.js: version 20+ (LTS recommended)
- Python: version 3.12+
- OS: macOS or Linux (WSL supported for Windows)
- Docker Desktop/Engine: 24.0.0+
- Access to ClickHouse Database: Connection URL with credentials
Get Your ClickHouse Connection String
You need a ClickHouse connection URL. Format looks like this:
http://username:password@host:port/?database=database_name
Try with ClickHouse Playground
Want to test without your own ClickHouse? Use the ClickHouse Playground with the connection string above. It has sample datasets (read-only) you can experiment with.
https://explorer:@play.clickhouse.com:443/?database=default
Install Moose & Bootstrap Your Project
Install Moose if you haven’t already:
# Install Moose
bash -i <(curl -fsSL https://fiveonefour.com/install.sh) moose
Create a new project:
# Bootstrap from your ClickHouse database
moose init my-project --from-remote <YOUR_CLICKHOUSE_CONNECTION_STRING> --language python
# Set up dependencies
cd my-project
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Bootstrap from your ClickHouse database
moose init my-project --from-remote <YOUR_CLICKHOUSE_CONNECTION_STRING> --language typescript
# Set up dependencies
cd my-project
npm install
Start Your Local Environment
moose dev
This spins up your entire data stack locally and generates typed models from your ClickHouse tables.
Check What Got Generated
Moose created data models for each of your ClickHouse tables. Check app/main.py
app/index.ts
to see what you got.
- main.py
- moose.config.toml
- requirements.txt
- setup.py
- index.ts
- moose.config.toml
- package.json
Type Mapping
Moose automatically maps ClickHouse data types to appropriate language-specific types:
ClickHouse Type | Python Type | Notes |
---|---|---|
String | str | |
Int8 , Int16 , Int32 , Int64 | int | With size annotations |
UInt8 , UInt16 , UInt32 , UInt64 | int | With size annotations |
Float32 , Float64 | float | With precision annotations |
Decimal(P,S) | Decimal | With precision/scale |
Date , Date32 | date | |
DateTime , DateTime64 | datetime | With precision for DateTime64 |
Array(T) | list[T] | Nested type mapping |
UUID | UUID | |
Boolean | bool | |
Nullable(T) | Optional[T] | |
LowCardinality(T) | T | Maps to base type |
JSON | any | Partial support |
ClickHouse Type | TypeScript Type | Notes |
---|---|---|
String | string | |
Int8 , Int16 , Int32 , Int64 | number | |
UInt8 , UInt16 , UInt32 , UInt64 | number | |
Float32 , Float64 | number | |
Decimal(P,S) | number | |
Date , Date32 | Date | |
DateTime , DateTime64 | Date | |
Array(T) | T[] | Nested type mapping |
UUID | string | |
Boolean | boolean | |
Nullable(T) | T | null | |
LowCardinality(T) | T | Maps to base type |
JSON | Record<string, any> | Partial support |
What’s Next?
Now that you’ve got Moose talking to your ClickHouse database:
1. Get Some Data Locally
# CLI command to seed from remote ClickHouse
moose seed from-remote-clickhouse <YOUR_CLICKHOUSE_CONNECTION_STRING> --rows 100
2. Build Materialized Views That Don’t Break
Moose lets you define materialized views in code with automatic schema management and dependency orchestration:
What you’re probably doing now:
-- Step 1: Create target table (remember the exact schema!)
CREATE TABLE daily_user_stats (
user_id UInt64,
date Date,
event_count UInt64
) ENGINE = MergeTree()
ORDER BY (user_id, date);
-- Step 2: Create materialized view (hope you got the dependency order right!)
CREATE MATERIALIZED VIEW daily_user_stats_mv TO daily_user_stats AS
SELECT
user_id,
toDate(timestamp) as date,
count(*) as event_count
FROM user_events
GROUP BY user_id, date;
-- Step 3: Copy-paste this to staging... then production...
-- Step 4: Pray you didn't miss anything when the schema changes
-- Step 5: Remember to DROP VIEW before changing SELECT logic
With Moose:
import { userEvents } from "./tables"; // Import existing tables
import { MaterializedView, sql } from "@514labs/moose-lib";
// Define your target schema
interface DailyUserStats {
user_id: number;
date: Date;
event_count: number;
}
// Define what you want - Moose generates all the DDL
const dailyStats = new MaterializedView<DailyUserStats>({
// ✅ SQL in actual code, directly reference table names and columns for autocompletion
selectStatement: sql`
SELECT
${userEvents.user_id},
toDate(${userEvents.timestamp}) as date,
count(*) as event_count
FROM ${userEvents}
GROUP BY ${userEvents.user_id}, date
`,
selectTables: [userEvents],
tableName: "daily_user_stats",
materializedViewName: "daily_user_stats_mv",
orderByFields: ["user_id", "date"]
});
// Moose automatically:
// ✅ Creates the target table with correct schema
// ✅ Sets up the materialized view
// ✅ Handles dependency order
// ✅ Manages schema migrations
// ✅ Provides hot-reload in development
from moose_lib import MaterializedView, OlapTable, MaterializedViewOptions
from pydantic import BaseModel
from datetime import date
from app.tables import user_events ## Import existing tables
## Define your target schema
class DailyUserStats(BaseModel):
user_id: int
date: date
event_count: int
# Define what you want - Moose generates all the DDL
daily_stats = MaterializedView[DailyUserStats](
MaterializedViewOptions(
select_statement="""
SELECT user_id, toDate(timestamp) as date, count(*) as event_count
FROM user_events GROUP BY user_id, date
""",
select_tables=[user_events],
table_name="daily_user_stats",
materialized_view_name="daily_user_stats_mv",
order_by_fields=["user_id", "date"]
)
)
# Moose automatically:
# ✅ Creates the target table with correct schema
# ✅ Sets up the materialized view
# ✅ Handles dependency order
# ✅ Manages schema migrations
# ✅ Provides hot-reload in development
Ready to dive deeper? → Materialized Views docs
3. Build APIs That Handle the Annoying Stuff
Consumption APIs are a way to write native TypeScriptPython functions that wrap your ClickHouse queries and make them available for your frontend clients via a type-safe REST API.
They are designed to take care of the boilerplate that slows you down like:
- Request validation
- Response type mapping
- SQL injection protection
- Documentation
- JWT validation (if you’re using it)
Ready to dive deeper? → Consumption APIs docs
4. Use AI to Generate Analytics & APIs
Want AI to generate insights and APIs from your data? Try Aurora - our AI analytics agent for Moose projects.
Current Limitations
What we’re working on adding:
- Selective table import: Imports all tables (filtering coming soon)
- Default values: Adding annotations to your models to set default values for your tables
- Advanced ClickHouse types:
Map(K, V)
typesTuple(T1, T2, ...)
typesGeo
types (Point, Ring, Polygon, etc.)
Tables with these types will be skipped
Right now, tables featuring these types will be skipped during the initialization process.
Known Limitations
Supported Types with Manual Type Adjustments:
LowCardinality(T)
Currently, LowCardinality(T)
maps to base type T
.
You can override the autogenerated data models by making the following adjustments to fields that are LowCardinality
in your generated code:
You can add LowCardinality optimization by using intersection types:
interface MyModel {
status: string & LowCardinality; // Optimized for low cardinality
category: string; // Regular string
}
LowCardinality is automatically applied to Literal
types:
from typing import Literal
class MyModel(BaseModel):
status: Literal["active", "inactive", "pending"] # Automatically
category: str # Regular string
JSON
types
Maps to any
Maps to Record<string, any>
Enum
types
Enum column types that have an empty string as a key will be skipped:
CREATE TABLE my_table (
my_enum Enum8(""=0, "A"=1, "B"=2)
);
## This does not work:
class my_enum(StringToEnumMixin, IntEnum):
"" = 0 ## Illegal to have an empty string as a key
A = 1
B = 2
## Instead what will be generated is:
class my_enum(StringToEnumMixin, IntEnum):
= 0 ## Will lead to a type error, consider using a different key
A = 1
B = 2
In Python, using a numeric key in a string enum will lead to a type error:
CREATE TABLE my_table (
my_enum Enum8("1"=0, "A"=1, "B"=2)
);
class my_enum(StringToEnumMixin, IntEnum):
1 = 0 ## Will lead to a type error, consider using a different key
A = 1
B = 2
Learning & Community Resources
Get help, learn, contribute, and stay connected with the Moose community