strata-storage 2.4.3 → 2.6.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 (114) hide show
  1. package/AI-INTEGRATION-GUIDE.md +115 -261
  2. package/README.md +426 -182
  3. package/android/AGENTS.md +51 -0
  4. package/android/CLAUDE.md +89 -0
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
  7. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +260 -203
  8. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +357 -69
  9. package/dist/README.md +426 -182
  10. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  11. package/dist/adapters/capacitor/FilesystemAdapter.js +2 -1
  12. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  13. package/dist/adapters/capacitor/PreferencesAdapter.js +2 -1
  14. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  15. package/dist/adapters/capacitor/SecureAdapter.js +2 -1
  16. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  17. package/dist/adapters/capacitor/SqliteAdapter.js +2 -1
  18. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
  19. package/dist/adapters/web/CacheAdapter.js +11 -3
  20. package/dist/adapters/web/CookieAdapter.d.ts +37 -1
  21. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
  22. package/dist/adapters/web/CookieAdapter.js +89 -9
  23. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  24. package/dist/adapters/web/IndexedDBAdapter.js +10 -2
  25. package/dist/adapters/web/LocalStorageAdapter.d.ts +31 -0
  26. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
  27. package/dist/adapters/web/LocalStorageAdapter.js +92 -19
  28. package/dist/adapters/web/MemoryAdapter.d.ts +24 -0
  29. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
  30. package/dist/adapters/web/MemoryAdapter.js +69 -18
  31. package/dist/adapters/web/SessionStorageAdapter.d.ts +24 -0
  32. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  33. package/dist/adapters/web/SessionStorageAdapter.js +71 -9
  34. package/dist/adapters/web/URLAdapter.d.ts +59 -0
  35. package/dist/adapters/web/URLAdapter.d.ts.map +1 -0
  36. package/dist/adapters/web/URLAdapter.js +234 -0
  37. package/dist/adapters/web/index.d.ts +1 -0
  38. package/dist/adapters/web/index.d.ts.map +1 -1
  39. package/dist/adapters/web/index.js +1 -0
  40. package/dist/android/AGENTS.md +51 -0
  41. package/dist/android/CLAUDE.md +89 -0
  42. package/dist/android/build.gradle +1 -1
  43. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
  44. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +260 -203
  45. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +357 -69
  46. package/dist/capacitor.d.ts.map +1 -1
  47. package/dist/capacitor.js +2 -1
  48. package/dist/core/BaseAdapter.d.ts +8 -0
  49. package/dist/core/BaseAdapter.d.ts.map +1 -1
  50. package/dist/core/BaseAdapter.js +34 -14
  51. package/dist/core/Strata.d.ts +56 -2
  52. package/dist/core/Strata.d.ts.map +1 -1
  53. package/dist/core/Strata.js +501 -53
  54. package/dist/features/encryption.d.ts.map +1 -1
  55. package/dist/features/encryption.js +3 -2
  56. package/dist/features/integrity.d.ts +16 -0
  57. package/dist/features/integrity.d.ts.map +1 -0
  58. package/dist/features/integrity.js +28 -0
  59. package/dist/features/observer.d.ts.map +1 -1
  60. package/dist/features/observer.js +2 -1
  61. package/dist/features/query.d.ts +7 -1
  62. package/dist/features/query.d.ts.map +1 -1
  63. package/dist/features/query.js +9 -2
  64. package/dist/features/sync.d.ts.map +1 -1
  65. package/dist/features/sync.js +4 -3
  66. package/dist/index.d.ts +35 -2
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +55 -30
  69. package/dist/integrations/angular/index.d.ts +158 -0
  70. package/dist/integrations/angular/index.d.ts.map +1 -0
  71. package/dist/integrations/angular/index.js +395 -0
  72. package/dist/integrations/index.d.ts +15 -0
  73. package/dist/integrations/index.d.ts.map +1 -0
  74. package/dist/integrations/index.js +18 -0
  75. package/dist/integrations/react/index.d.ts +75 -0
  76. package/dist/integrations/react/index.d.ts.map +1 -0
  77. package/dist/integrations/react/index.js +191 -0
  78. package/dist/integrations/vue/index.d.ts +103 -0
  79. package/dist/integrations/vue/index.d.ts.map +1 -0
  80. package/dist/integrations/vue/index.js +274 -0
  81. package/dist/ios/AGENTS.md +48 -0
  82. package/dist/ios/CLAUDE.md +84 -0
  83. package/dist/ios/Plugin/FilesystemStorage.swift +218 -0
  84. package/dist/ios/Plugin/KeychainStorage.swift +139 -50
  85. package/dist/ios/Plugin/SQLiteStorage.swift +279 -147
  86. package/dist/ios/Plugin/StrataStoragePlugin.m +23 -0
  87. package/dist/ios/Plugin/StrataStoragePlugin.swift +272 -65
  88. package/dist/package.json +21 -5
  89. package/dist/plugin/index.d.ts.map +1 -1
  90. package/dist/plugin/index.js +2 -1
  91. package/dist/types/index.d.ts +58 -9
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +0 -13
  94. package/dist/utils/errors.d.ts +7 -0
  95. package/dist/utils/errors.d.ts.map +1 -1
  96. package/dist/utils/errors.js +15 -3
  97. package/dist/utils/index.d.ts +63 -5
  98. package/dist/utils/index.d.ts.map +1 -1
  99. package/dist/utils/index.js +109 -16
  100. package/dist/utils/logger.d.ts +31 -0
  101. package/dist/utils/logger.d.ts.map +1 -0
  102. package/dist/utils/logger.js +63 -0
  103. package/ios/AGENTS.md +48 -0
  104. package/ios/CLAUDE.md +84 -0
  105. package/ios/Plugin/FilesystemStorage.swift +218 -0
  106. package/ios/Plugin/KeychainStorage.swift +139 -50
  107. package/ios/Plugin/SQLiteStorage.swift +279 -147
  108. package/ios/Plugin/StrataStoragePlugin.m +23 -0
  109. package/ios/Plugin/StrataStoragePlugin.swift +272 -65
  110. package/package.json +31 -20
  111. package/scripts/build.js +16 -5
  112. package/scripts/configure.js +2 -6
  113. package/scripts/postinstall.js +2 -2
  114. package/Readme.md +0 -271
