333 lines
14 KiB
Plaintext
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
|