// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #include "sql.h" #include "actor-state.h" #include #include #include #if _WIN32 #define strncasecmp _strnicmp #else #include #endif namespace workerd::api { // Maximum total size of all cached statements (measured in size of the SQL code). If cached // statements exceed this, we remove the LRU statement(s). // // Hopefully most apps don't ever hit this, but it's important to have a limit in case of // queries containing dynamic content or excessively large one-off queries. static constexpr uint SQL_STATEMENT_CACHE_MAX_SIZE = 1024 * 1024; SqlStorage::SqlStorage(jsg::Ref storage) : storage(kj::mv(storage)), statementCache(IoContext::current().addObject(kj::heap())) {} SqlStorage::~SqlStorage() {} jsg::Ref SqlStorage::exec( jsg::Lock& js, jsg::JsString querySql, jsg::Arguments bindings) { auto& context = IoContext::current(); TraceContext traceContext = context.makeUserTraceSpan("durable_object_storage_exec"_kjc); traceContext.setTag("db.system.name"_kjc, "cloudflare-durable-object-sql"_kjc); traceContext.setTag("db.operation.name"_kjc, "exec"_kjc); traceContext.setTag("db.query.text"_kjc, kj::str(querySql)); traceContext.setTag( "cloudflare.durable_object.query.bindings"_kjc, static_cast(bindings.size())); // Internalize the string, so that the cache can be keyed by string identity rather than content. // Any string we put into the cache is expected to live there for a while anyway, so even if it // is a one-off, internalizing it (which moves it to the old generation) shouldn't hurt. querySql = querySql.internalize(js); auto& db = getDb(js); auto& statementCache = *this->statementCache; kj::Rc& slot = statementCache.map.findOrCreate(querySql, [&]() { auto result = kj::rc(js, *this, db, querySql, js.toString(querySql)); statementCache.totalSize += result->statementSize; return result; }); // Move cached statement to end of LRU queue. if (slot->lruLink.isLinked()) { statementCache.lru.remove(*slot.get()); } statementCache.lru.add(*slot.get()); // In order to get accurate statistics, we have to keep the spans around until the query is // actually done, which for read queries that iterate over a cursor won't be until later. kj::Maybe> doneCallback; if (traceContext.isObserved()) { doneCallback = [traceContext = context.addObject(kj::heap(kj::mv(traceContext)))]( Cursor& cursor) mutable { int64_t rowsRead = cursor.getRowsRead(); int64_t rowsWritten = cursor.getRowsWritten(); traceContext->setTag("cloudflare.durable_object.response.rows_read"_kjc, rowsRead); traceContext->setTag("cloudflare.durable_object.response.rows_written"_kjc, rowsWritten); }; } if (slot->isShared()) { // Oops, this CachedStatement is currently in-use (presumably by a Cursor). // // SQLite only allows one instance of a statement to run at a time, so we will have to compile // the statement again as a one-off. // // In theory we could try to cache multiple copies of the statement, but as this is probably // exceedingly rare, it is not worth the added code complexity. SqliteDatabase::Regulator& regulator = *this; return js.alloc( js, kj::mv(doneCallback), db, regulator, js.toString(querySql), kj::mv(bindings)); } auto result = js.alloc(js, kj::mv(doneCallback), slot.addRef(), kj::mv(bindings)); // If the statement cache grew too big, drop the least-recently-used entry. while (statementCache.totalSize > SQL_STATEMENT_CACHE_MAX_SIZE) { auto& toRemove = *statementCache.lru.begin(); auto oldQuery = jsg::JsString(toRemove.query.getHandle(js)); statementCache.totalSize -= toRemove.statementSize; statementCache.lru.remove(toRemove); KJ_ASSERT(statementCache.map.eraseMatch(oldQuery)); } return result; } SqlStorage::IngestResult SqlStorage::ingest(jsg::Lock& js, kj::String querySql) { auto& context = IoContext::current(); TraceContext traceContext = context.makeUserTraceSpan("durable_object_storage_ingest"_kjc); SqliteDatabase::Regulator& regulator = *this; auto result = getDb(js).ingestSql(regulator, querySql); traceContext.setTag( "cloudflare.durable_object.response.rows_read"_kjc, static_cast(result.rowsRead)); traceContext.setTag("cloudflare.durable_object.response.rows_written"_kjc, static_cast(result.rowsWritten)); traceContext.setTag("cloudflare.durable_object.response.statement_count"_kjc, static_cast(result.statementCount)); return IngestResult( kj::str(result.remainder), result.rowsRead, result.rowsWritten, result.statementCount); } void SqlStorage::setMaxPageCountForTest(jsg::Lock& js, int count) { auto& db = getDb(js); db.run({.regulator = SqliteDatabase::TRUSTED}, kj::str("PRAGMA max_page_count = ", count)); } jsg::Ref SqlStorage::prepare(jsg::Lock& js, jsg::JsString query) { return js.alloc(js, JSG_THIS, query); } double SqlStorage::getDatabaseSize(jsg::Lock& js) { auto& context = IoContext::current(); TraceContext traceContext = context.makeUserTraceSpan("durable_object_storage_getDatabaseSize"_kjc); traceContext.setTag("db.operation.name"_kjc, "getDatabaseSize"_kjc); auto& db = getDb(js); int64_t pages = execMemoized(db, pragmaPageCount, "select (select * from pragma_page_count) - (select * from pragma_freelist_count);") .getInt64(0); auto dbSize = pages * getPageSize(db); traceContext.setTag( "cloudflare.durable_object.response.db_size"_kjc, static_cast(dbSize)); return dbSize; } bool SqlStorage::isAllowedName(kj::StringPtr name) const { if (util::Autogate::isEnabled(util::AutogateKey::SQL_RESTRICT_RESERVED_NAMES)) { return strncasecmp(name.begin(), "_cf_", 4) != 0; } if (name.size() >= 4 && strncasecmp(name.begin(), "_cf_", 4) == 0) { LOG_WARNING_PERIODICALLY("SQL identifier matches reserved _cf_ prefix case-insensitively"); } return !name.startsWith("_cf_"); } bool SqlStorage::isAllowedTrigger(kj::StringPtr name) const { return true; } void SqlStorage::onError(kj::Maybe sqliteErrorCode, kj::StringPtr message) const { JSG_ASSERT(false, Error, message); } bool SqlStorage::allowTransactions() const { JSG_FAIL_REQUIRE(Error, "To execute a transaction, please use the state.storage.transaction() or " "state.storage.transactionSync() APIs instead of the SQL BEGIN TRANSACTION or SAVEPOINT " "statements. The JavaScript API is safer because it will automatically roll back on " "exceptions, and because it interacts correctly with Durable Objects' automatic atomic " "write coalescing."); } bool SqlStorage::shouldAddQueryStats() const { // Bill for queries executed from JavaScript. return true; } SqlStorage::StatementCache::~StatementCache() noexcept(false) { for (auto& entry: lru) { lru.remove(entry); } } jsg::JsValue SqlStorage::wrapSqlValue(jsg::Lock& js, SqlValue value) { KJ_IF_SOME(v, value) { KJ_SWITCH_ONEOF(v) { KJ_CASE_ONEOF(bytes, kj::Array) { return jsg::JsValue(js.wrapBytes(kj::mv(bytes))); } KJ_CASE_ONEOF(text, kj::StringPtr) { return js.str(text); } KJ_CASE_ONEOF(number, double) { return js.num(number); } } KJ_UNREACHABLE; } else { return js.null(); } } SqlStorage::Cursor::State::State(SqliteDatabase& db, SqliteDatabase::Regulator& regulator, kj::StringPtr sqlCode, kj::Array bindingsParam) : bindings(kj::mv(bindingsParam)), query(db.run({.regulator = regulator}, sqlCode, mapBindings(bindings).asPtr())) {} SqlStorage::Cursor::State::State( kj::Rc cachedStatementParam, kj::Array bindingsParam) : bindings(kj::mv(bindingsParam)), query(cachedStatement.emplace(kj::mv(cachedStatementParam)) ->statement.run(mapBindings(bindings).asPtr())) {} SqlStorage::Cursor::~Cursor() noexcept(false) { // If this Cursor was created from a Statement, clear the Statement's currentCursor weak ref. KJ_IF_SOME(s, selfRef) { KJ_IF_SOME(p, s) { if (&p == this) { s = kj::none; } } } } void SqlStorage::Cursor::initColumnNames(jsg::Lock& js, State& stateRef) { KJ_IF_SOME(cached, stateRef.cachedStatement) { reusedCachedQuery = cached->useCount++ > 0; } js.withinHandleScope([&]() { v8::LocalVector vec(js.v8Isolate); for (auto i: kj::zeroTo(stateRef.query.columnCount())) { vec.push_back(js.str(stateRef.query.getColumnName(i))); } auto array = jsg::JsArray(v8::Array::New(js.v8Isolate, vec.data(), vec.size())); columnNames = jsg::JsRef(js, array); }); } double SqlStorage::Cursor::getRowsRead() { KJ_IF_SOME(st, state) { return static_cast(st->query.getRowsRead()); } else { return static_cast(rowsRead); } } double SqlStorage::Cursor::getRowsWritten() { KJ_IF_SOME(st, state) { return static_cast(st->query.getRowsWritten()); } else { return static_cast(rowsWritten); } } SqlStorage::Cursor::RowIterator::Next SqlStorage::Cursor::next(jsg::Lock& js) { auto self = JSG_THIS; auto maybeRow = rowIteratorNext(js, self); bool done = maybeRow == kj::none; return { .done = done, .value = kj::mv(maybeRow), }; } jsg::JsArray SqlStorage::Cursor::toArray(jsg::Lock& js) { auto self = JSG_THIS; v8::LocalVector results(js.v8Isolate); for (;;) { auto maybeRow = rowIteratorNext(js, self); KJ_IF_SOME(row, maybeRow) { results.push_back(row); } else { break; } } return jsg::JsArray(v8::Array::New(js.v8Isolate, results.data(), results.size())); } jsg::JsValue SqlStorage::Cursor::one(jsg::Lock& js) { auto self = JSG_THIS; auto result = JSG_REQUIRE_NONNULL(rowIteratorNext(js, self), Error, "Expected exactly one result from SQL query, but got no results."); KJ_IF_SOME(s, state) { // It appears that the query had more results, otherwise we would have set `state` to `none` // inside `iteratorImpl()`. endQuery(*s); JSG_FAIL_REQUIRE( Error, "Expected exactly one result from SQL query, but got multiple results."); } return result; } jsg::Ref SqlStorage::Cursor::rows(jsg::Lock& js) { return js.alloc(JSG_THIS); } kj::Maybe SqlStorage::Cursor::rowIteratorNext(jsg::Lock& js, jsg::Ref& obj) { KJ_IF_SOME(values, iteratorImpl(js, obj)) { auto names = obj->columnNames.getHandle(js); jsg::JsObject result = js.obj(); KJ_ASSERT(names.size() == values.size()); for (auto i: kj::zeroTo(names.size())) { result.set(js, names.get(js, i), jsg::JsValue(values[i])); } return result; } else { return kj::none; } } jsg::Ref SqlStorage::Cursor::raw(jsg::Lock& js) { return js.alloc(JSG_THIS); } // Returns the set of column names for the current Cursor. An exception will be thrown if the // iterator has already been fully consumed. The resulting columns may contain duplicate entries, // for instance a `SELECT *` across a join of two tables that share a column name. jsg::JsArray SqlStorage::Cursor::getColumnNames(jsg::Lock& js) { return columnNames.getHandle(js); } kj::Maybe SqlStorage::Cursor::rawIteratorNext(jsg::Lock& js, jsg::Ref& obj) { KJ_IF_SOME(values, iteratorImpl(js, obj)) { return jsg::JsArray(v8::Array::New(js.v8Isolate, values.data(), values.size())); } else { return kj::none; } } kj::Maybe> SqlStorage::Cursor::iteratorImpl( jsg::Lock& js, jsg::Ref& obj) { auto& state = *KJ_UNWRAP_OR(obj->state, { if (obj->canceled) { JSG_FAIL_REQUIRE(Error, "SQL cursor was closed because the same statement was executed again. If you need to " "run multiple copies of the same statement concurrently, you must create multiple " "prepared statement objects."); } else { // Query already done. return kj::none; } }); auto& query = state.query; if (query.isDone()) { obj->endQuery(state); return kj::none; } auto n = query.columnCount(); v8::LocalVector results(js.v8Isolate); results.reserve(n); for (auto i: kj::zeroTo(n)) { SqlValue value; KJ_SWITCH_ONEOF(query.getValue(i)) { KJ_CASE_ONEOF(data, kj::ArrayPtr) { value.emplace(kj::heapArray(data)); } KJ_CASE_ONEOF(text, kj::StringPtr) { value.emplace(text); } KJ_CASE_ONEOF(i, int64_t) { // int64 will become BigInt, but most applications won't want all their integers to be // BigInt. We will coerce to a double here. // TODO(someday): Allow applications to request that certain columns use BigInt. value.emplace(static_cast(i)); } KJ_CASE_ONEOF(d, double) { value.emplace(d); } KJ_CASE_ONEOF(_, decltype(nullptr)) { // leave value null } } results.push_back(wrapSqlValue(js, kj::mv(value))); } // Proactively iterate to the next row and, if it turns out the query is done, discard it. This // is an optimization to make sure that the statement can be returned to the statement cache once // the application has iterated over all results, even if the application fails to call next() // one last time to get `{done: true}`. A common case where this could happen is if the app is // expecting zero or one results, so it calls `exec(...).next()`. In the case that one result // was returned, the application may not bother calling `next()` again. If we hadn't proactively // iterated ahead by one, then the statement would not be returned to the cache until it was // GC'ed, which might prevent the cache from being effective in the meantime. // // Unfortunately, this does not help with the case where the application stops iterating with // results still available from the cursor. There's not much we can do about that case since // there's no way to know if the app might come back and try to use the cursor again later. query.nextRow(); if (query.isDone()) { obj->endQuery(state); } return kj::mv(results); } void SqlStorage::Cursor::endQuery(State& stateRef) { // Save off row counts before the query goes away. rowsRead = stateRef.query.getRowsRead(); rowsWritten = stateRef.query.getRowsWritten(); KJ_IF_SOME(cb, doneCallback) { cb(*this); doneCallback = kj::none; } // Clean up the query proactively. state = kj::none; } kj::Array SqlStorage::Cursor::mapBindings( kj::ArrayPtr values) { return KJ_MAP(value, values) -> SqliteDatabase::Query::ValuePtr { KJ_IF_SOME(v, value) { KJ_SWITCH_ONEOF(v) { KJ_CASE_ONEOF(data, kj::Array) { return data.asPtr(); } KJ_CASE_ONEOF(text, kj::String) { return text.asPtr(); } KJ_CASE_ONEOF(d, double) { return d; } } } else { return nullptr; } KJ_UNREACHABLE; }; } jsg::Ref SqlStorage::Statement::run( jsg::Lock& js, jsg::Arguments bindings) { return sqlStorage->exec(js, jsg::JsString(query.getHandle(js)), kj::mv(bindings)); } void SqlStorage::visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("storage", storage); tracker.trackFieldWithSize("IoPtr", sizeof(IoPtr)); if (pragmaPageCount != kj::none) { tracker.trackFieldWithSize( "IoPtr", sizeof(IoPtr)); } if (pragmaGetMaxPageCount != kj::none) { tracker.trackFieldWithSize( "IoPtr", sizeof(IoPtr)); } } } // namespace workerd::api