@@ -1,24 +1,44 @@
1
1
  package com.strata.storage;
2
2
 
3
3
  import android.content.Context;
4
+ import android.content.ContentValues;
4
5
  import android.database.Cursor;
5
6
  import android.database.sqlite.SQLiteDatabase;
6
7
  import android.database.sqlite.SQLiteOpenHelper;
7
- import android.content.ContentValues;
8
8
  import android.util.Log;
9
+ import com.getcapacitor.JSObject;
10
+ import java.nio.charset.StandardCharsets;
9
11
  import java.util.ArrayList;
10
- import java.util.List;
11
12
  import java.util.HashMap;
13
+ import java.util.HashSet;
14
+ import java.util.List;
12
15
  import java.util.Map;
16
+ import java.util.Set;
17
+ import org.json.JSONException;
13
18
  import org.json.JSONObject;
14
- import org.json.JSONArray;
15
- import java.nio.charset.StandardCharsets;
16
19
 
20
+ /**
21
+ * SQLite-backed storage. One {@link SQLiteStorage} instance maps to a single
22
+ * database file (one {@link SQLiteOpenHelper}). A single instance can serve
23
+ * multiple logical tables: every public method takes a sanitized {@code table}
24
+ * name and the table is created on first use via {@code CREATE TABLE IF NOT
25
+ * EXISTS}.
26
+ *
27
+ * <p>Value-shape contract (matches the TS {@code SqliteAdapter}): {@code set}
28
+ * receives the FULL wrapper object ({@code value, created, updated, expires?,
29
+ * tags?, metadata?}). The entire wrapper is JSON-serialized into the
30
+ * {@code value} column for round-trip fidelity, while {@code created},
31
+ * {@code updated}, {@code expires}, {@code tags} and {@code metadata} are also
32
+ * extracted into their own columns for TTL + query support. {@code get} parses
33
+ * the {@code value} column back into a {@link JSObject} wrapper.
34
+ *
35
+ * <p>All database access is synchronized on the instance so overlapping bridge
36
+ * calls cannot corrupt the connection.
37
+ */
17
38
  public class SQLiteStorage extends SQLiteOpenHelper {
18
39
  private static final int DATABASE_VERSION = 1;
19
- private static final String TABLE_NAME = "strata_storage";
20
- private static final String DEFAULT_DB_NAME = "strata.db";
21
-
40
+ private static final String DEFAULT_TABLE = "storage";
41
+
22
42
  private static final String KEY_ID = "key";
23
43
  private static final String KEY_VALUE = "value";
24
44
  private static final String KEY_CREATED = "created";
@@ -26,178 +46,181 @@ public class SQLiteStorage extends SQLiteOpenHelper {
26
46
  private static final String KEY_EXPIRES = "expires";
27
47
  private static final String KEY_TAGS = "tags";
28
48
  private static final String KEY_METADATA = "metadata";
29
-
30
- public SQLiteStorage(Context context) {
31
- this(context, DEFAULT_DB_NAME);
32
- }
33
-
49
+
50
+ /** Tables already created in this DB during the process lifetime. */
51
+ private final Set<String> ensuredTables = new HashSet<>();
52
+
53
+ /**
54
+ * @param context Android context
55
+ * @param dbName fully-resolved database file name (already sanitized +
56
+ * suffixed with ".db" by the plugin)
57
+ */
34
58
  public SQLiteStorage(Context context, String dbName) {
35
- super(context, dbName != null ? dbName : DEFAULT_DB_NAME, null, DATABASE_VERSION);
59
+ super(context, dbName, null, DATABASE_VERSION);
60
+ }
61
+
62
+ /**
63
+ * Sanitize a table name to a safe SQL identifier ({@code ^[A-Za-z0-9_]+$}).
64
+ * Table names cannot be passed as bound parameters in SQLite, so they MUST
65
+ * be validated before being interpolated into DDL/DML.
66
+ */
67
+ public static String sanitizeTable(String table) {
68
+ if (table == null || table.isEmpty()) {
69
+ return DEFAULT_TABLE;
70
+ }
71
+ String cleaned = table.replaceAll("[^A-Za-z0-9_]", "");
72
+ return cleaned.isEmpty() ? DEFAULT_TABLE : cleaned;
36
73
  }
37
-
74
+
38
75
  @Override
39
76
  public void onCreate(SQLiteDatabase db) {
40
- String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "("
77
+ // Tables are created lazily per (database, table) via ensureTable().
78
+ }
79
+
80
+ @Override
81
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
82
+ // No destructive migrations: tables are created on demand and persist.
83
+ }
84
+
85
+ /**
86
+ * Create the table for {@code safeTable} if it does not yet exist. Caller
87
+ * MUST pass an already-sanitized identifier.
88
+ */
89
+ private void ensureTable(SQLiteDatabase db, String safeTable) {
90
+ if (ensuredTables.contains(safeTable)) {
91
+ return;
92
+ }
93
+ String createTable = "CREATE TABLE IF NOT EXISTS " + safeTable + "("
41
94
  + KEY_ID + " TEXT PRIMARY KEY,"
42
- + KEY_VALUE + " BLOB NOT NULL,"
95
+ + KEY_VALUE + " TEXT NOT NULL,"
43
96
  + KEY_CREATED + " INTEGER NOT NULL,"
44
97
  + KEY_UPDATED + " INTEGER NOT NULL,"
45
98
  + KEY_EXPIRES + " INTEGER,"
46
99
  + KEY_TAGS + " TEXT,"
47
100
  + KEY_METADATA + " TEXT" + ")";
48
- db.execSQL(CREATE_TABLE);
49
- }
50
-
51
- @Override
52
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
53
- db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
54
- onCreate(db);
101
+ db.execSQL(createTable);
102
+ ensuredTables.add(safeTable);
55
103
  }
