imean-cassandra-orm
Version:
cassandra orm
1,420 lines (1,398 loc) • 46.1 kB
JavaScript
;
var cassandraDriver = require('cassandra-driver');
var client = require('@clickhouse/client');
var zod = require('zod');
// src/public/client.ts
// src/ports/write-hook.ts
var CompositeWriteHook = class {
constructor(hooks) {
this.hooks = hooks;
}
async onSuccess(ctx, durationMs) {
await Promise.all(this.hooks.map((h) => h.onSuccess(ctx, durationMs)));
}
async onFailure(ctx, error, durationMs) {
await Promise.all(this.hooks.map((h) => h.onFailure(ctx, error, durationMs)));
}
};
// src/ports/telemetry.ts
var NoopTelemetry = class {
debug() {
}
info() {
}
warn() {
}
error() {
}
async withSpan(_name, fn) {
return await fn();
}
};
// src/adapters/cassandra-driver-executor.ts
function guessTypeHint(value) {
if (value instanceof Date) return "timestamp";
if (typeof value === "string") return "text";
if (typeof value === "number") return Number.isInteger(value) ? "int" : "double";
if (typeof value === "boolean") return "boolean";
if (value instanceof Uint8Array) return "blob";
if (Array.isArray(value)) return "list";
if (typeof value === "object" && value !== null) return "text";
return null;
}
var CassandraDriverExecutor = class {
constructor(client) {
this.client = client;
}
async execute(cql, params, options) {
const opts = {
prepare: options?.prepare ?? true,
fetchSize: options?.fetchSize,
pageState: options?.pageState
};
if (params && Array.isArray(params)) {
const hints = params.map((p) => guessTypeHint(p)).filter(Boolean);
if (hints.length > 0) opts.hints = hints;
}
const result = await this.client.execute(cql, params ?? [], opts);
return {
rows: result.rows ?? [],
pageState: result.pageState ?? null
};
}
async batch(queries, options) {
const batchQueries = queries.map((q) => ({
query: q.query,
params: q.params ?? []
}));
return this.client.batch(batchQueries, {
prepare: options?.prepare ?? true
});
}
};
// src/adapters/cassandra-system-schema-inspector.ts
var COLUMNS_SQL = "SELECT column_name, type, kind FROM system_schema.columns WHERE keyspace_name = ? AND table_name = ?;";
var INDEXES_SQL = "SELECT index_name, options FROM system_schema.indexes WHERE keyspace_name = ? AND table_name = ?;";
function normalizeKind(kind) {
const k = kind.toLowerCase();
if (k === "partition_key") return "partition_key";
if (k === "clustering" || k === "clustering_key") return "clustering";
return "regular";
}
var CassandraSystemSchemaInspector = class {
constructor(executor) {
this.executor = executor;
}
async getTableInfo(keyspace, tableName) {
const colsRes = await this.executor.execute(COLUMNS_SQL, [
keyspace,
tableName
]);
if (!colsRes.rows || colsRes.rows.length === 0) return null;
const idxRes = await this.executor.execute(INDEXES_SQL, [
keyspace,
tableName
]);
const columns = colsRes.rows.map((r) => ({
name: r.column_name,
type: r.type,
kind: normalizeKind(r.kind)
}));
const indexes = (idxRes.rows ?? []).map((r) => ({
name: r.index_name,
options: r.options
}));
return { keyspace, tableName, columns, indexes };
}
};
// src/adapters/cassandra-schema-migrator.ts
var CassandraSchemaMigrator = class {
constructor(executor) {
this.executor = executor;
}
async execute(cql) {
await this.executor.execute(cql, [], { prepare: false });
}
};
// src/core/codec/clickhouse-type.ts
function cassandraTypeToClickHouseType(cassandraType) {
const type = cassandraType.toLowerCase();
if (type === "text" || type === "varchar" || type === "ascii") return "String";
if (type === "int" || type === "varint") return "Int32";
if (type === "bigint") return "Int64";
if (type === "double" || type === "float") return "Float64";
if (type === "boolean") return "UInt8";
if (type === "timestamp" || type === "date") return "DateTime";
if (type === "uuid") return "UUID";
if (type === "blob") return "String";
if (type.startsWith("list<")) {
const elementType = type.slice(5, -1);
return `Array(${cassandraTypeToClickHouseType(elementType)})`;
}
if (type.startsWith("set<")) {
const elementType = type.slice(4, -1);
return `Array(${cassandraTypeToClickHouseType(elementType)})`;
}
if (type.startsWith("map<")) {
const mapContent = type.slice(4, -1);
const commaIndex = mapContent.lastIndexOf(",");
if (commaIndex !== -1) {
const keyType = mapContent.substring(0, commaIndex).trim();
const valueType = mapContent.substring(commaIndex + 1).trim();
return `Map(${cassandraTypeToClickHouseType(keyType)}, ${cassandraTypeToClickHouseType(valueType)})`;
}
}
if (type.startsWith("vector<")) {
const vectorContent = type.slice(7, -1);
const commaIndex = vectorContent.indexOf(",");
if (commaIndex !== -1) {
const elementType = vectorContent.substring(0, commaIndex).trim();
return `Array(${cassandraTypeToClickHouseType(elementType)})`;
}
}
return "String";
}
// src/adapters/clickhouse-client.ts
var ClickHouseClientWrapper = class {
client;
config;
isConnected = false;
constructor(config) {
this.config = config;
const url = `${config.protocol || "http"}://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`;
this.client = client.createClient({
url,
compression: {
request: false,
response: false
}
});
}
async withConnection(operation) {
try {
if (!this.isConnected) {
await this.connect();
}
} catch (error) {
console.error("ClickHouse \u8FDE\u63A5\u5931\u8D25:", error);
throw error;
}
try {
return await operation();
} catch (error) {
console.error("ClickHouse \u64CD\u4F5C\u5931\u8D25:", error);
throw error;
}
}
async withCommand(query, operationName) {
await this.withConnection(async () => {
if (this.config.debug) {
console.log(`ClickHouse ${operationName} Query:`, query);
}
await this.client.command({ query });
});
}
async withQuery(query, operationName) {
return await this.withConnection(async () => {
if (this.config.debug) {
console.log(`ClickHouse ${operationName} Query:`, query);
}
const result = await this.client.query({ query });
return result;
});
}
async connect() {
try {
const result = await this.client.query({ query: "SELECT 1" });
const data = await result.json();
if (this.config.debug) {
console.log("ClickHouse Query: SELECT 1");
console.log("ClickHouse Result:", data);
}
this.isConnected = true;
console.log("\u2705 ClickHouse \u8FDE\u63A5\u6210\u529F");
} catch (error) {
console.error("\u274C ClickHouse \u8FDE\u63A5\u5931\u8D25:", error);
throw error;
}
}
async close() {
if (this.client) {
await this.client.close();
this.isConnected = false;
}
}
async insert(tableName, data) {
const dataArray = Array.isArray(data) ? data : [data];
await this.withCommand(
`
INSERT INTO ${tableName} (${Object.keys(dataArray[0]).map((col) => `\`${col}\``).join(", ")})
VALUES ${dataArray.map((row) => {
const formattedValues = Object.values(row).map(
(value) => this.formatValue(value)
);
return `(${formattedValues.join(", ")})`;
}).join(", ")}
`,
"Insert"
);
}
async update(tableName, setClause, whereClause) {
await this.withCommand(
`
ALTER TABLE ${tableName}
UPDATE ${setClause}
WHERE ${whereClause}
`,
"Update"
);
}
async delete(tableName, whereClause) {
await this.withCommand(
`
ALTER TABLE ${tableName}
DELETE WHERE ${whereClause}
`,
"Delete"
);
}
async query(query) {
return await this.withQuery(query, "Query");
}
formatValue(value) {
if (value === null || value === void 0) {
return "NULL";
}
if (typeof value === "string") {
return `'${value.replace(/'/g, "''")}'`;
}
if (typeof value === "boolean") {
return value ? "1" : "0";
}
if (value instanceof Date) {
return `'${value.toISOString()}'`;
}
if (typeof value === "object") {
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
}
return String(value);
}
async createTableIfNotExists(tableName, schema) {
await this.withCommand(
`
CREATE TABLE IF NOT EXISTS ${tableName} (
${Object.entries(schema).map(([field, type]) => {
const clickHouseType = cassandraTypeToClickHouseType(type);
return `\`${field}\` ${clickHouseType}`;
}).join(", ")}
) ENGINE = MergeTree()
ORDER BY tuple()
`,
"Create Table"
);
}
async getTableSchema(tableName) {
const result = await this.withQuery(
`
SELECT name, type
FROM system.columns
WHERE database = '${this.config.database}'
AND table = '${tableName}'
`,
"Get Table Schema"
);
const data = await result.json();
if (!Array.isArray(data) || data.length === 0) return null;
return data.reduce((acc, col) => {
acc[col.name] = col.type;
return acc;
}, {});
}
};
// src/adapters/clickhouse-writer.ts
function formatValue(value) {
if (value === null || value === void 0) return "NULL";
if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
if (typeof value === "boolean") return value ? "1" : "0";
if (value instanceof Date) return `'${value.toISOString()}'`;
if (typeof value === "object") {
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
}
return String(value);
}
function buildSetClause(patch) {
return Object.entries(patch).map(([k, v]) => `\`${k}\` = ${formatValue(v)}`).join(", ");
}
function buildWhereClause(where) {
return Object.entries(where).map(([k, v]) => `\`${k}\` = ${formatValue(v)}`).join(" AND ");
}
var ClickHouseWriterAdapter = class {
client;
database;
constructor(config) {
this.client = new ClickHouseClientWrapper(config);
this.database = config.database;
}
qualify(tableName) {
if (tableName.includes(".")) return tableName;
return `${this.database}.${tableName}`;
}
async insert(tableName, data) {
await this.client.insert(this.qualify(tableName), data);
}
async update(tableName, where, patch) {
const setClause = buildSetClause(patch);
const whereClause = buildWhereClause(where);
await this.client.update(this.qualify(tableName), setClause, whereClause);
}
async delete(tableName, where) {
const whereClause = buildWhereClause(where);
await this.client.delete(this.qualify(tableName), whereClause);
}
async batchInsert(tableName, data) {
await this.client.insert(this.qualify(tableName), data);
}
};
// src/services/dual-write-service.ts
var DualWriteService = class {
constructor(writer, policy, telemetry) {
this.writer = writer;
this.policy = policy;
this.telemetry = telemetry ?? new NoopTelemetry();
}
telemetry;
async onSuccess(ctx, durationMs) {
if (!this.policy.enabled) return;
if (this.policy.shouldWrite && !this.policy.shouldWrite(ctx)) return;
if (this.policy.operations?.[ctx.operation] === false) return;
const table = ctx.tableName;
try {
switch (ctx.operation) {
case "insert":
if (!ctx.data) return;
await this.writer.insert(table, ctx.data);
break;
case "update":
if (!ctx.where || !ctx.patch) return;
await this.writer.update(table, ctx.where, ctx.patch);
break;
case "delete":
if (!ctx.where) return;
await this.writer.delete(table, ctx.where);
break;
case "batchInsert":
if (ctx.batchData && ctx.batchData.length > 0) {
await this.writer.batchInsert(table, ctx.batchData);
} else if (ctx.data) {
await this.writer.batchInsert(table, [ctx.data]);
}
break;
default:
return;
}
} catch (error) {
this.telemetry.error("dual_write_failed", {
tableName: ctx.tableName,
operation: ctx.operation,
durationMs,
error: String(error?.message ?? error)
});
}
}
async onFailure(ctx, error, durationMs) {
this.telemetry.warn("primary_write_failed", {
tableName: ctx.tableName,
operation: ctx.operation,
durationMs,
error: String(error?.message ?? error)
});
}
};
function getDef(schema) {
return schema?._def ?? {};
}
function getTypeTag(schema) {
const def = getDef(schema);
return def.typeName ?? def.type;
}
function isType(schema, legacy, modern) {
const tag = getTypeTag(schema);
return tag === legacy || tag === modern;
}
function unwrapField(schema) {
let base = schema;
while (isType(base, "ZodOptional", "optional") || isType(base, "ZodNullable", "nullable") || isType(base, "ZodDefault", "default")) {
base = getDef(base).innerType;
}
while (isType(base, "ZodEffects", "effects") || isType(base, "ZodTransform", "transform") || isType(base, "ZodPipeline", "pipe")) {
const def = getDef(base);
base = def.schema ?? def.in ?? def.innerType ?? def;
}
return base;
}
function toCassandraValue(value, cassandraType) {
if (value === null || value === void 0) return null;
const type = cassandraType.toLowerCase();
if (type === "timestamp") {
if (value instanceof Date) return value;
if (typeof value === "string" || typeof value === "number") {
const d = new Date(value);
if (Number.isNaN(d.getTime())) {
throw new Error(`Invalid timestamp value: ${String(value)}`);
}
return d;
}
return new Date(value);
}
if (type === "uuid") return typeof value === "string" ? value : String(value);
if (type === "blob") {
if (value instanceof Uint8Array) return value;
if (Array.isArray(value)) return new Uint8Array(value);
throw new Error(`Invalid blob value: ${String(value)}`);
}
if (type === "text") return typeof value === "string" ? value : JSON.stringify(value);
if (type === "list<text>" || type === "set<text>") {
if (!Array.isArray(value)) return value;
return value.map((item) => typeof item === "string" ? item : JSON.stringify(item));
}
if (type.startsWith("vector<")) {
if (value instanceof cassandraDriver.types.Vector) return value;
if (!Array.isArray(value)) return value;
return new cassandraDriver.types.Vector(value, "float");
}
return value;
}
function fromCassandraValue(value, cassandraType, zodField) {
const type = cassandraType.toLowerCase();
if (value === null || value === void 0) {
if (type.startsWith("list<") || type.startsWith("set<")) return [];
return value;
}
if (type === "timestamp") {
return value instanceof Date ? value : new Date(value);
}
if (type === "uuid") {
return typeof value === "string" ? value : value?.toString?.() ?? String(value);
}
if (type === "blob") {
return value instanceof Uint8Array ? value : new Uint8Array(value);
}
if (type === "text") {
const baseField = zodField ? unwrapField(zodField) : void 0;
if (baseField && isType(baseField, "ZodString", "string")) return value;
if (typeof value !== "string") return value;
try {
return JSON.parse(value);
} catch {
return value;
}
}
if (type === "list<text>" || type === "set<text>") {
if (!Array.isArray(value)) return value;
return value.map((item) => {
if (typeof item !== "string") return item;
try {
return JSON.parse(item);
} catch {
return item;
}
});
}
if (type.startsWith("vector<")) {
if (value instanceof cassandraDriver.types.Vector) {
return value.elements ?? Array.from(value);
}
return value;
}
return value;
}
// src/core/codec/row.ts
function getDef2(schema) {
return schema?._def ?? {};
}
function getTypeTag2(schema) {
const def = getDef2(schema);
return def.typeName ?? def.type;
}
function isType2(schema, legacy, modern) {
const tag = getTypeTag2(schema);
return tag === legacy || tag === modern;
}
function getOptionalFlags(schema) {
let optional = false;
let nullable = false;
let base = schema;
while (true) {
if (isType2(base, "ZodOptional", "optional")) {
optional = true;
base = getDef2(base).innerType;
continue;
}
if (isType2(base, "ZodNullable", "nullable")) {
nullable = true;
base = getDef2(base).innerType;
continue;
}
if (isType2(base, "ZodDefault", "default")) {
optional = true;
base = getDef2(base).innerType;
continue;
}
if (isType2(base, "ZodEffects", "effects") || isType2(base, "ZodTransform", "transform") || isType2(base, "ZodPipeline", "pipe")) {
const def = getDef2(base);
base = def.schema ?? def.in ?? def.innerType ?? def;
continue;
}
break;
}
return { optional, nullable };
}
function rowToObject(row, fieldTypes, schema) {
const result = {};
const rowKeyMap = /* @__PURE__ */ new Map();
for (const key of Object.keys(row)) {
const lower2 = key.toLowerCase();
if (!rowKeyMap.has(lower2)) rowKeyMap.set(lower2, key);
}
for (const fieldName of Object.keys(fieldTypes)) {
const dbKey = rowKeyMap.get(fieldName.toLowerCase());
if (!dbKey) continue;
const cassandraType = fieldTypes[fieldName];
const zodField = schema.shape?.[fieldName];
if (row[dbKey] === null && zodField) {
const flags = getOptionalFlags(zodField);
result[fieldName] = flags.nullable ? null : flags.optional ? void 0 : null;
continue;
}
result[fieldName] = fromCassandraValue(row[dbKey], cassandraType, zodField);
}
return schema.parse(result);
}
function getDef3(schema) {
return schema?._def ?? {};
}
function getTypeTag3(schema) {
const def = getDef3(schema);
return def.typeName ?? def.type;
}
function isType3(schema, legacy, modern) {
const tag = getTypeTag3(schema);
return tag === legacy || tag === modern;
}
function getArrayElement(schema) {
const def = getDef3(schema);
return schema.element ?? def.type ?? def.element ?? def.valueType;
}
function unwrap(schema) {
let base = schema;
while (isType3(base, "ZodOptional", "optional") || isType3(base, "ZodNullable", "nullable") || isType3(base, "ZodDefault", "default")) {
base = getDef3(base).innerType;
}
while (isType3(base, "ZodEffects", "effects") || isType3(base, "ZodTransform", "transform") || isType3(base, "ZodPipeline", "pipe")) {
const def = getDef3(base);
base = def.schema ?? def.in ?? def.innerType ?? def;
}
return base;
}
function zodToCassandraType(schema) {
let probe = schema;
while (true) {
const marker = probe;
if (marker._cassandraType) return marker._cassandraType;
if (isType3(probe, "ZodOptional", "optional") || isType3(probe, "ZodNullable", "nullable") || isType3(probe, "ZodDefault", "default")) {
probe = getDef3(probe).innerType;
continue;
}
if (isType3(probe, "ZodEffects", "effects") || isType3(probe, "ZodTransform", "transform") || isType3(probe, "ZodPipeline", "pipe")) {
const def = getDef3(probe);
probe = def.schema ?? def.in ?? def.innerType ?? def;
continue;
}
break;
}
const base = unwrap(schema);
if (isType3(base, "ZodString", "string")) {
const checks = getDef3(base).checks ?? [];
if (checks.some(
(c) => String(c?.kind ?? "").startsWith("uuid") || String(c?.format ?? "").startsWith("uuid")
)) {
return "uuid";
}
return "text";
}
if (isType3(base, "ZodNumber", "number")) {
const checks = getDef3(base).checks ?? [];
if (checks.some(
(c) => c?.kind === "int" || c?.isInt === true || String(c?.format ?? "").includes("int")
)) {
return "int";
}
return "double";
}
if (isType3(base, "ZodBigInt", "bigint")) return "bigint";
if (isType3(base, "ZodBoolean", "boolean")) return "boolean";
if (isType3(base, "ZodDate", "date")) return "timestamp";
if (isType3(base, "ZodArray", "array")) {
const element = getArrayElement(base);
const elementType = element ? zodToCassandraType(element) : "text";
return `list<${elementType}>`;
}
if (isType3(base, "ZodSet", "set")) {
const elementType = zodToCassandraType(getDef3(base).valueType);
return `set<${elementType}>`;
}
if (isType3(base, "ZodRecord", "record")) {
const def = getDef3(base);
const keyType = zodToCassandraType(def.keyType);
const valueType = zodToCassandraType(def.valueType);
return `map<${keyType}, ${valueType}>`;
}
if (isType3(base, "ZodInstanceof", "instanceof") && getDef3(base).instanceof === Uint8Array) {
return "blob";
}
return "text";
}
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Uint8Array);
}
function parseWhere(filters) {
const predicates = [];
for (const [field, raw] of Object.entries(filters)) {
if (raw === void 0) continue;
if (isPlainObject(raw)) {
for (const [op, opVal] of Object.entries(raw)) {
if (opVal === void 0 && op !== "is_null") continue;
switch (op) {
case "=":
case "!=":
case ">":
case ">=":
case "<":
case "<=":
case "regex":
predicates.push({ field, op, value: opVal });
break;
case "in":
case "not_in":
if (!Array.isArray(opVal)) {
throw new Error(`Operator ${op} expects array value for field ${field}`);
}
predicates.push({ field, op, value: opVal });
break;
case "between":
if (!Array.isArray(opVal) || opVal.length !== 2) {
throw new Error(`Operator between expects [min, max] for field ${field}`);
}
predicates.push({ field, op: "between", value: [opVal[0], opVal[1]] });
break;
case "is_null":
predicates.push({ field, op: "is_null", value: Boolean(opVal) });
break;
default:
throw new Error(`Unsupported operator ${op} for field ${field}`);
}
}
continue;
}
predicates.push({ field, op: "=", value: raw });
}
return { predicates };
}
function isVectorType(fieldType) {
return typeof fieldType === "string" && fieldType.startsWith("vector<");
}
function compileWhere(where, fieldTypes) {
const conditions = [];
const params = [];
let vectorSearch;
for (const pred of where.predicates) {
const fieldType = fieldTypes[pred.field];
const encode = (v) => fieldType ? toCassandraValue(v, fieldType) : v;
if (pred.op === "=" && isVectorType(fieldType) && Array.isArray(pred.value)) {
vectorSearch = {
field: pred.field,
vector: new cassandraDriver.types.Vector(pred.value, "float")
};
continue;
}
switch (pred.op) {
case "is_null":
conditions.push(
pred.value ? `${pred.field} IS NULL` : `${pred.field} IS NOT NULL`
);
break;
case "between":
conditions.push(`${pred.field} BETWEEN ? AND ?`);
params.push(encode(pred.value[0]), encode(pred.value[1]));
break;
case "in":
conditions.push(`${pred.field} IN ?`);
params.push(pred.value.map((v) => encode(v)));
break;
case "not_in":
conditions.push(`${pred.field} NOT IN ?`);
params.push(pred.value.map((v) => encode(v)));
break;
case "regex":
conditions.push(`${pred.field} LIKE ?`);
params.push(
String(pred.value).replace(/[.*+?^${}()|[\]\\]/g, "%")
);
break;
default:
conditions.push(`${pred.field} ${pred.op} ?`);
params.push(encode(pred.value));
}
}
return {
clause: conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "",
params,
vectorSearch
};
}
function compileSelect(params) {
const tableRef = params.table.keyspace ? `${params.table.keyspace}.${params.table.tableName}` : params.table.tableName;
const fieldTypes = params.fieldTypes ?? {};
const where = params.where ? compileWhere(params.where, fieldTypes) : { clause: "", params: [] };
const limitClause = params.limit != null ? ` LIMIT ${params.limit}` : "";
const allowFiltering = params.allowFiltering ? ` ALLOW FILTERING` : "";
const vectorOrder = where.vectorSearch ? ` ORDER BY ${where.vectorSearch.field} ANN OF ?` : "";
const vectorParams = where.vectorSearch ? [where.vectorSearch.vector] : [];
return {
cql: `SELECT * FROM ${tableRef}${where.clause}${vectorOrder}${limitClause}${allowFiltering};`,
params: [...where.params, ...vectorParams]
};
}
function compileInsert(params) {
const tableRef = params.table.keyspace ? `${params.table.keyspace}.${params.table.tableName}` : params.table.tableName;
const columns = Object.keys(params.data);
const placeholders = columns.map(() => "?").join(", ");
const cql = `INSERT INTO ${tableRef} (${columns.join(", ")}) VALUES (${placeholders});`;
const bound = columns.map((c) => toCassandraValue(params.data[c], params.fieldTypes[c]));
return { cql, params: bound };
}
function compileDelete(params) {
const tableRef = params.table.keyspace ? `${params.table.keyspace}.${params.table.tableName}` : params.table.tableName;
const where = compileWhere(params.where, params.fieldTypes);
return { cql: `DELETE FROM ${tableRef}${where.clause};`, params: where.params };
}
function compileUpdate(params) {
const tableRef = params.table.keyspace ? `${params.table.keyspace}.${params.table.tableName}` : params.table.tableName;
const setCols = Object.keys(params.set);
const setClause = setCols.map((c) => `${c} = ?`).join(", ");
const setParams = setCols.map((c) => toCassandraValue(params.set[c], params.fieldTypes[c]));
const where = compileWhere(params.where, params.fieldTypes);
return {
cql: `UPDATE ${tableRef} SET ${setClause}${where.clause};`,
params: [...setParams, ...where.params]
};
}
// src/services/model-repository.ts
var ModelRepository = class {
_telemetry;
keyspace;
tableName;
schema;
executor;
writeHook;
fieldTypes;
constructor(opts) {
this.keyspace = opts.keyspace;
this.tableName = opts.tableName;
this.schema = opts.schema;
this.executor = opts.executor;
this._telemetry = opts.telemetry ?? new NoopTelemetry();
this.writeHook = opts.writeHook;
this.fieldTypes = Object.fromEntries(
Object.entries(this.schema.shape).map(([k, v]) => [
k,
zodToCassandraType(v)
])
);
}
async runWrite(operation, ctxExtra, fn) {
const ctx = {
keyspace: this.keyspace,
tableName: this.tableName,
operation,
...ctxExtra
};
const start = Date.now();
try {
await fn();
const durationMs = Date.now() - start;
this._telemetry.debug("write_success", {
operation,
tableName: this.tableName,
durationMs
});
await this.writeHook?.onSuccess(ctx, durationMs);
} catch (err) {
const durationMs = Date.now() - start;
await this.writeHook?.onFailure(ctx, err, durationMs);
throw err;
}
}
async insert(data) {
const validated = this.schema.parse(data);
const compiled = compileInsert({
table: { keyspace: this.keyspace, tableName: this.tableName },
data: validated,
fieldTypes: this.fieldTypes
});
await this.runWrite("insert", { data: validated }, async () => {
await this.executor.execute(compiled.cql, compiled.params, { prepare: true });
});
}
async batchInsert(data) {
const validated = data.map((item) => this.schema.parse(item));
const queries = validated.map(
(item) => compileInsert({
table: { keyspace: this.keyspace, tableName: this.tableName },
data: item,
fieldTypes: this.fieldTypes
})
);
await this.runWrite(
"batchInsert",
{ data: validated[0], batchData: validated },
async () => {
await this.executor.batch(
queries.map((q) => ({ query: q.cql, params: q.params })),
{ prepare: true }
);
}
);
}
async update(where, patch) {
const validatedPatch = this.schema.partial().parse(patch);
const whereAst = parseWhere(where);
const compiled = compileUpdate({
table: { keyspace: this.keyspace, tableName: this.tableName },
set: validatedPatch,
where: whereAst,
fieldTypes: this.fieldTypes
});
await this.runWrite(
"update",
{ keySummary: where, patch: validatedPatch, where },
async () => {
await this.executor.execute(compiled.cql, compiled.params, { prepare: true });
}
);
}
async delete(where) {
const whereAst = parseWhere(where);
const compiled = compileDelete({
table: { keyspace: this.keyspace, tableName: this.tableName },
where: whereAst,
fieldTypes: this.fieldTypes
});
await this.runWrite("delete", { keySummary: where, where }, async () => {
await this.executor.execute(compiled.cql, compiled.params, { prepare: true });
});
}
async find(where, opts) {
const whereAst = parseWhere(where);
const compiled = compileSelect({
table: { keyspace: this.keyspace, tableName: this.tableName },
where: whereAst,
fieldTypes: this.fieldTypes,
limit: opts?.limit,
allowFiltering: opts?.allowFiltering
});
const res = await this.executor.execute(compiled.cql, compiled.params, { prepare: true });
return res.rows.map((r) => rowToObject(r, this.fieldTypes, this.schema));
}
async findPaged(where, opts) {
const whereAst = parseWhere(where);
const compiled = compileSelect({
table: { keyspace: this.keyspace, tableName: this.tableName },
where: whereAst,
fieldTypes: this.fieldTypes,
limit: opts?.limit,
allowFiltering: opts?.allowFiltering
});
const res = await this.executor.execute(compiled.cql, compiled.params, {
prepare: true,
fetchSize: opts?.fetchSize,
pageState: opts?.pageState
});
return {
data: res.rows.map((r) => rowToObject(r, this.fieldTypes, this.schema)),
pageState: res.pageState ?? null
};
}
async findOne(where, opts) {
const rows = await this.find(where, { limit: 1, allowFiltering: opts?.allowFiltering });
return rows[0] ?? null;
}
};
// src/core/errors.ts
var OrmError = class extends Error {
name = "OrmError";
cause;
constructor(message, opts) {
super(message);
this.cause = opts?.cause;
}
};
var SchemaSyncError = class extends OrmError {
name = "SchemaSyncError";
constructor(message, opts) {
super(message, opts);
}
};
// src/core/schema-sync/ddl.ts
function dropTableCql(keyspace, tableName) {
return `DROP TABLE IF EXISTS ${keyspace}.${tableName};`;
}
function dropIndexCql(keyspace, indexName2) {
return `DROP INDEX IF EXISTS ${keyspace}.${indexName2};`;
}
function createSaiIndexCql(params) {
const optionsString = JSON.stringify(params.options ?? {}).replace(/"/g, "'");
return `CREATE INDEX IF NOT EXISTS ${params.indexName} ON ${params.keyspace}.${params.tableName} (${params.field}) USING 'sai' WITH OPTIONS = ${optionsString}`;
}
function createTableCql(spec) {
const cols = spec.columns.map((c) => `${c.name} ${c.type}`).join(",\n ");
const partitionKeys = spec.columns.filter((c) => c.kind === "partition_key").map((c) => c.name);
const clusteringKeys = spec.columns.filter((c) => c.kind === "clustering").map((c) => c.name);
const pk = clusteringKeys.length > 0 ? `((${partitionKeys.join(", ")}), ${clusteringKeys.join(", ")})` : partitionKeys.length === 1 ? `(${partitionKeys.join(", ")})` : `((${partitionKeys.join(", ")}))`;
const clusteringOrder = spec.clusteringOrder && spec.clusteringOrder.length > 0 ? ` WITH CLUSTERING ORDER BY (${spec.clusteringOrder.map((c) => `${c.field} ${c.order}`).join(", ")})` : "";
return `CREATE TABLE IF NOT EXISTS ${spec.keyspace}.${spec.tableName} (
${cols},
PRIMARY KEY ${pk}
)${clusteringOrder};`;
}
function addColumnsCql(keyspace, tableName, cols) {
const columnsDefinition = cols.map((c) => `${c.name} ${c.type}`).join(",\n ");
return `ALTER TABLE ${keyspace}.${tableName} ADD (${columnsDefinition});`;
}
function dropColumnsCql(keyspace, tableName, colNames) {
return `ALTER TABLE ${keyspace}.${tableName} DROP (${colNames.join(", ")});`;
}
// src/core/schema-sync/diff.ts
function lower(s) {
return s.toLowerCase();
}
function jsonStable(value) {
try {
return JSON.stringify(value ?? null);
} catch {
return String(value);
}
}
function indexesEqual(a, b) {
return lower(a.name) === lower(b.name) && jsonStable(a.options) === jsonStable(b.options);
}
function diffTable(params) {
const { desired, existing } = params;
const reasons = [];
const ops = [];
if (!existing) {
ops.push({ kind: "create_table", cql: createTableCql(desired) });
for (const idx of desired.indexes ?? []) {
ops.push({
kind: "create_index",
cql: createSaiIndexCql({
keyspace: desired.keyspace,
tableName: desired.tableName,
indexName: idx.name,
field: idx.field,
options: idx.options
})
});
}
return { hasChanges: true, requiresRecreate: false, reasons, operations: ops };
}
const existingByName = /* @__PURE__ */ new Map();
for (const c of existing.columns) existingByName.set(lower(c.name), c);
const desiredByName = /* @__PURE__ */ new Map();
for (const c of desired.columns) desiredByName.set(lower(c.name), c);
let requiresRecreate = false;
const addedRegular = [];
for (const d of desired.columns) {
const ex = existingByName.get(lower(d.name));
if (!ex) {
if (d.kind === "regular") {
addedRegular.push({ name: d.name, type: d.type });
} else {
requiresRecreate = true;
reasons.push(`\u65B0\u589E\u4E3B\u952E\u5B57\u6BB5\u9700\u8981\u91CD\u5EFA\u8868: ${d.name} (${d.kind})`);
}
continue;
}
if (lower(ex.type) !== lower(d.type)) {
requiresRecreate = true;
reasons.push(`\u5B57\u6BB5\u7C7B\u578B\u53D8\u66F4\u9700\u8981\u91CD\u5EFA\u8868: ${d.name} (${ex.type} -> ${d.type})`);
}
if (ex.kind !== d.kind) {
requiresRecreate = true;
reasons.push(`\u5B57\u6BB5 kind \u53D8\u66F4\u9700\u8981\u91CD\u5EFA\u8868: ${d.name} (${ex.kind} -> ${d.kind})`);
}
}
const deletedRegular = [];
for (const ex of existing.columns) {
const d = desiredByName.get(lower(ex.name));
if (!d) {
if (ex.kind === "regular") {
deletedRegular.push(ex.name);
} else {
requiresRecreate = true;
reasons.push(`\u5220\u9664\u4E3B\u952E\u5B57\u6BB5\u9700\u8981\u91CD\u5EFA\u8868: ${ex.name} (${ex.kind})`);
}
}
}
const desiredIndexes = desired.indexes ?? [];
const existingIndexes = existing.indexes ?? [];
const toDrop = [];
const toCreate = [];
for (const ex of existingIndexes) {
const match = desiredIndexes.find((d) => lower(d.name) === lower(ex.name));
if (!match) toDrop.push(ex.name);
}
for (const d of desiredIndexes) {
const match = existingIndexes.find((ex) => lower(ex.name) === lower(d.name));
if (!match) toCreate.push(d);
else if (!indexesEqual(d, { name: match.name, options: match.options })) {
toDrop.push(match.name);
toCreate.push(d);
}
}
if (requiresRecreate) {
if (params.forceRecreate) {
ops.push({ kind: "drop_table", cql: dropTableCql(desired.keyspace, desired.tableName) });
ops.push({ kind: "create_table", cql: createTableCql(desired) });
}
} else {
if (addedRegular.length > 0) {
ops.push({
kind: "add_columns",
cql: addColumnsCql(desired.keyspace, desired.tableName, addedRegular)
});
}
if (deletedRegular.length > 0) {
ops.push({
kind: "drop_columns",
cql: dropColumnsCql(desired.keyspace, desired.tableName, deletedRegular)
});
}
}
for (const idxName of toDrop) {
ops.push({ kind: "drop_index", cql: dropIndexCql(desired.keyspace, idxName) });
}
for (const idx of toCreate) {
ops.push({
kind: "create_index",
cql: createSaiIndexCql({
keyspace: desired.keyspace,
tableName: desired.tableName,
indexName: idx.name,
field: idx.field,
options: idx.options
})
});
}
return {
hasChanges: ops.length > 0,
requiresRecreate,
reasons,
operations: ops
};
}
// src/services/schema-sync-service.ts
var SchemaSyncService = class {
constructor(inspector, migrator) {
this.inspector = inspector;
this.migrator = migrator;
}
async plan(params) {
const existing = await this.inspector.getTableInfo(
params.desired.keyspace,
params.desired.tableName
);
return diffTable({
desired: params.desired,
existing,
forceRecreate: params.forceRecreate
});
}
async apply(params) {
const plan = await this.plan(params);
if (plan.requiresRecreate && !params.forceRecreate) {
throw new SchemaSyncError(
`Schema \u53D8\u66F4\u9700\u8981\u91CD\u5EFA\u8868\uFF0C\u4F46\u672A\u542F\u7528 forceRecreate: ${params.desired.keyspace}.${params.desired.tableName}`,
{ cause: plan.reasons }
);
}
for (const op of plan.operations) {
await this.migrator.execute(op.cql);
}
return plan;
}
};
// src/core/schema-sync/format.ts
function formatMigrationPlan(plan) {
const lines = [];
if (!plan.hasChanges) return "";
if (plan.requiresRecreate && plan.reasons.length > 0) {
lines.push("-- requires_recreate");
for (const r of plan.reasons) lines.push(`-- ${r}`);
}
for (const op of plan.operations) {
lines.push(op.cql.trimEnd());
}
return lines.join("\n");
}
// src/public/model.ts
var Model = class {
schema;
repo;
schemaSync;
constructor(opts) {
this.schema = opts.schema;
this.repo = new ModelRepository({
keyspace: opts.schema.keyspace,
tableName: opts.schema.tableName,
schema: opts.schema.modelSchema,
executor: opts.executor,
telemetry: opts.telemetry,
writeHook: opts.writeHook
});
if (opts.inspector && opts.migrator) {
this.schemaSync = new SchemaSyncService(opts.inspector, opts.migrator);
}
}
async syncSchema(opts) {
if (!this.schemaSync) {
throw new Error("Schema sync is not configured (missing inspector/migrator).");
}
const plan = await this.schemaSync.apply({
desired: this.schema.toDesiredTableSpec(),
forceRecreate: opts?.forceRecreate
});
return formatMigrationPlan(plan);
}
insert(data) {
return this.repo.insert(data);
}
batchInsert(data) {
return this.repo.batchInsert(data);
}
update(where, patch) {
return this.repo.update(where, patch);
}
delete(where) {
return this.repo.delete(where);
}
find(where, opts) {
return this.repo.find(where, opts);
}
findPaged(where, opts) {
return this.repo.findPaged(where, opts);
}
findOne(where, opts) {
return this.repo.findOne(where, opts);
}
};
// src/public/client.ts
var Client = class {
executor;
inspector;
migrator;
telemetry;
writeHook;
constructor(opts) {
this.executor = opts.executor;
const autoSchemaSync = opts.autoSchemaSync ?? true;
this.inspector = opts.inspector ?? (autoSchemaSync ? new CassandraSystemSchemaInspector(this.executor) : void 0);
this.migrator = opts.migrator ?? (autoSchemaSync ? new CassandraSchemaMigrator(this.executor) : void 0);
this.telemetry = opts.telemetry ?? new NoopTelemetry();
this.writeHook = opts.writeHook;
}
model(schema) {
return new Model({
schema,
executor: this.executor,
inspector: this.inspector,
migrator: this.migrator,
telemetry: this.telemetry,
writeHook: this.writeHook
});
}
};
async function createClient2(config) {
const driver = new cassandraDriver.Client(config.cassandra);
await driver.connect();
const executor = new CassandraDriverExecutor(driver);
const schemaSyncEnabled = config.schemaSync?.enabled !== false;
const inspector = schemaSyncEnabled ? new CassandraSystemSchemaInspector(executor) : void 0;
const migrator = schemaSyncEnabled ? new CassandraSchemaMigrator(executor) : void 0;
let writeHook = config.writeHook;
if (config.clickhouse && config.clickhouse.enabled !== false) {
const writer = new ClickHouseWriterAdapter(config.clickhouse.config);
const policy = config.clickhouse.policy ?? { enabled: true };
const dualWrite = new DualWriteService(writer, policy, config.telemetry);
writeHook = writeHook ? new CompositeWriteHook([writeHook, dualWrite]) : dualWrite;
}
const client = new Client({
executor,
inspector,
migrator,
telemetry: config.telemetry,
writeHook,
autoSchemaSync: false
});
return {
client,
shutdown: async () => {
await driver.shutdown();
}
};
}
function indexName(tableName, fieldName) {
return `${tableName}_${fieldName.toLowerCase()}_sai_idx`;
}
var TableSchema = class {
keyspace;
tableName;
fields;
partitionKey;
clusteringKey;
indexes;
types;
schemas;
constructor(config) {
this.keyspace = config.keyspace;
this.tableName = config.tableName;
this.fields = zod.z.object(config.fields);
this.partitionKey = [...config.partitionKey];
this.clusteringKey = config.clusteringKey;
this.indexes = config.indexes;
this.types = {
model: {},
partitionKey: {},
clusteringKey: {}
};
const partitionKeyShape = {};
this.partitionKey.forEach((key) => {
partitionKeyShape[String(key)] = this.fields.shape[key];
});
const partitionKeySchema = zod.z.object(
partitionKeyShape
);
let clusteringKeySchema;
if (this.clusteringKey) {
const clusteringKeyShape = {};
Object.keys(this.clusteringKey).forEach((key) => {
clusteringKeyShape[key] = this.fields.shape[key];
});
clusteringKeySchema = zod.z.object(
clusteringKeyShape
);
}
this.schemas = {
model: this.fields,
partitionKey: partitionKeySchema,
clusteringKey: clusteringKeySchema
};
}
get modelSchema() {
return this.fields;
}
toDesiredTableSpec() {
const pkSet = new Set(this.partitionKey.map(String));
const ckOrder = this.clusteringKey ? Object.entries(this.clusteringKey).map(([field, order]) => ({
field,
order: order.toUpperCase()
})) : [];
const ckSet = new Set(ckOrder.map((c) => c.field));
const columns = Object.entries(this.fields.shape).map(([name, schema]) => {
const kind = pkSet.has(name) ? "partition_key" : ckSet.has(name) ? "clustering" : "regular";
return {
name,
type: zodToCassandraType(schema),
kind
};
});
const indexes = [];
if (this.indexes) {
for (const [field, opt] of Object.entries(this.indexes)) {
if (!opt) continue;
indexes.push({
name: indexName(this.tableName, field),
field,
options: opt === true ? {} : opt
});
}
}
return {
keyspace: this.keyspace,
tableName: this.tableName,
columns,
clusteringOrder: ckOrder.length > 0 ? ckOrder : void 0,
indexes: indexes.length > 0 ? indexes : void 0
};
}
};
function defineTable(config) {
return new TableSchema(config);
}
function createCassandraType(schema, cassandraType) {
const markedSchema = schema;
markedSchema._cassandraType = cassandraType;
const describe = markedSchema.describe;
if (describe) {
markedSchema.describe = (description) => {
const described = describe.call(markedSchema, description);
described._cassandraType = cassandraType;
return described;
};
}
return markedSchema;
}
var Types = {
boolean: zod.z.boolean,
text: zod.z.string,
double: zod.z.number,
enum: zod.z.enum,
object: zod.z.object,
json: () => createCassandraType(zod.z.any(), "text"),
record: (keyType, valueType) => createCassandraType(
zod.z.record(keyType, valueType),
`text`
),
float: () => createCassandraType(
zod.z.number().transform((val) => parseFloat(val.toString())),
"float"
),
int: () => createCassandraType(zod.z.number().int(), "int"),
bigint: () => createCassandraType(zod.z.bigint(), "bigint"),
timestamp: () => createCassandraType(zod.z.date(), "timestamp"),
time: () => createCassandraType(zod.z.date(), "time"),
date: () => createCassandraType(zod.z.date(), "date"),
uuid: () => createCassandraType(zod.z.string().uuid(), "uuid"),
email: () => createCassandraType(zod.z.string().email(), "text"),
blob: () => createCassandraType(zod.z.instanceof(Uint8Array), "blob"),
list: (elementType) => createCassandraType(
zod.z.array(elementType),
`list<${zodToCassandraType(elementType)}>`
),
set: (elementType) => createCassandraType(
zod.z.set(elementType),
`set<${zodToCassandraType(elementType)}>`
),
vector: (dimension) => createCassandraType(
zod.z.array(zod.z.number()).length(dimension, {
message: `\u5411\u91CF\u7EF4\u5EA6\u5FC5\u987B\u4E3A ${dimension}`
}).transform((val) => val.map((v) => parseFloat(v.toString()))),
`vector<float, ${dimension}>`
)
};
exports.Client = Client;
exports.Model = Model;
exports.TableSchema = TableSchema;
exports.Types = Types;
exports.createClient = createClient2;
exports.defineTable = defineTable;