clisma 0.1.0 โ 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# ๐ clisma
|
|
2
|
+
|
|
3
|
+
**A ClickHouse migrations CLI with templated SQL and environment-aware config.**
|
|
4
|
+
|
|
5
|
+
<small>_"clisma" is a mashup of ClickHouse + Prisma. A dumb pun, but it stuck._</small>
|
|
6
|
+
|
|
7
|
+
This project borrows ideas from tools we like:
|
|
8
|
+
|
|
9
|
+
- **[Atlas](https://atlasgo.io/)** for the idea of [templated migrations](https://atlasgo.io/concepts/migrations#template) and [config-driven environments](https://atlasgo.io/concepts/dev-database).
|
|
10
|
+
|
|
11
|
+
- **[Prisma](https://www.prisma.io/)** for the simple, friendly CLI experience.
|
|
12
|
+
|
|
13
|
+
### So why it exists?
|
|
14
|
+
|
|
15
|
+
- **Templates in migrations** โ Atlas has this, but it is paid; clisma keeps it simple and open.
|
|
16
|
+
- **Multi-statement migrations** โ write real SQL without splitting into tiny files.
|
|
17
|
+
- **Declarative environments** โ keep local/staging/prod configs in one place.
|
|
18
|
+
|
|
19
|
+
## ๐ฆ How to use it
|
|
20
|
+
|
|
21
|
+
### Global installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g clisma
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### NPM
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --save-dev clisma
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### NPX
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx clisma
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ๐ Quickstart
|
|
40
|
+
|
|
41
|
+
Create `clisma.hcl`:
|
|
42
|
+
|
|
43
|
+
```hcl
|
|
44
|
+
env "local" {
|
|
45
|
+
url = "http://default:password@localhost:8123/mydb"
|
|
46
|
+
|
|
47
|
+
migrations {
|
|
48
|
+
dir = "migrations"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run migrations:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
clisma run --env local
|
|
57
|
+
clisma status --env local
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## ๐งฉ Config basics
|
|
61
|
+
|
|
62
|
+
- `env "name"` defines an environment.
|
|
63
|
+
- `migrations` holds migration settings.
|
|
64
|
+
- `variable "name"` defines inputs for `var.*`.
|
|
65
|
+
- `env("NAME")` reads environment variables.
|
|
66
|
+
|
|
67
|
+
### Example with variables and templates
|
|
68
|
+
|
|
69
|
+
```hcl
|
|
70
|
+
variable "ttl_days" {
|
|
71
|
+
type = string
|
|
72
|
+
default = "30"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
env "production" {
|
|
76
|
+
url = env("CLICKHOUSE_PROD_URL")
|
|
77
|
+
cluster_name = "prod-cluster"
|
|
78
|
+
|
|
79
|
+
migrations {
|
|
80
|
+
dir = "migrations"
|
|
81
|
+
vars = {
|
|
82
|
+
is_replicated = true
|
|
83
|
+
create_table_options = "ON CLUSTER prod-cluster"
|
|
84
|
+
ttl_days = var.ttl_days
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**`cluster_name`** affects how the migrations tracking table is created (replicated or not). And the CLI will warn if the actual cluster does not match the config.
|
|
91
|
+
|
|
92
|
+
#### If your ClickHouse server has clusters configured, `cluster_name` is required
|
|
93
|
+
|
|
94
|
+
## ๐งช Templates
|
|
95
|
+
|
|
96
|
+
Templates are [Handlebars](https://handlebarsjs.com/guide/expressions.html). Variables come from `migrations.vars` (and
|
|
97
|
+
`cluster_name` is available as `{{cluster_name}}`).
|
|
98
|
+
|
|
99
|
+
```sql
|
|
100
|
+
CREATE TABLE IF NOT EXISTS events {{create_table_options}} (
|
|
101
|
+
id UUID,
|
|
102
|
+
created_at DateTime DEFAULT now()
|
|
103
|
+
)
|
|
104
|
+
{{#if is_replicated}}
|
|
105
|
+
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/events', '{replica}')
|
|
106
|
+
{{else}}
|
|
107
|
+
ENGINE = MergeTree()
|
|
108
|
+
{{/if}}
|
|
109
|
+
ORDER BY id;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Multi-statement migrations are supported (split on semicolons outside strings/comments).
|
|
113
|
+
|
|
114
|
+
## ๐ ๏ธ CLI
|
|
115
|
+
|
|
116
|
+
Common commands:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
clisma run --env local
|
|
120
|
+
clisma status --env local
|
|
121
|
+
clisma create --name create_events
|
|
122
|
+
clisma checksum ./migrations/20240101123045_create_events.sql
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Additional flags
|
|
126
|
+
|
|
127
|
+
- `--config <path>`
|
|
128
|
+
- `--env <name>`
|
|
129
|
+
- `--env-file <path>`
|
|
130
|
+
- `--var <key=value>` (repeatable)
|
|
131
|
+
|
|
132
|
+
The CLI requires a config file. Use `--config` or place `clisma.hcl` in the current directory.
|
|
133
|
+
|
|
134
|
+
Example with variables and env file:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
clisma run --env local --var ttl_days=30 --env-file .env
|
|
138
|
+
```
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"integration.test.d.ts","sourceRoot":"","sources":["../../src/tests/integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
const exec = promisify(execFile);
|
|
10
|
-
const isDockerAvailable = async () => {
|
|
11
|
-
try {
|
|
12
|
-
await exec("docker", ["--version"]);
|
|
13
|
-
await exec("docker", ["compose", "version"]);
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
const waitForClickHouse = async (baseUrl) => {
|
|
21
|
-
const deadline = Date.now() + 30_000;
|
|
22
|
-
while (Date.now() < deadline) {
|
|
23
|
-
try {
|
|
24
|
-
const response = await fetch(`${baseUrl}/ping`);
|
|
25
|
-
if (response.ok) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// Ignore until ready.
|
|
31
|
-
}
|
|
32
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
33
|
-
}
|
|
34
|
-
throw new Error("ClickHouse did not become ready in time");
|
|
35
|
-
};
|
|
36
|
-
const runCli = async (repoRoot, args) => {
|
|
37
|
-
const cliPath = path.join(repoRoot, "packages/cli/src/cli.ts");
|
|
38
|
-
await exec("node", ["--import", "tsx", cliPath, ...args], { cwd: repoRoot });
|
|
39
|
-
};
|
|
40
|
-
const queryClickHouse = async (baseUrl, query) => {
|
|
41
|
-
const response = await fetch(`${baseUrl}/?query=${encodeURIComponent(query)}`);
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
const text = await response.text();
|
|
44
|
-
throw new Error(`ClickHouse query failed: ${text}`);
|
|
45
|
-
}
|
|
46
|
-
return response.text();
|
|
47
|
-
};
|
|
48
|
-
test("cli applies migrations against ClickHouse", async (t) => {
|
|
49
|
-
if (!(await isDockerAvailable())) {
|
|
50
|
-
t.skip("Docker not available");
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
|
|
54
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clisma-it-"));
|
|
55
|
-
const migrationsDir = path.join(tempDir, "migrations");
|
|
56
|
-
await fs.mkdir(migrationsDir, { recursive: true });
|
|
57
|
-
const baseUrl = "http://localhost:8123";
|
|
58
|
-
const dbName = `clisma_it_${Date.now()}`;
|
|
59
|
-
await exec("docker", ["compose", "up", "-d"], { cwd: repoRoot });
|
|
60
|
-
t.after(async () => {
|
|
61
|
-
try {
|
|
62
|
-
await exec("docker", ["compose", "down", "-v"], { cwd: repoRoot });
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// Ignore cleanup failures.
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
await waitForClickHouse(baseUrl);
|
|
69
|
-
await queryClickHouse(baseUrl, `CREATE DATABASE IF NOT EXISTS ${dbName}`);
|
|
70
|
-
const configPath = path.join(tempDir, "clisma.hcl");
|
|
71
|
-
await fs.writeFile(configPath, `env "local" {\n url = "http://default:@localhost:8123/${dbName}"\n\n migrations {\n dir = "migrations"\n }\n}\n`, "utf8");
|
|
72
|
-
const migrationFile = path.join(migrationsDir, "20240101120000_create_table.sql");
|
|
73
|
-
await fs.writeFile(migrationFile, "CREATE TABLE IF NOT EXISTS test_table (id UInt64) ENGINE = MergeTree() ORDER BY id;", "utf8");
|
|
74
|
-
await runCli(repoRoot, ["run", "--config", configPath, "--env", "local"]);
|
|
75
|
-
const tableExists = await queryClickHouse(baseUrl, `EXISTS TABLE ${dbName}.test_table`);
|
|
76
|
-
assert.equal(tableExists.trim(), "1");
|
|
77
|
-
const migrationsCount = await queryClickHouse(baseUrl, `SELECT count() FROM ${dbName}.schema_migrations`);
|
|
78
|
-
assert.equal(migrationsCount.trim(), "1");
|
|
79
|
-
await queryClickHouse(baseUrl, `DROP DATABASE IF EXISTS ${dbName}`);
|
|
80
|
-
});
|