@photostructure/sqlite 0.0.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +38 -2
  2. package/README.md +47 -483
  3. package/SECURITY.md +27 -83
  4. package/binding.gyp +69 -22
  5. package/dist/index.cjs +185 -18
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +552 -100
  8. package/dist/index.d.mts +552 -100
  9. package/dist/index.d.ts +552 -100
  10. package/dist/index.mjs +183 -18
  11. package/dist/index.mjs.map +1 -1
  12. package/package.json +51 -41
  13. package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
  14. package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
  15. package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
  16. package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
  17. package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
  18. package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
  19. package/prebuilds/test_extension.so +0 -0
  20. package/prebuilds/win32-arm64/@photostructure+sqlite.glibc.node +0 -0
  21. package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
  22. package/src/aggregate_function.cpp +503 -235
  23. package/src/aggregate_function.h +57 -42
  24. package/src/binding.cpp +117 -14
  25. package/src/dirname.ts +1 -1
  26. package/src/index.ts +122 -332
  27. package/src/lru-cache.ts +84 -0
  28. package/src/shims/env-inl.h +6 -15
  29. package/src/shims/node_errors.h +7 -1
  30. package/src/shims/sqlite_errors.h +168 -0
  31. package/src/shims/util.h +29 -4
  32. package/src/sql-tag-store.ts +140 -0
  33. package/src/sqlite_exception.h +49 -0
  34. package/src/sqlite_impl.cpp +736 -129
  35. package/src/sqlite_impl.h +84 -6
  36. package/src/{stack_path.ts → stack-path.ts} +7 -1
  37. package/src/types/aggregate-options.ts +22 -0
  38. package/src/types/changeset-apply-options.ts +18 -0
  39. package/src/types/database-sync-instance.ts +203 -0
  40. package/src/types/database-sync-options.ts +69 -0
  41. package/src/types/session-options.ts +10 -0
  42. package/src/types/sql-tag-store-instance.ts +51 -0
  43. package/src/types/sqlite-authorization-actions.ts +77 -0
  44. package/src/types/sqlite-authorization-results.ts +15 -0
  45. package/src/types/sqlite-changeset-conflict-types.ts +19 -0
  46. package/src/types/sqlite-changeset-resolution.ts +15 -0
  47. package/src/types/sqlite-open-flags.ts +50 -0
  48. package/src/types/statement-sync-instance.ts +73 -0
  49. package/src/types/user-functions-options.ts +14 -0
  50. package/src/upstream/node_sqlite.cc +960 -259
  51. package/src/upstream/node_sqlite.h +127 -2
  52. package/src/upstream/sqlite.js +1 -14
  53. package/src/upstream/sqlite3.c +4510 -1411
  54. package/src/upstream/sqlite3.h +390 -195
  55. package/src/upstream/sqlite3ext.h +7 -0
  56. package/src/user_function.cpp +88 -36
  57. package/src/user_function.h +2 -1
