UNPKG

imean-cassandra-orm

Version:

cassandra orm

1,420 lines (1,398 loc) 46.1 kB
'use strict'; 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;