restored/remote_sql.inc
← All source files · Download this file
21,208 bytes · SHA256 8ee738e9f72b95d0541c39f7c66638e49f73eff10c4630279ddafd985cfd8e07
// Explicit remote SQL API. SQLite's db command is deliberately unchanged.
static void remoteSqlValidate(const CodeLine& c) {
count(c,2,6); const auto& a=c.arg; const auto& s=c.source;
if(a[1].text=="open") { count(c,5,5); destination(a[2],s); }
else if(a[1].text=="query" || a[1].text=="exec") { count(c,6,6); destination(a[2],s); destination(a[5],s); }
else if(a[1].text=="begin" || a[1].text=="commit" || a[1].text=="rollback" || a[1].text=="close") count(c,3,3);
else fail(s,"unknown sql operation");
}
static Json remoteSqlProfile(const std::string& path,const std::string& name,const Source& s) {
int fd=::open(path.c_str(),O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NONBLOCK);
if(fd<0) fail(s,"SQL profile file unavailable");
struct Guard { int fd; ~Guard(){::close(fd);} } guard{fd};
struct stat st{};
if(fstat(fd,&st) || !S_ISREG(st.st_mode) || st.st_uid!=geteuid() || (st.st_mode&077)!=0 || st.st_size>65536)
fail(s,"SQL profile file must be owned by current user, private (0600), regular, <=64 KiB");
std::string data; char buf[4096];
for(;;) { auto n=::read(fd,buf,sizeof(buf)); if(n<0 && errno==EINTR) continue; if(n<0) fail(s,"SQL profile read failed"); if(!n) break; data.append(buf,static_cast<std::size_t>(n)); if(data.size()>65536) failLimit(s,"SQL profile size limit"); }
Json config;
try { config=Json::parse(data); } catch(...) { fail(s,"invalid SQL profile JSON (details redacted)"); }
if(!config.is_object() || !config.contains(name) || !config[name].is_object()) fail(s,"SQL profile not found");
return config[name];
}
struct vScript::RemoteDatabase {
MYSQL* mysql=nullptr;
SQLHENV env=SQL_NULL_HENV;
SQLHDBC dbc=SQL_NULL_HDBC;
bool transaction=false;
unsigned timeout=2;
~RemoteDatabase() {
if(mysql) mysql_close(mysql);
if(dbc!=SQL_NULL_HDBC) { if(transaction) SQLEndTran(SQL_HANDLE_DBC,dbc,SQL_ROLLBACK); SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC,dbc); }
if(env!=SQL_NULL_HENV) SQLFreeHandle(SQL_HANDLE_ENV,env);
}
static void od(SQLRETURN rc,SQLSMALLINT kind,SQLHANDLE handle,const Source& s) {
if(rc==SQL_SUCCESS) return;
// Do not expose driver messages: they may echo credentials, SQL or data.
SQLCHAR state[6]{}; SQLINTEGER native=0; SQLCHAR message[2]{}; SQLSMALLINT size=0;
SQLGetDiagRec(kind,handle,1,state,&native,message,2,&size);
fail(s,"remote SQL ODBC error SQLSTATE="+std::string(reinterpret_cast<char*>(state),5)+" code="+std::to_string(native));
}
static std::string brace(const std::string& x) {
std::string r="{"; for(char ch:x) { r+=ch; if(ch=='}') r+=ch; } return r+"}";
}
void connect(const std::string& driver,const Json& p,const Source& s) {
auto text=[&](const char* key) { if(!p.contains(key) || !p[key].is_string()) fail(s,"SQL profile missing string field"); auto x=p[key].get<std::string>(); if(x.size()>4096 || x.find('\0')!=std::string::npos) fail(s,"invalid SQL profile field"); return x; };
if(text("driver")!=driver) fail(s,"SQL profile driver mismatch");
auto host=text("host"), user=text("user"), pass=text("password"), database=text("database");
if(!p.contains("port") || !p["port"].is_number_unsigned()) fail(s,"SQL profile port must be positive integer");
auto port=p["port"].get<unsigned long long>(); if(port<1 || port>65535) fail(s,"SQL profile port out of range");
if(p.contains("timeout")) { if(!p["timeout"].is_number_unsigned() || p["timeout"].get<unsigned long long>()<1 || p["timeout"].get<unsigned long long>()>30) fail(s,"SQL timeout range is 1..30 seconds"); timeout=p["timeout"].get<unsigned>(); }
std::string tls=p.contains("tls") ? text("tls") : "verify";
if(tls!="verify" && tls!="insecure-loopback") fail(s,"SQL TLS mode must be verify or insecure-loopback");
if(tls=="insecure-loopback" && host!="127.0.0.1" && host!="::1") fail(s,"insecure SQL permitted only on numeric loopback");
if(driver=="mysql" || driver=="mariadb") {
mysql=mysql_init(nullptr); if(!mysql) fail(s,"remote SQL allocation failed");
auto opt=[&](enum mysql_option key,const void* v) { if(mysql_optionsv(mysql,key,v)) fail(s,"remote SQL option failed"); };
opt(MYSQL_OPT_CONNECT_TIMEOUT,&timeout); opt(MYSQL_OPT_READ_TIMEOUT,&timeout); opt(MYSQL_OPT_WRITE_TIMEOUT,&timeout);
unsigned int local=0; opt(MYSQL_OPT_LOCAL_INFILE,&local);
my_bool reconnect=0; opt(MYSQL_OPT_RECONNECT,&reconnect);
opt(MYSQL_SET_CHARSET_NAME,"utf8mb4");
unsigned int protocol=MYSQL_PROTOCOL_TCP; opt(MYSQL_OPT_PROTOCOL,&protocol);
std::string ca;
if(tls=="verify") { ca=text("ca"); opt(MYSQL_OPT_SSL_CA,ca.c_str()); my_bool yes=1; opt(MYSQL_OPT_SSL_ENFORCE,&yes); opt(MYSQL_OPT_SSL_VERIFY_SERVER_CERT,&yes); }
if(!mysql_real_connect(mysql,host.c_str(),user.c_str(),pass.c_str(),database.c_str(),static_cast<unsigned>(port),nullptr,0))
fail(s,"remote SQL connect failed code="+std::to_string(mysql_errno(mysql)));
if(tls=="verify" && !mysql_get_ssl_cipher(mysql)) fail(s,"remote SQL verified TLS required");
} else {
od(SQLAllocHandle(SQL_HANDLE_ENV,SQL_NULL_HANDLE,&env),SQL_HANDLE_ENV,env,s);
od(SQLSetEnvAttr(env,SQL_ATTR_ODBC_VERSION,reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3),0),SQL_HANDLE_ENV,env,s);
od(SQLAllocHandle(SQL_HANDLE_DBC,env,&dbc),SQL_HANDLE_ENV,env,s);
od(SQLSetConnectAttr(dbc,SQL_ATTR_LOGIN_TIMEOUT,reinterpret_cast<SQLPOINTER>(static_cast<uintptr_t>(timeout)),0),SQL_HANDLE_DBC,dbc,s);
auto server="tcp:"+host+","+std::to_string(port);
std::string cs="Driver={ODBC Driver 18 for SQL Server};Server="+brace(server)+";UID="+brace(user)+";PWD="+brace(pass)+";Database="+brace(database)+";Encrypt=yes;TrustServerCertificate="+(tls=="verify" ? "no" : "yes")+";";
auto rc=SQLDriverConnect(dbc,nullptr,reinterpret_cast<SQLCHAR*>(cs.data()),SQL_NTS,nullptr,0,nullptr,SQL_DRIVER_NOPROMPT);
if(rc==SQL_SUCCESS_WITH_INFO) {
// SQL Server reports database/language selection as connect info.
for(SQLSMALLINT i=1;;++i) { SQLCHAR state[6]{}; SQLINTEGER native=0; SQLCHAR message[2]{}; SQLSMALLINT size=0;
auto diag=SQLGetDiagRec(SQL_HANDLE_DBC,dbc,i,state,&native,message,2,&size); if(diag==SQL_NO_DATA) break;
if(!SQL_SUCCEEDED(diag) || std::memcmp(state,"01000",5)!=0 || (native!=5701 && native!=5703)) od(rc,SQL_HANDLE_DBC,dbc,s);
}
} else od(rc,SQL_HANDLE_DBC,dbc,s);
}
}
void control(const std::string& op,const Source& s) {
if(op=="begin" ? transaction : !transaction) fail(s,"remote SQL transaction state mismatch");
if(mysql) {
const char* sql=op=="begin" ? "START TRANSACTION" : op=="commit" ? "COMMIT" : "ROLLBACK";
if(mysql_real_query(mysql,sql,static_cast<unsigned long>(std::strlen(sql)))) fail(s,"remote SQL transaction failed code="+std::to_string(mysql_errno(mysql)));
} else if(op=="begin") od(SQLSetConnectAttr(dbc,SQL_ATTR_AUTOCOMMIT,reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF),0),SQL_HANDLE_DBC,dbc,s);
else { od(SQLEndTran(SQL_HANDLE_DBC,dbc,op=="commit" ? SQL_COMMIT : SQL_ROLLBACK),SQL_HANDLE_DBC,dbc,s); od(SQLSetConnectAttr(dbc,SQL_ATTR_AUTOCOMMIT,reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_ON),0),SQL_HANDLE_DBC,dbc,s); }
transaction=op=="begin";
}
struct Result {
Json rows=Json::array(); long long changed=0; std::size_t bytes=128, nodes=1;
void add(Json row,const Source& s) {
if(rows.size()>=1024) failLimit(s,"remote SQL row limit (1024)");
nodes+=row.size()+1; if(nodes>itemLimit) failLimit(s,"remote SQL node limit");
bytes+=jsonSize(row,s)+sizeof(Json); if(bytes>valueLimit) failLimit(s,"remote SQL result byte limit (1 MiB)");
rows.push_back(std::move(row));
}
};
static void name(std::vector<std::string>& names,std::string n,const Source& s) {
if(n.empty() || n.size()>256 || std::find(names.begin(),names.end(),n)!=names.end()) fail(s,"remote SQL column names must be unique, nonempty and <=256 bytes; use aliases");
names.push_back(std::move(n));
}
static Json cell(const std::string& text,bool integral,bool real,const Source& s) {
try {
if(integral) return integer(text,-9007199254740991LL,9007199254740991LL,s);
if(real) return number(text,s);
} catch(const Error&) { fail(s,"remote SQL numeric result outside supported range"); }
return text;
}
Result run(const std::string& sql,const Json& params,bool query,const Source& s) {
if(sql.empty() || sql.size()>65536 || sql.find('\0')!=std::string::npos) fail(s,"remote SQL requires nonempty SQL <=64 KiB without NUL");
if(!params.is_array() || params.size()>256) fail(s,"remote SQL parameters require array of <=256 scalars");
for(const auto& p:params) if(!(p.is_null() || p.is_boolean() || p.is_number() || p.is_string())) fail(s,"remote SQL parameters must be scalar JSON");
// Conservative lexical policy; not a SQL parser or a security sandbox.
// Quoted semicolons are data. Comments/batches/control SQL are unsupported.
std::string word; char quote=0;
auto token=[&]() { if(word=="BEGIN" || word=="COMMIT" || word=="ROLLBACK" || word=="SAVEPOINT" || word=="TRANSACTION" || word=="AUTOCOMMIT" || word=="IMPLICIT_TRANSACTIONS" || word=="CALL" || word=="EXEC" || word=="EXECUTE") fail(s,"remote SQL transaction/control SQL unsupported; use sql commands"); word.clear(); };
for(std::size_t i=0;i<sql.size();++i) { char ch=sql[i]; if(quote) { if(ch=='\\') fail(s,"remote SQL backslash quoted escapes unsupported; bind values"); if(ch==quote) { if(i+1<sql.size() && sql[i+1]==quote) ++i; else quote=0; } continue; }
if(ch=='\'' || ch=='"' || ch=='`' || ch=='[') { token(); quote=ch=='[' ? ']' : ch; }
else if(ch==';' || ch=='#' || (ch=='-' && i+1<sql.size() && sql[i+1]=='-') || (ch=='/' && i+1<sql.size() && sql[i+1]=='*')) fail(s,"remote SQL comments and semicolon batches unsupported");
else if(std::isalnum(static_cast<unsigned char>(ch)) || ch=='_') word+=static_cast<char>(std::toupper(static_cast<unsigned char>(ch))); else token(); }
token(); if(quote) fail(s,"remote SQL unterminated quoted token");
return mysql ? runMysql(sql,params,query,s) : runOdbc(sql,params,query,s);
}
Result runMysql(const std::string& sql,const Json& params,bool query,const Source& s) {
std::unique_ptr<MYSQL_STMT,decltype(&mysql_stmt_close)> stmt(mysql_stmt_init(mysql),mysql_stmt_close);
if(!stmt) fail(s,"remote SQL statement allocation failed");
auto raw=stmt.get();
auto check=[&](int rc) { if(rc) fail(s,"remote SQL statement failed code="+std::to_string(mysql_stmt_errno(raw))); };
check(mysql_stmt_prepare(raw,sql.data(),static_cast<unsigned long>(sql.size())));
if(mysql_stmt_param_count(raw)!=params.size()) fail(s,"remote SQL parameter count mismatch");
std::vector<MYSQL_BIND> binds(params.size()); std::vector<long long> ints(params.size()); std::vector<double> doubles(params.size()); std::vector<unsigned long> lengths(params.size());
for(std::size_t i=0;i<params.size();++i) { const auto& p=params[i]; auto& b=binds[i];
if(p.is_null()) b.buffer_type=MYSQL_TYPE_NULL;
else if(p.is_boolean() || p.is_number_integer()) { ints[i]=p.is_boolean() ? (p.get<bool>()?1:0) : p.get<long long>(); b.buffer_type=MYSQL_TYPE_LONGLONG; b.buffer=&ints[i]; }
else if(p.is_number()) { doubles[i]=p.get<double>(); b.buffer_type=MYSQL_TYPE_DOUBLE; b.buffer=&doubles[i]; }
else { auto& str=p.get_ref<const std::string&>(); lengths[i]=static_cast<unsigned long>(str.size()); b.buffer_type=MYSQL_TYPE_STRING; b.buffer=const_cast<char*>(str.data()); b.buffer_length=lengths[i]; b.length=&lengths[i]; }
}
if(!binds.empty()) check(mysql_stmt_bind_param(raw,binds.data()));
auto columns=mysql_stmt_field_count(raw); if(columns>128) failLimit(s,"remote SQL column limit (128)");
if(query ? !columns : columns!=0) fail(s,"remote SQL query requires rows; exec cannot return rows");
check(mysql_stmt_execute(raw)); Result result;
if(!query) { auto n=mysql_stmt_affected_rows(raw); if(n>9007199254740991ULL) fail(s,"remote SQL affected rows out of safe range"); result.changed=static_cast<long long>(n); return result; }
std::unique_ptr<MYSQL_RES,decltype(&mysql_free_result)> metadata(mysql_stmt_result_metadata(raw),mysql_free_result);
if(!metadata) fail(s,"remote SQL result metadata failed");
auto fields=mysql_fetch_fields(metadata.get());
std::vector<std::string> names; std::vector<MYSQL_BIND> out(columns); std::vector<unsigned long> sizes(columns); std::vector<double> reals(columns); std::unique_ptr<my_bool[]> nulls(new my_bool[columns]{});
for(unsigned i=0;i<columns;++i) { name(names,std::string(fields[i].name,fields[i].name_length),s); out[i].buffer_type=MYSQL_TYPE_STRING; out[i].length=&sizes[i]; out[i].is_null=&nulls[i];
if(fields[i].type==MYSQL_TYPE_DOUBLE || fields[i].type==MYSQL_TYPE_FLOAT) { out[i].buffer_type=MYSQL_TYPE_DOUBLE; out[i].buffer=&reals[i]; out[i].buffer_length=sizeof(double); }
}
check(mysql_stmt_bind_result(raw,out.data()));
for(;;) { auto rc=mysql_stmt_fetch(raw); if(rc==MYSQL_NO_DATA) break; if(rc!=0 && rc!=MYSQL_DATA_TRUNCATED) check(rc); Json row=Json::object();
for(unsigned i=0;i<columns;++i) { if(nulls[i]) { row[names[i]]=nullptr; continue; } if(sizes[i]>65536) failLimit(s,"remote SQL cell limit (64 KiB)");
auto type=fields[i].type;
if(type==MYSQL_TYPE_DOUBLE || type==MYSQL_TYPE_FLOAT) { if(!std::isfinite(reals[i])) fail(s,"remote SQL nonfinite number"); row[names[i]]=reals[i]; continue; }
if(fields[i].charsetnr==63 && (type==MYSQL_TYPE_BLOB || type==MYSQL_TYPE_LONG_BLOB || type==MYSQL_TYPE_MEDIUM_BLOB || type==MYSQL_TYPE_TINY_BLOB || type==MYSQL_TYPE_STRING || type==MYSQL_TYPE_VAR_STRING)) fail(s,"remote SQL binary results unsupported");
std::string text(sizes[i],'\0'); MYSQL_BIND b{}; b.buffer_type=MYSQL_TYPE_STRING; b.buffer=text.data(); b.buffer_length=sizes[i]; check(mysql_stmt_fetch_column(raw,&b,i,0));
bool integral=type==MYSQL_TYPE_TINY || type==MYSQL_TYPE_SHORT || type==MYSQL_TYPE_LONG || type==MYSQL_TYPE_LONGLONG || type==MYSQL_TYPE_INT24 || type==MYSQL_TYPE_YEAR;
row[names[i]]=cell(text,integral,type==MYSQL_TYPE_FLOAT || type==MYSQL_TYPE_DOUBLE,s);
} result.add(std::move(row),s);
} return result;
}
Result runOdbc(const std::string& sql,const Json& params,bool query,const Source& s) {
SQLHSTMT stmt=SQL_NULL_HSTMT; od(SQLAllocHandle(SQL_HANDLE_STMT,dbc,&stmt),SQL_HANDLE_DBC,dbc,s);
struct Guard { SQLHSTMT h; ~Guard(){SQLFreeHandle(SQL_HANDLE_STMT,h);} } guard{stmt};
auto check=[&](SQLRETURN rc) { od(rc,SQL_HANDLE_STMT,stmt,s); };
check(SQLSetStmtAttr(stmt,SQL_ATTR_QUERY_TIMEOUT,reinterpret_cast<SQLPOINTER>(static_cast<uintptr_t>(timeout)),0));
check(SQLPrepare(stmt,reinterpret_cast<SQLCHAR*>(const_cast<char*>(sql.data())),static_cast<SQLINTEGER>(sql.size())));
SQLSMALLINT countParams=0; check(SQLNumParams(stmt,&countParams)); if(static_cast<std::size_t>(countParams)!=params.size()) fail(s,"remote SQL parameter count mismatch");
std::vector<long long> ints(params.size()); std::vector<double> doubles(params.size()); std::vector<SQLLEN> lengths(params.size());
for(std::size_t i=0;i<params.size();++i) { const auto& p=params[i]; SQLSMALLINT ctype=SQL_C_CHAR, type=SQL_VARCHAR; SQLPOINTER ptr=nullptr; SQLULEN size=1; SQLLEN capacity=0;
if(p.is_null()) { lengths[i]=SQL_NULL_DATA; ptr=&ints[i]; }
else if(p.is_boolean() || p.is_number_integer()) { ints[i]=p.is_boolean() ? (p.get<bool>()?1:0) : p.get<long long>(); ctype=SQL_C_SBIGINT; type=SQL_BIGINT; ptr=&ints[i]; size=19; capacity=sizeof(long long); }
else if(p.is_number()) { doubles[i]=p.get<double>(); ctype=SQL_C_DOUBLE; type=SQL_DOUBLE; ptr=&doubles[i]; size=15; capacity=sizeof(double); }
else { const auto& str=p.get_ref<const std::string&>(); type=SQL_WVARCHAR; ptr=const_cast<char*>(str.data()); size=std::max<std::size_t>(1,str.size()); lengths[i]=static_cast<SQLLEN>(str.size()); capacity=lengths[i]; }
check(SQLBindParameter(stmt,static_cast<SQLUSMALLINT>(i+1),SQL_PARAM_INPUT,ctype,type,size,0,ptr,capacity,&lengths[i]));
}
auto executed=SQLExecute(stmt); if(executed!=SQL_NO_DATA) check(executed);
SQLSMALLINT columns=0; check(SQLNumResultCols(stmt,&columns));
if(columns>128) failLimit(s,"remote SQL column limit (128)");
if(query ? columns==0 : columns!=0) fail(s,"remote SQL query requires rows; exec cannot return rows");
Result result;
if(!query) { SQLLEN n=0; check(SQLRowCount(stmt,&n)); result.changed=static_cast<long long>(n); }
else {
std::vector<std::string> names; std::vector<SQLSMALLINT> types;
for(SQLUSMALLINT i=1;i<=static_cast<SQLUSMALLINT>(columns);++i) { SQLCHAR buf[257]{}; SQLSMALLINT len=0,type=0,dec=0,nullable=0; SQLULEN size=0;
check(SQLDescribeCol(stmt,i,buf,sizeof(buf),&len,&type,&size,&dec,&nullable)); if(len<0 || len>256) failLimit(s,"remote SQL column name limit"); name(names,std::string(reinterpret_cast<char*>(buf),static_cast<std::size_t>(len)),s); types.push_back(type); }
for(;;) { auto rc=SQLFetch(stmt); if(rc==SQL_NO_DATA) break; check(rc); Json row=Json::object();
for(SQLUSMALLINT i=1;i<=static_cast<SQLUSMALLINT>(columns);++i) { auto type=types[i-1]; if(type==SQL_BINARY || type==SQL_VARBINARY || type==SQL_LONGVARBINARY) fail(s,"remote SQL binary results unsupported");
std::vector<char> buf(65537); SQLLEN len=0; auto got=SQLGetData(stmt,i,SQL_C_CHAR,buf.data(),static_cast<SQLLEN>(buf.size()),&len);
if(len==SQL_NULL_DATA) { check(got); row[names[i-1]]=nullptr; continue; }
if(len==SQL_NO_TOTAL || len>65536) failLimit(s,"remote SQL cell limit (64 KiB)");
check(got); if(len<0) fail(s,"remote SQL invalid column length");
bool integral=type==SQL_BIT || type==SQL_TINYINT || type==SQL_SMALLINT || type==SQL_INTEGER || type==SQL_BIGINT;
row[names[i-1]]=cell(std::string(buf.data(),static_cast<std::size_t>(len)),integral,type==SQL_REAL || type==SQL_FLOAT || type==SQL_DOUBLE,s);
} result.add(std::move(row),s);
}
}
auto more=SQLMoreResults(stmt); if(more!=SQL_NO_DATA) { check(more); fail(s,"remote SQL multiple results unsupported; effects may have occurred"); }
return result;
}
};
bool vScript::remoteSql(Frame& frame,const CodeLine& c) {
const auto& a=c.arg; if(a[0].text!="sql") return false; const auto& s=c.source; const auto& op=a[1].text;
if(!options.allowDb || !options.allowNetwork) fail(s,"remote SQL requires --allow-db and --allow-network (not a sandbox)");
auto val=[&](std::size_t i){return value(frame,a[i],s);};
if(op=="open") {
lookup(frame,a[2],s);
if(options.sqlConfig.empty()) fail(s,"remote SQL requires operator --sql-config file");
if(remoteDatabases.size()>=8) failLimit(s,"remote SQL handle limit (8)");
auto driver=val(3); if(driver!="mysql" && driver!="mariadb" && driver!="mssql") fail(s,"remote SQL driver must be mysql, mariadb or mssql");
auto db=std::make_shared<RemoteDatabase>(); db->connect(driver,remoteSqlProfile(options.sqlConfig,val(4),s),s);
auto id="sql:"+std::to_string(nextRemoteDatabase++); assign(frame,a[2],id,s); remoteDatabases.emplace(id,std::move(db));
} else {
bool statement=op=="exec" || op=="query"; if(statement) lookup(frame,a[2],s);
auto it=remoteDatabases.find(val(statement?3:2)); if(it==remoteDatabases.end()) fail(s,"invalid or closed remote SQL handle");
if(op=="close") { remoteDatabases.erase(it); return true; }
try {
if(++remoteSqlCalls>1024) failLimit(s,"remote SQL call limit (1024)");
if(statement) { auto r=it->second->run(val(4),jsonValue(frame,a[5],s),op=="query",s);
if(op=="exec") assign(frame,a[2],std::to_string(r.changed),s);
else { if(r.bytes>16*valueLimit-remoteSqlBytes) failLimit(s,"remote SQL cumulative result limit (16 MiB)"); remoteSqlBytes+=r.bytes; putJson(frame,a[2],std::move(r.rows),s); }
} else it->second->control(op,s);
} catch(...) { remoteDatabases.erase(it); throw; } // no reuse after ambiguous state
}
return true;
}