@g9tm/lynxcontrols-native 0.0.0-stage → 1.4.0

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 (70) hide show
  1. package/README.md +85 -2
  2. package/android/build.gradle.kts +45 -0
  3. package/android/consumer-rules.pro +3 -0
  4. package/android/src/main/AndroidManifest.xml +5 -0
  5. package/android/src/main/java/com/g9lynx/g9native/G9Bridge.kt +67 -0
  6. package/android/src/main/java/com/g9lynx/g9native/G9HostModule.kt +159 -0
  7. package/android/src/main/java/com/g9lynx/g9native/G9SqliteModule.kt +49 -0
  8. package/android/src/main/java/com/g9lynx/g9native/generated/G9HostModuleSpec.java +48 -0
  9. package/android/src/main/java/com/g9lynx/g9native/generated/G9SqliteModuleSpec.java +41 -0
  10. package/android/src/main/java/com/g9lynx/g9native/host/core/G9PreferencesCore.kt +100 -0
  11. package/android/src/main/java/com/g9lynx/g9native/sqlite/androidx/AndroidxSqlEngine.kt +88 -0
  12. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqlEngine.kt +61 -0
  13. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteCore.kt +260 -0
  14. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteService.kt +131 -0
  15. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteTransport.kt +125 -0
  16. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteValues.kt +71 -0
  17. package/android/src/main/java/com/g9lynx/g9native/video/G9VideoUI.kt +214 -0
  18. package/android/src/main/java/com/g9lynx/g9native/video/core/G9VideoCore.kt +67 -0
  19. package/dist/.tsbuildinfo +1 -0
  20. package/dist/driver.d.ts +20 -0
  21. package/dist/driver.d.ts.map +1 -0
  22. package/dist/driver.js +159 -0
  23. package/dist/driver.js.map +1 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +3 -0
  27. package/dist/media.d.ts +60 -0
  28. package/dist/media.d.ts.map +1 -0
  29. package/dist/media.jsx +119 -0
  30. package/dist/media.jsx.map +1 -0
  31. package/dist/protocol.d.ts +115 -0
  32. package/dist/protocol.d.ts.map +1 -0
  33. package/dist/protocol.js +1 -0
  34. package/dist/services.d.ts +36 -0
  35. package/dist/services.d.ts.map +1 -0
  36. package/dist/services.js +111 -0
  37. package/dist/services.js.map +1 -0
  38. package/dist/transport.d.ts +10 -0
  39. package/dist/transport.d.ts.map +1 -0
  40. package/dist/transport.js +77 -0
  41. package/dist/transport.js.map +1 -0
  42. package/generated/G9HostModule.ts +71 -0
  43. package/generated/G9SqliteModule.ts +68 -0
  44. package/ios/Tests/G9NativeTestSuite.h +25 -0
  45. package/ios/Tests/G9NativeTestSuite.m +560 -0
  46. package/ios/Tests/G9NativeTests.m +52 -0
  47. package/ios/g9lynx-native.podspec +29 -0
  48. package/ios/src/G9HostModule.h +14 -0
  49. package/ios/src/G9HostModule.m +121 -0
  50. package/ios/src/G9SqliteModule.h +14 -0
  51. package/ios/src/G9SqliteModule.m +167 -0
  52. package/ios/src/G9VideoUI.h +21 -0
  53. package/ios/src/G9VideoUI.m +250 -0
  54. package/ios/src/core/G9HostPaths.h +20 -0
  55. package/ios/src/core/G9HostPaths.m +23 -0
  56. package/ios/src/core/G9PreferencesCore.h +43 -0
  57. package/ios/src/core/G9PreferencesCore.m +114 -0
  58. package/ios/src/core/G9SqliteCore.h +62 -0
  59. package/ios/src/core/G9SqliteCore.m +497 -0
  60. package/ios/src/core/G9SqliteTransport.h +16 -0
  61. package/ios/src/core/G9SqliteTransport.m +85 -0
  62. package/ios/src/core/G9VideoCore.h +26 -0
  63. package/ios/src/core/G9VideoCore.m +37 -0
  64. package/ios/src/generated/G9HostModuleSpec.h +23 -0
  65. package/ios/src/generated/G9HostModuleSpec.m +2 -0
  66. package/ios/src/generated/G9SqliteModuleSpec.h +20 -0
  67. package/ios/src/generated/G9SqliteModuleSpec.m +2 -0
  68. package/lynx.lib.json +12 -0
  69. package/package.json +49 -4
  70. package/types/platform-native-module.d.ts +46 -0
