better-sqlite3
is a low-level Node.js package that provides bindings to SQLite. better-sqlite3
is not an ORM, and does not lend itself to specific types of applications or frameworks.
Anything that SQLite does not directly provide is considered out-of-scope for better-sqlite3
. Anything that SQLite does directly provide may be considered in-scope for better-sqlite3
, with the additional requirements that it:
- can be implemented sensibly and safely (i.e., it cannot lead to undefined behavior)
- is used commonly enough to warrant the extra code complexity that it brings
- cannot be reasonably implemented by a user in JavaScript (e.g., by monkey-patching)
better-sqlite3
is a combination of JavaScript and C++. The C++ part is necessary in order to communicate with the underlying SQLite library, which is written in C. Node.js supports C++ addons through a build system called node-gyp
, which is automatically bundled with every installation of npm. On most systems, C++ addons will simply be compiled as part of the installation process when running npm install
. However, history has shown that Windows users have struggled significantly when trying to build C++ addons for Node.js. This is an issue with Node.js as a whole, and not specific to better-sqlite3
.
better-sqlite3
is a Node.js package, not an Electron package. Electron is considered a third-party platform that is not officially supported. However, many users do find great success in using better-sqlite3
with Electron, and helpful contributors such as @mceachen have provided support to the Electron community.
Lastly, better-sqlite3
is a JavaScript package, not a TypeScript package. Type definitions have been generously provided by the community at @types/better-sqlite3
, but no official support for TypeScript is currently provided (this may change in the future).
Code that gets contributed to better-sqlite3
must adhere to the following principles, prioritized from first to last:
The code must behave as expected in all situations. Often when writing new features, only the nominal case is considered. However, many edge cases exist when you consider race conditions, uncommon states, and improper usage. All possibilities of improper usage must be detected, and an appropriate error must be thrown (never ignored). All possibilities of proper usage must be supported, and must behave as expected.
better-sqlite3
's public API must be as simple as possible. Rather than calling 3 functions in a specific order, it's simpler for users to call a single function. Rather than providing many similar functions for doing similar things (e.g., "convenience functions"), there should just be one function that is already convenient by design. Sane defaults should be applied when possible. A function's minimal call signature should be as small as possible, with progressively complex customization available when needed. Function names should only be as long as necessary to convey their purpose. For any new feature, it should be easy to showcase code examples that is are so simple that they are self-explanatory.
This principle only applies to the public API, not necessarily to internal functions.
Code must be written in a way that is intuitive and understandable by other programmers, now and in the future. Some code is naturally complex, and thus should be explained with comments (only when necessary). Code should be written in a style that is similar to existing code.
Code should be written such that it does not use unnecessary computing resources. If a task can be accomplished without copying a potentially large buffer, it should be. If a complex algorithm can generally be avoided with a simple check, it should be. Calls to the operating system or filesystem should be limited to only occur when absolutely necessary. The public API should naturally encourage good performance habits, such as re-using prepared statements.
It's okay to sacrifice readability for performance if doing so has a clear, measurable benefit to users.
If you've never written a native addon for Node.js before, you should start by reading the official documentation on the subject.
The C++ code in better-sqlite3
is written using a tool called lzz
, which alleviates the programmer from needing to write header files. If you plan on changing any C++ code, you'll need to edit *.lzz
files and then re-compile them into *.cpp
and *.hpp
by running npm run lzz
(while the lzz
executable is in your PATH). You can learn how to download and install lzz
here.
There is currently no linter or style guide associated with better-sqlite3
(this may change in the future). For now, just try to match the style of existing code as much as possible. Code owners will reject your PR or rewrite your changes if they feel that you've used a coding style that doesn't match the existing code. Although the rules aren't layed out formally, you are expected to adhere to them by using your eyeballs.
All tests are written in JavaScript, and they test better-sqlite3
's public API. All new features must be accompanied by a robust set of tests that scrutinize the new feature under all manner of circumstances and edge cases. It's not enough to simply test the "common case". If you write code that detects errors and throws exceptions, those error cases should be tested too, to ensure that all errors are being properly detected. If a new feature interacts with existing features, those interactions must be tested as well.
All new features must be accompanied by clear documentation. All new methods and classes must be included in the Table of Contents, and must include code examples. Documentation must follow the existing formatting:
- Literal values use monospace code formatting
- Examples:
"my string"
,true
,false
,null
,undefined
,123
- Examples:
- Package names and code identifiers use monospace code formatting
- Examples:
better-sqlite3
,db.myMethod()
,options.readOnly
,this
- Examples:
- Primitive data types are lower-cased, while other data types are capitalized
- Examples:
string
,number
,Buffer
,Database
- Examples:
- References to other classes or methods must be linked and use monospace code formatting
- Examples:
.get()
,new Database()
- Examples:
- Function signatures are written as: .funcName(requiredArg, [optionalArg]) -> returnValue
- Note that the arguments and return values are italicized
- Note that optional arguments are surrounded by square brackets []
- All code blocks should be highlighted using
js
syntax, except for bash commands which don't need highlighting
Depending on the nature of your contribution, it will be held to a different level of scrutiny, from lowest to highest:
These changes are self-explanatory. They include:
- Updating the bundled version of SQLite (using this workflow)
- Updating dependencies in
package.json
- Adding prebuild binaries for a new version of Node.js or Electron
- Adding prebuild binaries for a new architecture or operating system
These kinds of updates happen on a regular basis, and require zero knowledge of better-sqlite3
's code. Trusted contributors can merge these changes without approval from the original author.
Changes to documentation are usually helpful and harmless. However, they should be treated with a higher level of scrutiny because they affect how users learn about and use better-sqlite3
. Importance is placed on the correctness and truthfulness of documentation. For example, documentation should not "go out of date" based on events outside of our control.
Depending on the type of documentation, trusted contributors might be able to merge these changes without approval from the original author.
These are code changes with a very small blast radius, such as adding a new read-only property to an object, or augmenting a function with a new option that gets passed directly to SQLite. These changes are probably harmless, but require additional scrutiny because they must be thoroughly tested and documented. These changes must be completely backwards-compatible, unless they're part of a major version update.
It's considered a backwards-incompatible change for a prebuilt binary to be removed.
These are code changes with a substantial blast radius, such as implementing a new class or method. These changes must be completely backwards-compatible, unless they're part of a major version update.
New features are rarely accepted from external contributors because they are rarely held to the extremely high standard that better-sqlite3
sets for itself. New features must behave correctly in all possible circumstances, including race conditions and edge cases. Likewise, even the most obscure circumstances must have test cases covering them.
When implementing a new feature, ask yourself:
- What could go wrong if I use this feature while executing a user-defined function?
- What could go wrong if I use this feature while iterating through a prepared statement?
- What could go wrong if I use this feature while the database is closed?
- What could go wrong if I use this feature from within the verbose callback?
- What could go wrong if I use this feature from within a transaction?
- What could go wrong if I use this feature on a prepared statement that has bound parameters?
- What could go wrong if I use this feature within a worker thread?
- What could go wrong if I pass the wrong data type?
- What could go wrong if I pass an unexpected value, such as
null
,undefined
,""
,NaN
, a negative/non-integer number, etc.? - Should the user's 64-bit integer setting affect this feature?
- If this feature accepts a callback function:
- What could go wrong if that callback function throws an exception?
- What could go wrong if that callback function is triggered during one of the above scenarios?
- Could this feature cause memory leaks?
- What if a C++ object gets garbage-collected from JavaScript while it has open handles?
- What if a JavaScript error is thrown within a callback, after I allocated a C++ object?
People love better-sqlite3
because of its robustness and reliability. Each and every feature of better-sqlite3
accounts for every single scenario listed above. Additionally, all possible error scenarios are explicitly handled and tested. Any new feature of better-sqlite3
must be held to the same standard. Currently, no new features are merged without approval from the original author.
Trusted contributors have the privileges necessary to create a release. Here are the steps to create a release:
- Run this workflow from the
master
branch to create a new version tag- Select
patch
for bug fixes and general maintenance - Select
minor
for larger releases with new features - Select
major
for releases with backwards-incompatible changes
- Select
- Draft a new release, and select the version tag that you just created
- Leave the "Release title" blank, and click "Auto-generate release notes"
- Click "Publish release"
- Wait for the
build
job to complete (here)