56
-
57
- public boolean set(String key, Object value, Long expires, String tags, String metadata) {
58
- byte[] valueBytes;
59
-
60
- try {
61
- if (value instanceof byte[]) {
62
- valueBytes = (byte[]) value;
63
- } else if (value instanceof String) {
64
- valueBytes = ((String) value).getBytes(StandardCharsets.UTF_8);
65
- } else {
66
- // Convert complex objects to JSON then to bytes
67
- String json;
68
- if (value instanceof JSONObject) {
69
- json = ((JSONObject) value).toString();
70
- } else {
71
- // Use helper method to convert object to JSON string
72
- try {
73
- json = objectToJsonString(value);
74
- } catch (Exception jsonEx) {
75
- // Fallback to string representation
76
- json = value.toString();
77
- }
78
- }
79
- valueBytes = json.getBytes(StandardCharsets.UTF_8);
80
- }
81
- } catch (Exception e) {
82
- Log.e("StrataStorage", "Failed to set value in SQLite", e);
104
+
105
+ /**
106
+ * Persist the full wrapper for {@code key}. The wrapper is stored verbatim
107
+ * as JSON in the {@code value} column; {@code created}/{@code updated}/
108
+ * {@code expires}/{@code tags}/{@code metadata} are mirrored into columns
109
+ * for TTL + query.
110
+ *
111
+ * @param wrapper the full {@link StorageValue} wrapper as a {@link JSObject}
112
+ */
113
+ public synchronized boolean set(String table, String key, JSObject wrapper) {
114
+ if (wrapper == null) {
83
115
  return false;
84
116
  }
85
- SQLiteDatabase db = this.getWritableDatabase();
86
- ContentValues values = new ContentValues();
87
-
117
+ String safeTable = sanitizeTable(table);
88
118
  long now = System.currentTimeMillis();
119
+
120
+ // Column extraction for TTL + query. Defaults keep legacy fidelity.
121
+ long created = wrapper.optLong(KEY_CREATED, now);
122
+ long updated = wrapper.optLong(KEY_UPDATED, now);
123
+
124
+ ContentValues values = new ContentValues();
89
125
  values.put(KEY_ID, key);
90
- values.put(KEY_VALUE, valueBytes);
91
- values.put(KEY_CREATED, now);
92
- values.put(KEY_UPDATED, now);
93
-
94
- if (expires != null) {
95
- values.put(KEY_EXPIRES, expires);
126
+ values.put(KEY_VALUE, wrapper.toString());
127
+ values.put(KEY_CREATED, created);
128
+ values.put(KEY_UPDATED, updated);
129
+
130
+ if (wrapper.has(KEY_EXPIRES) && !wrapper.isNull(KEY_EXPIRES)) {
131
+ values.put(KEY_EXPIRES, wrapper.optLong(KEY_EXPIRES));
96
132
  }
97
- if (tags != null) {
98
- values.put(KEY_TAGS, tags);
133
+ // tags / metadata are stored as their JSON text for query use.
134
+ Object tags = wrapper.opt(KEY_TAGS);
135
+ if (tags != null && tags != JSONObject.NULL) {
136
+ values.put(KEY_TAGS, tags.toString());
99
137
  }
100
- if (metadata != null) {
101
- values.put(KEY_METADATA, metadata);
138
+ Object metadata = wrapper.opt(KEY_METADATA);
139
+ if (metadata != null && metadata != JSONObject.NULL) {
140
+ values.put(KEY_METADATA, metadata.toString());
141
+ }
142
+
143
+ try {
144
+ SQLiteDatabase db = getWritableDatabase();
145
+ ensureTable(db, safeTable);
146
+ long result = db.insertWithOnConflict(
147
+ safeTable, null, values, SQLiteDatabase.CONFLICT_REPLACE);
148
+ return result != -1;
149
+ } catch (Exception e) {
150
+ Log.e("StrataStorage", "Failed to set value in SQLite", e);
151
+ return false;
102
152
  }
103
-
104
- long result = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
105
- db.close();
106
- return result != -1;
107
- }
108
-
109
- // Convenience method for simple Object values
110
- public boolean set(String key, Object value) {
111
- return set(key, value, null, null, null);
112
153
  }
113
-
114
- public Map<String, Object> get(String key) {
115
- SQLiteDatabase db = this.getReadableDatabase();
154
+
155
+ /**
156
+ * Read the full wrapper for {@code key}, parsed back from the JSON stored in
157
+ * the {@code value} column. Missing row → {@code null}. Unparseable legacy
158
+ * bytes → {@code null} (treated as a miss; never throws).
159
+ */
160
+ public synchronized JSObject get(String table, String key) {
161
+ String safeTable = sanitizeTable(table);
162
+ SQLiteDatabase db = getReadableDatabase();
163
+ ensureTable(db, safeTable);
116
164
  Cursor cursor = null;
117
165
  try {
118
- cursor = db.query(TABLE_NAME, null, KEY_ID + "=?",
166
+ cursor = db.query(safeTable, new String[]{KEY_VALUE}, KEY_ID + "=?",
119
167
  new String[]{key}, null, null, null, null);
120
-
121
- Map<String, Object> result = null;
122
168
  if (cursor != null && cursor.moveToFirst()) {
123
- result = new HashMap<>();
124
- result.put("key", key);
125
- result.put("value", cursor.getBlob(cursor.getColumnIndex(KEY_VALUE)));
126
- result.put("created", cursor.getLong(cursor.getColumnIndex(KEY_CREATED)));
127
- result.put("updated", cursor.getLong(cursor.getColumnIndex(KEY_UPDATED)));
128
-
129
- int expiresIndex = cursor.getColumnIndex(KEY_EXPIRES);
130
- if (!cursor.isNull(expiresIndex)) {
131
- result.put("expires", cursor.getLong(expiresIndex));
132
- }
133
-
134
- int tagsIndex = cursor.getColumnIndex(KEY_TAGS);
135
- if (!cursor.isNull(tagsIndex)) {
136
- result.put("tags", cursor.getString(tagsIndex));
169
+ String stored = cursor.getString(0);
170
+ if (stored == null) {
171
+ return null;
137
172
  }
138
-
139
- int metadataIndex = cursor.getColumnIndex(KEY_METADATA);
140
- if (!cursor.isNull(metadataIndex)) {
141
- result.put("metadata", cursor.getString(metadataIndex));
173
+ try {
174
+ return new JSObject(stored);
175
+ } catch (JSONException parseError) {
176
+ // Legacy / corrupted payload: treat as a miss rather than throw.
177
+ Log.w("StrataStorage", "Unparseable SQLite value for key " + key);
178
+ return null;
142
179
  }
143
180
  }
144
- return result;
181
+ return null;
145
182
  } finally {
146
183
  if (cursor != null) {
147
184
  cursor.close();
148
185
  }
149
- db.close();
150
186
  }
151
187
  }
152
-
153
- public boolean remove(String key) {
154
- SQLiteDatabase db = this.getWritableDatabase();
155
- int result = db.delete(TABLE_NAME, KEY_ID + " = ?", new String[]{key});
156
- db.close();
188
+
189
+ public synchronized boolean remove(String table, String key) {
190
+ String safeTable = sanitizeTable(table);
191
+ SQLiteDatabase db = getWritableDatabase();
192
+ ensureTable(db, safeTable);
193
+ int result = db.delete(safeTable, KEY_ID + " = ?", new String[]{key});
157
194
  return result > 0;
158
195
  }
159
-
160
- public boolean clear() {
161
- return clear(null);
162
- }
163
-
164
- public boolean clear(String prefix) {
165
- SQLiteDatabase db = this.getWritableDatabase();
196
+
197
+ public synchronized boolean clear(String table, String prefix) {
198
+ String safeTable = sanitizeTable(table);
199
+ SQLiteDatabase db = getWritableDatabase();
200
+ ensureTable(db, safeTable);
166
201
  int result;
167
-
168
202
  if (prefix != null) {
169
- // Clear only keys with the given prefix
170
- result = db.delete(TABLE_NAME, KEY_ID + " LIKE ?", new String[]{prefix + "%"});
203
+ result = db.delete(safeTable, KEY_ID + " LIKE ?", new String[]{prefix + "%"});
171
204
  } else {
172
- // Clear all keys
173
- result = db.delete(TABLE_NAME, null, null);
205
+ result = db.delete(safeTable, null, null);
174
206
  }
175
-
176
- db.close();
177
207
  return result >= 0;
178
208
  }
179
-
180
- public List<String> keys() {
181
- return keys(null);
182
- }
183
-
184
- public List<String> keys(String pattern) {
185
- List<String> keys = new ArrayList<>();
186
- String selectQuery;
187
- String[] selectionArgs = null;
188
209
 
189
- if (pattern != null) {
190
- selectQuery = "SELECT " + KEY_ID + " FROM " + TABLE_NAME + " WHERE " + KEY_ID + " LIKE ?";
191
- selectionArgs = new String[]{"%" + pattern + "%"};
192
- } else {
193
- selectQuery = "SELECT " + KEY_ID + " FROM " + TABLE_NAME;
194
- }
195
-
196
- SQLiteDatabase db = this.getReadableDatabase();
210
+ public synchronized List<String> keys(String table, String pattern) {
211
+ String safeTable = sanitizeTable(table);
212
+ List<String> keys = new ArrayList<>();
213
+ SQLiteDatabase db = getReadableDatabase();
214
+ ensureTable(db, safeTable);
197
215
  Cursor cursor = null;
198
216
  try {
199
- cursor = db.rawQuery(selectQuery, selectionArgs);
200
-
217
+ if (pattern != null) {
218
+ cursor = db.query(safeTable, new String[]{KEY_ID}, KEY_ID + " LIKE ?",
219
+ new String[]{"%" + pattern + "%"}, null, null, null, null);
220
+ } else {
221
+ cursor = db.query(safeTable, new String[]{KEY_ID},
222
+ null, null, null, null, null);
223
+ }
201
224
  if (cursor != null && cursor.moveToFirst()) {
202
225
  do {
203
226
  keys.add(cursor.getString(0));
@@ -208,107 +231,141 @@ public class SQLiteStorage extends SQLiteOpenHelper {
208
231
  if (cursor != null) {
209
232
  cursor.close();
210
233
  }
211
- db.close();
212
234
  }
213
235
  }
214
-
215
- public boolean has(String key) {
216
- SQLiteDatabase db = this.getReadableDatabase();
236
+
237
+ public synchronized boolean has(String table, String key) {
238
+ String safeTable = sanitizeTable(table);
239
+ SQLiteDatabase db = getReadableDatabase();
240
+ ensureTable(db, safeTable);
217
241
  Cursor cursor = null;
218
242
  try {
219
- cursor = db.query(TABLE_NAME, new String[]{KEY_ID}, KEY_ID + "=?",
243
+ cursor = db.query(safeTable, new String[]{KEY_ID}, KEY_ID + "=?",
220
244
  new String[]{key}, null, null, null, null);
221
- boolean exists = cursor != null && cursor.getCount() > 0;
222
- return exists;
245
+ return cursor != null && cursor.getCount() > 0;
223
246
  } finally {
224
247
  if (cursor != null) {
225
248
  cursor.close();
226
249
  }
227
- db.close();
228
250
  }
229
251
  }
230
-
231
- public SizeInfo size() {
232
- SQLiteDatabase db = this.getReadableDatabase();
252
+
253
+ /**
254
+ * Enumerate every key in the table. The TS {@code SqliteAdapter} applies the
255
+ * real query condition by re-fetching/filtering each key, so this only needs
256
+ * to surface candidate keys.
257
+ */
258
+ public synchronized List<Map<String, Object>> query(String table) {
259
+ String safeTable = sanitizeTable(table);
260
+ List<Map<String, Object>> rows = new ArrayList<>();
261
+ SQLiteDatabase db = getReadableDatabase();
262
+ ensureTable(db, safeTable);
233
263
  Cursor cursor = null;
234
264
  try {
235
- cursor = db.rawQuery("SELECT COUNT(*), SUM(LENGTH(" + KEY_VALUE + ")) FROM " + TABLE_NAME, null);
236
-
237
- long totalSize = 0;
238
- int count = 0;
239
-
265
+ cursor = db.query(safeTable, new String[]{KEY_ID},
266
+ null, null, null, null, null);
240
267
  if (cursor != null && cursor.moveToFirst()) {
241
- count = cursor.getInt(0);
242
- // Check for NULL values from SQL SUM function
243
- if (!cursor.isNull(1)) {
244
- totalSize = cursor.getLong(1);
245
- }
268
+ do {
269
+ Map<String, Object> row = new HashMap<>();
270
+ row.put("key", cursor.getString(0));
271
+ rows.add(row);
272
+ } while (cursor.moveToNext());
246
273
  }
247
-
248
- return new SizeInfo(totalSize, count);
274
+ return rows;
249
275
  } finally {
250
276
  if (cursor != null) {
251
277
  cursor.close();
252
278
  }
253
- db.close();
254
279
  }
255
280
  }
256
-
281
+
257
282
  /**
258
- * Convert an object to JSON string using reflection
283
+ * Size information. {@code total} = Σ(length(key) + length(value)), matching
284
+ * the web adapters' convention (key bytes are included). When {@code
285
+ * detailed} is true the byte breakdown is also populated: {@code keys} = Σ
286
+ * length(key), {@code values} = Σ length(value), {@code metadata} = Σ
287
+ * length(metadata) (0 for null); {@code count} is the row count.
259
288
  */
260
- private String objectToJsonString(Object obj) throws Exception {
261
- if (obj == null) {
262
- return "null";
263
- }
264
-
265
- if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
266
- return obj.toString();
267
- }
268
-
269
- if (obj instanceof Map) {
270
- JSONObject jsonObj = new JSONObject();
271
- Map<?, ?> map = (Map<?, ?>) obj;
272
- for (Map.Entry<?, ?> entry : map.entrySet()) {
273
- String key = entry.getKey().toString();
274
- jsonObj.put(key, entry.getValue());
275
- }
276
- return jsonObj.toString();
277
- }
278
-
279
- if (obj instanceof List || obj.getClass().isArray()) {
280
- JSONArray jsonArray = new JSONArray();
281
- if (obj instanceof List) {
282
- List<?> list = (List<?>) obj;
283
- for (Object item : list) {
284
- jsonArray.put(item);
285
- }
286
- } else {
287
- Object[] array = (Object[]) obj;
288
- for (Object item : array) {
289
- jsonArray.put(item);
289
+ public synchronized SizeInfo size(String table, boolean detailed) {
290
+ String safeTable = sanitizeTable(table);
291
+ SQLiteDatabase db = getReadableDatabase();
292
+ ensureTable(db, safeTable);
293
+ Cursor cursor = null;
294
+ try {
295
+ if (!detailed) {
296
+ cursor = db.rawQuery("SELECT COUNT(*), SUM(LENGTH(" + KEY_ID + ") + LENGTH("
297
+ + KEY_VALUE + ")) FROM " + safeTable, null);
298
+ long totalSize = 0;
299
+ int count = 0;
300
+ if (cursor != null && cursor.moveToFirst()) {
301
+ count = cursor.getInt(0);
302
+ if (!cursor.isNull(1)) {
303
+ totalSize = cursor.getLong(1);
304
+ }
290
305
  }
306
+ return new SizeInfo(totalSize, count);
307
+ }
308
+
309
+ cursor = db.query(safeTable,
310
+ new String[]{KEY_ID, KEY_VALUE, KEY_METADATA},
311
+ null, null, null, null, null);
312
+ long keysBytes = 0;
313
+ long valuesBytes = 0;
314
+ long metadataBytes = 0;
315
+ int count = 0;
316
+ if (cursor != null && cursor.moveToFirst()) {
317
+ do {
318
+ count++;
319
+ String key = cursor.getString(0);
320
+ String value = cursor.getString(1);
321
+ String metadata = cursor.getString(2);
322
+ if (key != null) {
323
+ keysBytes += key.getBytes(StandardCharsets.UTF_8).length;
324
+ }
325
+ if (value != null) {
326
+ valuesBytes += value.getBytes(StandardCharsets.UTF_8).length;
327
+ }
328
+ if (metadata != null) {
329
+ metadataBytes += metadata.getBytes(StandardCharsets.UTF_8).length;
330
+ }
331
+ } while (cursor.moveToNext());
332
+ }
333
+ return new SizeInfo(keysBytes + valuesBytes, count, keysBytes, valuesBytes, metadataBytes);
334
+ } finally {
335
+ if (cursor != null) {
336
+ cursor.close();
291
337
  }
292
- return jsonArray.toString();
293
338
  }
294
-
295
- // For other objects, create a simple JSON object with their string representation
296
- JSONObject jsonObj = new JSONObject();
297
- jsonObj.put("value", obj.toString());
298
- jsonObj.put("type", obj.getClass().getSimpleName());
299
- return jsonObj.toString();
300
339
  }
301
-
340
+
302
341
  /**
303
- * Size information class
342
+ * Size information. The detailed byte breakdown is only meaningful when
343
+ * {@link #detailed} is true.
304
344
  */
305
345
  public static class SizeInfo {
306
346
  public final long total;
307
347
  public final int count;
308
-
348
+ public final boolean detailed;
349
+ public final long keysBytes;
350
+ public final long valuesBytes;
351
+ public final long metadataBytes;
352
+
309
353
  public SizeInfo(long total, int count) {
310
354
  this.total = total;
311
355
  this.count = count;
356
+ this.detailed = false;
357
+ this.keysBytes = 0;
358
+ this.valuesBytes = 0;
359
+ this.metadataBytes = 0;
360
+ }
361
+
362
+ public SizeInfo(long total, int count, long keysBytes, long valuesBytes, long metadataBytes) {
363
+ this.total = total;
364
+ this.count = count;
365
+ this.detailed = true;
366
+ this.keysBytes = keysBytes;
367
+ this.valuesBytes = valuesBytes;
368
+ this.metadataBytes = metadataBytes;
312
369
  }
313
370
  }
314
- }
371
+ }