Files
SnowLuma/.pnpm-store/v11/files/e5/a84c03aae84d56b109e33e95f0549b007082dd0512d41ad0e3a5756840063a2f2a6bc17322b23614132267f622d2bf95acece30337dcf5e6ac57c1bac73461
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

333 lines
14 KiB
Plaintext

#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 <memory>
#include <string>
#include <vector>
#include <sqlite3.h>
#include <napi.h>
#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 <class T> inline Integer(T _name, int64_t val) :
Field(_name, SQLITE_INTEGER), value(val) {}
int64_t value;
virtual ~Integer() override = default;
};
struct Float : Field {
template <class T> inline Float(T _name, double val) :
Field(_name, SQLITE_FLOAT), value(val) {}
double value;
virtual ~Float() override = default;
};
struct Text : Field {
template <class T> 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 <class T> 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<Cell> Row;
typedef std::vector<Row> Rows;
typedef std::vector<std::unique_ptr<Values::Field>> 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<std::string> 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<Values::Field> 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<std::string>* 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<std::string>* 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