@@ -0,0 +1,114 @@
1
+ #import "G9PreferencesCore.h"
2
+
3
+ static NSDictionary *G9PreferenceError(NSString *code, NSString *message) {
4
+ return [NSDictionary dictionaryWithObjectsAndKeys:code, @"code", message, @"message", nil];
5
+ }
6
+
7
+ static BOOL G9IsInt(id value) {
8
+ if (![value isKindOfClass:[NSNumber class]]) return NO;
9
+ const char t = [value objCType][0];
10
+ return t == 'i' || t == 'q' || t == 'l' || t == 's' || t == 'c';
11
+ }
12
+
13
+ @implementation G9PreferencesCore
14
+
15
+ - (instancetype)initWithBackend:(id<G9PreferenceBackend>)backend {
16
+ if ((self = [super init])) _backend = backend;
17
+ return self;
18
+ }
19
+
20
+ - (id)get:(NSString *)key {
21
+ id value = [[_backend allValues] objectForKey:key];
22
+ if (G9IsInt(value)) {
23
+ return [NSDictionary dictionaryWithObjectsAndKeys:@"int", @"type", [NSNumber numberWithDouble:[value doubleValue]], @"value", nil];
24
+ }
25
+ if ([value isKindOfClass:[NSString class]]) {
26
+ return [NSDictionary dictionaryWithObjectsAndKeys:@"string", @"type", value, @"value", nil];
27
+ }
28
+ return [NSNull null];
29
+ }
30
+
31
+ - (BOOL)set:(NSString *)key kind:(NSString *)kind value:(NSString *)value error:(NSDictionary **)error {
32
+ if (![key isKindOfClass:[NSString class]] || [key length] == 0) {
33
+ if (error) *error = G9PreferenceError(@"preference-key", @"A preference key must be a non-empty string.");
34
+ return NO;
35
+ }
36
+ if ([kind isEqualToString:@"int"]) {
37
+ NSScanner *scanner = value != nil ? [NSScanner scannerWithString:value] : nil;
38
+ long long parsed = 0;
39
+ if (scanner == nil || ![scanner scanLongLong:&parsed] || ![scanner isAtEnd] || parsed < INT32_MIN || parsed > INT32_MAX) {
40
+ if (error) {
41
+ *error = G9PreferenceError(@"preference-value", [NSString stringWithFormat:@"Preference '%@' needs a 32-bit integer, got %@.", key, value]);
42
+ }
43
+ return NO;
44
+ }
45
+ [_backend setValue:[NSNumber numberWithInt:(int)parsed] forPreference:key];
46
+ } else if ([kind isEqualToString:@"string"]) {
47
+ if (value == nil || (id)value == (id)[NSNull null]) [_backend removePreference:key];
48
+ else [_backend setValue:value forPreference:key];
49
+ } else if ([kind isEqualToString:@"remove"]) {
50
+ [_backend removePreference:key];
51
+ } else {
52
+ if (error) *error = G9PreferenceError(@"preference-value", [NSString stringWithFormat:@"Unknown preference kind '%@'.", kind]);
53
+ return NO;
54
+ }
55
+ return YES;
56
+ }
57
+
58
+ - (NSArray *)entries:(NSString *)prefix {
59
+ NSDictionary *all = [_backend allValues];
60
+ NSArray *keys = [[all allKeys] sortedArrayUsingSelector:@selector(compare:)];
61
+ NSMutableArray *out = [NSMutableArray array];
62
+ for (NSString *key in keys) {
63
+ if (![key isKindOfClass:[NSString class]] || ![key hasPrefix:prefix]) continue;
64
+ id value = [all objectForKey:key];
65
+ if (G9IsInt(value)) [out addObject:[NSArray arrayWithObjects:key, [NSNumber numberWithDouble:[value doubleValue]], nil]];
66
+ else if ([value isKindOfClass:[NSString class]]) [out addObject:[NSArray arrayWithObjects:key, value, nil]];
67
+ }
68
+ return out;
69
+ }
70
+
71
+ @end
72
+
73
+ @implementation G9UserDefaultsBackend
74
+ - (instancetype)initWithSuiteName:(NSString *)suite {
75
+ if ((self = [super init])) {
76
+ _suite = [suite copy];
77
+ _defaults = [[NSUserDefaults alloc] initWithSuiteName:suite];
78
+ }
79
+ return self;
80
+ }
81
+ - (NSDictionary *)allValues {
82
+ // Only this suite's own values (dictionaryRepresentation would add the global and registration domains).
83
+ NSDictionary *domain = [_defaults persistentDomainForName:_suite];
84
+ return domain != nil ? domain : [NSDictionary dictionary];
85
+ }
86
+ - (void)setValue:(id)value forPreference:(NSString *)key {
87
+ [_defaults setObject:value forKey:key];
88
+ }
89
+ - (void)removePreference:(NSString *)key {
90
+ [_defaults removeObjectForKey:key];
91
+ }
92
+ @end
93
+
94
+ @implementation G9MemoryPreferenceBackend
95
+ - (instancetype)init {
96
+ if ((self = [super init])) _values = [[NSMutableDictionary alloc] init];
97
+ return self;
98
+ }
99
+ - (NSDictionary *)allValues {
100
+ @synchronized(self) {
101
+ return [NSDictionary dictionaryWithDictionary:_values];
102
+ }
103
+ }
104
+ - (void)setValue:(id)value forPreference:(NSString *)key {
105
+ @synchronized(self) {
106
+ [_values setObject:value forKey:key];
107
+ }
108
+ }
109
+ - (void)removePreference:(NSString *)key {
110
+ @synchronized(self) {
111
+ [_values removeObjectForKey:key];
112
+ }
113
+ }
114
+ @end
@@ -0,0 +1,62 @@
1
+ // The iOS SQLite driver core (plan/specs/06 §5): sessions, transaction tokens, a statement cache per session, value
2
+ // conversion and error mapping over the system sqlite3 C API. Foundation + sqlite3 only, written so it also compiles
3
+ // with clang + GNUstep on Linux (no blocks, no subscripting, explicit ivars): the contract runs there in the cloud
4
+ // (`npm run test:ios-core`) and on a simulator through XCTest.
5
+ //
6
+ // Errors are returned through `error` as G9 error dictionaries (`code`, `message`, `sqliteCode`, `sql`,
7
+ // `completedStatements`), the same fields the Android core sends. Calls for one session must not overlap (the module
8
+ // runs each session on its own serial queue); `interrupt:` may be called from any thread.
9
+ #import <Foundation/Foundation.h>
10
+
11
+ NS_ASSUME_NONNULL_BEGIN
12
+
13
+ /** Largest integer JS represents exactly (2^53 − 1). */
14
+ extern const long long G9MaxSafeInteger;
15
+
16
+ /** A G9 error dictionary. */
17
+ NSDictionary *G9SqliteErrorMake(NSString *code, NSString *message);
18
+
19
+ /** SQLite primary result code → G9 error code (mirrors `sqliteErrorCodeFor` in @g9tm/lynxcontrols-sqlite). */
20
+ NSString *G9SqliteErrorCodeFor(int resultCode);
21
+
22
+ @interface G9SqliteCore : NSObject {
23
+ @private
24
+ NSMutableDictionary *_sessions; // token → session
25
+ NSMutableDictionary *_txs; // tx → token
26
+ long long _counter;
27
+ }
28
+
29
+ /** Capabilities of the linked SQLite (`interrupt` is true: sqlite3_interrupt). */
30
+ - (nullable NSDictionary *)capabilitiesWithError:(NSDictionary *_Nullable *_Nullable)error;
31
+
32
+ /** `{ path, readOnly?, create?, pragmas[] }` → session token. */
33
+ - (nullable NSString *)open:(NSDictionary *)options error:(NSDictionary *_Nullable *_Nullable)error;
34
+
35
+ /**
36
+ * Runs a batch. `statements`: `[{ sql, params }]` with bridge values (NSNumber, NSString, NSData, NSNull,
37
+ * `{ int64 }`, `{ undefined: true }`). `options`: `{ tx?, maxRows?, atomic? }`. Returns one result per statement:
38
+ * `{ columns, rows, changes, lastInsertRowId }` with typed cells (NSNumber long long / double, NSString, NSData,
39
+ * NSNull); G9SqliteTransport encodes them for the bridge.
40
+ */
41
+ - (nullable NSArray *)run:(NSString *)session
42
+ statements:(NSArray *)statements
43
+ options:(NSDictionary *)options
44
+ error:(NSDictionary *_Nullable *_Nullable)error;
45
+
46
+ - (nullable NSString *)begin:(NSString *)session mode:(NSString *)mode error:(NSDictionary *_Nullable *_Nullable)error;
47
+ - (BOOL)commit:(NSString *)tx error:(NSDictionary *_Nullable *_Nullable)error;
48
+ - (BOOL)rollback:(NSString *)tx error:(NSDictionary *_Nullable *_Nullable)error;
49
+ - (BOOL)close:(NSString *)session error:(NSDictionary *_Nullable *_Nullable)error;
50
+
51
+ /** The session a transaction belongs to (the module queues tx work there), or nil. */
52
+ - (nullable NSString *)sessionOfTx:(NSString *)tx;
53
+
54
+ /** Interrupts whatever runs on the session (`sqlite3_interrupt`); the batch fails with `interrupted`. Any thread. */
55
+ - (void)interrupt:(NSString *)session;
56
+
57
+ /** Closes every session (module teardown). */
58
+ - (void)closeAll;
59
+
60
+ @end
61
+
62
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,497 @@
1
+ #import "G9SqliteCore.h"
2
+
3
+ #include <math.h>
4
+ #include <sqlite3.h>
5
+
6
+ const long long G9MaxSafeInteger = 9007199254740991LL;
7
+ static const NSUInteger G9StatementCacheSize = 64;
8
+
9
+ NSDictionary *G9SqliteErrorMake(NSString *code, NSString *message) {
10
+ return [NSDictionary dictionaryWithObjectsAndKeys:code, @"code", message, @"message", nil];
11
+ }
12
+
13
+ NSString *G9SqliteErrorCodeFor(int resultCode) {
14
+ switch (resultCode & 0xff) {
15
+ case SQLITE_BUSY:
16
+ return @"busy";
17
+ case SQLITE_LOCKED:
18
+ return @"locked";
19
+ case SQLITE_CONSTRAINT:
20
+ return @"constraint";
21
+ case SQLITE_CORRUPT:
22
+ case SQLITE_NOTADB:
23
+ return @"corrupt";
24
+ case SQLITE_READONLY:
25
+ return @"readonly";
26
+ case SQLITE_FULL:
27
+ return @"full";
28
+ case SQLITE_INTERRUPT:
29
+ return @"interrupted";
30
+ case SQLITE_MISUSE:
31
+ return @"misuse";
32
+ case SQLITE_CANTOPEN:
33
+ return @"not-found";
34
+ default:
35
+ return @"other";
36
+ }
37
+ }
38
+
39
+ /** An error with optional context; `sqliteCode` < 0 means none. */
40
+ static NSDictionary *G9Error(NSString *code, NSString *message, int sqliteCode, NSString *sql, NSInteger completed) {
41
+ NSMutableDictionary *e = [NSMutableDictionary dictionaryWithDictionary:G9SqliteErrorMake(code, message)];
42
+ if (sqliteCode >= 0) [e setObject:[NSNumber numberWithInt:sqliteCode] forKey:@"sqliteCode"];
43
+ if (sql != nil) [e setObject:sql forKey:@"sql"];
44
+ if (completed >= 0) [e setObject:[NSNumber numberWithInteger:completed] forKey:@"completedStatements"];
45
+ return e;
46
+ }
47
+
48
+ /** The connection's current error. */
49
+ static NSDictionary *G9DbError(sqlite3 *db, NSString *sql, NSInteger completed) {
50
+ int code = sqlite3_extended_errcode(db);
51
+ NSString *message = [NSString stringWithUTF8String:sqlite3_errmsg(db)];
52
+ return G9Error(G9SqliteErrorCodeFor(code), message != nil ? message : @"SQLite error", code, sql, completed);
53
+ }
54
+
55
+ /** Adds statement context to an error that has none yet. */
56
+ static NSDictionary *G9WithContext(NSDictionary *error, NSString *sql, NSInteger completed) {
57
+ NSMutableDictionary *e = [NSMutableDictionary dictionaryWithDictionary:error];
58
+ if ([e objectForKey:@"sql"] == nil && sql != nil) [e setObject:sql forKey:@"sql"];
59
+ if ([e objectForKey:@"completedStatements"] == nil && completed >= 0) {
60
+ [e setObject:[NSNumber numberWithInteger:completed] forKey:@"completedStatements"];
61
+ }
62
+ return e;
63
+ }
64
+
65
+ static void G9SetError(NSDictionary **out, NSDictionary *error) {
66
+ if (out != NULL) *out = error;
67
+ }
68
+
69
+ @interface G9SqliteSession : NSObject {
70
+ @public
71
+ sqlite3 *db;
72
+ NSMutableDictionary *statements; // sql → NSValue(sqlite3_stmt *)
73
+ NSMutableArray *order; // least recently used first
74
+ NSString *tx;
75
+ long long lastInsertRowId;
76
+ }
77
+ @end
78
+
79
+ @implementation G9SqliteSession
80
+ - (instancetype)init {
81
+ if ((self = [super init])) {
82
+ statements = [[NSMutableDictionary alloc] init];
83
+ order = [[NSMutableArray alloc] init];
84
+ }
85
+ return self;
86
+ }
87
+ @end
88
+
89
+ @implementation G9SqliteCore
90
+
91
+ - (instancetype)init {
92
+ if ((self = [super init])) {
93
+ _sessions = [[NSMutableDictionary alloc] init];
94
+ _txs = [[NSMutableDictionary alloc] init];
95
+ }
96
+ return self;
97
+ }
98
+
99
+ - (NSString *)nextToken:(NSString *)prefix {
100
+ @synchronized(self) {
101
+ _counter += 1;
102
+ return [NSString stringWithFormat:@"%@%lld", prefix, _counter];
103
+ }
104
+ }
105
+
106
+ - (G9SqliteSession *)session:(NSString *)token error:(NSDictionary **)error {
107
+ G9SqliteSession *session = nil;
108
+ if ([token isKindOfClass:[NSString class]]) {
109
+ @synchronized(self) {
110
+ session = [_sessions objectForKey:token];
111
+ }
112
+ }
113
+ if (session == nil) G9SetError(error, G9SqliteErrorMake(@"misuse", @"The database session is closed or unknown."));
114
+ return session;
115
+ }
116
+
117
+ - (NSDictionary *)capabilitiesWithError:(NSDictionary **)error {
118
+ NSString *version = [NSString stringWithUTF8String:sqlite3_libversion()];
119
+ int number = sqlite3_libversion_number();
120
+ return [NSDictionary dictionaryWithObjectsAndKeys:version, @"sqliteVersion",
121
+ [NSNumber numberWithInt:32766], @"maxVariables",
122
+ [NSNumber numberWithBool:YES], @"json1",
123
+ [NSNumber numberWithBool:number >= 3035000], @"returning",
124
+ [NSNumber numberWithBool:number >= 3024000], @"upsert",
125
+ [NSNumber numberWithBool:YES], @"interrupt", nil];
126
+ }
127
+
128
+ - (NSString *)open:(NSDictionary *)options error:(NSDictionary **)error {
129
+ NSString *path = [options objectForKey:@"path"];
130
+ if (![path isKindOfClass:[NSString class]]) {
131
+ G9SetError(error, G9SqliteErrorMake(@"misuse", @"open needs a path."));
132
+ return nil;
133
+ }
134
+ BOOL readOnly = [[options objectForKey:@"readOnly"] isEqual:[NSNumber numberWithBool:YES]];
135
+ BOOL create = [[options objectForKey:@"create"] isEqual:[NSNumber numberWithBool:YES]];
136
+ BOOL memory = [path length] == 0 || [path isEqualToString:@":memory:"];
137
+ if (!memory) {
138
+ NSFileManager *files = [NSFileManager defaultManager];
139
+ if (!create && ![files fileExistsAtPath:path]) {
140
+ G9SetError(error, G9SqliteErrorMake(@"not-found", [NSString stringWithFormat:@"Database file does not exist: %@", path]));
141
+ return nil;
142
+ }
143
+ if (create) {
144
+ [files createDirectoryAtPath:[path stringByDeletingLastPathComponent]
145
+ withIntermediateDirectories:YES
146
+ attributes:nil
147
+ error:NULL];
148
+ }
149
+ }
150
+ int flags = (readOnly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE) | (create && !readOnly ? SQLITE_OPEN_CREATE : 0) |
151
+ SQLITE_OPEN_FULLMUTEX;
152
+ sqlite3 *db = NULL;
153
+ int rc = sqlite3_open_v2(memory ? ":memory:" : [path fileSystemRepresentation], &db, flags, NULL);
154
+ if (rc != SQLITE_OK) {
155
+ NSDictionary *e = db != NULL ? G9DbError(db, nil, -1)
156
+ : G9Error(G9SqliteErrorCodeFor(rc), @"Cannot open the database.", rc, nil, -1);
157
+ sqlite3_close(db);
158
+ G9SetError(error, e);
159
+ return nil;
160
+ }
161
+ sqlite3_extended_result_codes(db, 1);
162
+ NSArray *pragmas = [options objectForKey:@"pragmas"];
163
+ if ([pragmas isKindOfClass:[NSArray class]]) {
164
+ for (NSString *sql in pragmas) {
165
+ if (sqlite3_exec(db, [sql UTF8String], NULL, NULL, NULL) != SQLITE_OK) {
166
+ // journal_mode cannot change on a read-only or in-memory database; tuning never fails an open.
167
+ if (![sql hasPrefix:@"PRAGMA journal_mode"]) {
168
+ NSDictionary *e = G9DbError(db, sql, -1);
169
+ sqlite3_close(db);
170
+ G9SetError(error, e);
171
+ return nil;
172
+ }
173
+ }
174
+ }
175
+ }
176
+ G9SqliteSession *session = [[G9SqliteSession alloc] init];
177
+ session->db = db;
178
+ NSString *token = [self nextToken:@"s"];
179
+ @synchronized(self) {
180
+ [_sessions setObject:session forKey:token];
181
+ }
182
+ return token;
183
+ }
184
+
185
+ - (sqlite3_stmt *)prepare:(G9SqliteSession *)session sql:(NSString *)sql error:(NSDictionary **)error {
186
+ NSValue *cached = [session->statements objectForKey:sql];
187
+ if (cached != nil) {
188
+ [session->order removeObject:sql];
189
+ [session->order addObject:sql];
190
+ return (sqlite3_stmt *)[cached pointerValue];
191
+ }
192
+ sqlite3_stmt *stmt = NULL;
193
+ if (sqlite3_prepare_v2(session->db, [sql UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
194
+ G9SetError(error, G9DbError(session->db, sql, -1));
195
+ return NULL;
196
+ }
197
+ [session->statements setObject:[NSValue valueWithPointer:stmt] forKey:sql];
198
+ [session->order addObject:sql];
199
+ if ([session->order count] > G9StatementCacheSize) {
200
+ NSString *eldest = [session->order objectAtIndex:0];
201
+ sqlite3_finalize((sqlite3_stmt *)[[session->statements objectForKey:eldest] pointerValue]);
202
+ [session->statements removeObjectForKey:eldest];
203
+ [session->order removeObjectAtIndex:0];
204
+ }
205
+ return stmt;
206
+ }
207
+
208
+ /** Binds one bridge value; NO with `error` set when the value is not a SQL value. */
209
+ - (BOOL)bind:(id)value to:(sqlite3_stmt *)stmt at:(int)index sql:(NSString *)sql error:(NSDictionary **)error {
210
+ int rc;
211
+ if (value == nil || value == (id)[NSNull null]) {
212
+ rc = sqlite3_bind_null(stmt, index);
213
+ } else if ([value isKindOfClass:[NSString class]]) {
214
+ NSString *text = value;
215
+ if ([text rangeOfString:[NSString stringWithFormat:@"%C", (unichar)0]].location != NSNotFound) {
216
+ G9SetError(error, G9Error(@"misuse", [NSString stringWithFormat:@"Parameter %d is text containing NUL; use a BLOB.", index], -1, sql, -1));
217
+ return NO;
218
+ }
219
+ rc = sqlite3_bind_text(stmt, index, [text UTF8String], -1, SQLITE_TRANSIENT);
220
+ } else if ([value isKindOfClass:[NSData class]]) {
221
+ NSData *data = value;
222
+ rc = [data length] == 0 ? sqlite3_bind_zeroblob(stmt, index, 0)
223
+ : sqlite3_bind_blob(stmt, index, [data bytes], (int)[data length], SQLITE_TRANSIENT);
224
+ } else if ([value isKindOfClass:[NSNumber class]]) {
225
+ NSNumber *number = value;
226
+ const char *type = [number objCType];
227
+ BOOL integral = type[0] == 'q' || type[0] == 'l' || type[0] == 'i' || type[0] == 's' || type[0] == 'c' ||
228
+ type[0] == 'Q' || type[0] == 'L' || type[0] == 'I' || type[0] == 'S' || type[0] == 'C' || type[0] == 'B';
229
+ if (integral) {
230
+ rc = sqlite3_bind_int64(stmt, index, [number longLongValue]);
231
+ } else {
232
+ double d = [number doubleValue];
233
+ if (isnan(d)) {
234
+ G9SetError(error, G9Error(@"misuse", [NSString stringWithFormat:@"Parameter %d is NaN.", index], -1, sql, -1));
235
+ return NO;
236
+ }
237
+ // Safe-integer doubles bind as INTEGER, as Node does (Number.isSafeInteger).
238
+ if (d == floor(d) && fabs(d) <= (double)G9MaxSafeInteger) {
239
+ rc = sqlite3_bind_int64(stmt, index, (sqlite3_int64)d);
240
+ } else {
241
+ rc = sqlite3_bind_double(stmt, index, d);
242
+ }
243
+ }
244
+ } else if ([value isKindOfClass:[NSDictionary class]] && [[value objectForKey:@"int64"] isKindOfClass:[NSString class]]) {
245
+ NSString *decimal = [value objectForKey:@"int64"];
246
+ NSScanner *scanner = [NSScanner scannerWithString:decimal];
247
+ long long parsed = 0;
248
+ if (![scanner scanLongLong:&parsed] || ![scanner isAtEnd]) {
249
+ G9SetError(error, G9Error(@"misuse", [NSString stringWithFormat:@"Parameter %d is not a 64-bit integer.", index], -1, sql, -1));
250
+ return NO;
251
+ }
252
+ rc = sqlite3_bind_int64(stmt, index, parsed);
253
+ } else if ([value isKindOfClass:[NSDictionary class]] && [value objectForKey:@"undefined"] != nil) {
254
+ G9SetError(error, G9Error(@"misuse", @"undefined is not a SQL value (use null).", -1, sql, -1));
255
+ return NO;
256
+ } else {
257
+ G9SetError(error, G9Error(@"misuse", [NSString stringWithFormat:@"Parameter %d is not a SQL value.", index], -1, sql, -1));
258
+ return NO;
259
+ }
260
+ if (rc != SQLITE_OK) {
261
+ G9SetError(error, G9Error(G9SqliteErrorCodeFor(rc), [NSString stringWithUTF8String:sqlite3_errstr(rc)], rc, sql, -1));
262
+ return NO;
263
+ }
264
+ return YES;
265
+ }
266
+
267
+ static id G9ReadColumn(sqlite3_stmt *stmt, int column) {
268
+ switch (sqlite3_column_type(stmt, column)) {
269
+ case SQLITE_NULL:
270
+ return [NSNull null];
271
+ case SQLITE_INTEGER:
272
+ return [NSNumber numberWithLongLong:sqlite3_column_int64(stmt, column)];
273
+ case SQLITE_FLOAT:
274
+ return [NSNumber numberWithDouble:sqlite3_column_double(stmt, column)];
275
+ case SQLITE_TEXT: {
276
+ const unsigned char *text = sqlite3_column_text(stmt, column);
277
+ int length = sqlite3_column_bytes(stmt, column);
278
+ NSString *s = [[NSString alloc] initWithBytes:text length:(NSUInteger)length encoding:NSUTF8StringEncoding];
279
+ return s != nil ? s : @"";
280
+ }
281
+ default: {
282
+ const void *bytes = sqlite3_column_blob(stmt, column);
283
+ int length = sqlite3_column_bytes(stmt, column);
284
+ return length > 0 ? [NSData dataWithBytes:bytes length:(NSUInteger)length] : [NSData data];
285
+ }
286
+ }
287
+ }
288
+
289
+ - (NSDictionary *)execute:(G9SqliteSession *)session
290
+ statement:(NSDictionary *)statement
291
+ maxRows:(NSInteger)maxRows
292
+ error:(NSDictionary **)error {
293
+ NSString *sql = [statement isKindOfClass:[NSDictionary class]] ? [statement objectForKey:@"sql"] : nil;
294
+ if (![sql isKindOfClass:[NSString class]]) {
295
+ G9SetError(error, G9SqliteErrorMake(@"misuse", @"A statement must be an object { sql, params }."));
296
+ return nil;
297
+ }
298
+ sqlite3_stmt *stmt = [self prepare:session sql:sql error:error];
299
+ if (stmt == NULL) return nil;
300
+ NSArray *params = [statement objectForKey:@"params"];
301
+ NSDictionary *result = nil;
302
+ BOOL ok = YES;
303
+ if ([params isKindOfClass:[NSArray class]]) {
304
+ for (NSUInteger i = 0; i < [params count] && ok; i++) {
305
+ ok = [self bind:[params objectAtIndex:i] to:stmt at:(int)i + 1 sql:sql error:error];
306
+ }
307
+ }
308
+ if (ok) {
309
+ int count = sqlite3_column_count(stmt);
310
+ if (count > 0) {
311
+ NSMutableArray *columns = [NSMutableArray arrayWithCapacity:(NSUInteger)count];
312
+ for (int c = 0; c < count; c++) [columns addObject:[NSString stringWithUTF8String:sqlite3_column_name(stmt, c)]];
313
+ NSMutableArray *rows = [NSMutableArray array];
314
+ int rc;
315
+ while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
316
+ if (maxRows >= 0 && (NSInteger)[rows count] >= maxRows) {
317
+ G9SetError(error, G9Error(@"too-many-rows", [NSString stringWithFormat:@"The statement returned more than %ld rows.", (long)maxRows], -1, sql, -1));
318
+ ok = NO;
319
+ break;
320
+ }
321
+ NSMutableArray *row = [NSMutableArray arrayWithCapacity:(NSUInteger)count];
322
+ for (int c = 0; c < count; c++) [row addObject:G9ReadColumn(stmt, c)];
323
+ [rows addObject:row];
324
+ }
325
+ if (ok && rc != SQLITE_DONE) {
326
+ G9SetError(error, G9DbError(session->db, sql, -1));
327
+ ok = NO;
328
+ }
329
+ if (ok) {
330
+ result = [NSDictionary dictionaryWithObjectsAndKeys:columns, @"columns", rows, @"rows",
331
+ [NSNumber numberWithLongLong:0], @"changes",
332
+ [NSNumber numberWithLongLong:session->lastInsertRowId], @"lastInsertRowId", nil];
333
+ }
334
+ } else {
335
+ int rc;
336
+ while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
337
+ }
338
+ if (rc != SQLITE_DONE) {
339
+ G9SetError(error, G9DbError(session->db, sql, -1));
340
+ ok = NO;
341
+ } else {
342
+ session->lastInsertRowId = sqlite3_last_insert_rowid(session->db);
343
+ result = [NSDictionary dictionaryWithObjectsAndKeys:[NSArray array], @"columns", [NSArray array], @"rows",
344
+ [NSNumber numberWithLongLong:sqlite3_changes(session->db)], @"changes",
345
+ [NSNumber numberWithLongLong:session->lastInsertRowId], @"lastInsertRowId", nil];
346
+ }
347
+ }
348
+ }
349
+ sqlite3_reset(stmt);
350
+ sqlite3_clear_bindings(stmt);
351
+ return ok ? result : nil;
352
+ }
353
+
354
+ - (NSArray *)run:(NSString *)token statements:(NSArray *)statements options:(NSDictionary *)options error:(NSDictionary **)error {
355
+ G9SqliteSession *session = [self session:token error:error];
356
+ if (session == nil) return nil;
357
+ NSString *tx = [options objectForKey:@"tx"];
358
+ if (![tx isKindOfClass:[NSString class]]) tx = nil;
359
+ if (tx != nil && ![tx isEqual:session->tx]) {
360
+ G9SetError(error, G9SqliteErrorMake(@"misuse", @"The transaction is finished or belongs to another session."));
361
+ return nil;
362
+ }
363
+ if (tx == nil && session->tx != nil) {
364
+ G9SetError(error, G9SqliteErrorMake(@"misuse", @"A transaction is open on this session; pass its token (tx)."));
365
+ return nil;
366
+ }
367
+ NSNumber *maxRowsValue = [options objectForKey:@"maxRows"];
368
+ NSInteger maxRows = [maxRowsValue isKindOfClass:[NSNumber class]] ? [maxRowsValue integerValue] : -1;
369
+ BOOL atomic = [[options objectForKey:@"atomic"] isEqual:[NSNumber numberWithBool:YES]] && tx == nil && [statements count] > 0;
370
+ if (atomic && sqlite3_exec(session->db, "BEGIN", NULL, NULL, NULL) != SQLITE_OK) {
371
+ G9SetError(error, G9DbError(session->db, @"BEGIN", 0));
372
+ return nil;
373
+ }
374
+ NSMutableArray *results = [NSMutableArray arrayWithCapacity:[statements count]];
375
+ for (NSDictionary *statement in statements) {
376
+ NSDictionary *failure = nil;
377
+ NSDictionary *result = [self execute:session statement:statement maxRows:maxRows error:&failure];
378
+ if (result == nil) {
379
+ NSString *sql = [statement isKindOfClass:[NSDictionary class]] ? [statement objectForKey:@"sql"] : nil;
380
+ if (atomic) sqlite3_exec(session->db, "ROLLBACK", NULL, NULL, NULL); // SQLite may already have rolled back.
381
+ G9SetError(error, G9WithContext(failure, [sql isKindOfClass:[NSString class]] ? sql : nil, (NSInteger)[results count]));
382
+ return nil;
383
+ }
384
+ [results addObject:result];
385
+ }
386
+ if (atomic && sqlite3_exec(session->db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) {
387
+ NSDictionary *e = G9DbError(session->db, @"COMMIT", (NSInteger)[results count]);
388
+ sqlite3_exec(session->db, "ROLLBACK", NULL, NULL, NULL);
389
+ G9SetError(error, e);
390
+ return nil;
391
+ }
392
+ return results;
393
+ }
394
+
395
+ - (NSString *)begin:(NSString *)token mode:(NSString *)mode error:(NSDictionary **)error {
396
+ G9SqliteSession *session = [self session:token error:error];
397
+ if (session == nil) return nil;
398
+ if (session->tx != nil) {
399
+ G9SetError(error, G9SqliteErrorMake(@"misuse", @"A transaction is already open on this session."));
400
+ return nil;
401
+ }
402
+ const char *sql = [mode isEqualToString:@"immediate"] ? "BEGIN IMMEDIATE" : "BEGIN DEFERRED";
403
+ if (sqlite3_exec(session->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
404
+ G9SetError(error, G9DbError(session->db, nil, -1));
405
+ return nil;
406
+ }
407
+ NSString *tx = [self nextToken:@"x"];
408
+ session->tx = tx;
409
+ @synchronized(self) {
410
+ [_txs setObject:token forKey:tx];
411
+ }
412
+ return tx;
413
+ }
414
+
415
+ - (NSString *)sessionOfTx:(NSString *)tx {
416
+ if (![tx isKindOfClass:[NSString class]]) return nil;
417
+ @synchronized(self) {
418
+ return [_txs objectForKey:tx];
419
+ }
420
+ }
421
+
422
+ - (G9SqliteSession *)sessionForTx:(NSString *)tx error:(NSDictionary **)error {
423
+ NSString *token = [self sessionOfTx:tx];
424
+ G9SqliteSession *session = nil;
425
+ if (token != nil) {
426
+ @synchronized(self) {
427
+ session = [_sessions objectForKey:token];
428
+ }
429
+ }
430
+ if (session == nil || ![session->tx isEqual:tx]) {
431
+ G9SetError(error, G9SqliteErrorMake(@"misuse", @"The transaction is finished or unknown."));
432
+ return nil;
433
+ }
434
+ return session;
435
+ }
436
+
437
+ - (BOOL)commit:(NSString *)tx error:(NSDictionary **)error {
438
+ G9SqliteSession *session = [self sessionForTx:tx error:error];
439
+ if (session == nil) return NO;
440
+ if (sqlite3_exec(session->db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) {
441
+ // A failed COMMIT (busy) leaves the transaction open: the caller may retry or roll back.
442
+ G9SetError(error, G9DbError(session->db, nil, -1));
443
+ return NO;
444
+ }
445
+ session->tx = nil;
446
+ @synchronized(self) {
447
+ [_txs removeObjectForKey:tx];
448
+ }
449
+ return YES;
450
+ }
451
+
452
+ - (BOOL)rollback:(NSString *)tx error:(NSDictionary **)error {
453
+ G9SqliteSession *session = [self sessionForTx:tx error:error];
454
+ if (session == nil) return NO;
455
+ session->tx = nil;
456
+ @synchronized(self) {
457
+ [_txs removeObjectForKey:tx];
458
+ }
459
+ // SQLite may already have rolled back on an error; the transaction is over either way.
460
+ sqlite3_exec(session->db, "ROLLBACK", NULL, NULL, NULL);
461
+ return YES;
462
+ }
463
+
464
+ - (BOOL)close:(NSString *)token error:(NSDictionary **)error {
465
+ G9SqliteSession *session = [self session:token error:error];
466
+ if (session == nil) return NO;
467
+ @synchronized(self) {
468
+ [_sessions removeObjectForKey:token];
469
+ if (session->tx != nil) [_txs removeObjectForKey:session->tx];
470
+ session->tx = nil;
471
+ for (NSValue *value in [session->statements allValues]) sqlite3_finalize((sqlite3_stmt *)[value pointerValue]);
472
+ [session->statements removeAllObjects];
473
+ [session->order removeAllObjects];
474
+ // Under the lock, so a concurrent interrupt: never touches a closed handle.
475
+ sqlite3_close_v2(session->db);
476
+ session->db = NULL;
477
+ }
478
+ return YES;
479
+ }
480
+
481
+ - (void)interrupt:(NSString *)token {
482
+ if (![token isKindOfClass:[NSString class]]) return;
483
+ @synchronized(self) {
484
+ G9SqliteSession *session = [_sessions objectForKey:token];
485
+ if (session != nil && session->db != NULL) sqlite3_interrupt(session->db);
486
+ }
487
+ }
488
+
489
+ - (void)closeAll {
490
+ NSArray *tokens;
491
+ @synchronized(self) {
492
+ tokens = [_sessions allKeys];
493
+ }
494
+ for (NSString *token in tokens) [self close:token error:NULL];
495
+ }
496
+
497
+ @end
@@ -0,0 +1,16 @@
1
+ // How results cross to JS (plan/specs/06 §6), byte-identical to the Android encoder (G9SqliteTransport.kt) and pinned
2
+ // by packages/native/contract/transport-golden.json: `columnar` (default) or `rows`. Foundation only.
3
+ #import <Foundation/Foundation.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface G9SqliteTransport : NSObject
8
+ /** A 64-bit integer as the bridge carries it: a safe integer as a double NSNumber, otherwise `{ int64: "…" }`. */
9
+ + (id)wireNumber:(long long)value;
10
+ /** `{ transport, results }` for the core's results. */
11
+ + (NSDictionary *)encode:(NSArray *)results transport:(nullable NSString *)transport;
12
+ + (NSDictionary *)rows:(NSDictionary *)result;
13
+ + (NSDictionary *)columnar:(NSDictionary *)result;
14
+ @end
15
+
16
+ NS_ASSUME_NONNULL_END