@clisma/core 0.1.0

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.
@@ -0,0 +1,5 @@
1
+ export { MigrationRunner, runMigrations } from "./migrations/runner.js";
2
+ export type { MigrationCommand, MigrationRunnerOptions, } from "./migrations/types.js";
3
+ export { findConfigFile, parseConfig } from "@clisma/config";
4
+ export type { ClismaConfig, EnvConfig, MigrationConfig, VariableConfig, } from "@clisma/config";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACxE,YAAY,EACV,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7D,YAAY,EACV,YAAY,EACZ,SAAS,EACT,eAAe,EACf,cAAc,GACf,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { MigrationRunner, runMigrations } from "./migrations/runner.js";
2
+ export { findConfigFile, parseConfig } from "@clisma/config";
@@ -0,0 +1,19 @@
1
+ import type { ClickHouseClient } from "@clickhouse/client";
2
+ import type { MigrationContext, MigrationRecord, PendingMigrationsResult } from "./types.js";
3
+ type MigrationRepositoryOptions = {
4
+ client: ClickHouseClient;
5
+ migrationsDir: string;
6
+ tableName: string;
7
+ replicationPath?: string;
8
+ };
9
+ export declare class MigrationRepository {
10
+ #private;
11
+ constructor(options: MigrationRepositoryOptions);
12
+ getContext(): MigrationContext;
13
+ initialize(clusterName?: string): Promise<string | null>;
14
+ ensureMigrationsTable(): Promise<void>;
15
+ getAppliedMigrations(): Promise<Map<string, MigrationRecord>>;
16
+ getPendingMigrations(applied: Map<string, MigrationRecord>): Promise<PendingMigrationsResult>;
17
+ }
18
+ export {};
19
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/migrations/repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EAEf,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAGpB,KAAK,0BAA0B,GAAG;IAChC,MAAM,EAAE,gBAAgB,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,qBAAa,mBAAmB;;gBAOlB,OAAO,EAAE,0BAA0B;IAO/C,UAAU,IAAI,gBAAgB;IAQxB,UAAU,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA8DxD,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BtC,oBAAoB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAgB7D,oBAAoB,CACxB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GACpC,OAAO,CAAC,uBAAuB,CAAC;CA8DpC"}
@@ -0,0 +1,142 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import kleur from "kleur";
4
+ import { calculateChecksum } from "./sql.js";
5
+ export class MigrationRepository {
6
+ #client;
7
+ #ctx = null;
8
+ #migrationsDir;
9
+ #tableName;
10
+ #replicationPath;
11
+ constructor(options) {
12
+ this.#client = options.client;
13
+ this.#migrationsDir = options.migrationsDir;
14
+ this.#tableName = options.tableName;
15
+ this.#replicationPath = options.replicationPath;
16
+ }
17
+ getContext() {
18
+ if (!this.#ctx) {
19
+ throw new Error("Migration repository not initialized");
20
+ }
21
+ return this.#ctx;
22
+ }
23
+ async initialize(clusterName) {
24
+ if (this.#ctx) {
25
+ return this.#ctx.cluster || null;
26
+ }
27
+ const clusters = await this.#listClusters();
28
+ const clusterNames = Array.from(new Set(clusters.map((cluster) => cluster.cluster)));
29
+ const hasNonDefaultCluster = clusterNames.some((name) => name !== "default");
30
+ const defaultClusterRows = clusters.filter((cluster) => cluster.cluster === "default");
31
+ const defaultHasReplicasOrShards = defaultClusterRows.some((row) => row.replica_num > 1 || row.shard_num > 1);
32
+ if (!clusterName) {
33
+ if (hasNonDefaultCluster || defaultHasReplicasOrShards) {
34
+ const available = clusterNames.length
35
+ ? clusterNames.join(", ")
36
+ : "none";
37
+ throw new Error(`Cluster detected but no cluster_name provided. ` +
38
+ `Set env.cluster_name in config. Available clusters: ${available}`);
39
+ }
40
+ }
41
+ else if (!clusterNames.includes(clusterName)) {
42
+ const available = clusterNames.length ? clusterNames.join(", ") : "none";
43
+ throw new Error(`Cluster "${clusterName}" not found. Available clusters: ${available}`);
44
+ }
45
+ if (clusterName) {
46
+ const safeClusterName = clusterName.replace(/"/g, '\\"');
47
+ this.#ctx = {
48
+ is_replicated: true,
49
+ create_table_options: `ON CLUSTER "${safeClusterName}"`,
50
+ cluster: clusterName,
51
+ };
52
+ return clusterName;
53
+ }
54
+ this.#ctx = {
55
+ is_replicated: false,
56
+ create_table_options: "",
57
+ cluster: "",
58
+ };
59
+ return null;
60
+ }
61
+ async ensureMigrationsTable() {
62
+ const ctx = this.getContext();
63
+ const replicationPath = this.#replicationPath
64
+ ? this.#replicationPath
65
+ : `/clickhouse/tables/cluster-{cluster}/shard-{shard}/{database}/${this.#tableName}`;
66
+ const engine = ctx.is_replicated
67
+ ? `ReplicatedReplacingMergeTree('${replicationPath}', '{replica}')`
68
+ : "ReplacingMergeTree()";
69
+ const tableOptions = ctx.create_table_options || "";
70
+ await this.#client.command({
71
+ query: `
72
+ CREATE TABLE IF NOT EXISTS ${this.#tableName} ${tableOptions} (
73
+ version String,
74
+ name String,
75
+ checksum String,
76
+ hostname String DEFAULT '',
77
+ applied_by String DEFAULT '',
78
+ cli_version String DEFAULT '',
79
+ applied_at DateTime DEFAULT now()
80
+ ) ENGINE = ${engine}
81
+ ORDER BY version
82
+ `,
83
+ });
84
+ }
85
+ async getAppliedMigrations() {
86
+ const result = await this.#client.query({
87
+ query: `
88
+ SELECT version, name, checksum, hostname, applied_by, cli_version, applied_at
89
+ FROM ${this.#tableName} FINAL
90
+ ORDER BY version`,
91
+ format: "JSONEachRow",
92
+ });
93
+ const migrations = await result.json();
94
+ return new Map(migrations.map((migration) => [migration.version, migration]));
95
+ }
96
+ async getPendingMigrations(applied) {
97
+ const files = (await fs.readdir(this.#migrationsDir))
98
+ .filter((file) => file.endsWith(".sql"))
99
+ .sort();
100
+ const pending = [];
101
+ for (const file of files) {
102
+ const match = file.match(/^(\d+)_(.+)\.sql$/);
103
+ if (!match) {
104
+ console.warn(kleur.yellow(`Skipping invalid filename: ${file}`));
105
+ continue;
106
+ }
107
+ const [, version, name] = match;
108
+ const filePath = path.join(this.#migrationsDir, file);
109
+ const content = await fs.readFile(filePath, "utf8");
110
+ const checksum = calculateChecksum(content);
111
+ const appliedMigration = applied.get(version);
112
+ if (!appliedMigration) {
113
+ pending.push({ version, name, file, content, checksum });
114
+ continue;
115
+ }
116
+ if (appliedMigration.checksum !== checksum) {
117
+ return {
118
+ pending: [],
119
+ error: new Error(`Migration ${version}_${name} has been modified. ` +
120
+ `Expected checksum: ${appliedMigration.checksum}. ` +
121
+ `Actual checksum: ${checksum}. `),
122
+ };
123
+ }
124
+ }
125
+ return { pending, error: null };
126
+ }
127
+ async #listClusters() {
128
+ try {
129
+ const result = await this.#client.query({
130
+ query: `
131
+ SELECT cluster, shard_num, replica_num
132
+ FROM system.clusters
133
+ `,
134
+ format: "JSONEachRow",
135
+ });
136
+ return await result.json();
137
+ }
138
+ catch {
139
+ return [];
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,10 @@
1
+ import type { MigrationCommand, MigrationRunnerOptions } from "./types.js";
2
+ export declare class MigrationRunner {
3
+ #private;
4
+ constructor(options: MigrationRunnerOptions);
5
+ run(): Promise<void>;
6
+ status(): Promise<void>;
7
+ close(): Promise<void>;
8
+ }
9
+ export declare const runMigrations: (options: MigrationRunnerOptions, command: MigrationCommand) => Promise<void>;
10
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/migrations/runner.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AA4B3E,qBAAa,eAAe;;gBASd,OAAO,EAAE,sBAAsB;IAwDrC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA+EpB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED,eAAO,MAAM,aAAa,GACxB,SAAS,sBAAsB,EAC/B,SAAS,gBAAgB,KACxB,OAAO,CAAC,IAAI,CAYd,CAAC"}
@@ -0,0 +1,175 @@
1
+ import { createClient } from "@clickhouse/client";
2
+ import ora from "ora";
3
+ import kleur from "kleur";
4
+ import os from "node:os";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { splitStatements } from "./sql.js";
9
+ import { renderTemplate } from "./template.js";
10
+ import { MigrationRepository } from "./repository.js";
11
+ const resolveAppliedBy = () => {
12
+ try {
13
+ return os.userInfo().username || process.env.USER || "";
14
+ }
15
+ catch {
16
+ return process.env.USER || "";
17
+ }
18
+ };
19
+ const resolveCliVersion = async () => {
20
+ try {
21
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
22
+ const packagePath = path.resolve(currentDir, "../../package.json");
23
+ const contents = await fs.readFile(packagePath, "utf8");
24
+ const parsed = JSON.parse(contents);
25
+ return parsed.version || "";
26
+ }
27
+ catch {
28
+ return "";
29
+ }
30
+ };
31
+ export class MigrationRunner {
32
+ #client;
33
+ #repository;
34
+ #tableName;
35
+ #clusterName;
36
+ #replicationPath;
37
+ #templateVars;
38
+ #initialized = false;
39
+ constructor(options) {
40
+ const url = new URL(options.connectionString);
41
+ if (!url.username) {
42
+ throw new Error("ClickHouse connection string must include username");
43
+ }
44
+ const database = url.pathname.replace("/", "");
45
+ if (!database) {
46
+ throw new Error("ClickHouse connection string must include database name");
47
+ }
48
+ this.#tableName = options.tableName || "schema_migrations";
49
+ this.#clusterName = options.clusterName;
50
+ this.#replicationPath = options.replicationPath;
51
+ this.#templateVars = options.templateVars || {};
52
+ this.#client = createClient({
53
+ url: `${url.protocol}//${url.host}`,
54
+ username: decodeURIComponent(url.username),
55
+ password: decodeURIComponent(url.password),
56
+ database,
57
+ });
58
+ this.#repository = new MigrationRepository({
59
+ client: this.#client,
60
+ migrationsDir: options.migrationsDir,
61
+ tableName: this.#tableName,
62
+ replicationPath: this.#replicationPath,
63
+ });
64
+ }
65
+ async #initialize() {
66
+ if (this.#initialized) {
67
+ return;
68
+ }
69
+ const spinner = ora("Detecting cluster configuration...").start();
70
+ const clusterName = await this.#repository.initialize(this.#clusterName);
71
+ if (clusterName) {
72
+ const message = this.#clusterName
73
+ ? `Using cluster from config: ${kleur.bold(clusterName)}`
74
+ : `Detected cluster: ${kleur.bold(clusterName)}`;
75
+ spinner.succeed(kleur.green(message));
76
+ }
77
+ else {
78
+ spinner.succeed("No cluster detected, using non-replicated mode");
79
+ }
80
+ this.#initialized = true;
81
+ }
82
+ async run() {
83
+ await this.#initialize();
84
+ await this.#repository.ensureMigrationsTable();
85
+ const applied = await this.#repository.getAppliedMigrations();
86
+ const { pending, error } = await this.#repository.getPendingMigrations(applied);
87
+ if (error) {
88
+ throw error;
89
+ }
90
+ if (pending.length === 0) {
91
+ console.log("✓ No pending migrations");
92
+ return;
93
+ }
94
+ console.log(kleur.bold(`\nFound ${kleur.yellow(pending.length)} pending migration(s):`));
95
+ pending.forEach((migration) => console.log(kleur.dim(` • ${migration.version}_${migration.name}`)));
96
+ console.log("");
97
+ for (const migration of pending) {
98
+ const spinner = ora(`Applying ${kleur.bold(migration.version)}_${migration.name}`).start();
99
+ try {
100
+ const sql = renderTemplate(migration.content, this.#templateVars);
101
+ const statements = splitStatements(sql);
102
+ for (const statement of statements) {
103
+ await this.#client.command({
104
+ query: statement,
105
+ });
106
+ }
107
+ const cliVersion = await resolveCliVersion();
108
+ await this.#client.insert({
109
+ table: this.#tableName,
110
+ values: [
111
+ {
112
+ version: migration.version,
113
+ name: migration.name,
114
+ checksum: migration.checksum,
115
+ hostname: os.hostname(),
116
+ applied_by: resolveAppliedBy(),
117
+ cli_version: cliVersion,
118
+ },
119
+ ],
120
+ format: "JSONEachRow",
121
+ });
122
+ spinner.succeed(kleur.green(`Applied ${kleur.bold(migration.version)}_${migration.name}`));
123
+ }
124
+ catch (err) {
125
+ spinner.fail(kleur.red(`Failed to apply ${migration.version}_${migration.name}`));
126
+ throw err;
127
+ }
128
+ }
129
+ console.log(kleur.bold(kleur.green("\n✓ All migrations applied successfully")));
130
+ }
131
+ async status() {
132
+ await this.#initialize();
133
+ await this.#repository.ensureMigrationsTable();
134
+ const applied = await this.#repository.getAppliedMigrations();
135
+ const { pending, error } = await this.#repository.getPendingMigrations(applied);
136
+ if (error) {
137
+ throw error;
138
+ }
139
+ console.log("");
140
+ console.log(kleur.bold("Migration Status"));
141
+ console.log(`${kleur.green("Applied:")} ${kleur.bold(applied.size.toString())}`);
142
+ console.log(`${kleur.yellow("Pending:")} ${kleur.bold(pending.length.toString())}`);
143
+ console.log("");
144
+ if (applied.size > 0) {
145
+ console.log(kleur.bold("Applied migrations:"));
146
+ Array.from(applied.values())
147
+ .sort((a, b) => a.version.localeCompare(b.version))
148
+ .forEach((migration) => console.log(kleur.dim(` ✓ ${migration.version}_${migration.name}`) +
149
+ kleur.gray(` (${migration.applied_at})`)));
150
+ console.log("");
151
+ }
152
+ if (pending.length > 0) {
153
+ console.log(kleur.bold("Pending migrations:"));
154
+ pending.forEach((migration) => console.log(kleur.yellow(` • ${migration.version}_${migration.name}`)));
155
+ console.log("");
156
+ }
157
+ }
158
+ async close() {
159
+ await this.#client.close();
160
+ }
161
+ }
162
+ export const runMigrations = async (options, command) => {
163
+ const runner = new MigrationRunner(options);
164
+ try {
165
+ if (command === "status") {
166
+ await runner.status();
167
+ }
168
+ else {
169
+ await runner.run();
170
+ }
171
+ }
172
+ finally {
173
+ await runner.close();
174
+ }
175
+ };
@@ -0,0 +1,3 @@
1
+ export declare const calculateChecksum: (content: string) => string;
2
+ export declare const splitStatements: (sql: string) => string[];
3
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/migrations/sql.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,MAEnD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,KAAG,MAAM,EAuFnD,CAAC"}
@@ -0,0 +1,74 @@
1
+ import crypto from "node:crypto";
2
+ export const calculateChecksum = (content) => {
3
+ return crypto.createHash("sha256").update(content).digest("hex");
4
+ };
5
+ export const splitStatements = (sql) => {
6
+ const statements = [];
7
+ let current = "";
8
+ let isSingleQuoted = false;
9
+ let isDoubleQuoted = false;
10
+ let isLineComment = false;
11
+ let isBlockComment = false;
12
+ for (let index = 0; index < sql.length; index += 1) {
13
+ const char = sql[index];
14
+ const next = sql[index + 1];
15
+ if (isLineComment) {
16
+ current += char;
17
+ if (char === "\n") {
18
+ isLineComment = false;
19
+ }
20
+ continue;
21
+ }
22
+ if (isBlockComment) {
23
+ current += char;
24
+ if (char === "*" && next === "/") {
25
+ current += next;
26
+ index += 1;
27
+ isBlockComment = false;
28
+ }
29
+ continue;
30
+ }
31
+ if (!isSingleQuoted && !isDoubleQuoted) {
32
+ if (char === "-" && next === "-") {
33
+ current += char + next;
34
+ index += 1;
35
+ isLineComment = true;
36
+ continue;
37
+ }
38
+ if (char === "/" && next === "*") {
39
+ current += char + next;
40
+ index += 1;
41
+ isBlockComment = true;
42
+ continue;
43
+ }
44
+ }
45
+ if (char === "'" && !isDoubleQuoted) {
46
+ const prev = sql[index - 1];
47
+ const isEscaped = prev === "\\" && sql[index - 2] !== "\\";
48
+ if (!isEscaped) {
49
+ isSingleQuoted = !isSingleQuoted;
50
+ }
51
+ }
52
+ else if (char === '"' && !isSingleQuoted) {
53
+ const prev = sql[index - 1];
54
+ const isEscaped = prev === "\\" && sql[index - 2] !== "\\";
55
+ if (!isEscaped) {
56
+ isDoubleQuoted = !isDoubleQuoted;
57
+ }
58
+ }
59
+ if (char === ";" && !isSingleQuoted && !isDoubleQuoted) {
60
+ const trimmed = current.trim();
61
+ if (trimmed.length > 0) {
62
+ statements.push(trimmed);
63
+ }
64
+ current = "";
65
+ continue;
66
+ }
67
+ current += char;
68
+ }
69
+ const trimmed = current.trim();
70
+ if (trimmed.length > 0) {
71
+ statements.push(trimmed);
72
+ }
73
+ return statements;
74
+ };
@@ -0,0 +1,3 @@
1
+ import type { MigrationRunnerOptions } from "./types.js";
2
+ export declare const renderTemplate: (content: string, ctx?: MigrationRunnerOptions["templateVars"]) => string;
3
+ //# sourceMappingURL=template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/migrations/template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,eAAO,MAAM,cAAc,GACzB,SAAS,MAAM,EACf,MAAK,sBAAsB,CAAC,cAAc,CAAM,KAC/C,MAIF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import Handlebars from "handlebars";
2
+ export const renderTemplate = (content, ctx = {}) => {
3
+ const template = Handlebars.compile(content);
4
+ return template(ctx);
5
+ };
@@ -0,0 +1,35 @@
1
+ export type MigrationRunnerOptions = {
2
+ migrationsDir: string;
3
+ connectionString: string;
4
+ clusterName?: string;
5
+ tableName?: string;
6
+ replicationPath?: string;
7
+ templateVars?: Record<string, unknown>;
8
+ };
9
+ export type MigrationCommand = "run" | "status";
10
+ export type MigrationContext = {
11
+ is_replicated: boolean;
12
+ create_table_options: string;
13
+ cluster: string;
14
+ };
15
+ export type MigrationRecord = {
16
+ version: string;
17
+ name: string;
18
+ checksum: string;
19
+ hostname: string;
20
+ applied_by: string;
21
+ cli_version: string;
22
+ applied_at: string;
23
+ };
24
+ export type PendingMigration = {
25
+ version: string;
26
+ name: string;
27
+ file: string;
28
+ content: string;
29
+ checksum: string;
30
+ };
31
+ export type PendingMigrationsResult = {
32
+ pending: PendingMigration[];
33
+ error: Error | null;
34
+ };
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEhD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sql.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.test.d.ts","sourceRoot":"","sources":["../../src/tests/sql.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { calculateChecksum, splitStatements } from "../migrations/sql.js";
4
+ test("splitStatements handles basic semicolons", () => {
5
+ const sql = "SELECT 1; SELECT 2;";
6
+ const statements = splitStatements(sql);
7
+ assert.deepEqual(statements, ["SELECT 1", "SELECT 2"]);
8
+ });
9
+ test("splitStatements ignores semicolons in strings and comments", () => {
10
+ const sql = `
11
+ -- comment with ;
12
+ SELECT 'value;still_string';
13
+ /* block ; comment */
14
+ SELECT "value;still_string";
15
+ `;
16
+ const statements = splitStatements(sql);
17
+ assert.equal(statements.length, 2);
18
+ assert.ok(statements[0].includes("SELECT 'value;still_string'"));
19
+ assert.ok(statements[1].includes('SELECT "value;still_string"'));
20
+ });
21
+ test("calculateChecksum returns stable sha256", () => {
22
+ const checksum = calculateChecksum("hello");
23
+ assert.equal(checksum, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
24
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@clisma/core",
3
+ "version": "0.1.0",
4
+ "description": "Core ClickHouse migration logic for clisma",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "dependencies": {
12
+ "@clisma/config": "*",
13
+ "@clickhouse/client": "^1.11.2",
14
+ "handlebars": "^4.7.8",
15
+ "kleur": "^4.1.5",
16
+ "ora": "^9.1.0"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -b tsconfig.json",
20
+ "test": "node --import tsx --test src/tests/*.test.ts",
21
+ "lint": "oxlint src",
22
+ "lint:fix": "oxlint --fix src",
23
+ "format": "oxfmt --check src",
24
+ "format:fix": "oxfmt src"
25
+ },
26
+ "devDependencies": {
27
+ "oxfmt": "^0.4.0",
28
+ "oxlint": "^0.12.0"
29
+ }
30
+ }