@@ -0,0 +1,168 @@
1
+ #ifndef SRC_SHIMS_SQLITE_ERRORS_H_
2
+ #define SRC_SHIMS_SQLITE_ERRORS_H_
3
+
4
+ #include "../sqlite_exception.h"
5
+ #include <napi.h>
6
+ #include <sqlite3.h>
7
+ #include <string>
8
+
9
+ namespace node {
10
+
11
+ // Helper function to get SQLite error code name
12
+ inline const char *GetSqliteErrorCodeName(int code) {
13
+ switch (code) {
14
+ case SQLITE_OK:
15
+ return "SQLITE_OK";
16
+ case SQLITE_ERROR:
17
+ return "SQLITE_ERROR";
18
+ case SQLITE_INTERNAL:
19
+ return "SQLITE_INTERNAL";
20
+ case SQLITE_PERM:
21
+ return "SQLITE_PERM";
22
+ case SQLITE_ABORT:
23
+ return "SQLITE_ABORT";
24
+ case SQLITE_BUSY:
25
+ return "SQLITE_BUSY";
26
+ case SQLITE_LOCKED:
27
+ return "SQLITE_LOCKED";
28
+ case SQLITE_NOMEM:
29
+ return "SQLITE_NOMEM";
30
+ case SQLITE_READONLY:
31
+ return "SQLITE_READONLY";
32
+ case SQLITE_INTERRUPT:
33
+ return "SQLITE_INTERRUPT";
34
+ case SQLITE_IOERR:
35
+ return "SQLITE_IOERR";
36
+ case SQLITE_CORRUPT:
37
+ return "SQLITE_CORRUPT";
38
+ case SQLITE_NOTFOUND:
39
+ return "SQLITE_NOTFOUND";
40
+ case SQLITE_FULL:
41
+ return "SQLITE_FULL";
42
+ case SQLITE_CANTOPEN:
43
+ return "SQLITE_CANTOPEN";
44
+ case SQLITE_PROTOCOL:
45
+ return "SQLITE_PROTOCOL";
46
+ case SQLITE_EMPTY:
47
+ return "SQLITE_EMPTY";
48
+ case SQLITE_SCHEMA:
49
+ return "SQLITE_SCHEMA";
50
+ case SQLITE_TOOBIG:
51
+ return "SQLITE_TOOBIG";
52
+ case SQLITE_CONSTRAINT:
53
+ return "SQLITE_CONSTRAINT";
54
+ case SQLITE_MISMATCH:
55
+ return "SQLITE_MISMATCH";
56
+ case SQLITE_MISUSE:
57
+ return "SQLITE_MISUSE";
58
+ case SQLITE_NOLFS:
59
+ return "SQLITE_NOLFS";
60
+ case SQLITE_AUTH:
61
+ return "SQLITE_AUTH";
62
+ case SQLITE_FORMAT:
63
+ return "SQLITE_FORMAT";
64
+ case SQLITE_RANGE:
65
+ return "SQLITE_RANGE";
66
+ case SQLITE_NOTADB:
67
+ return "SQLITE_NOTADB";
68
+ case SQLITE_NOTICE:
69
+ return "SQLITE_NOTICE";
70
+ case SQLITE_WARNING:
71
+ return "SQLITE_WARNING";
72
+ case SQLITE_ROW:
73
+ return "SQLITE_ROW";
74
+ case SQLITE_DONE:
75
+ return "SQLITE_DONE";
76
+ default:
77
+ // For extended error codes, get the base error code
78
+ int baseCode = code & 0xFF;
79
+ return GetSqliteErrorCodeName(baseCode);
80
+ }
81
+ }
82
+
83
+ // Enhanced SQLite error that includes system errno information
84
+ inline void ThrowEnhancedSqliteError(Napi::Env env, sqlite3 *db,
85
+ int sqlite_code,
86
+ const std::string &message) {
87
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
88
+ // where passing std::string directly to Napi::Error::New can result in
89
+ // truncated or corrupted error messages
90
+ Napi::Error error = Napi::Error::New(env, message.c_str());
91
+
92
+ // Add SQLite error code information
93
+ error.Set("sqliteCode", Napi::Number::New(env, sqlite_code));
94
+
95
+ if (db) {
96
+ // Get extended error code (more specific than basic error code)
97
+ int extended_code = sqlite3_extended_errcode(db);
98
+ error.Set("sqliteExtendedCode", Napi::Number::New(env, extended_code));
99
+
100
+ // Get system errno if available (for I/O errors)
101
+ int sys_errno = sqlite3_system_errno(db);
102
+ if (sys_errno != 0) {
103
+ error.Set("systemErrno", Napi::Number::New(env, sys_errno));
104
+ }
105
+ }
106
+
107
+ // Set a standard error code property for compatibility
108
+ const char *code_name = GetSqliteErrorCodeName(sqlite_code);
109
+ error.Set("code", Napi::String::New(env, code_name));
110
+
111
+ // Also set the human-readable error string
112
+ const char *err_str = sqlite3_errstr(sqlite_code);
113
+ if (err_str) {
114
+ error.Set("sqliteErrorString", Napi::String::New(env, err_str));
115
+ }
116
+
117
+ error.ThrowAsJavaScriptException();
118
+ }
119
+
120
+ // Database-aware version available when sqlite_impl.h is included
121
+ // This will be specialized in sqlite_impl.cpp to avoid forward declaration
122
+ // issues
123
+
124
+ // Helper to create enhanced error from just a message (when we have the db
125
+ // handle)
126
+ inline void ThrowSqliteError(Napi::Env env, sqlite3 *db,
127
+ const std::string &message) {
128
+ if (db) {
129
+ int errcode = sqlite3_errcode(db);
130
+ ThrowEnhancedSqliteError(env, db, errcode, message);
131
+ } else {
132
+ // Fallback to simple error when no db handle available
133
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
134
+ Napi::Error::New(env, message.c_str()).ThrowAsJavaScriptException();
135
+ }
136
+ }
137
+
138
+ // Helper to throw from a SqliteException with captured error info
139
+ inline void
140
+ ThrowFromSqliteException(Napi::Env env,
141
+ const photostructure::sqlite::SqliteException &ex) {
142
+ Napi::Error error = Napi::Error::New(env, ex.what());
143
+
144
+ // Add all captured error information
145
+ error.Set("sqliteCode", Napi::Number::New(env, ex.sqlite_code()));
146
+ error.Set("sqliteExtendedCode", Napi::Number::New(env, ex.extended_code()));
147
+
148
+ if (ex.system_errno() != 0) {
149
+ error.Set("systemErrno", Napi::Number::New(env, ex.system_errno()));
150
+ }
151
+
152
+ // Set the error code name
153
+ const char *code_name = GetSqliteErrorCodeName(ex.sqlite_code());
154
+ error.Set("code", Napi::String::New(env, code_name));
155
+
156
+ // Also set the human-readable error string
157
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
158
+ if (!ex.error_string().empty()) {
159
+ error.Set("sqliteErrorString",
160
+ Napi::String::New(env, ex.error_string().c_str()));
161
+ }
162
+
163
+ error.ThrowAsJavaScriptException();
164
+ }
165
+
166
+ } // namespace node
167
+
168
+ #endif // SRC_SHIMS_SQLITE_ERRORS_H_
package/src/shims/util.h CHANGED
@@ -10,16 +10,41 @@
10
10
  namespace node {
11
11
 
12
12
  // Environment class for Node.js context
13
+ // Note: This class is a shim for upstream Node.js code compatibility.
14
+ // It's not actively used in our implementation but provides the interface.
13
15
  class Environment {
16
+ private:
17
+ using InstanceMap =
18
+ std::unordered_map<napi_env, std::unique_ptr<Environment>>;
19
+
20
+ // Thread-local storage accessor - the thread_local is on the variable, not
21
+ // the return type
22
+ static InstanceMap &GetInstances() {
23
+ static thread_local InstanceMap instances;
24
+ return instances;
25
+ }
26
+
27
+ // Cleanup hook called when napi_env is destroyed
28
+ static void CleanupHook(void *arg) {
29
+ napi_env env = static_cast<napi_env>(arg);
30
+ InstanceMap &instances = GetInstances();
31
+ auto it = instances.find(env);
32
+ if (it != instances.end()) {
33
+ instances.erase(it);
34
+ }
35
+ }
36
+
14
37
  public:
15
38
  static Environment *GetCurrent(Napi::Env env) {
16
- // Store per-environment instances
17
- static thread_local std::unordered_map<napi_env,
18
- std::unique_ptr<Environment>>
19
- instances;
39
+ InstanceMap &instances = GetInstances();
20
40
  auto it = instances.find(env);
21
41
  if (it == instances.end()) {
22
42
  auto result = instances.emplace(env, std::make_unique<Environment>(env));
43
+
44
+ // Register cleanup hook to remove this entry when env is destroyed
45
+ // This prevents memory leaks when worker threads terminate
46
+ napi_add_env_cleanup_hook(env, CleanupHook, static_cast<void *>(env));
47
+
23
48
  return result.first->second.get();
24
49
  }
25
50
  return it->second.get();
@@ -0,0 +1,140 @@
1
+ import { LRUCache } from "./lru-cache";
2
+ import { DatabaseSyncInstance } from "./types/database-sync-instance";
3
+ import type { StatementSyncInstance } from "./types/statement-sync-instance";
4
+
5
+ /**
6
+ * Default capacity for the statement cache.
7
+ * Matches Node.js SQLTagStore default.
8
+ */
9
+ const DEFAULT_CAPACITY = 1000;
10
+
11
+ /**
12
+ * SQLTagStore provides cached prepared statements via tagged template syntax.
13
+ *
14
+ * @example
15
+ * ```js
16
+ * const sql = db.createTagStore();
17
+ * sql.run`INSERT INTO users VALUES (${id}, ${name})`;
18
+ * const user = sql.get`SELECT * FROM users WHERE id = ${id}`;
19
+ * ```
20
+ */
21
+ export class SQLTagStore {
22
+ private readonly database: DatabaseSyncInstance;
23
+ private readonly cache: LRUCache<string, StatementSyncInstance>;
24
+ private readonly maxCapacity: number;
25
+
26
+ constructor(db: DatabaseSyncInstance, capacity: number = DEFAULT_CAPACITY) {
27
+ if (!db.isOpen) {
28
+ throw new Error("Database is not open");
29
+ }
30
+ this.database = db;
31
+ this.maxCapacity = capacity;
32
+ this.cache = new LRUCache(capacity);
33
+ }
34
+
35
+ /**
36
+ * Returns the associated database instance.
37
+ */
38
+ get db(): DatabaseSyncInstance {
39
+ return this.database;
40
+ }
41
+
42
+ /**
43
+ * Returns the maximum capacity of the statement cache.
44
+ */
45
+ get capacity(): number {
46
+ return this.maxCapacity;
47
+ }
48
+
49
+ /**
50
+ * Returns the current number of cached statements.
51
+ */
52
+ size(): number {
53
+ return this.cache.size();
54
+ }
55
+
56
+ /**
57
+ * Clears all cached statements.
58
+ */
59
+ clear(): void {
60
+ this.cache.clear();
61
+ }
62
+
63
+ /**
64
+ * Execute an INSERT, UPDATE, DELETE or other statement that doesn't return rows.
65
+ * Returns an object with `changes` and `lastInsertRowid`.
66
+ */
67
+ run(
68
+ strings: TemplateStringsArray,
69
+ ...values: unknown[]
70
+ ): { changes: number; lastInsertRowid: number | bigint } {
71
+ const stmt = this.getOrPrepare(strings);
72
+ return stmt.run(...values);
73
+ }
74
+
75
+ /**
76
+ * Execute a query and return the first row, or undefined if no rows.
77
+ */
78
+ get(strings: TemplateStringsArray, ...values: unknown[]): unknown {
79
+ const stmt = this.getOrPrepare(strings);
80
+ return stmt.get(...values);
81
+ }
82
+
83
+ /**
84
+ * Execute a query and return all rows as an array.
85
+ */
86
+ all(strings: TemplateStringsArray, ...values: unknown[]): unknown[] {
87
+ const stmt = this.getOrPrepare(strings);
88
+ return stmt.all(...values);
89
+ }
90
+
91
+ /**
92
+ * Execute a query and return an iterator over the rows.
93
+ */
94
+ iterate(
95
+ strings: TemplateStringsArray,
96
+ ...values: unknown[]
97
+ ): IterableIterator<unknown> {
98
+ const stmt = this.getOrPrepare(strings);
99
+ return stmt.iterate(...values);
100
+ }
101
+
102
+ /**
103
+ * Get a cached statement or prepare a new one.
104
+ * If a cached statement has been finalized, it's evicted and a new one is prepared.
105
+ */
106
+ private getOrPrepare(strings: TemplateStringsArray): StatementSyncInstance {
107
+ if (!this.database.isOpen) {
108
+ throw new Error("Database is not open");
109
+ }
110
+
111
+ const sql = this.buildSQL(strings);
112
+
113
+ // Check cache - evict if finalized
114
+ const cached = this.cache.get(sql);
115
+ if (cached) {
116
+ if (!cached.finalized) {
117
+ return cached;
118
+ }
119
+ // Statement was finalized externally - remove from cache
120
+ this.cache.delete(sql);
121
+ }
122
+
123
+ // Prepare new statement and cache it
124
+ const stmt = this.database.prepare(sql);
125
+ this.cache.set(sql, stmt);
126
+ return stmt;
127
+ }
128
+
129
+ /**
130
+ * Build the SQL string by joining template parts with `?` placeholders.
131
+ */
132
+ private buildSQL(strings: TemplateStringsArray): string {
133
+ let sql = strings[0] ?? "";
134
+ for (let i = 1; i < strings.length; i++) {
135
+ // eslint-disable-next-line security/detect-object-injection -- Index is from controlled for-loop
136
+ sql += "?" + (strings[i] ?? "");
137
+ }
138
+ return sql;
139
+ }
140
+ }
@@ -0,0 +1,49 @@
1
+ #ifndef SRC_SQLITE_EXCEPTION_H_
2
+ #define SRC_SQLITE_EXCEPTION_H_
3
+
4
+ #include <exception>
5
+ #include <sqlite3.h>
6
+ #include <string>
7
+
8
+ namespace photostructure {
9
+ namespace sqlite {
10
+
11
+ // Custom exception that captures SQLite error information at the point of error
12
+ class SqliteException : public std::exception {
13
+ public:
14
+ SqliteException(sqlite3 *db, int result_code, const std::string &message)
15
+ : message_(message), sqlite_code_(result_code), extended_code_(0),
16
+ system_errno_(0) {
17
+
18
+ if (db) {
19
+ // Capture error codes immediately while they're still valid
20
+ extended_code_ = sqlite3_extended_errcode(db);
21
+ system_errno_ = sqlite3_system_errno(db);
22
+ }
23
+
24
+ // Get the error string for the code
25
+ const char *err_str = sqlite3_errstr(result_code);
26
+ if (err_str) {
27
+ error_string_ = err_str;
28
+ }
29
+ }
30
+
31
+ const char *what() const noexcept override { return message_.c_str(); }
32
+
33
+ int sqlite_code() const { return sqlite_code_; }
34
+ int extended_code() const { return extended_code_; }
35
+ int system_errno() const { return system_errno_; }
36
+ const std::string &error_string() const { return error_string_; }
37
+
38
+ private:
39
+ std::string message_;
40
+ std::string error_string_;
41
+ int sqlite_code_;
42
+ int extended_code_;
43
+ int system_errno_;
44
+ };
45
+
46
+ } // namespace sqlite
47
+ } // namespace photostructure
48
+
49
+ #endif // SRC_SQLITE_EXCEPTION_H_