/// Minimal runtime for the standalone WebAssembly build of the SQL parser. /// /// The real implementations of these live in `Common/Exception.cpp`, `Common/StackTrace.cpp`, /// `Common/MemoryTracker.cpp` and friends. They are deliberately NOT built here: /// /// * stack traces need libunwind, DWARF parsing and a patched libc++ /// (`STD_EXCEPTION_HAS_STACK_TRACE`, which is 0 here), /// * memory tracking and thread status need thread-local server bookkeeping, /// * logging needs Poco's channel/formatter machinery. /// /// None of that is meaningful in a browser, and all of it dominates the bundle size, so the /// WebAssembly build substitutes the no-op versions below. #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace DB { bool terminate_on_any_exception = false; std::atomic_bool abort_on_logical_error = false; thread_local bool Exception::enable_job_stack_trace = false; thread_local bool Exception::can_use_thread_frame_pointers = false; thread_local Exception::ThreadFramePointers Exception::thread_frame_pointers; const Exception::ThreadFramePointersBase Exception::dummy_frame_pointers{}; std::function Exception::callback; Exception::ThreadFramePointers::ThreadFramePointers() = default; Exception::ThreadFramePointers::~ThreadFramePointers() = default; const Exception::ThreadFramePointersBase & Exception::getThreadFramePointers() { return dummy_frame_pointers; } void Exception::setThreadFramePointers(ThreadFramePointersBase) { } void Exception::clearThreadFramePointers() { } /// No query masking rules exist in a browser, so the message is passed through unchanged. Exception::MessageMasked::MessageMasked(const std::string & msg_, std::string format_string_) : msg(msg_), format_string(std::move(format_string_)) { } Exception::MessageMasked::MessageMasked(std::string && msg_, std::string format_string_) : msg(std::move(msg_)), format_string(std::move(format_string_)) { } Exception::Exception(const MessageMasked & msg_masked, int code, bool remote_) : Poco::Exception(msg_masked.msg, code), remote(remote_), message_format_string(msg_masked.format_string) { } Exception::Exception(MessageMasked && msg_masked, int code, bool remote_) : Poco::Exception(std::move(msg_masked.msg), code), remote(remote_), message_format_string(msg_masked.format_string) { } Exception::~Exception() = default; void Exception::addMessage(const MessageMasked & msg_masked) { extendedMessage(msg_masked.msg); } std::string Exception::getStackTraceString() const { return {}; } Exception::Trace Exception::getStackFramePointers() const { return {}; } bool Exception::isErrorCodeImportant() const { return false; } template Exception::Exception(int, FormatStringHelperImpl<>); /// `Core/Settings.cpp` is not built: one call (`ParserSetQuery` asking whether a bare `SET x` /// names a Bool setting) would otherwise pull in the whole settings schema - every /// `SettingField*Traits` specialization - which dwarfs the parser itself. Answering /// "yes, it could be a Bool" keeps `SET x` parsing; the server still validates the name. Field Settings::castValueUtil(std::string_view, const Field & value) { return value; } /// `Core/SettingsSecrets.cpp` is not built: the module formats with secrets shown, so no setting /// value is masked, and the URL masking it reaches stays out of the bundle. std::optional CoreSettings::renderSecretSettingValue(const String &, const Field &) { return {}; } /// Logging is not wired up: nothing consumes it in a browser. bool currentThreadHasGroup() { return false; } LogsLevel currentThreadLogsLevel() { return LogsLevel::none; } } /// Memory accounting is a server concern; in WebAssembly `malloc` is the only budget there is. AllocationTrace CurrentMemoryTracker::alloc(Int64) { return AllocationTrace(0.0); } AllocationTrace CurrentMemoryTracker::allocNoThrow(Int64) { return AllocationTrace(0.0); } AllocationTrace CurrentMemoryTracker::allocThrow(Int64) { return AllocationTrace(0.0); } AllocationTrace CurrentMemoryTracker::free(Int64) { return AllocationTrace(0.0); } void CurrentMemoryTracker::check() {} void CurrentMemoryTracker::injectFault() {} void AllocationTrace::onAllocImpl(void *, size_t) const {} void AllocationTrace::onFreeImpl(void *, size_t) const {} constinit FiberLocal MemoryTrackerBlockerInThread::level; MemoryTrackerBlockerInThread::MemoryTrackerBlockerInThread(VariableContext) {} MemoryTrackerBlockerInThread::MemoryTrackerBlockerInThread(MemoryTrackerBlockerInThread &&) noexcept = default; MemoryTrackerBlockerInThread & MemoryTrackerBlockerInThread::operator=(MemoryTrackerBlockerInThread &&) noexcept = default; void MemoryTrackerBlockerInThread::reset() {} MemoryTrackerBlockerInThread::~MemoryTrackerBlockerInThread() {} namespace ProfileEvents { void incrementForLogMessage(int) {} void incrementLoggerElapsedNanoseconds(UInt64) {} } /// --------------------------------------------------------------------------------------------- /// Server-side facilities the parser links against but never meaningfully uses in a browser. /// --------------------------------------------------------------------------------------------- #include #include #include #include #include #include constinit FiberLocal LockMemoryExceptionInThread::counter; constinit FiberLocal LockMemoryExceptionInThread::level; constinit FiberLocal LockMemoryExceptionInThread::block_fault_injections; LockMemoryExceptionInThread::LockMemoryExceptionInThread(VariableContext, bool) : previous_level(VariableContext::Global), previous_block_fault_injections(false) {} LockMemoryExceptionInThread::~LockMemoryExceptionInThread() = default; /// WebAssembly cannot walk its own call stack from user code. StackTrace::StackTrace() = default; std::string StackTrace::toString() const { return {}; } /// The timezone database is generated into the binary by `contrib/cctz-cmake` and is far larger /// than the parser; the WebAssembly build leaves it out and reports no timezone data. std::string_view getTimeZone(const char *) { return {}; } /// `if_nametoindex`/`if_indextoname` are named by Poco::Net::IPAddress for scoped IPv6 addresses. extern "C" unsigned int if_nametoindex(const char *) { return 0; } extern "C" char * if_indextoname(unsigned int, char * name) { return name; } #include #include #include #include #include namespace Poco { ProcessImpl::PIDImpl ProcessImpl::idImpl() { return 1; } ThreadImpl * ThreadImpl::currentImpl() { return nullptr; } /// `Poco::LocalDateTime` (a conversion target of `Poco::Dynamic::Var`, which reads the JSON /// handed to `ch_format_json`) asks the local timezone; the real implementation reads /// `tzset`/`tzname`, which wasi-libc does not have. A browser sandbox is UTC. int Timezone::utcOffset() { return 0; } int Timezone::dst() { return 0; } bool Timezone::isDst(const Timestamp &) { return false; } int Timezone::tzd() { return 0; } std::string Timezone::name() { return "UTC"; } std::string Timezone::standardName() { return "UTC"; } std::string Timezone::dstName() { return "UTC"; } /// Named by `VarHolderImpl` conversions nothing here performs - AST JSON deserialization /// converts a `Var` to strings, numbers and booleans, never to a `DateTime` or a UTF-16 /// string - but virtual `convert` overloads survive the link whether or not they are called. /// Both answers are the fail-closed ones: "the string did not parse as a date", which the /// caller turns into a `Poco::RangeException`, and a thrown `Poco::NotImplementedException`, /// both of which the error boundary below reports as an error. bool DateTimeParser::tryParse(const std::string &, const std::string &, DateTime &, int &) { return false; } void UnicodeConverter::convert(const std::string &, UTF16String &) { throw NotImplementedException("There is no UTF-16 conversion in WebAssembly"); } } namespace ProfileEvents { void incrementForLogMessage(Poco::Message::Priority) {} } namespace FS { bool isSymlink(const std::filesystem::path &) { return false; } std::filesystem::path readSymlink(const std::filesystem::path &) { return {}; } } namespace DB { namespace ErrorCodes { extern const int ATTEMPT_TO_READ_AFTER_EOF; extern const int LOGICAL_ERROR; } void throwReadAfterEOF() { throw Exception(ErrorCodes::ATTEMPT_TO_READ_AFTER_EOF, "Attempt to read after EOF"); } /// Query masking rules are configured on the server; there is nothing to mask here. std::string wipeSensitiveDataAndCutToLength(std::string str, size_t max_length, bool) { if (max_length && str.size() > max_length) str.resize(max_length); return str; } void tryLogCurrentException(const char *, const std::string &, LogsLevel) {} void tryLogCurrentException(Poco::Logger *, const std::string &, LogsLevel) {} void tryLogCurrentException(LoggerPtr, const std::string &, LogsLevel) {} bool CurrentThread::isInitialized() { return false; } } /// --------------------------------------------------------------------------------------------- /// The last few server entry points reached from `Access/Common` and `base/Decimal`. /// --------------------------------------------------------------------------------------------- #include #include #include #include void throwError(const char * err) { throw DB::Exception::createDeprecated(err, DB::ErrorCodes::LOGICAL_ERROR); } namespace DB { /// There is no server, therefore no access control: `AccessRightsElement` formatting falls back /// to its defaults, which is what a client-side formatter wants anyway. bool AccessControl::isEnabledUserNameAccessType() const { return false; } bool AccessControl::isEnabledReadWriteGrants() const { return false; } ThreadStatus & CurrentThread::get() { throw Exception(ErrorCodes::LOGICAL_ERROR, "There is no current thread status in WebAssembly"); } ContextPtr ThreadStatus::tryGetQueryContext() const { return nullptr; } const AccessControl & Context::getAccessControl() const { throw Exception(ErrorCodes::LOGICAL_ERROR, "There is no access control in WebAssembly"); } /// `Interpreters/Context.cpp` is not built either, and these two are defined there. There is no /// server context in a browser, so they stay null and every `Context::getGlobalContextInstance` /// in the parser - the `storage_shared_set_join_use_inner_uuid` lookup in `CreateQueryUUIDs`, the /// access-control lookups in `AccessRightsElement` - takes its contextless branch. ContextPtr ContextData::global_context_instance; ContextPtr ContextData::background_context_instance; /// No timezone database is linked in (see `getTimeZone` above), so nothing validates. void SettingFieldTimezone::validateTimezone(const std::string &) {} /// `DateLUT` reads the session timezone out of the settings; without a server context it never /// gets that far, but the reference still has to resolve. namespace Setting { extern const SettingsTimezone session_timezone; const SettingsTimezone session_timezone{}; } const SettingFieldTimezone & Settings::operator[](SettingsTimezone) const { static const SettingFieldTimezone empty{}; return empty; } } /// --------------------------------------------------------------------------------------------- /// Exceptions. /// /// The build uses `-fignore-exceptions`: `throw`, `try` and `catch` still compile, but no landing /// pads or unwind tables are emitted, so nothing can be caught, and defining `__cxa_throw` here /// keeps libc++abi's exception machinery out of the bundle entirely. /// /// A syntax error does not come this way - `tryParseQuery` returns null and fills in the message, /// and `src/Parsers` contains no `catch` at all, which a style check enforces. But a handful of /// checks in the parser still report an invalid query by throwing (`Frame start cannot be /// UNBOUNDED FOLLOWING`, for one), and stopping the module on an ordinary user mistake is not /// acceptable. So every exported entry point arms a `setjmp` boundary and a throw returns to it, /// with the message, as a parse failure. /// /// The unwinding this replaces would have run destructors; `longjmp` does not, so a throw leaks /// whatever the parser allocated below the boundary. The alternative is `-fwasm-exceptions`, which /// costs 262 KB and an engine implementing the exception-handling proposal - too much to pay for /// tidiness on a path that a browser hits only on an invalid query. /// /// Recovery is for anything deriving from `Poco::Exception` and nothing else. The object arriving /// at `__cxa_throw` is untyped, so its dynamic type has to be established from the `type_info` /// argument before it can be read as anything; a `std::bad_alloc` from `operator new` is an /// unrelated object, so only its type name can be reported, and the module stops. `DB::Exception` /// is the exception the parser throws; the rest of the Poco hierarchy is thrown by /// `Poco::JSON::Parser` and the `readJSON` overrides below `IAST::createFromJSON`, whose input - /// the document handed to `ch_format_json` - is arbitrary, so a malformed one must come back as /// an error rather than stop the module. (`createFromJSON` does contain `catch` blocks converting /// Poco exceptions, but `-fignore-exceptions` emits no landing pads, so they never run here.) /// /// There is no `dynamic_cast`-style hierarchy walk available - libc++abi's machinery is not linked /// - but the Itanium RTTI data is present and self-describing: the `type_info` of a class with a /// single public non-virtual base is an `abi::__si_class_type_info` holding a pointer to the /// base's `type_info`, and every Poco exception is such a class. wasi-sdk exposes neither the /// class (its `cxxabi.h` ends at the `__cxa_*` entry points) nor its `type_info` (so a `typeid` /// comparison would not link), which is why the walk below matches the metatype by its mangled /// name and reads the base pointer through a hand-declared layout. /// /// The boundary itself is in `wasm_sjlj.c`, which is not part of the LTO unit; see the comment /// there for why the two calls cannot live in this file. /// --------------------------------------------------------------------------------------------- namespace { /// The Itanium ABI layout of `abi::__si_class_type_info`: the `std::type_info` part (a vptr and /// the mangled name), then the base class's `type_info`. struct SiClassTypeInfoLayout { void * vptr; const char * type_name; const std::type_info * base_type; }; bool derivesFromPocoException(const std::type_info * type) { /// Bounded, though no real hierarchy comes close: an RTTI graph is acyclic, but this reads /// ABI internals and must not loop forever if it misreads them. for (size_t depth = 0; type && depth < 16; ++depth) { if (*type == typeid(Poco::Exception)) return true; /// Single public non-virtual inheritance is all the Poco hierarchy uses; a class whose /// `type_info` is anything else (`__class_type_info` for a root such as `std::bad_alloc`, /// `__vmi_class_type_info` for multiple or virtual bases) is not a Poco exception. if (strcmp(typeid(*type).name(), "N10__cxxabiv120__si_class_type_infoE") != 0) return false; type = reinterpret_cast(type)->base_type; } return false; } } extern "C" { void * __cxa_allocate_exception(size_t size) noexcept { /// The exception object is constructed in place here, so the storage has to be aligned for any /// type that can be thrown, not just for `char`. alignas(std::max_align_t) static char buffer[512]; return size <= sizeof(buffer) ? static_cast(buffer) : nullptr; } void __cxa_free_exception(void *) noexcept { } [[noreturn]] void __cxa_throw(void * thrown, void * type_info, void (*)(void *)) { const auto * thrown_type = static_cast(type_info); if (thrown_type && derivesFromPocoException(thrown_type)) { /// A single-inheritance chain keeps the base at offset zero, so the cast is sound. const auto * exception = static_cast(thrown); /// A `DB::Exception` message is self-contained; a Poco one is often empty or a detail /// ("Unexpected token", the offending string) that needs the exception's name to mean /// anything. Formatted without allocating: an allocation failing here would throw again. const char * message; static char composed[1024]; if (*thrown_type == typeid(DB::Exception)) { message = exception->what(); } else { std::snprintf(composed, sizeof(composed), "%s: %s", exception->name(), exception->message().c_str()); message = composed; } if (chParserRecoveryArmed()) chParserLongjmp(message); std::fprintf(stderr, "ClickHouse parser: unrecoverable error: %s\n", message); std::abort(); } /// The object cannot be read, so the type name is all there is to report. std::fprintf(stderr, "ClickHouse parser: unrecoverable error of type %s\n", thrown_type ? thrown_type->name() : "unknown"); std::abort(); } }