package aceboot::orm.common
import std.sync.*
/**
* 读写分离路由驱动:包裹一个 primary(主,写) + 若干 replica(从,读),透明地把读路由到从库、
* 把写与事务强制留在主库——仓储 / QueryBuilder / EntityManager 全部零改动(它们只认 `ds.driver: Driver`)。
*
* 路由规则:
* - `exec` / `begin` / `commit` / `rollback` / `close` / `affectedRows` / `lastInsertId` / `dialect` → 一律 primary。
* - `query()`:
* · 事务内(`aceTxDepth() > 0`)→ primary(保证「读己之写」一致性,避免读到从库未同步的旧数据);
* · 写语句(非 SELECT,如 `INSERT ... RETURNING`、`SAVEPOINT`、DDL)→ primary;
* · 其余只读 SELECT → 轮询(round-robin)一个 replica。
* 无 replica 配置时一律走 primary,完全向后兼容单库部署。
*
* 约束:主从须同型(同方言);dialect 取 primary。RoutingDriver 自身不持连接,只委派,
* 与 sqlite perOp / 连接池 / InstrumentedDriver 装饰器可任意叠加。
*/
public class RoutingDriver <: Driver {
let primary: Driver
let replicas: Array<Driver>
let rr = AtomicInt64(0)
public init(primary: Driver, replicas: Array<Driver>) {
this.primary = primary
this.replicas = replicas
}
public func dialect(): Dialect {
primary.dialect()
}
public func query(sql: String, params: Array<DbValue>): Array<Row> {
// 仅「有从库 + 不在事务中 + 纯只读 SELECT」才路由到从库;其余保守地走主库。
if (replicas.size > 0 && aceTxDepth() == 0 && isReadSql(sql)) {
let idx = rr.fetchAdd(1) % Int64(replicas.size)
return replicas[idx].query(sql, params)
}
return primary.query(sql, params)
}
public func exec(sql: String, params: Array<DbValue>): Int64 {
primary.exec(sql, params)
}
public func begin(): Unit {
primary.begin()
}
public func commit(): Unit {
primary.commit()
}
public func rollback(): Unit {
primary.rollback()
}
public func close(): Unit {
primary.close()
for (r in replicas) {
r.close()
}
}
public func affectedRows(): Int64 {
primary.affectedRows()
}
public func lastInsertId(): Int64 {
primary.lastInsertId()
}
}
// 判断 SQL 是否纯只读(首关键字为 SELECT)。保守策略:除明确 SELECT 外一律视为写/走主库,
// 故 INSERT ... RETURNING(经 query 执行)、WITH ... 写 CTE、DDL 都正确留在主库。
func isReadSql(sql: String): Bool {
leadingKeyword(sql) == "select"
}
// 取 SQL 的首个英文关键字并转小写(跳过前导空白与左括号)。
func leadingKeyword(sql: String): String {
let bytes = sql.toArray()
var i = 0
while (i < bytes.size) {
let c = bytes[i]
if (c == 32u8 || c == 9u8 || c == 10u8 || c == 13u8 || c == 40u8) {
i += 1
} else {
break
}
}
let start = i
while (i < bytes.size) {
let c = bytes[i]
if ((c >= 65u8 && c <= 90u8) || (c >= 97u8 && c <= 122u8)) {
i += 1
} else {
break
}
}
if (i <= start) {
return ""
}
let slice = Array<UInt8>(i - start, {k => bytes[start + k]})
return lowerAscii(String.fromUtf8(slice))
}
// ASCII 小写化(SQL 关键字判定用;不触碰非 ASCII 字节)。
func lowerAscii(s: String): String {
let bs = s.toArray()
let out = Array<UInt8>(bs.size, {i =>
let c = bs[i]
if (c >= 65u8 && c <= 90u8) {
c + 32u8
} else {
c
}
})
String.fromUtf8(out)
}