#ifndef NODE_SQLITE3_SRC_CONVERT_H #define NODE_SQLITE3_SRC_CONVERT_H // Shared value marshalling (Deliverable 06, extracted from the Deliverable 02 // code in statement.cc by the plan's instruction: "Do not write a second // converter"). Both directions of statement binding and both directions of // user-defined function arguments/results go through these helpers, so a // value's type behaviour cannot drift between the two surfaces. // // - ConvertToField: JS value -> Values::Field (bind / function result) // - ValueToCell: sqlite3_value* -> Cell (function arguments) // - CellToJS: Cell -> JS value (rows / function arguments) // - ConvertInt64ToJS: int64 -> JS honouring the database's integer mode // // The Values::Field/Cell/Row/Columns data types moved here from statement.h // so this header has no dependency on the class. #include #include #include #include #include #include "database.h" namespace node_sqlite3 { namespace Values { struct Field { inline Field(unsigned short _index, unsigned short _type = SQLITE_NULL) : type(_type), index(_index) {} inline Field(const char* _name, unsigned short _type = SQLITE_NULL) : type(_type), index(0), name(_name) {} unsigned short type; unsigned short index; std::string name; // Set when the value came from an explicit `undefined` (as opposed // to `null`): used only to recognise the historical // "accidental undefined" call shape against statements without // parameters. bool from_undefined = false; virtual ~Field() = default; }; struct Integer : Field { template inline Integer(T _name, int64_t val) : Field(_name, SQLITE_INTEGER), value(val) {} int64_t value; virtual ~Integer() override = default; }; struct Float : Field { template inline Float(T _name, double val) : Field(_name, SQLITE_FLOAT), value(val) {} double value; virtual ~Float() override = default; }; struct Text : Field { template inline Text(T _name, size_t len, const char* val) : Field(_name, SQLITE_TEXT), value(val, len) {} std::string value; virtual ~Text() override = default; }; struct Blob : Field { template inline Blob(T _name, size_t len, const void* val) : Field(_name, SQLITE_BLOB), length(len) { value = new char[len]; if (len > 0) { memcpy(value, val, len); } } inline virtual ~Blob() override { delete[] value; } size_t length; char* value; }; typedef Field Null; } // A converted result cell: a flat value type instead of a per-cell heap // object. TEXT payload and BLOB bytes live in `str` (binary-safe). struct Cell { unsigned short type = SQLITE_NULL; int64_t integer = 0; double real = 0.; std::string str; Cell() = default; explicit Cell(unsigned short t) : type(t) {} Cell(const Cell&) = default; Cell(Cell&&) = default; Cell& operator=(const Cell&) = default; Cell& operator=(Cell&&) = default; }; typedef std::vector Row; typedef std::vector Rows; typedef std::vector> Parameters; // Result column names captured from a prepared statement, shared by every // row of one batch instead of being stored per cell. // // The shape is fixed for one execution: sqlite3_prepare_v2 may re-prepare // transparently behind sqlite3_step() when the schema changed, but it keeps // the original result columns. Capturing once per call therefore stays // correct without relying on the names surviving across calls. struct Columns { std::vector names; // Populates the names on first use. Called on the thread that steps the // statement, once per execution. inline void EnsureLoaded(sqlite3_stmt* stmt) { if (!names.empty()) return; int cols = sqlite3_column_count(stmt); names.reserve(cols); for (int i = 0; i < cols; i++) { const char* name = sqlite3_column_name(stmt, i); names.emplace_back(name != NULL ? name : ""); } } }; // --- JS -> SQLite direction (bind parameters, function return values) ------ // Where a converted field will be bound: a 1-based parameter index for // positional binds, or a name for named ones. Function return values use // the default (neither) — they are never bound to a parameter. struct FieldPos { int index = 0; const char* name = ""; }; // Converts one JS value into a field for binding. `subject` names the value // in error messages, e.g. "parameter 3", "parameter $name" or "the return // value of function 'foo'". Returns nullptr only with a TypeError or // RangeError pending — no value is ever silently skipped or coerced. std::unique_ptr ConvertToField(const Napi::Value source, const std::string& subject, const FieldPos& pos = FieldPos()); // --- SQLite -> JS direction (result columns, function arguments) ----------- // Reads one sqlite3_value into a Cell. Pure C: safe on any thread, which is // where user-function arguments arrive. void ValueToCell(Cell* cell, sqlite3_value* value); // Names the value a conversion error is about, *without* building the // name unless the error actually happens. // // The phrase is used by exactly one message: the 'number'-mode RangeError // for an out-of-range integer. Building it eagerly cost a heap allocation // and a concatenation for every cell of every row — on a 20,000 x 4 read, // 80,000 allocations to describe an error that is not occurring. Callers // that already hold a finished phrase (function arguments, changeset // fields) pass a string and are converted implicitly; the row path passes // the statement's cached column names plus an index, and pays nothing // until Describe() is called. struct ValueOrigin { // A ready-made phrase, e.g. "argument 0". Borrowed, not owned; one of // the two spellings callers already use. const std::string* literal = nullptr; const char* literal_cstr = nullptr; // Or: a position in a statement's cached column names. Borrowed. const std::vector* column_names = nullptr; size_t index = 0; // Both conversions are implicit by design, so every existing call site // keeps compiling — including the ones passing a string literal, which // cannot reach a std::string parameter through a second user-defined // conversion. Safe against temporaries: every use is inside the call // expression that creates it. ValueOrigin(const std::string& phrase) // NOLINT(runtime/explicit) : literal(&phrase) {} ValueOrigin(const char* phrase) // NOLINT(runtime/explicit) : literal_cstr(phrase) {} ValueOrigin(const std::vector* names, size_t i) : column_names(names), index(i) {} // "column 'x'" when the name is known, "result column N" otherwise. std::string Describe() const; }; // Converts an int64 according to the database's integer mode (see // Database::IntegerMode). Throws a RangeError in 'number' mode for unsafe // values; callers must check env.IsExceptionPending() afterwards. Napi::Value ConvertInt64ToJS(Napi::Env env, sqlite3_int64 value, int integer_mode, const ValueOrigin& origin); // Converts a Cell into a JS value: number/BigInt by integer mode, string, // Buffer for blobs. `origin` names the value in the 'number'-mode // RangeError, and is only formatted if that error is raised. // `move_payload` enables the zero-copy external Buffer for blobs >= 4096 // bytes, moving the payload out of the cell (the row-conversion path, whose // Cells are discarded afterwards); function arguments must pass false — // their Cells outlive the conversion, so those blobs are copied. // Throws (leaves a pending exception) only on the RangeError path. // `raised` is the same per-cell failure channel as ColumnToJS's: it lets a // row loop read a plain bool instead of calling napi_is_exception_pending // per cell. Pass nullptr when the caller checks the env afterwards anyway. Napi::Value CellToJS(Napi::Env env, Cell& cell, int integer_mode, const ValueOrigin& origin, bool move_payload = false, bool* raised = nullptr); // Converts one live result column straight into a JS value, skipping the // intermediate Cell entirely. // // The Cell exists for the *async* paths, where rows are read on a worker // thread and converted later on the JS thread — there the copy is what // makes the hand-off possible. The synchronous paths have no hand-off: // they were paying for a full materialisation of the result set (a Row // per row, a std::string per text/blob cell) and then converting it, so // every string was copied twice and every row allocated twice. // // Semantics match CellToJS exactly, including an empty string/Buffer for a // zero-length or NULL-pointer payload. Blobs are copied rather than // adopted: the bytes belong to SQLite only until the next step, so there // is nothing to move — which still leaves one copy, where the Cell route // took two for blobs under 4096 bytes. // // The returned value borrows nothing from the statement; it is safe to // step again immediately afterwards. // // `raised` is the per-cell failure channel: the row loop reads a plain // bool instead of calling napi_is_exception_pending per cell (measured: // inside run-to-run noise on its own, but it keeps the hot loop free of // an avoidable ABI call). The only failure this conversion can produce // is the 'number'-mode RangeError on an unsafe integer, so the flag is // set exactly there; pass nullptr when the caller checks the env // afterwards anyway. Napi::Value ColumnToJS(Napi::Env env, sqlite3_stmt* stmt, int column, int integer_mode, const ValueOrigin& origin, bool* raised = nullptr); // Binds one JS value straight onto a prepared statement, with no // intermediate Values::Field. // // The Field exists for the *asynchronous* paths, where the bind arguments // are read on the JS thread and applied later on a worker thread — there // the copy is what makes the hand-off possible. The synchronous paths have // no hand-off, so a Field per parameter was a heap allocation (two, for // text: the Field and its std::string) on a path whose whole point is // minimal overhead. // // Type dispatch, coercions and error text match ConvertToField exactly; // the two live side by side so they cannot drift apart unnoticed. // // Text payloads are malloc'd and handed to SQLite with `free` as the // destructor, so ownership transfers instead of the value being copied a // second time. SQLite runs that destructor even when the bind call fails, // so there is no leak on the error path. Blobs are bound // SQLITE_TRANSIENT: SQLite copies them immediately, which is what makes // it safe not to keep the JS value alive. // // `rc` receives the sqlite3_bind_* result. `from_undefined` reports an // explicit `undefined` argument, for the zero-parameter escape hatch in // Statement::BindArgumentsDirect. Returns false with a pending JS // exception for a value that cannot be bound at all. // Names a bind position in an error message ("parameter 3", // "parameter $name"), formatted only when an error is actually raised. // Building it eagerly cost a heap allocation per parameter on every // successful bind, which is the whole call for a small insert. struct BindSubject { int index = 0; const char* name = nullptr; explicit BindSubject(int i) : index(i) {} explicit BindSubject(const char* n) : name(n) {} std::string Describe() const; }; bool BindValueDirect(sqlite3_stmt* stmt, int pos, const Napi::Value source, const BindSubject& subject, int* rc, bool* from_undefined); // True for a plain-object instance of the named global constructor // ("Date", "RegExp"), matching JS instanceof. // // Declared here rather than re-declared per translation unit: the bind // paths and the converter must agree on what counts as a Date or a // RegExp, and a second declaration at another scope links against // nothing under the addon's -undefined dynamic_lookup. bool OtherInstanceOf(Napi::Object source, const char* object_type); // True when a bind argument should be read as a map of named parameters // rather than as a single positional value. // // Both bind paths have to answer this identically, so they share one // implementation instead of two hand-inverted copies of the same // predicate chain. bool IsNamedParameterMap(Napi::Value source); // One key of a named-parameter object, classified. // // A key that reads as an integer selects a bind position directly // ({ 1: 'a' }); anything else is a parameter name ({ $a: 'x' }). struct NamedKey { bool positional = false; int index = 0; // NUL-terminated, owned by the `storage` passed to ResolveNamedKey. const char* name = nullptr; }; // Classifies one key from a named-parameter object, for both the sync and // the async bind paths — they must agree on every key, so they share this. // // The obvious implementation coerces the key with ToNumber and compares // Int32Value() against DoubleValue(). That is a real JS number coercion // per parameter per call, and it was among the largest costs of a named // bind — spent almost entirely on keys like "$a" that cannot possibly be // numeric. // // So the string is read once into a stack buffer, and a key whose first // byte cannot begin a numeric literal skips the coercion: for those, // ToNumber is necessarily NaN, NaN != NaN, and the slow path would have // reached the same "this is a name" verdict. Every key that *could* read // as a number — digits, signs, a dot, leading whitespace, the empty // string — still goes through ToNumber, so behaviour is unchanged // including for the odd spellings ("0x10", " 1", "1.0"). // // `storage` is reused across the keys of one call, so a short name // (every real one) costs no allocation at all. // // Returns false with a pending exception if the key cannot be read. bool ResolveNamedKey(Napi::Env env, Napi::Value key, NamedKey* out, std::string* storage); } #endif